using rocket instead of astro
|
|
@ -1,12 +1,11 @@
|
|||
/public/wallpapers.json
|
||||
/public/style.css
|
||||
|
||||
/dist/
|
||||
/node_modules/
|
||||
/target/
|
||||
|
||||
/.idea/
|
||||
/.vscode/
|
||||
.DS_Store
|
||||
|
||||
.gitea/
|
||||
/readme.md
|
||||
/Dockerfile
|
||||
/.dockerignore
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
7
.vscode/settings.json
vendored
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"deno.enable": true,
|
||||
"deno.unstable": true,
|
||||
"deno.enablePaths":[
|
||||
"./utils/"
|
||||
]
|
||||
}
|
||||
2627
Cargo.lock
generated
Normal file
29
Cargo.toml
Normal 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"
|
||||
52
Dockerfile
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
7270
package-lock.json
generated
20
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 477 KiB After Width: | Height: | Size: 225 KiB |
BIN
public/phil.png
|
Before Width: | Height: | Size: 2 MiB |
BIN
public/pub/phil.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
public/pub/philt3r.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
public/pub/talks/clion.pdf
Normal file
BIN
public/pub/talks/git-devops.pdf
Normal file
BIN
public/pub/talks/git-devops2.pdf
Normal file
BIN
public/pub/talks/git-tek.pdf
Normal file
BIN
public/pub/talks/pass4thewin.pdf
Normal file
BIN
public/pub/talks/vim.pdf
Normal 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
|
|
@ -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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="bg-black">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<p>© 2015 - {year} Philippe Loctaux</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="astro/client" />
|
||||
11
src/filters.rs
Normal 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
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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>© 2015 - {{ year }} Philippe Loctaux</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
10
templates/content.html
Normal 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 %}
|
||||
11
templates/icons/calendar.html
Normal 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 |
14
templates/icons/email.html
Normal 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 |
9
templates/icons/github.html
Normal 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
|
|
@ -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 |
9
templates/icons/linkedin.html
Normal 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 |
11
templates/icons/location.html
Normal 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 |
9
templates/icons/mastodon.html
Normal 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 |
9
templates/icons/telegram.html
Normal 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 |
9
templates/icons/twitter.html
Normal 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
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "content.html" %}
|
||||
|
||||
{% block content %}
|
||||
<p>This page could not be found.</p>
|
||||
{% endblock %}
|
||||
25
templates/pages/email.html
Normal 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
|
|
@ -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 %}
|
||||
24
templates/pages/root/friends.html
Normal 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>
|
||||
8
templates/pages/root/hero-content.html
Normal 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>
|
||||
70
templates/pages/root/hero-image.html
Normal 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>
|
||||
64
templates/pages/root/jobs.html
Normal 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>
|
||||
108
templates/pages/root/project-with-image.html
Normal 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>
|
||||
91
templates/pages/root/project-without-image.html
Normal 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>
|
||||
23
templates/pages/root/projects.html
Normal 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>
|
||||
46
templates/pages/root/talks.html
Normal 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>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<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"
|
||||
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"
|
||||
/>
|
||||
|
|
@ -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>
|
||||
39
templates/pages/root/www.html
Normal 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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
|
|
@ -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));
|
||||