using rocket instead of astro

This commit is contained in:
Philippe Loctaux 2023-12-01 10:54:31 +01:00
parent eb72400722
commit e61ef1d4c3
79 changed files with 4406 additions and 8501 deletions

View file

@ -1,12 +1,11 @@
/public/wallpapers.json
/public/style.css
/dist/
/node_modules/
/target/
/.idea/
/.vscode/
.DS_Store
.gitea/
/readme.md
/Dockerfile
/.dockerignore

View file

@ -18,7 +18,7 @@ jobs:
- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: git.philt3r
registry: git.int.philt3r.eu
username: ${{ gitea.repository_owner }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Login to Container Registry x4m3rocks
@ -33,5 +33,5 @@ jobs:
ACTIONS_RUNTIME_TOKEN: ''
with:
push: true
tags: git.philt3r/${{ gitea.repository }}:latest,registry-registry.y.z.x4m3.rocks/${{ gitea.repository }}:latest
tags: git.int.philt3r.eu/${{ gitea.repository }}:latest,registry-registry.y.z.x4m3.rocks/${{ gitea.repository }}:latest

19
.gitignore vendored
View file

@ -1,20 +1,11 @@
# wallpapers
/public/wallpapers.json
/src/wallpapers.rs
# built css
/public/style.css
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
/target
# environment variables
.env

View file

@ -1,7 +0,0 @@
{
"deno.enable": true,
"deno.unstable": true,
"deno.enablePaths":[
"./utils/"
]
}

2627
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

29
Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "plcom"
version = "0.1.0"
edition = "2021"
publish = false
default-run = "plcom"
[[bin]]
name = "plcom"
path = "src/main.rs"
[[bin]]
name = "gen-wallpapers"
path = "src/gen-wallpapers.rs"
[dependencies]
rocket = "0.5"
rocket_async_compression = "0.5"
askama = { version = "0.12.1", features = ["with-rocket"] }
askama_rocket = "0.12.0"
chrono = "0.4.31"
minify-html = "0.11.1"
# wallpapers
nanorand = { version = "0.7.0", features = ["chacha"] }
kamadak-exif = "0.5.5"
reqwest = { version = "0.11.22", features = ["blocking", "json"] }
serde_json = "1.0.108"
dms-coordinates = "1.1.0"

View file

@ -1,31 +1,39 @@
ARG DENO_VERSION=1.34.1
ARG RUST_VERSION=1.74.0
# build wallpapers
FROM denoland/deno:alpine-${DENO_VERSION} as wallpapers
FROM rust:${RUST_VERSION}-slim-bookworm as builder
WORKDIR /wallpapers
COPY utils/wallpapers.ts .
COPY public/wallpapers ./wallpapers/
RUN deno run --allow-read --allow-net wallpapers.ts --sourceDir ./wallpapers/ --destinationDir /wallpapers > wallpapers.json
# tailwind
WORKDIR /usr/bin/
ADD https://github.com/tailwindlabs/tailwindcss/releases/download/v3.3.5/tailwindcss-linux-x64 tailwindcss
RUN chmod +x /usr/bin/tailwindcss
# build astro
FROM node:18.16-alpine as astro
# openssl + CA certs
RUN apt-get update; \
apt-get install -y --no-install-recommends ca-certificates pkg-config libssl-dev
WORKDIR /astro
WORKDIR /usr/src/plcom
COPY css/ css/
COPY public/ public/
COPY templates/ templates/
COPY src/ src/
COPY build.rs .
COPY Cargo.lock .
COPY Cargo.toml .
COPY tailwind.config.cjs .
COPY package.json package-lock.json ./
RUN npm ci
# generate wallpapers
RUN cargo build --bin gen-wallpapers --release
RUN cargo run --bin gen-wallpapers --release
COPY . .
COPY --from=wallpapers /wallpapers/wallpapers.json ./public/
RUN npm run build
# build project
RUN cargo build --bin plcom --release
# web server
FROM denoland/deno:alpine-${DENO_VERSION}
FROM debian:12-slim
WORKDIR /usr/share/plcom
COPY --from=builder /usr/src/plcom/public /usr/share/plcom/public
COPY --from=builder /usr/src/plcom/target/release/plcom /usr/share/plcom
ENV ROCKET_CLI_COLORS=0
ENV ROCKET_ADDRESS=0.0.0.0
EXPOSE 8000
WORKDIR /web
COPY --from=astro /astro/dist .
RUN deno cache /web/server/entry.mjs
CMD ["run", "--allow-net", "--allow-read", "--allow-env", "/web/server/entry.mjs"]
ENTRYPOINT ["/usr/share/plcom/plcom"]

View file

@ -1,12 +0,0 @@
import { defineConfig } from 'astro/config';
import tailwind from "@astrojs/tailwind";
import compress from "astro-compress";
import deno from "@astrojs/deno";
// https://astro.build/config
export default defineConfig({
integrations: [tailwind(), compress()],
compressHTML: true,
output: "server",
adapter: deno({port: 8000})
});

21
build.rs Normal file
View file

@ -0,0 +1,21 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=tailwind.config.cjs");
println!("cargo:rerun-if-changed=templates/");
let result = std::process::Command::new("tailwindcss")
.arg("-i")
.arg("./css/tailwind.css")
.arg("-o")
.arg("./public/style.css")
.output()
.unwrap();
println!("{:?}", result);
if !result.status.success() {
panic!("Failed to run tailwindcss")
}
Ok(())
}

3
css/tailwind.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

