using rocket instead of astro
|
|
@ -1,12 +1,11 @@
|
||||||
/public/wallpapers.json
|
/public/style.css
|
||||||
|
|
||||||
/dist/
|
/target/
|
||||||
/node_modules/
|
|
||||||
|
|
||||||
/.idea/
|
/.idea/
|
||||||
/.vscode/
|
/.vscode/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.gitea/
|
||||||
/readme.md
|
/readme.md
|
||||||
/Dockerfile
|
/Dockerfile
|
||||||
/.dockerignore
|
/.dockerignore
|
||||||
|
|
@ -18,7 +18,7 @@ jobs:
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: git.philt3r
|
registry: git.int.philt3r.eu
|
||||||
username: ${{ gitea.repository_owner }}
|
username: ${{ gitea.repository_owner }}
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
- name: Login to Container Registry x4m3rocks
|
- name: Login to Container Registry x4m3rocks
|
||||||
|
|
@ -33,5 +33,5 @@ jobs:
|
||||||
ACTIONS_RUNTIME_TOKEN: ''
|
ACTIONS_RUNTIME_TOKEN: ''
|
||||||
with:
|
with:
|
||||||
push: true
|
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
|
# wallpapers
|
||||||
/public/wallpapers.json
|
/src/wallpapers.rs
|
||||||
|
|
||||||
|
# built css
|
||||||
|
/public/style.css
|
||||||
|
|
||||||
# build output
|
# build output
|
||||||
dist/
|
/target
|
||||||
# generated types
|
|
||||||
.astro/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.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 rust:${RUST_VERSION}-slim-bookworm as builder
|
||||||
FROM denoland/deno:alpine-${DENO_VERSION} as wallpapers
|
|
||||||
|
|
||||||
WORKDIR /wallpapers
|
# tailwind
|
||||||
COPY utils/wallpapers.ts .
|
WORKDIR /usr/bin/
|
||||||
COPY public/wallpapers ./wallpapers/
|
ADD https://github.com/tailwindlabs/tailwindcss/releases/download/v3.3.5/tailwindcss-linux-x64 tailwindcss
|
||||||
RUN deno run --allow-read --allow-net wallpapers.ts --sourceDir ./wallpapers/ --destinationDir /wallpapers > wallpapers.json
|
RUN chmod +x /usr/bin/tailwindcss
|
||||||
|
|
||||||
# build astro
|
# openssl + CA certs
|
||||||
FROM node:18.16-alpine as astro
|
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 ./
|
# generate wallpapers
|
||||||
RUN npm ci
|
RUN cargo build --bin gen-wallpapers --release
|
||||||
|
RUN cargo run --bin gen-wallpapers --release
|
||||||
|
|
||||||
COPY . .
|
# build project
|
||||||
COPY --from=wallpapers /wallpapers/wallpapers.json ./public/
|
RUN cargo build --bin plcom --release
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# web server
|
FROM debian:12-slim
|
||||||
FROM denoland/deno:alpine-${DENO_VERSION}
|
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
|
EXPOSE 8000
|
||||||
|
ENTRYPOINT ["/usr/share/plcom/plcom"]
|
||||||
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"]
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
## tech
|
||||||
|
|
||||||
- https://astro.build
|
- https://rocket.rs
|
||||||
- https://tailwindcss.com
|
- https://tailwindcss.com
|
||||||
|
|
||||||
## colors
|
## colors
|
||||||
|
|
@ -16,7 +16,7 @@ http://philippeloctaux.com
|
||||||
## wallpapers
|
## wallpapers
|
||||||
|
|
||||||
1. place **JPEG** files in `public/wallpapers` and make sure they have exif data (GPS + date)
|
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
|
## 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} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
content: {
|
||||||
|
files: ["./templates/**/*.html"],
|
||||||
|
},
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
height: {
|
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:flex md:flex-row-reverse items-center">
|
||||||
<div class="md:w-1/2 mb-4 md:mb-0">
|
<div class="md:w-1/2 mb-4 md:mb-0">
|
||||||
<img
|
<img
|
||||||
src="/phil.png"
|
src="/pub/phil.png"
|
||||||
alt="Phil"
|
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"
|
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">
|
<div class="text-lg space-y-6">
|
||||||
<p>
|
<p>
|
||||||
I got into computer science by creating websites, learning about
|
I got into computer science by learning about the Linux kernel and administrating servers.
|
||||||
the Linux kernel and administrating servers.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
After high school, I became a student at Epitech and learned to
|
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.
|
on small projects.
|
||||||
</p>
|
</p>
|
||||||
<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));
|
|
||||||