using rocket instead of astro

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

54
src/cache.rs Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,40 +0,0 @@
<div class="md:flex md:flex-row-reverse items-center">
<div class="md:w-1/2 mb-4 md:mb-0">
<img
src="/phil.png"
alt="Phil"
class="rounded-3xl bg-sky-900 h-36 w-36 md:mx-auto md:h-56 md:w-56 lg:h-64 lg:w-64 mb-2 md:mb-0"
/>
</div>
<div class="md:w-1/2">
<h1 class="text-4xl font-bold mb-4">About Phil</h1>
<h2 class="text-2xl font-semibold mb-4">Developer of all sorts</h2>
<div class="text-lg space-y-6">
<p>
I got into computer science by creating websites, learning about
the Linux kernel and administrating servers.
</p>
<p>
After high school, I became a student at Epitech and learned to
tackle technical concepts and applying them quickly by working
on small projects.
</p>
<p>
During my studies at Epitech, I had the opportunity to be a
teacher. My role was to assist students with technical problems
in their projects.
</p>
<p>
Now I have experience in software engineering, full-stack web
and mobile development, system administration and CI/CD, as well
as embedded development.
</p>
<p>
My goal is to use my knowledge and experience to make software
helping its users accomplish their needs.
</p>
</div>
</div>
</div>

View file

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

1
src/env.d.ts vendored
View file

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

11
src/filters.rs Normal file
View file

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

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

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

View file

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

61
src/main.rs Normal file
View file

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

39
src/minify.rs Normal file
View file

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

View file

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

View file

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

View file

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

45
src/templates.rs Normal file
View file

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

50
src/types.rs Normal file
View file

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

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

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

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

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

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

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

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

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

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

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