7270
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
{
"name": "new-plcom",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "deno run --allow-net --allow-read --allow-env ./dist/server/entry.mjs",
"astro": "astro",
"wallpapers": "deno run --allow-read --allow-net utils/wallpapers.ts --sourceDir ./public/wallpapers/ --destinationDir /wallpapers > ./public/wallpapers.json"
},
"dependencies": {
"@astrojs/deno": "^4.1.1",
"@astrojs/tailwind": "^3.1.3",
"astro": "^2.5.6",
"astro-compress": "^1.1.43",
"tailwindcss": "^3.3.2"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

BIN
public/pub/phil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
public/pub/philt3r.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
public/pub/talks/clion.pdf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/pub/talks/vim.pdf Normal file

Binary file not shown.

View file

@ -6,7 +6,7 @@ http://philippeloctaux.com
## tech
- https://astro.build
- https://rocket.rs
- https://tailwindcss.com
## colors
@ -16,7 +16,7 @@ http://philippeloctaux.com
## wallpapers
1. place **JPEG** files in `public/wallpapers` and make sure they have exif data (GPS + date)
2. install https://deno.land and run `npm run wallpapers` to generate wallpaper metadata
2. run `cargo run --bin gen-wallpapers` to generate wallpaper metadata
## icons

54
src/cache.rs Normal file
View file

@ -0,0 +1,54 @@
use rocket::fairing::{self, Fairing};
use rocket::http::{ContentType, Header};
use rocket::{Request, Response};
#[derive(Debug)]
pub struct CacheControl {
duration_secs: u32,
types: Vec<ContentType>,
routes: Vec<&'static str>,
}
impl Default for CacheControl {
fn default() -> Self {
CacheControl {
duration_secs: 60 * 60, // 60 secs * 60 minutes
types: vec![ContentType::CSS, ContentType::JavaScript],
routes: vec!["/wallpapers", "/pub", "/images", "/icons"],
}
}
}
#[rocket::async_trait]
impl Fairing for CacheControl {
fn info(&self) -> fairing::Info {
fairing::Info {
name: "Cache Control",
kind: fairing::Kind::Response,
}
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
let mut should_cache = false;
// Check if content type matches
if let Some(content_type) = response.content_type() {
if self.types.contains(&content_type) {
should_cache = true;
}
}
// Check if route matches
self.routes
.iter()
.filter(|s| request.uri().path().starts_with(*s))
.for_each(|_| should_cache = true);
if should_cache {
response.set_header(Header::new(
"Cache-Control",
format!("public, max-age={}", self.duration_secs),
));
}
}
}

View file

@ -1,132 +0,0 @@
---
interface Props {
backgroundColor: string;
logo?: string;
logoTransparentBackground?: boolean;
name: string;
dates: string;
title: string;
link?: { label: string; uri: string };
notAvailable?: boolean;
image?: { src: string; rightPosition: boolean };
tech?: string[];
}
const {
backgroundColor,
logo,
logoTransparentBackground,
name,
dates,
title,
link,
notAvailable,
image,
tech,
} = Astro.props;
---
<div
class={`${
image
? `2xl:flex ${
image.rightPosition ? "2xl:flex-row-reverse" : ""
} items-stretch justify-between`
: ""
} w-full rounded-2xl ${backgroundColor}`}
>
<!-- Optional image on the side -->
{
image && (
<img
loading="lazy"
src={image.src}
class={`flex w-full 2xl:w-1/2 grow ${
image.rightPosition
? "2xl:rounded-tl-none 2xl:rounded-r-2xl"
: "2xl:rounded-tr-none 2xl:rounded-l-2xl"
} rounded-t-2xl object-cover`}
/>
)
}
<!-- Main content -->
<div
class=`p-6 ${image ? "flex flex-col grow-0" : ""} justify-between h-full`
>
<div class="flex flex-col">
<div class="flex flex-row">
{
logo && (
<img
loading="lazy"
src={logo}
class={`h-16 w-16 rounded-xl mr-4 ${
logoTransparentBackground === true
? "p-2 bg-white"
: ""
}`}
/>
)
}
<div class="flex flex-col justify-evenly">
<div class="text-xl md:text-2xl font-semibold">{name}</div>
<div class="text-xs md:text-sm">{dates}</div>
</div>
</div>
<p class="text-xl md:text-2xl font-semibold my-4">{title}</p>
</div>
<div class="space-y-2">
<!-- Description -->
<slot />
<!-- List technologies -->
{
tech && (
<div>
<div class="text-xl font-semibold mt-8 mb-2">
Technologies
</div>
<ul>
{tech.map((t) => (
<li>{t}</li>
))}
</ul>
</div>
)
}
</div>
<!-- Link with label -->
{
link && notAvailable !== true && (
<a
href={link.uri}
target="_blank"
class="mt-4 inline-flex max-w-fit bg-transparent hover:bg-sky-700 text-white font-semibold py-1.5 px-4 rounded-xl items-center border border-white hover:border-transparent transition-all duration-200"
>
<div class="inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 fill-current mr-2"
>
<path d="M12.232 4.232a2.5 2.5 0 013.536 3.536l-1.225 1.224a.75.75 0 001.061 1.06l1.224-1.224a4 4 0 00-5.656-5.656l-3 3a4 4 0 00.225 5.865.75.75 0 00.977-1.138 2.5 2.5 0 01-.142-3.667l3-3z" />
<path d="M11.603 7.963a.75.75 0 00-.977 1.138 2.5 2.5 0 01.142 3.667l-3 3a2.5 2.5 0 01-3.536-3.536l1.225-1.224a.75.75 0 00-1.061-1.06l-1.224 1.224a4 4 0 105.656 5.656l3-3a4 4 0 00-.225-5.865z" />
</svg>
<span>{link.label}</span>
</div>
</a>
)
}
<!-- No longer available -->
{
notAvailable === true && (
<span class="mt-4 cursor-not-allowed inline-flex max-w-fit bg-gray-400 text-gray-600 font-semibold py-1.5 px-4 rounded-xl items-center">
No longer available
</span>
)
}
</div>
</div>

View file

@ -1,9 +0,0 @@
---
const year = new Date().getFullYear();
---
<footer class="bg-black">
<div class="container mx-auto px-4 py-8">
<p>&copy; 2015 - {year} Philippe Loctaux</p>
</div>
</footer>

View file

@ -1,38 +0,0 @@
---
interface Props {
name: string;
linkUri: string;
}
const { name, linkUri } = Astro.props;
function extractInitials(name: string) {
// Split the name into words
const words = name.split(" ");
// Iterate over the words and extract the initials
const initials = words.map((word) => word.charAt(0).toUpperCase()).join("");
return initials;
}
const initials = extractInitials(name);
const linkLabel = linkUri.replace(/^https?:\/\//, "");
---
<li class="py-2">
<a
href={linkUri}
target="_blank"
class="hover:bg-gray-500 transition-all duration-200 flex items-center rounded-lg p-2"
>
<span
class="rounded-full flex-shrink-0 mr-4 w-10 h-10 bg-sky-900 text-white flex items-center justify-center text-lg font-medium"
>{initials}</span
>
<div>
<p class="font-bold">{name}</p>
<p class="">{linkLabel}</p>
</div>
</a>
</li>

View file

@ -1,24 +0,0 @@
---
import FriendCard from "./friend-card.astro";
---
<div>
<h1 class="text-4xl font-bold mb-4">Friends</h1>
<p class="text-lg">
Folks I worked with, or I like what they do.
</p>
<ul
class="mt-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 sm:gap-4"
>
<FriendCard name="Jamie Bishop" linkUri="https://jamiebi.shop" />
<FriendCard name="Ayden Panhuyzen" linkUri="https://ayden.dev" />
<FriendCard name="Corbin Crutchley" linkUri="https://crutchcorn.dev" />
<FriendCard name="James Fenn" linkUri="https://jfenn.me" />
<FriendCard name="Alex Dueppen" linkUri="https://ajd.sh" />
<FriendCard name="Peter Soboyejo" linkUri="https://petersoboyejo.com" />
<FriendCard name="Alexandre Wagner" linkUri="https://wagnerwave.com" />
<FriendCard name="Aidan Follestad" linkUri="https://af.codes" />
<FriendCard name="Victor Simon" linkUri="https://simonvictor.com" />
</ul>
</div>

View file

@ -1,18 +0,0 @@
---
import Wallpaper from "./wallpaper.astro";
---
<Wallpaper>
<div
class="container mx-auto px-8 py-16 w-full h-full justify-center items-center flex flex-col"
>
<div
class="inline-block backdrop-blur-lg backdrop-brightness-75 rounded-3xl shadow-2xl px-4 py-6 sm:px-8 sm:py-12 space-y-4"
>
<h1 class="text-3xl sm:text-4xl font-bold">Philippe Loctaux</h1>
<h2 class="sm:text-xl font-semibold">
Computer science student at Epitech, graduating in 2023.
</h2>
</div>
</div>
</Wallpaper>

View file

@ -1,153 +0,0 @@
---
import ExperienceCard from "./experience-card.astro";
---
<div>
<h1 class="text-4xl font-bold mb-4">Professional Experiences</h1>
<div class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
<ExperienceCard
backgroundColor="bg-sky-950"
logo="/icons/acklio.png"
logoTransparentBackground={true}
name="Acklio"
dates="March 2023 - May 2023"
title="Rust developer"
tech={["Rust", "SCHC", "STM32 controllers", "LoRa", "LoRaWAN"]}
>
<p>
The first usage of the SCHC framework (<a
href="https://www.rfc-editor.org/rfc/rfc8724"
target="_blank"
class="underline">RFC 8724</a
>) on Rust!
</p>
<div>
<ul class="list-disc mt-6">
<li class="ml-5">
Creation of Rust bindings of a C library implementing
the SCHC framework
</li>
<li class="ml-5">
Demonstration of SCHC with applications in Rust on x86
platform
</li>
<li class="ml-5">
Proof of concept usage of embedded STM32 controllers
exclusively in Rust
</li>
<li class="ml-5">
Transmission of knowledge to the technical team
</li>
</ul>
</div>
</ExperienceCard>
<ExperienceCard
backgroundColor="bg-sky-950"
logo="/icons/velorail.png"
logoTransparentBackground={true}
name="Vélorail du Kreiz Breizh"
dates="August 2021 - April 2022"
title="Freelance developer"
link={{ uri: "https://resa.velorail.bzh", label: "Online booking" }}
tech={["Angular", "NestJS", "GraphQL", "Rust", "Stripe"]}
>
<p>
Creation of an online booking platform focused on the tourist
activity of rail biking (vélorail).
</p>
<p>
During the first 5 months with the platform, 43% of the bookings
were made online.
</p>
<div>
<ul class="list-disc mt-6">
<li class="ml-5">
Design, UX, booking and payment flow for customers
</li>
<li class="ml-5">
Dashboard for managers with calendar view, manual
bookings, slots management
</li>
<li class="ml-5">
Ability to generate invoices, booking recaps for
managers
</li>
<li class="ml-5">
Sending emails to customers and managers about bookings
</li>
<li class="ml-5">
Online deployment, maintenance of the service
</li>
</ul>
</div>
</ExperienceCard>
<ExperienceCard
backgroundColor="bg-sky-950"
logo="/icons/yaakadev.png"
name="Yaakadev"
dates="April 2021 - July 2021"
title="Full-Stack developer"
tech={["NodeJS", "ExpressJS", "Angular", "MongoDB", "CI/CD"]}
>
<p>Maintenance of existing projects for clients</p>
<p>
Design, development and deployment of multiple projects from
scratch:
</p>
<div>
<ul class="list-disc mt-6">
<li class="ml-5">
Admin dashboard of a local merchants solution
</li>
<li class="ml-5">
Calendar planning application with filtering and custom
views
</li>
<li class="ml-5">
Intranet to upload and download documents
</li>
</ul>
</div>
</ExperienceCard>
<ExperienceCard
backgroundColor="bg-sky-950"
logo="/icons/epitech.png"
logoTransparentBackground={true}
name="Epitech"
dates="February 2020 - April 2021, September 2022 - February 2023"
title="Teaching assistant (AER)"
tech={["C", "C++", "Haskell", "Rust", "Web and mobile development"]}
>
<p>Pedagogical supervision of three classes of students</p>
<p>Conducting educational activities throughout the school year</p>
<div>
<ul class="list-disc mt-6">
<li class="ml-5">Start of projects</li>
<li class="ml-5">Technical help and guidance</li>
<li class="ml-5">Proctoring exams</li>
<li class="ml-5">Grading students on their work</li>
</ul>
</div>
</ExperienceCard>
<ExperienceCard
backgroundColor="bg-sky-950"
logo="/icons/ubiscale.png"
logoTransparentBackground={true}
name="Ubiscale"
dates="August 2019 - December 2019"
title="Embedded developer"
tech={["C on a ESP8266 controller", "Wi-Fi", "Bluetooth"]}
>
<p>Creation of a home Wifi gateway for an IoT object</p>
<p>
Research, reverse engineering of existing products, design and
implementation
</p>
</ExperienceCard>
</div>
</div>

View file

@ -1,195 +0,0 @@
---
import ExperienceCard from "./experience-card.astro";
---
<div>
<h1 class="text-4xl font-bold mb-4">Projects</h1>
<div class="mt-4 space-y-4">
<div
class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0"
>
<ExperienceCard
backgroundColor="bg-pink-950"
logo="/icons/ezidam.png"
logoTransparentBackground={true}
name="ezidam"
dates="January 2023 - May 2023"
title="Identity and Access Management system"
tech={[
"Rust",
"SQLite",
"OAuth2 / OIDC",
"TOTP",
"SMTP",
"Docker",
]}
>
<p>
A simple identity and access management system for SMEs or
personal use.
</p>
<p>Low maintenance required, easy to deploy and to backup.</p>
<div>
<ul class="list-disc mt-6">
<li class="ml-5">Users management</li>
<li class="ml-5">Roles management</li>
<li class="ml-5">
Assign users to roles and the other way around
</li>
<li class="ml-5">
OAuth2 / OIDC applications (code flow)
</li>
<li class="ml-5">Multi-Factor Authentication (TOTP)</li>
<li class="ml-5">
Password reset (via email or backup token)
</li>
<li class="ml-5">Simple administration panel</li>
<li class="ml-5">
Good security measures for users and administrators
</li>
</ul>
</div>
</ExperienceCard>
<ExperienceCard
backgroundColor="bg-pink-950"
name="pass4thewin"
dates="November 2020 - January 2021"
title="Password manager"
link={{
uri: "https://github.com/x4m3/pass4thewin",
label: "Source code",
}}
tech={["Windows", "Rust", "OpenPGP", "libgit2"]}
>
<p>
Port of <a
href="https://passwordstore.org"
class="underline"
target="_blank">pass</a
>, the standard unix password manager on the Windows
platform.
</p>
<p>
Command line application, compatible with existing secrets
</p>
<div>
<ul class="list-disc mt-6">
<li class="ml-5">Creation of a store</li>
<li class="ml-5">List secrets</li>
<li class="ml-5">Decrypt secret</li>
<li class="ml-5">
<s>Insert or generate secrets</s> Causes data corruption
</li>
<li class="ml-5">
<s>Edit existing secrets</s> Causes data corruption
</li>
<li class="ml-5">Synchronisation with git</li>
<li class="ml-5">TOTP support</li>
</ul>
</div>
</ExperienceCard>
</div>
<ExperienceCard
backgroundColor="bg-pink-950"
logo="/icons/naviarent.png"
name="NaviaRent"
dates="September 2020 - January 2023"
title="Epitech Innovative Project"
link={{ uri: "https://naviarent.fr", label: "Website" }}
image={{ src: "/images/naviarent.jpg", rightPosition: true }}
tech={[
"NodeJS",
"Angular",
"Kotlin",
"SwiftUI",
"Docker",
"GitLab CI/CD",
"Raspberry Pi",
"ESP32",
]}
>
<p>A B2B platform helping rentals of standup paddle boards.</p>
<div>
<ul class="list-disc mt-6">
<li class="ml-5">
DevOps of all software in the NaviaRent stack
</li>
<li class="ml-5">Creation of the iOS application</li>
<li class="ml-5">
Contributions to the Android application
</li>
<li class="ml-5">Contributions to the backend server</li>
<li class="ml-5">
Creation and contributions to the web client
</li>
<li class="ml-5">Server administration, backups</li>
</ul>
</div>
</ExperienceCard>
<div
class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0"
>
<ExperienceCard
backgroundColor="bg-pink-950"
name="epitok"
dates="June 2020 - September 2020"
title="Presence system at Epitech"
notAvailable={true}
tech={["Rust", "HTML", "Bootstrap", "jQuery", "Docker"]}
>
<p>
A library and web client to simplify students presence at
Epitech.
</p>
<p>
Students are handed a piece of paper with a 6 digits number
(called a "token") to verify their presence at school
events.
</p>
<p>
Teachers use epitok to scan student cards with QR codes on
them instead of printing and handing tokens to students.
</p>
</ExperienceCard>
<ExperienceCard
backgroundColor="bg-pink-950"
name="epi.today"
dates="December 2019 - February 2020"
title="Calendar for Epitech"
notAvailable={true}
tech={["TypeScript", "HTML", "Bootstrap", "Docker"]}
>
<p>A viewer of the Epitech intranet calendar.</p>
<p>
Students and teachers glance at their planning without the
need to go on the school's intranet.
</p>
</ExperienceCard>
</div>
<ExperienceCard
backgroundColor="bg-pink-950"
logo="/icons/canvas.png"
name="canvas.place"
dates="April 2017 - January 2020"
title="Timelapse"
link={{ uri: "https://timelapse.canvas.place", label: "Website" }}
image={{ src: "/images/canvas.png", rightPosition: false }}
tech={["FFmpeg", "Shell scripting", "nginx"]}
>
<p>canvas.place is a shared place to express creativity.</p>
<p>
People from all over the world share one single canvas to paint
on.
</p>
<p>I created and maintained a timelapse of the virtual canvas.</p>
</ExperienceCard>
</div>
</div>

View file

@ -1,77 +0,0 @@
---
interface Props {
title: string;
date: string;
location: string;
linkUri: string;
linkLabel: string;
}
const { title, date, location, linkUri, linkLabel } = Astro.props;
---
<div class="rounded-2xl w-full bg-teal-950 p-6">
<!-- Title -->
<h3 class="text-xl font-semibold mb-4">{title}</h3>
<!-- Date -->
<div class="flex">
<div class="inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z"
clip-rule="evenodd"></path>
</svg>
<span class="ml-2">{date}</span>
</div>
</div>
<!-- Location -->
<div class="flex">
<div class="inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M9.69 18.933l.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 00.281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 103 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 002.273 1.765 11.842 11.842 0 00.976.544l.062.029.018.008.006.003zM10 11.25a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5z"
clip-rule="evenodd"></path>
</svg>
<span class="ml-2">{location}</span>
</div>
</div>
<!-- Link with label -->
<a
href={linkUri}
target="_blank"
class="mt-4 inline-flex bg-transparent hover:bg-sky-700 text-white font-semibold py-1.5 px-4 rounded-xl items-center border border-white hover:border-transparent transition-all duration-200"
>
<div class="inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 fill-current mr-2"
>
<path
d="M12.232 4.232a2.5 2.5 0 013.536 3.536l-1.225 1.224a.75.75 0 001.061 1.06l1.224-1.224a4 4 0 00-5.656-5.656l-3 3a4 4 0 00.225 5.865.75.75 0 00.977-1.138 2.5 2.5 0 01-.142-3.667l3-3z"
></path>
<path
d="M11.603 7.963a.75.75 0 00-.977 1.138 2.5 2.5 0 01.142 3.667l-3 3a2.5 2.5 0 01-3.536-3.536l1.225-1.224a.75.75 0 00-1.061-1.06l-1.224 1.224a4 4 0 105.656 5.656l3-3a4 4 0 00-.225-5.865z"
></path>
</svg>
<span>{linkLabel}</span>
</div>
</a>
</div>

View file

@ -1,58 +0,0 @@
---
import TalkCard from "./talk-card.astro";
---
<div>
<h1 class="text-4xl font-bold mb-4">Talks</h1>
<p class="text-lg">
Giving a talk is the opportunity to share what I know, and helps me
reduce my fear of public speaking.
</p>
<div
class="mt-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 place-content-center"
>
<TalkCard
title="Vim"
date="February 2023"
location="Epitech Rennes"
linkUri="https://x4m3.rocks/talks/vim.pdf"
linkLabel="Slides"
/>
<TalkCard
title="CLion"
date="March 2021"
location="Epitech Rennes"
linkUri="https://x4m3.rocks/talks/clion.pdf"
linkLabel="Slides"
/>
<TalkCard
title="git & devops 2"
date="February 2021"
location="Epitech Rennes"
linkUri="https://x4m3.rocks/talks/git-devops2.pdf"
linkLabel="Slides"
/>
<TalkCard
title="pass4thewin"
date="February 2021"
location="Epitech Rennes"
linkUri="https://x4m3.rocks/talks/pass4thewin.pdf"
linkLabel="Slides"
/>
<TalkCard
title="git & devops"
date="May 2020"
location="Epitech Rennes"
linkUri="https://x4m3.rocks/talks/git-devops.pdf"
linkLabel="Slides"
/>
<TalkCard
title="git gud"
date="May 2019"
location="Epitech Rennes"
linkUri="https://x4m3.rocks/talks/git-tek.pdf"
linkLabel="Slides"
/>
</div>
</div>

View file

@ -1,90 +0,0 @@
---
import wallpapers from "../../public/wallpapers.json";
const wallpaper = wallpapers[Math.floor(Math.random() * wallpapers.length)];
// Get first part of location
const [location1, ...locationRest] = wallpaper.location.split(",");
// Get rest of location
const location2 = locationRest.join(",");
---
<style>
/* Fixed background when scrolling in tailwind
* Disabled on iOS devices
* https://tailwindcss.com/docs/background-attachment
* https://stackoverflow.com/a/60220757/4809297
*/
@supports (-webkit-touch-callout: none) {
/* CSS specific to iOS devices */
#wallpaper {
background-attachment: scroll;
}
}
@supports not (-webkit-touch-callout: none) {
/* CSS for other than iOS devices */
#wallpaper {
background-attachment: fixed;
}
}
</style>
<div class="bg-gradient-to-r from-red-900 via-teal-900 to-fuchsia-900">
<div
id="wallpaper"
class="relative text-white w-full h-almostscreen bg-center bg-cover"
style=`background-image: url(${wallpaper.file});`
>
<!-- Content inside -->
<slot />
<!-- Exif info -->
<div
class="absolute bottom-3 sm:bottom-5 left-2 sm:left-5 inline-block backdrop-blur-lg backdrop-brightness-75 rounded-xl shadow-2xl p-2 space-y-0.5 sm:space-y-2"
>
<!-- Location -->
<div class="flex">
<div class="inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M9.69 18.933l.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 00.281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 103 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 002.273 1.765 11.842 11.842 0 00.976.544l.062.029.018.008.006.003zM10 11.25a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5z"
clip-rule="evenodd"></path>
</svg>
<span class="ml-1 text-sm"
>{location1}<span class="hidden md:inline"
>,{location2}</span
></span
>
</div>
</div>
<!-- Date -->
<div class="flex">
<div class="inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z"
clip-rule="evenodd"></path>
</svg>
<span class="ml-1 text-sm">{wallpaper.date}</span>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,140 +0,0 @@
<div class="grid grid-cols-3 lg:grid-cols-6 gap-4 place-content-center">
<div class="w-full h-auto md:w-auto">
<div class="text-center">
<!-- Twitter -->
<a
href="https://twitter.com/philippeloctaux"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
<div class="inline-flex items-center">
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"
></path></svg
>
<span class="hidden sm:inline">Twitter</span>
</div>
</a>
</div>
</div>
<div class="w-full h-auto md:w-auto">
<div class="text-center">
<!-- Telegram -->
<a
href="https://t.me/philippeloctaux"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
<div class="inline-flex items-center">
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"
></path></svg
>
<span class="hidden sm:inline">Telegram</span>
</div>
</a>
</div>
</div>
<div class="w-full h-auto md:w-auto">
<div class="text-center">
<!-- Mastodon -->
<a
rel="me"
href="https://mastodon.social/@philt3r"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
<div class="inline-flex items-center">
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"
></path></svg
>
<span class="hidden sm:inline">Mastodon</span>
</div>
</a>
</div>
</div>
<div class="w-full h-auto md:w-auto">
<div class="text-center">
<!-- GitHub -->
<a
href="https://github.com/x4m3"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
<div class="inline-flex items-center">
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
></path></svg
>
<span class="hidden sm:inline">GitHub</span>
</div>
</a>
</div>
</div>
<div class="w-full h-auto md:w-auto">
<div class="text-center">
<!-- LinkedIn -->
<a
href="https://linkedin.com/in/philippeloctaux"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
<div class="inline-flex items-center">
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><path
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
></path></svg
>
<span class="hidden sm:inline">LinkedIn</span>
</div>
</a>
</div>
</div>
<div class="w-full h-auto md:w-auto">
<div class="text-center">
<!-- Email -->
<a
href="/email"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
<div class="inline-flex items-center">
<svg
class="w-6 h-6 mr-0 sm:mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
></path>
</svg>
<span class="hidden sm:inline">Email</span>
</div>
</a>
</div>
</div>
</div>

1
src/env.d.ts vendored
View file

@ -1 +0,0 @@
/// <reference types="astro/client" />

11
src/filters.rs Normal file
View file

@ -0,0 +1,11 @@
const SUFFIX: &str = "Philippe Loctaux";
pub fn title<T: std::fmt::Display>(s: T) -> ::askama::Result<String> {
let prefix = s.to_string();
Ok(if prefix != SUFFIX {
format!("{prefix} - {SUFFIX}")
} else {
prefix
})
}

204
src/gen-wallpapers.rs Normal file
View file

@ -0,0 +1,204 @@
use dms_coordinates::Bearing;
use exif::{DateTime, Exif, In, Tag};
use std::fs::read_dir;
use std::io::{BufReader, Write};
use std::path::PathBuf;
const WALLPAPERS_PATH: &str = "/public/wallpapers";
const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR");
const IMPORTS: &str = r#"use crate::types::{Location,Wallpaper};"#;
#[derive(Debug)]
struct Metadata {
file: String,
date: String,
latitude: f32,
longitude: f32,
}
fn parse_coordinates(exif: &Exif, tag: Tag, r#ref: Tag) -> Option<f32> {
let mut coord = None;
let mut coord_ref = None;
// Parse DMS coordinates
if let Some(field) = exif.get_field(tag, In::PRIMARY) {
match field.value {
exif::Value::Rational(ref vec) if !vec.is_empty() => {
let deg = vec[0].to_f64() as i32;
let min = vec[1].to_f64() as i32;
let sec = vec[2].to_f64();
coord = Some((deg, min, sec));
}
_ => {}
}
}
// Get bearing
if let Some(field) = exif.get_field(r#ref, In::PRIMARY) {
coord_ref = Some(field.display_value().to_string());
}
match (coord, coord_ref) {
(Some((deg, min, sec)), Some(r#ref)) => {
use Bearing::*;
let bearing = match r#ref.as_str() {
"N" => North,
"NE" => NorthEast,
"NW" => NorthWest,
"S" => South,
"SE" => SouthEast,
"SW" => SouthWest,
"E" => East,
"W" => West,
_ => return None,
};
let dms = dms_coordinates::DMS::new(deg, min, sec, bearing);
Some(dms.to_decimal_degrees() as f32)
}
(_, _) => None,
}
}
fn main() {
let mut wallpapers = format!(
"// AUTO GENERATED FILE.\n// PLEASE DO NOT EDIT MANUALLY.\n{IMPORTS}\npub static WALLPAPERS: &[&Wallpaper] = &[\n"
);
// Get list of files
let local_wallpaper_path = format!("{}{}", CRATE_PATH, WALLPAPERS_PATH);
let metadata: Vec<Metadata> = match read_dir(local_wallpaper_path) {
Ok(dir) => {
let mut files = vec![];
for file in dir {
let Ok(file) = file else {
continue;
};
// Get filename
let Ok(filename) = file.file_name().into_string() else {
continue;
};
// Read exif from file
let Ok(file) = std::fs::File::open(file.path()) else {
continue;
};
let mut reader = BufReader::new(file);
let Ok(exif) = exif::Reader::new().read_from_container(&mut reader) else {
continue;
};
// Get GPS coordinates
let latitude = parse_coordinates(&exif, Tag::GPSLatitude, Tag::GPSLatitudeRef);
let longitude = parse_coordinates(&exif, Tag::GPSLongitude, Tag::GPSLongitudeRef);
// Get date
let mut date = None;
if let Some(field) = exif.get_field(Tag::DateTime, In::PRIMARY) {
match field.value {
exif::Value::Ascii(ref vec) if !vec.is_empty() => {
if let Ok(datetime) = DateTime::from_ascii(&vec[0]) {
let datetime = datetime.to_string();
let split: Vec<&str> = datetime.split(' ').collect();
date = split.first().map(|str| str.to_string());
}
}
_ => {}
}
}
match (date, latitude, longitude) {
(Some(date), Some(latitude), Some(longitude)) => files.push(Metadata {
file: format!("/wallpapers/{}", filename),
date,
latitude,
longitude,
}),
(_, _, _) => {
continue;
}
}
}
files
}
Err(_) => vec![],
};
for wallpaper in metadata {
println!("\nProcessing `{}`", wallpaper.file);
let client = reqwest::blocking::Client::new();
let (precise, broad) = match client
.get(format!(
// Documentation: https://nominatim.org/release-docs/develop/api/Reverse/
"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={}&lon={}&zoom=8",
wallpaper.latitude, wallpaper.longitude,
))
.header("User-Agent", "https://philippeloctaux.com")
.send()
.and_then(|data| data.json::<serde_json::Value>())
{
Ok(data) => {
let location = &data["display_name"];
let (precise, broad) = if location.is_string() {
let location = location.to_string();
// Remove first and last characters (the string is wrapped in double quotes '"')
let location = {
let mut chars = location.chars();
chars.next();
chars.next_back();
chars.as_str()
};
println!("Raw location is `{}`", location);
let mut location = location.split(',');
let precise = location.next().unwrap_or("?").to_string();
let mut broad: String =
location.collect::<Vec<&str>>().join(",").trim().to_string();
if broad.is_empty() {
broad.push('?');
}
(precise, broad)
} else {
println!("Failed to find location.");
("?".into(), "?".into())
};
(precise, broad)
}
Err(_) => {
println!("Failed to make API call to get location.");
("?".into(), "?".into())
}
};
println!("Precise location is `{}`", precise);
println!("Broad location is `{}`", broad);
// Construct struct
let wallpaper_struct = format!("&Wallpaper {{ file: \"{}\", date: \"{}\", location: Location {{ precise: \"{}\", broad: \"{}\", latitude: {}, longitude: {} }} }},\n", wallpaper.file, wallpaper.date, precise, broad, wallpaper.latitude, wallpaper.longitude);
wallpapers.push_str(&wallpaper_struct);
}
wallpapers.push_str("];\n");
// Write string to file
let mut output_wallpapers_path: PathBuf = CRATE_PATH.into();
output_wallpapers_path.push("src/wallpapers.rs");
let mut output_file =
std::fs::File::create(&output_wallpapers_path).expect("file already exists");
output_file
.write_all(wallpapers.as_bytes())
.expect("write dictionary to file");
}

View file

@ -1,33 +0,0 @@
---
import Footer from "../components/footer.astro";
interface Props {
title?: string;
}
let { title } = Astro.props;
title = title !== undefined ? `${title} - ` : title;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}Philippe Loctaux</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#0c4a6e">
<meta name="msapplication-TileColor" content="#0c4a6e">
<meta name="theme-color" content="#0c4a6e">
<script defer data-domain="philippeloctaux.com" src="https://plausible.y.z.x4m3.rocks/js/script.js"></script>
</head>
<body class="flex flex-col min-h-screen bg-gray-900 text-white">
<div class="flex-grow">
<slot />
</div>
<Footer />
</body>
</html>

61
src/main.rs Normal file
View file

@ -0,0 +1,61 @@
use self::types::*;
use chrono::Datelike;
use rocket::fs::FileServer;
use rocket::{catch, catchers, get, launch, routes};
mod cache;
mod filters;
mod minify;
mod templates;
mod types;
mod wallpapers;
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", FileServer::from("public"))
.mount("/", routes![root, email])
.register("/", catchers![not_found])
.attach(
rocket_async_compression::CachedCompression::path_suffix_fairing(vec![
// Code
".js".into(),
".css".into(),
// Documents
".pdf".into(),
".txt".into(),
]),
)
.attach(cache::CacheControl::default())
.attach(minify::Minify)
}
#[catch(404)]
fn not_found() -> templates::NotFound<'static> {
templates::NotFound {
title: "404 Not found",
year: chrono::Utc::now().year(),
}
}
#[get("/")]
fn root() -> templates::Root<'static> {
templates::Root {
title: "Philippe Loctaux",
year: chrono::Utc::now().year(),
wallpaper: Wallpaper::random(),
networks: Network::new(),
jobs: Job::new(),
talks: Talk::new(),
friends: Friend::new(),
projects: ProjectKind::new(),
}
}
#[get("/email")]
fn email() -> templates::Email<'static> {
templates::Email {
title: "Email",
year: chrono::Utc::now().year(),
}
}

39
src/minify.rs Normal file
View file

@ -0,0 +1,39 @@
use rocket::fairing::{self, Fairing, Kind};
use rocket::http::ContentType;
use rocket::{Request, Response};
pub struct Minify;
#[rocket::async_trait]
impl Fairing for Minify {
fn info(&self) -> fairing::Info {
fairing::Info {
name: "Minify HTML",
kind: Kind::Response,
}
}
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
if response.content_type() == Some(ContentType::HTML) {
let body = response.body_mut();
if let Ok(original) = body.to_bytes().await {
let cfg = minify_html::Cfg {
// Be HTML spec compliant
do_not_minify_doctype: true,
ensure_spec_compliant_unquoted_attribute_values: true,
keep_spaces_between_attributes: true,
// The rest
keep_closing_tags: true,
keep_html_and_head_opening_tags: true,
minify_css: false,
minify_js: true,
..Default::default()
};
let minified = minify_html::minify(&original, &cfg);
response.set_sized_body(minified.len(), std::io::Cursor::new(minified));
}
}
}
}

View file

@ -1,12 +0,0 @@
---
import Page from "../layouts/page.astro";
---
<Page title="404">
<main class="container mx-auto px-4 py-16">
<h1 class="text-3xl sm:text-4xl font-bold">404 Not Found</h1>
<div class="mt-8">
<p>This page could not be found.</p>
</div>
</main>
</Page>

View file

@ -1,54 +0,0 @@
---
import Page from "../layouts/page.astro";
---
<Page title="Email">
<main class="container mx-auto px-4 py-16">
<h1 class="text-3xl sm:text-4xl font-bold">Email</h1>
<div class="mt-8">
<p>
Send an email if you want to work with me, propose a project
idea, or just to say hi!
</p>
<div class="my-4">
<a
href="mailto:pATphilippeloctauxDOTcom"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
<div class="inline-flex items-center">
<svg
class="w-6 h-6 mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
></path>
</svg>
<span class="text-center"
>p at philippeloctaux dot com</span
>
</div>
</a>
</div>
<p class="mb-2">
If you want to encrypt your message, I have a <a
href="/pgp-0x69771CD04BA82EC0.txt"
class="underline">pgp key</a
> at your disposal.
</p>
<p class="mb-2">
I also have a <a href="/keybase.txt" class="underline"
>Keybase</a
> account, but I do not check it often.
</p>
</div>
</main>
</Page>

View file

@ -1,24 +0,0 @@
---
import Page from "../layouts/page.astro";
import Hero from "../components/hero.astro";
import Whoami from "../components/whoami.astro";
import Www from "../components/www.astro";
import Talks from "../components/talks.astro";
import Friends from "../components/friends.astro";
import ProfessionalExperience from "../components/professional-experience.astro";
import Projects from "../components/projects.astro";
---
<Page>
<Hero />
<main class="container mx-auto px-4 md:px-8 lg:px-16 py-16">
<Whoami />
<div class="my-16 space-y-16 md:space-y-32">
<Www />
<ProfessionalExperience />
<Projects />
<Talks />
<Friends />
</div>
</main>
</Page>

45
src/templates.rs Normal file
View file

@ -0,0 +1,45 @@
use crate::filters;
use crate::types::*;
use askama::Template;
#[derive(Template)]
#[template(path = "pages/root.html")]
pub struct Root<'a> {
pub title: &'a str,
pub year: i32,
pub wallpaper: Option<&'static &'static Wallpaper>,
pub networks: Vec<Network>,
pub jobs: Vec<Job>,
pub talks: Vec<Talk>,
pub friends: Vec<Friend>,
pub projects: Vec<ProjectKind>,
}
#[derive(rocket::Responder)]
struct RootResponder<'a> {
template: Root<'a>,
}
#[derive(Template)]
#[template(path = "pages/404.html")]
pub struct NotFound<'a> {
pub title: &'a str,
pub year: i32,
}
#[derive(rocket::Responder)]
struct NotFoundResponder<'a> {
template: NotFound<'a>,
}
#[derive(Template)]
#[template(path = "pages/email.html")]
pub struct Email<'a> {
pub title: &'a str,
pub year: i32,
}
#[derive(rocket::Responder)]
struct EmailResponder<'a> {
template: Email<'a>,
}

50
src/types.rs Normal file
View file

@ -0,0 +1,50 @@
use rocket::http::uri::Absolute;
pub mod friend;
pub mod job;
pub mod network;
pub mod project;
pub mod talk;
pub use self::friend::*;
pub use self::job::*;
pub use self::network::*;
pub use self::project::*;
pub use self::talk::*;
pub use crate::wallpapers::WALLPAPERS;
pub struct Logo {
pub file: &'static str,
pub transparent_background: bool,
}
pub struct Link {
pub label: &'static str,
pub uri: Absolute<'static>,
}
pub struct Location {
pub precise: &'static str,
pub broad: &'static str,
pub latitude: f32,
pub longitude: f32,
}
pub struct Wallpaper {
pub file: &'static str,
pub date: &'static str,
pub location: Location,
}
impl Wallpaper {
pub fn random() -> Option<&'static &'static Wallpaper> {
use nanorand::{ChaCha20, Rng};
use std::ops::Range;
let range = Range {
start: 0,
end: WALLPAPERS.len(),
};
WALLPAPERS.get(ChaCha20::new().generate_range(range))
}
}

84
src/types/friend.rs Normal file
View file

@ -0,0 +1,84 @@
use rocket::http::uri::Absolute;
use rocket::uri;
pub struct Friend {
pub first_name: &'static str,
pub last_name: &'static str,
pub uri: Absolute<'static>,
}
impl Friend {
pub fn initials(&self) -> String {
let first = self
.first_name
.to_uppercase()
.chars()
.next()
.expect("Invalid first name");
let last = self
.last_name
.to_uppercase()
.chars()
.next()
.expect("Invalid last name");
format!("{first}{last}")
}
pub fn domain_name(&self) -> String {
match self.uri.authority() {
Some(authority) => authority.to_string(),
None => self.uri.to_string(),
}
}
pub fn new() -> Vec<Self> {
vec![
Friend {
first_name: "Jamie",
last_name: "Bishop",
uri: uri!("https://jamiebi.shop"),
},
Friend {
first_name: "Ayden",
last_name: "Panhuyzen",
uri: uri!("https://ayden.dev"),
},
Friend {
first_name: "Corbin",
last_name: "Crutchley",
uri: uri!("https://crutchcorn.dev"),
},
Friend {
first_name: "James",
last_name: "Fenn",
uri: uri!("https://jfenn.me"),
},
Friend {
first_name: "Alex",
last_name: "Dueppen",
uri: uri!("https://ajd.sh"),
},
Friend {
first_name: "Peter",
last_name: "Sobolev",
uri: uri!("https://petersoboyejo.com"),
},
Friend {
first_name: "Alexandre",
last_name: "Wagner",
uri: uri!("https://wagnerwave.com"),
},
Friend {
first_name: "Aidan",
last_name: "Follestad",
uri: uri!("https://af.codes"),
},
Friend {
first_name: "Victor",
last_name: "Simon",
uri: uri!("https://simonvictor.com"),
},
]
}
}

111
src/types/job.rs Normal file
View file

@ -0,0 +1,111 @@
use crate::types::Logo;
pub struct Job {
pub company: &'static str,
pub title: &'static str,
pub dates: &'static str,
pub logo: Logo,
pub description: Vec<&'static str>,
pub accomplishments: Vec<&'static str>,
pub technologies: Vec<&'static str>,
}
impl Job {
pub fn new() -> Vec<Self> {
vec![
Job {
company: "Acklio",
title: "Rust developer",
dates: "March 2023 - May 2023",
logo: Logo {
file: "/icons/acklio.png",
transparent_background: true,
},
description: vec!["The first usage of the SCHC framework (RFC 8724) on Rust!"],
accomplishments: vec![
"Creation of Rust bindings of a C library implementing the SCHC framework",
"Demonstration of SCHC with applications in Rust on x86 platform",
"Proof of concept usage of embedded STM32 controllers exclusively in Rust",
"Transmission of knowledge to the technical team",
],
technologies: vec!["Rust", "SCHC", "STM32 controllers", "LoRa", "LoRaWAN"],
},
Job {
company: "Vélorail du Kreiz Breizh",
title: "Freelance developer",
dates: "August 2021 - April 2022",
logo: Logo {
file: "/icons/velorail.png",
transparent_background: true,
},
description: vec![
"Creation of an online booking platform focused on the tourist activity of rail biking (vélorail).",
"During the first 5 months with the platform, 43% of the bookings were made online.",
],
accomplishments: vec![
"Design, UX, booking and payment flow for customers",
"Dashboard for managers with calendar view, manual bookings, slots management",
"Ability to generate invoices, booking recaps for managers",
"Sending emails to customers and managers about bookings",
"Online deployment, maintenance of the service",
],
technologies: vec!["Angular", "NestJS", "GraphQL", "Rust", "Stripe"],
},
Job {
company: "Yaakadev",
title: "Full-Stack developer",
dates: "April 2021 - July 2021",
logo: Logo {
file: "/icons/yaakadev.png",
transparent_background: false,
},
description: vec![
"Maintenance of existing projects for clients",
"Design, development and deployment of multiple projects from scratch:",
],
accomplishments: vec![
"Admin dashboard of a local merchants solution",
"Calendar planning application with filtering and custom views",
"Intranet to upload and download documents",
],
technologies: vec!["NodeJS", "ExpressJS", "Angular", "MongoDB", "CI/CD"],
},
Job {
company: "Epitech",
title: "Teaching assistant (AER)",
dates: "February 2020 - April 2021, September 2022 - February 2023",
logo: Logo {
file: "/icons/epitech.png",
transparent_background: true,
},
description: vec![
"Pedagogical supervision of three classes of students.",
"Conducting educational activities throughout the school year.",
],
accomplishments: vec![
"Start of projects",
"Technical help and guidance",
"Proctoring exams",
"Grading students on their work",
],
technologies: vec!["C", "C++", "Haskell", "Rust", "Web and mobile development"],
},
Job {
company: "Ubiscale",
title: "Embedded developer",
dates: "August 2019 - December 2019",
logo: Logo {
file: "/icons/ubiscale.png",
transparent_background: true,
},
description: vec!["Creation of a home Wifi gateway for an IoT object."],
accomplishments: vec![
"Research, reverse engineering of existing products",
"Design and implementation.",
],
technologies: vec!["C on a ESP8266 controller", "Wi-Fi", "Bluetooth"],
},
]
}
}

52
src/types/network.rs Normal file
View file

@ -0,0 +1,52 @@
use rocket::http::uri::Absolute;
use rocket::uri;
pub enum Icon {
Email,
Github,
Linkedin,
Mastodon,
Telegram,
Twitter,
}
pub struct Network {
pub name: &'static str,
pub uri: Absolute<'static>,
pub icon: Icon,
}
impl Network {
pub fn new() -> Vec<Self> {
vec![
Network {
name: "Twitter",
uri: uri!("https://twitter.com/philippeloctaux"),
icon: Icon::Twitter,
},
Network {
name: "Telegram",
uri: uri!("https://t.me/philippeloctaux"),
icon: Icon::Telegram,
},
Network {
name: "Mastodon",
uri: uri!("https://mastodon.social/@philt3r"),
icon: Icon::Mastodon,
},
Network {
name: "GitHub",
uri: uri!("https://github.com/x4m3"),
icon: Icon::Github,
},
Network {
name: "LinkedIn",
uri: uri!("https://linkedin.com/in/philippeloctaux"),
icon: Icon::Linkedin,
},
Network {
name: "Email",
uri: uri!("https://philippeloctaux.com/email"),
icon: Icon::Email,
},
]
}
}

247
src/types/project.rs Normal file
View file

@ -0,0 +1,247 @@
use crate::types::{Link, Logo};
use rocket::uri;
pub enum Position {
Left,
Right,
}
pub struct Image {
pub file: &'static str,
pub position: Position,
}
pub enum ProjectLink {
Available(Link),
NotAvailable,
}
pub struct ProjectWithImage {
pub name: &'static str,
pub tagline: &'static str,
pub dates: &'static str,
pub description: Vec<&'static str>,
pub accomplishments: Vec<&'static str>,
pub technologies: Vec<&'static str>,
pub link: Option<ProjectLink>,
pub logo: Option<Logo>,
pub image: Image,
}
#[derive(Default)]
pub struct ProjectWithoutImage {
pub name: &'static str,
pub tagline: &'static str,
pub dates: &'static str,
pub description: Vec<&'static str>,
pub accomplishments: Vec<&'static str>,
pub technologies: Vec<&'static str>,
pub link: Option<ProjectLink>,
pub logo: Option<Logo>,
}
pub enum ProjectKind {
WithImage(ProjectWithImage),
WithoutImage((ProjectWithoutImage, ProjectWithoutImage)),
}
impl ProjectKind {
pub fn new() -> Vec<Self> {
vec![
ProjectKind::WithoutImage((
ProjectWithoutImage {
name: "ezidam",
tagline: "Identity and Access Management system",
dates: "Since January 2023",
description: vec![
"A simple identity and access management system for SMEs or personal use.",
"Low maintenance required, easy to deploy and to backup.",
],
accomplishments: vec![
"Users management",
"Roles management",
"Assign users to roles and the other way around",
"OAuth2 / OIDC applications (code flow)",
"Multi-Factor Authentication (TOTP)",
"Password reset (via email or backup token)",
"Simple administration panel",
"Good security measures for users and administrators",
],
technologies: vec![
"Rust",
"SQLite",
"OAuth2 / OIDC",
"TOTP",
"SMTP",
"Docker",
],
logo: Some(Logo {
file: "/icons/ezidam.png",
transparent_background: true,
}),
..Default::default()
},
ProjectWithoutImage {
name: "pass4thewin",
tagline: "Password manager",
dates: "November 2020 - January 2021",
description: vec![
"Port of passwordstore, the standard unix password manager on the Windows platform.",
"Warning! Unfinished command line application, may cause data corruption when using existing passwords.",
],
accomplishments: vec![
"Creation of a store",
"List secrets",
"Decrypt secret",
"Insert or generate secrets",
"Edit existing secrets",
"Synchronisation with git",
"TOTP support",
],
technologies: vec![
"Windows",
"Rust",
"OpenPGP",
"libgit2",
],
link: Some(ProjectLink::Available(Link {
uri: uri!("https://github.com/x4m3/pass4thewin"),
label: "Source code",
})),
..Default::default()
},
)),
ProjectKind::WithImage(
ProjectWithImage{
name: "NaviaRent",
tagline: "Epitech Innovative Project",
dates: "September 2020 - January 2023",
description: vec!["A B2B platform helping rentals of standup paddle boards."],
accomplishments: vec![
"DevOps of all software in the NaviaRent stack",
"Creation of the iOS application",
"Contributions to the Android application",
"Contributions to the backend server",
"Creation and contributions to the web client",
"Server administration, backups",
],
technologies: vec![
"NodeJS",
"Angular",
"Kotlin",
"SwiftUI",
"Docker",
"GitLab CI/CD",
"Raspberry Pi",
"ESP32",
],
logo: Some(Logo {
file: "/icons/naviarent.png",
transparent_background: false,
}),
image: Image {
file: "/images/naviarent.jpg",
position: Position::Right,
},
link: Some(ProjectLink::NotAvailable),
},
),
ProjectKind::WithoutImage((
ProjectWithoutImage{
name: "epitok",
tagline: "Presence system at Epitech",
dates: "June 2020 - September 2020",
description: vec![
"A library and web client to simplify students presence at Epitech.",
"Students are handed a piece of paper with a 6 digits number (called a \"token\") to verify their presence at school events.",
"Teachers use epitok to scan student cards with QR codes on them instead of printing and handing tokens to students.",
],
accomplishments: vec![
"Reverse engineering of a partially documented web API",
"Design, conception",
"User experience",
"Improvements based of usage of the application",
],
technologies: vec![
"Rust",
"HTML",
"Bootstrap",
"jQuery",
"Docker",
],
link: Some(ProjectLink::Available(Link {
uri: uri!("https://github.com/x4m3/epitok"),
label: "Source code",
})),
..Default::default()
},
ProjectWithoutImage{
name: "epi.today",
tagline: "Calendar for Epitech",
dates: "December 2019 - February 2020",
description: vec![
"A viewer of the Epitech intranet calendar.",
"Students and teachers glance at their planning without the need to go on the school's intranet.",
],
accomplishments: vec![],
technologies: vec![
"TypeScript",
"HTML",
"Bootstrap",
"Docker",
],
link: Some(ProjectLink::Available(Link {
uri: uri!("https://github.com/x4m3/epi.today"),
label: "Source code",
})),
..Default::default()
},
)),
ProjectKind::WithImage(
ProjectWithImage{
name: "canvas.place",
tagline: "Timelapse",
dates: "April 2017 - January 2020",
description: vec![
"canvas.place is a shared place to express creativity.",
"People from all over the world share one single canvas to paint on.",
"I created and maintained a timelapse of the virtual canvas.".into()
],
accomplishments: vec![],
technologies: vec!["FFmpeg", "Shell scripting", "nginx".into()],
logo: Some(Logo {
file: "/icons/canvas.png",
transparent_background: false,
}),
image: Image {
file: "/images/canvas.png",
position: Position::Left,
},
link: Some(ProjectLink::Available(Link {
uri: uri!("https://timelapse.canvas.place"),
label: "Website",
})),
},
)
]
}
}

70
src/types/talk.rs Normal file
View file

@ -0,0 +1,70 @@
use crate::types::Link;
use rocket::uri;
pub struct Talk {
pub title: &'static str,
pub date: &'static str,
pub location: &'static str,
pub link: Link,
}
impl Talk {
pub fn new() -> Vec<Self> {
vec![
Talk {
title: "Vim",
date: "February 2023",
location: "Epitech Rennes",
link: Link {
uri: uri!("https://philippeloctaux.com/pub/talks/vim.pdf"),
label: "Slides",
},
},
Talk {
title: "CLion",
date: "March 2021",
location: "Epitech Rennes",
link: Link {
uri: uri!("https://philippeloctaux.com/pub/talks/clion.pdf"),
label: "Slides",
},
},
Talk {
title: "git & devops 2",
date: "February 2021",
location: "Epitech Rennes",
link: Link {
uri: uri!("https://philippeloctaux.com/pub/talks/git-devops2.pdf"),
label: "Slides",
},
},
Talk {
title: "pass4thewin",
date: "February 2021",
location: "Epitech Rennes",
link: Link {
uri: uri!("https://philippeloctaux.com/pub/talks/pass4thewin.pdf"),
label: "Slides",
},
},
Talk {
title: "git & devops",
date: "May 2020",
location: "Epitech Rennes",
link: Link {
uri: uri!("https://philippeloctaux.com/pub/talks/git-devops.pdf"),
label: "Slides",
},
},
Talk {
title: "git gud",
date: "May 2019",
location: "Epitech Rennes",
link: Link {
uri: uri!("https://philippeloctaux.com/pub/talks/git-tek.pdf"),
label: "Slides",
},
},
]
}
}

View file

@ -1,6 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
content: {
files: ["./templates/**/*.html"],
},
theme: {
extend: {
height: {

26
templates/base.html Normal file
View file

@ -0,0 +1,26 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width"/>
<title>{{ title|title }}</title>
<link rel="stylesheet" href="/style.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#0c4a6e">
<meta name="msapplication-TileColor" content="#0c4a6e">
<meta name="theme-color" content="#0c4a6e">
<script defer data-domain="philippeloctaux.com" src="https://plausible.y.z.x4m3.rocks/js/script.js"></script>
</head>
<body class="flex flex-col min-h-screen bg-gray-900 text-white">
<div class="flex-grow">
{% block container %}{% endblock %}
</div>
<footer class="bg-black">
<div class="container mx-auto px-4 py-8">
<p>&copy; 2015 - {{ year }} Philippe Loctaux</p>
</div>
</footer>
</body>
</html>

10
templates/content.html Normal file
View file

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block container %}
<main class="container mx-auto px-4 py-16">
<h1 class="text-3xl sm:text-4xl font-bold">{{ title }}</h1>
<div class="mt-8">
{% block content %}{% endblock %}
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,11 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z"
clip-rule="evenodd"></path>
</svg>

After

Width:  |  Height:  |  Size: 547 B

View file

@ -0,0 +1,14 @@
<svg
class="w-6 h-6 mr-0 sm:mr-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75"
></path>
</svg>

After

Width:  |  Height:  |  Size: 556 B

View file

@ -0,0 +1,9 @@
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
></path>
</svg>

After

Width:  |  Height:  |  Size: 887 B

13
templates/icons/link.html Normal file
View file

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 fill-current mr-2"
>
<path
d="M12.232 4.232a2.5 2.5 0 013.536 3.536l-1.225 1.224a.75.75 0 001.061 1.06l1.224-1.224a4 4 0 00-5.656-5.656l-3 3a4 4 0 00.225 5.865.75.75 0 00.977-1.138 2.5 2.5 0 01-.142-3.667l3-3z"
></path>
<path
d="M11.603 7.963a.75.75 0 00-.977 1.138 2.5 2.5 0 01.142 3.667l-3 3a2.5 2.5 0 01-3.536-3.536l1.225-1.224a.75.75 0 00-1.061-1.06l-1.224 1.224a4 4 0 105.656 5.656l3-3a4 4 0 00-.225-5.865z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 596 B

View file

@ -0,0 +1,9 @@
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 673 B

View file

@ -0,0 +1,11 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M9.69 18.933l.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 00.281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 103 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 002.273 1.765 11.842 11.842 0 00.976.544l.062.029.018.008.006.003zM10 11.25a2.25 2.25 0 100-4.5 2.25 2.25 0 000 4.5z"
clip-rule="evenodd"></path>
</svg>

After

Width:  |  Height:  |  Size: 613 B

View file

@ -0,0 +1,9 @@
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,9 @@
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 805 B

View file

@ -0,0 +1,9 @@
<svg
class="w-6 h-6 fill-current mr-0 sm:mr-2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"
></path>
</svg>

After

Width:  |  Height:  |  Size: 666 B

5
templates/pages/404.html Normal file
View file

@ -0,0 +1,5 @@
{% extends "content.html" %}
{% block content %}
<p>This page could not be found.</p>
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends "content.html" %}
{% block content %}
<p>Send an email if you want to work with me, propose a project idea, or just to say hi!</p>
<div class="my-4">
<a
href="mailto:helloATphilippeloctauxDOTcom"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
<div class="inline-flex items-center">
{% include "icons/email.html" %}
<span class="ml-2 sm:ml-0 text-center">hello at philippeloctaux dot com</span>
</div>
</a>
</div>
<p class="mb-2">
If you want to encrypt your message, I have a
<a href="/pub/pgp-0x69771CD04BA82EC0.txt" class="underline">pgp key</a> at your disposal.
</p>
<p class="mb-2">
I also have a <a href="/keybase.txt" class="underline">Keybase</a> account, but I do not check it often.
</p>
{% endblock %}

15
templates/pages/root.html Normal file
View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block container %}
{% include "pages/root/hero-image.html" %}
<main class="container mx-auto px-4 md:px-8 lg:px-16 py-16">
{% include "pages/root/whoami.html" %}
<div class="my-16 space-y-16 md:space-y-32">
{% include "pages/root/www.html" %}
{% include "pages/root/jobs.html" %}
{% include "pages/root/projects.html" %}
{% include "pages/root/talks.html" %}
{% include "pages/root/friends.html" %}
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,24 @@
<div>
<h1 class="text-4xl font-bold mb-4">Friends</h1>
<p class="text-lg">Folks I worked with, or I like what they do.</p>
<ul class="mt-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 sm:gap-4">
{% for friend in friends %}
<li class="py-2">
<a
href={{friend.uri}}
target="_blank"
class="hover:bg-gray-500 transition-all duration-200 flex items-center rounded-lg p-2"
>
<span class="rounded-full flex-shrink-0 mr-4 w-10 h-10 bg-sky-900 text-white flex items-center justify-center text-lg font-medium"
>{{ friend.initials() }}</span
>
<div>
<p class="font-bold">{{ friend.first_name }} {{ friend.last_name }}</p>
<p>{{ friend.domain_name() }}</p>
</div>
</a>
</li>
{% endfor %}
</ul>
</div>

View file

@ -0,0 +1,8 @@
<div class="container mx-auto px-8 py-16 w-full h-full justify-center items-center flex flex-col">
<div class="inline-block backdrop-blur-lg backdrop-brightness-75 rounded-3xl shadow-2xl px-4 py-6 sm:px-8 sm:py-12 space-y-4">
<h1 class="text-3xl sm:text-4xl font-bold">Philippe Loctaux</h1>
<h2 class="sm:text-xl font-semibold">
Developer of all sorts. Epitech alumni, class of 2023.
</h2>
</div>
</div>

View file

@ -0,0 +1,70 @@
<style>
/* Fixed background when scrolling in tailwind
* Disabled on iOS devices
* https://tailwindcss.com/docs/background-attachment
* https://stackoverflow.com/a/60220757/4809297
*/
@supports (-webkit-touch-callout: none) {
/* CSS specific to iOS devices */
#wallpaper {
background-attachment: scroll;
}
}
@supports not (-webkit-touch-callout: none) {
/* CSS for other than iOS devices */
#wallpaper {
background-attachment: fixed;
}
}
</style>
<div class="bg-gradient-to-r from-red-900 via-teal-900 to-fuchsia-900">
{% let wallpaper_uri -%}
{% match wallpaper %}
{% when Some with (wallpaper) %}
{% let wallpaper_uri = wallpaper.file -%}
{% when None %}
{% let wallpaper_uri = "" -%}
{% endmatch %}
<div
id="wallpaper"
class="relative text-white w-full h-almostscreen bg-center bg-cover"
style="background-image: url({{ wallpaper_uri }});"
>
<!-- Content inside -->
{% include "pages/root/hero-content.html" %}
{% match wallpaper %}
{% when Some with (wallpaper) %}
<!-- Exif info -->
<div
class="absolute bottom-3 sm:bottom-5 left-2 sm:left-5 inline-block backdrop-blur-lg backdrop-brightness-75 rounded-xl shadow-2xl p-2 space-y-0.5 sm:space-y-2"
>
<!-- Location -->
<div class="flex">
<div class="inline-flex items-center">
{% include "icons/location.html" %}
<span class="ml-1 text-sm"
>{{ wallpaper.location.precise }}<span class="hidden md:inline"
>, {{ wallpaper.location.broad }}</span
></span
>
</div>
</div>
<!-- Date -->
<div class="flex">
<div class="inline-flex items-center">
{% include "icons/calendar.html" %}
<span class="ml-1 text-sm">{{ wallpaper.date }}</span>
</div>
</div>
</div>
{% when None %}
{% endmatch %}
</div>
</div>

View file

@ -0,0 +1,64 @@
<div>
<h1 class="text-4xl font-bold mb-4">Professional Experiences</h1>
<div class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
{% for job in jobs %}
<div class="w-full rounded-2xl bg-sky-950">
<div class="p-6 justify-between h-full">
<!-- Header -->
<div class="flex flex-col">
<div class="flex flex-row">
{% let css -%}
{# If css updates make sure to update in both places! #}
{% if job.logo.transparent_background == true -%}
{% let css = "h-16 w-16 rounded-xl mr-4 p-2 bg-white" -%}
{% else -%}
{% let css = "h-16 w-16 rounded-xl mr-4" -%}
{% endif -%}
<img loading="lazy" src="{{ job.logo.file }}" alt="{{ job.company }} logo" class="{{ css }}">
<div class="flex flex-col justify-evenly">
<div class="text-xl md:text-2xl font-semibold">{{ job.company }}</div>
<div class="text-xs md:text-sm">{{ job.dates }}</div>
</div>
</div>
<p class="text-xl md:text-2xl font-semibold my-4">{{ job.title }}</p>
</div>
<!-- Content -->
<div class="space-y-2">
<!-- Description -->
{% for desc in job.description %}
<p>{{ desc }}</p>
{% endfor %}
<!-- Accomplishments -->
<div>
<ul class="list-disc mt-6">
{% for accomplishment in job.accomplishments %}
<li class="ml-5">{{ accomplishment }}</li>
{% endfor %}
</ul>
</div>
<!-- Technologies -->
<div>
<div class="text-xl font-semibold mt-8 mb-2">
Technologies
</div>
<ul>
{% for technology in job.technologies %}
<li>{{ technology }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>

View file

@ -0,0 +1,108 @@
{% let css_image_position -%}
{% let css_image_position_corner -%}
{# If css updates make sure to update in both places! #}
{% match project.image.position %}
{% when Position::Left %}
{% let css_image_position = "2xl:flex items-stretch justify-between" -%}
{% let css_image_position_corner = "flex w-full 2xl:w-1/2 grow rounded-t-2xl object-cover 2xl:rounded-tr-none 2xl:rounded-l-2xl" -%}
{% when Position::Right %}
{% let css_image_position = "2xl:flex items-stretch justify-between 2xl:flex-row-reverse" -%}
{% let css_image_position_corner = "flex w-full 2xl:w-1/2 grow rounded-t-2xl object-cover 2xl:rounded-tl-none 2xl:rounded-r-2xl" -%}
{% endmatch %}
<div class="w-full rounded-2xl bg-pink-950 {{ css_image_position }}">
<!-- Image on the side -->
<img loading="lazy" src="{{ project.image.file }}" alt="{{ project.name }} image" class="{{ css_image_position_corner }}">
<div class="p-6 flex flex-col grow-0 justify-between h-full">
<!-- Header -->
<div class="flex flex-col">
<div class="flex flex-row">
{% match project.logo %}
{% when Some with (logo) %}
{% let css_logo -%}
{# If css updates make sure to update in both places! #}
{% if logo.transparent_background == true -%}
{% let css_logo = "h-16 w-16 rounded-xl mr-4 p-2 bg-white" -%}
{% else -%}
{% let css_logo = "h-16 w-16 rounded-xl mr-4" -%}
{% endif -%}
<img loading="lazy" src="{{ logo.file }}" alt="{{ project.name }} logo" class="{{ css_logo }}">
{% when None %}
{% endmatch %}
<div class="flex flex-col justify-evenly">
<div class="text-xl md:text-2xl font-semibold">{{ project.name }}</div>
<div class="text-xs md:text-sm">{{ project.dates }}</div>
</div>
</div>
<p class="text-xl md:text-2xl font-semibold my-4">{{ project.tagline }}</p>
</div>
<!-- Content -->
<div class="space-y-2">
<!-- Description -->
{% for desc in project.description %}
<p>{{ desc }}</p>
{% endfor %}
<!-- Accomplishments -->
<div>
<ul class="list-disc mt-6">
{% for accomplishment in project.accomplishments %}
<li class="ml-5">{{ accomplishment }}</li>
{% endfor %}
</ul>
</div>
<!-- Technologies -->
<div>
<div class="text-xl font-semibold mt-8 mb-2">
Technologies
</div>
<ul>
{% for technology in project.technologies %}
<li>{{ technology }}</li>
{% endfor %}
</ul>
</div>
</div>
<!-- Link with label -->
{% match project.link %}
{% when Some with (link) %}
{% match link %}
{% when ProjectLink::Available(project_link) %}
<a
href="{{ project_link.uri }}"
target="_blank"
class="mt-4 inline-flex max-w-fit bg-transparent hover:bg-sky-700 text-white font-semibold py-1.5 px-4 rounded-xl items-center border border-white hover:border-transparent transition-all duration-200"
>
<div class="inline-flex items-center">
{% include "icons/link.html" %}
<span>{{ project_link.label }}</span>
</div>
</a>
<!-- Link is not available -->
{% when ProjectLink::NotAvailable %}
<span class="mt-4 cursor-not-allowed inline-flex max-w-fit bg-gray-400 text-gray-600 font-semibold py-1.5 px-4 rounded-xl items-center">
No longer available
</span>
{% endmatch %}
<!-- No link -->
{% when None %}
{% endmatch %}
</div>
</div>

View file

@ -0,0 +1,91 @@
<div class="w-full rounded-2xl bg-pink-950">
<div class="p-6 justify-between h-full">
<!-- Header -->
<div class="flex flex-col">
<div class="flex flex-row">
{% match project.logo %}
{% when Some with (logo) %}
{% let css_logo -%}
{# If css updates make sure to update in both places! #}
{% if logo.transparent_background == true -%}
{% let css_logo = "h-16 w-16 rounded-xl mr-4 p-2 bg-white" -%}
{% else -%}
{% let css_logo = "h-16 w-16 rounded-xl mr-4" -%}
{% endif -%}
<img loading="lazy" src="{{ logo.file }}" alt="{{ project.name }} logo" class="{{ css_logo }}">
{% when None %}
{% endmatch %}
<div class="flex flex-col justify-evenly">
<div class="text-xl md:text-2xl font-semibold">{{ project.name }}</div>
<div class="text-xs md:text-sm">{{ project.dates }}</div>
</div>
</div>
<p class="text-xl md:text-2xl font-semibold my-4">{{ project.tagline }}</p>
</div>
<!-- Content -->
<div class="space-y-2">
<!-- Description -->
{% for desc in project.description %}
<p>{{ desc }}</p>
{% endfor %}
<!-- Accomplishments -->
<div>
<ul class="list-disc mt-6">
{% for accomplishment in project.accomplishments %}
<li class="ml-5">{{ accomplishment }}</li>
{% endfor %}
</ul>
</div>
<!-- Technologies -->
<div>
<div class="text-xl font-semibold mt-8 mb-2">
Technologies
</div>
<ul>
{% for technology in project.technologies %}
<li>{{ technology }}</li>
{% endfor %}
</ul>
</div>
</div>
<!-- Link with label -->
{% match project.link %}
{% when Some with (link) %}
{% match link %}
{% when ProjectLink::Available(project_link) %}
<a
href="{{ project_link.uri }}"
target="_blank"
class="mt-4 inline-flex max-w-fit bg-transparent hover:bg-sky-700 text-white font-semibold py-1.5 px-4 rounded-xl items-center border border-white hover:border-transparent transition-all duration-200"
>
<div class="inline-flex items-center">
{% include "icons/link.html" %}
<span>{{ project_link.label }}</span>
</div>
</a>
<!-- Link is not available -->
{% when ProjectLink::NotAvailable %}
<span class="mt-4 cursor-not-allowed inline-flex max-w-fit bg-gray-400 text-gray-600 font-semibold py-1.5 px-4 rounded-xl items-center">
No longer available
</span>
{% endmatch %}
<!-- No link -->
{% when None %}
{% endmatch %}
</div>
</div>

View file

@ -0,0 +1,23 @@
<div>
<h1 class="text-4xl font-bold mb-4">Projects</h1>
<div class="mt-4 space-y-4">
{% for project in projects %}
{% match project %}
{% when ProjectKind::WithoutImage((project1, project2)) %}
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
{% let project = project1 -%}
{% include "pages/root/project-without-image.html" %}
{% let project = project2 -%}
{% include "pages/root/project-without-image.html" %}
</div>
{% when ProjectKind::WithImage(project) %}
{% include "pages/root/project-with-image.html" %}
{% endmatch %}
{% endfor %}
</div>
</div>

View file

@ -0,0 +1,46 @@
<div>
<h1 class="text-4xl font-bold mb-4">Talks</h1>
<p class="text-lg">
Giving a talk is the opportunity to share what I know, and helps me
reduce my fear of public speaking.
</p>
<div class="mt-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 place-content-center">
{% for talk in talks %}
<div class="rounded-2xl w-full bg-teal-950 p-6">
<!-- Title -->
<h3 class="text-xl font-semibold mb-4">{{talk.title}}</h3>
<!-- Date -->
<div class="flex">
<div class="inline-flex items-center">
{% include "icons/calendar.html" %}
<span class="ml-2">{{talk.date}}</span>
</div>
</div>
<!-- Location -->
<div class="flex">
<div class="inline-flex items-center">
{% include "icons/location.html" %}
<span class="ml-2">{{talk.location}}</span>
</div>
</div>
<!-- Link with label -->
<a
href={{talk.link.uri}}
target="_blank"
class="mt-4 inline-flex bg-transparent hover:bg-sky-700 text-white font-semibold py-1.5 px-4 rounded-xl items-center border border-white hover:border-transparent transition-all duration-200"
>
<div class="inline-flex items-center">
{% include "icons/link.html" %}
<span>{{talk.link.label}}</span>
</div>
</a>
</div>
{% endfor %}
</div>
</div>

View file

@ -1,9 +1,9 @@
<div class="md:flex md:flex-row-reverse items-center">
<div class="md:w-1/2 mb-4 md:mb-0">
<img
src="/phil.png"
alt="Phil"
class="rounded-3xl bg-sky-900 h-36 w-36 md:mx-auto md:h-56 md:w-56 lg:h-64 lg:w-64 mb-2 md:mb-0"
src="/pub/phil.png"
alt="Phil"
class="rounded-3xl bg-sky-900 h-36 w-36 md:mx-auto md:h-56 md:w-56 lg:h-64 lg:w-64 mb-2 md:mb-0"
/>
</div>
@ -13,12 +13,11 @@
<div class="text-lg space-y-6">
<p>
I got into computer science by creating websites, learning about
the Linux kernel and administrating servers.
I got into computer science by learning about the Linux kernel and administrating servers.
</p>
<p>
After high school, I became a student at Epitech and learned to
tackle technical concepts and applying them quickly by working
tackle technical concepts and apply them quickly by working
on small projects.
</p>
<p>

View file

@ -0,0 +1,39 @@
<div class="grid grid-cols-3 lg:grid-cols-6 gap-4 place-content-center">
{% for network in networks %}
<div class="w-full h-auto md:w-auto">
<div class="text-center">
<a
target="_blank"
href="{{ network.uri }}"
class="inline-flex bg-sky-900 hover:bg-sky-700 transition-all duration-200 text-white font-bold py-2 px-4 rounded-xl items-center"
>
{% match network.icon %}
{% when Icon::Email %}
{% include "icons/email.html" %}
{% when Icon::Github %}
{% include "icons/github.html" %}
{% when Icon::Linkedin %}
{% include "icons/linkedin.html" %}
{% when Icon::Mastodon %}
{% include "icons/mastodon.html" %}
{% when Icon::Telegram %}
{% include "icons/telegram.html" %}
{% when Icon::Twitter %}
{% include "icons/twitter.html" %}
{% endmatch %}
<div class="inline-flex items-center">
<span class="hidden sm:inline">{{ network.name }}</span>
</div>
</a>
</div>
</div>
{% endfor %}
</div>

View file

@ -1,3 +0,0 @@
{
"extends": "astro/tsconfigs/strict"
}

View file

@ -1,80 +0,0 @@
import exifr from "npm:exifr@^7.1.3";
import { mime } from "https://deno.land/x/mimetypes@v1.0.0/mod.ts";
import { parse } from "https://deno.land/std@0.190.0/flags/mod.ts";
interface Arguments {
sourceDir?: string;
destinationDir?: string;
}
const cli: Arguments = parse(Deno.args, {
string: ["sourceDir", "destinationDir"],
});
if (cli.sourceDir === undefined) {
console.error("Need source folder for images (relative directory where deno script is ran)");
Deno.exit(1);
}
if (cli.destinationDir === undefined) {
console.error("Need destination folder for images");
Deno.exit(1);
}
// Put all wallpapers here
const imagesPath = `${Deno.cwd()}/${cli.sourceDir}`;
interface MyWallpaper {
file: string;
location: string;
date: string;
}
const wallpapers: MyWallpaper[] = [];
// For each file in the wallpapers directory
for await (const dirEntry of Deno.readDir(imagesPath)) {
// Make sure it is a file and it is an image
if (!dirEntry.isFile || !mime.getType(dirEntry.name)?.startsWith("image/")) {
continue;
}
// Don't pollute stdout, that's where the json will be put
console.error(`Processing ${dirEntry.name}`);
// Open file
const path = `${imagesPath}/${dirEntry.name}`;
const bytes = await Deno.readFile(path);
// Parse exif
const exif = await exifr.parse(bytes);
// Timezones are too hard
const timestamp = Date.parse(exif.DateTimeOriginal);
const dateObject = new Date(timestamp);
const date = dateObject.toISOString().split("T")[0];
// Http request to get location
// Documentation: https://nominatim.org/release-docs/develop/api/Reverse/
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${exif.latitude}&lon=${exif.longitude}&zoom=8`,
{ headers: { "User-Agent": "https://philippeloctaux.com" } }
);
const data = await response.json();
const location = data?.display_name;
// Final filename
const file = `${cli.destinationDir}/${dirEntry.name}`;
wallpapers.push({
file,
date,
location,
});
}
// Put final array to stdout
console.log(JSON.stringify(wallpapers));