rewrote website with leptos
13
.editorconfig
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
[*.js]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
14
.gitignore
vendored
|
|
@ -1,11 +1,12 @@
|
||||||
# wallpapers
|
|
||||||
/src/wallpapers.rs
|
|
||||||
|
|
||||||
# built css
|
# built css
|
||||||
/public/style.css
|
/public/style.css
|
||||||
|
|
||||||
|
# wallpapers
|
||||||
|
wallpapers.json
|
||||||
|
|
||||||
# build output
|
# build output
|
||||||
/target
|
target/
|
||||||
|
pkg
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
|
|
@ -15,4 +16,7 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# ide
|
# ide
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
|
||||||
1893
Cargo.lock
generated
33
Cargo.toml
|
|
@ -1,26 +1,15 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/gen-wallpapers"]
|
resolver = "2"
|
||||||
|
members = ["crates/gen-wallpapers", "crates/plcom"]
|
||||||
|
|
||||||
[package]
|
[workspace.dependencies]
|
||||||
name = "plcom"
|
serde = "1.0"
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
default-run = "plcom"
|
|
||||||
|
|
||||||
[[bin]]
|
# Defines a size-optimized profile for the WASM bundle in release mode
|
||||||
name = "plcom"
|
[profile.wasm-release]
|
||||||
path = "src/main.rs"
|
inherits = "release"
|
||||||
|
opt-level = 'z'
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
[[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"
|
|
||||||
nanorand = { version = "0.7.0", features = ["chacha"] }
|
|
||||||
|
|
|
||||||
21
build.rs
|
|
@ -1,21 +0,0 @@
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,7 @@ fn main() {
|
||||||
|
|
||||||
let metadata = MetadataList::process_folder(dir, true);
|
let metadata = MetadataList::process_folder(dir, true);
|
||||||
|
|
||||||
let json = match metadata.to_json() {
|
let json = match metadata.to_pretty_json() {
|
||||||
Ok(json) => json,
|
Ok(json) => json,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("failed to serialize json: {e}");
|
eprintln!("failed to serialize json: {e}");
|
||||||
|
|
|
||||||
|
|
@ -165,8 +165,8 @@ impl MetadataList {
|
||||||
Self(files)
|
Self(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json(&self) -> serde_json::Result<String> {
|
pub fn to_pretty_json(&self) -> serde_json::Result<String> {
|
||||||
serde_json::to_string(&self.0)
|
serde_json::to_string_pretty(&self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1
crates/plcom/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
css/tailwind-output.css
|
||||||
118
crates/plcom/Cargo.toml
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
[package]
|
||||||
|
name = "plcom"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# leptos + axum
|
||||||
|
axum = { version = "0.7", optional = true }
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
|
leptos = { version = "0.6" }
|
||||||
|
leptos_axum = { version = "0.6", optional = true }
|
||||||
|
leptos_meta = { version = "0.6" }
|
||||||
|
leptos_router = { version = "0.6" }
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||||
|
tower = { version = "0.4", optional = true }
|
||||||
|
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||||
|
wasm-bindgen = "=0.2.92"
|
||||||
|
thiserror = "1"
|
||||||
|
tracing = { version = "0.1", optional = true }
|
||||||
|
http = "1"
|
||||||
|
|
||||||
|
# external crates
|
||||||
|
tailwind_fuse = { version = "0.2.0", features = ["variant"] }
|
||||||
|
chrono = "0.4.37"
|
||||||
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
leptos-leaflet = "0.8.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = "1.0"
|
||||||
|
gen-wallpapers = { path = "../../crates/gen-wallpapers" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate", "leptos-leaflet/hydrate"]
|
||||||
|
ssr = [
|
||||||
|
"dep:axum",
|
||||||
|
"dep:tokio",
|
||||||
|
"dep:tower",
|
||||||
|
"dep:tower-http",
|
||||||
|
"dep:leptos_axum",
|
||||||
|
"leptos/ssr",
|
||||||
|
"leptos_meta/ssr",
|
||||||
|
"leptos_router/ssr",
|
||||||
|
"dep:tracing",
|
||||||
|
"leptos-leaflet/ssr"
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.leptos]
|
||||||
|
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||||
|
output-name = "plcom"
|
||||||
|
|
||||||
|
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||||
|
site-root = "target/site"
|
||||||
|
|
||||||
|
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||||
|
# Defaults to pkg
|
||||||
|
site-pkg-dir = "pkg"
|
||||||
|
|
||||||
|
# The tailwind input file.
|
||||||
|
#
|
||||||
|
# Optional, Activates the tailwind build
|
||||||
|
tailwind-input-file = "css/main.css"
|
||||||
|
|
||||||
|
# The tailwind config file.
|
||||||
|
#
|
||||||
|
# Optional, defaults to "tailwind.config.js" which if is not present
|
||||||
|
# is generated for you
|
||||||
|
tailwind-config-file = "tailwind.config.js"
|
||||||
|
|
||||||
|
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||||
|
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||||
|
#
|
||||||
|
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||||
|
assets-dir = "../../public"
|
||||||
|
|
||||||
|
# Additional files triggering recompilation
|
||||||
|
watch-additional-files = ["resume.json", "wallpapers.json"]
|
||||||
|
|
||||||
|
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||||
|
site-addr = "0.0.0.0:3000"
|
||||||
|
|
||||||
|
# The port to use for automatic reload monitoring
|
||||||
|
reload-port = 3001
|
||||||
|
|
||||||
|
# The browserlist query used for optimizing the CSS.
|
||||||
|
browserquery = "defaults"
|
||||||
|
|
||||||
|
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||||
|
env = "DEV"
|
||||||
|
|
||||||
|
# The features to use when compiling the bin target
|
||||||
|
#
|
||||||
|
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||||
|
bin-features = ["ssr"]
|
||||||
|
|
||||||
|
# If the --no-default-features flag should be used when compiling the bin target
|
||||||
|
#
|
||||||
|
# Optional. Defaults to false.
|
||||||
|
bin-default-features = false
|
||||||
|
|
||||||
|
# The features to use when compiling the lib target
|
||||||
|
#
|
||||||
|
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||||
|
lib-features = ["hydrate"]
|
||||||
|
|
||||||
|
# If the --no-default-features flag should be used when compiling the lib target
|
||||||
|
#
|
||||||
|
# Optional. Defaults to false.
|
||||||
|
lib-default-features = false
|
||||||
|
|
||||||
|
# The profile to use for the lib target when compiling for release
|
||||||
|
#
|
||||||
|
# Optional. Defaults to "release".
|
||||||
|
lib-profile-release = "wasm-release"
|
||||||
365
crates/plcom/build.rs
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Work {
|
||||||
|
name: String,
|
||||||
|
position: String,
|
||||||
|
start_date: String,
|
||||||
|
end_date: Option<String>,
|
||||||
|
logo: Logo,
|
||||||
|
description: String,
|
||||||
|
highlights: Vec<String>,
|
||||||
|
technologies: Vec<String>,
|
||||||
|
link: Option<Link>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Project {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
start_date: String,
|
||||||
|
end_date: Option<String>,
|
||||||
|
presentation: Vec<String>,
|
||||||
|
highlights: Vec<String>,
|
||||||
|
keywords: Vec<String>,
|
||||||
|
link: Option<Link>,
|
||||||
|
logo: Option<Logo>,
|
||||||
|
image: Option<Image>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Image {
|
||||||
|
file: String,
|
||||||
|
// TODO: use enum directly instead of string
|
||||||
|
// position: ImagePosition,
|
||||||
|
position: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
// TODO: possible implementation: https://www.reddit.com/r/rust/comments/10bab4v/serdejson_how_to_deserialize_and_serialize_an/
|
||||||
|
enum ImagePosition {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Link {
|
||||||
|
uri: String,
|
||||||
|
label: String,
|
||||||
|
not_available: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Logo {
|
||||||
|
file: String,
|
||||||
|
transparent_background: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Education {
|
||||||
|
institution: String,
|
||||||
|
study_type: String,
|
||||||
|
area: String,
|
||||||
|
start_date: String,
|
||||||
|
end_date: Option<String>,
|
||||||
|
logo: Option<Logo>,
|
||||||
|
courses: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Wallpaper {
|
||||||
|
filename: String,
|
||||||
|
date: String,
|
||||||
|
gps: Gps,
|
||||||
|
location: Location,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Gps {
|
||||||
|
latitude: f32,
|
||||||
|
longitude: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Location {
|
||||||
|
precise: String,
|
||||||
|
broad: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vec_strings(vec: &Vec<String>) -> String {
|
||||||
|
let mut string = String::new();
|
||||||
|
string.push('[');
|
||||||
|
for el in vec {
|
||||||
|
string.push_str(&format!("{:?},", el));
|
||||||
|
}
|
||||||
|
string.push(']');
|
||||||
|
string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resume(dest: &std::path::Path, source: std::fs::File) {
|
||||||
|
let reader = std::io::BufReader::new(source);
|
||||||
|
let data: serde_json::Value = serde_json::from_reader(reader).expect("Failed to parse JSON");
|
||||||
|
let mut final_ser = String::new();
|
||||||
|
|
||||||
|
// Work
|
||||||
|
let work_json = &data["work"];
|
||||||
|
let work_data: Vec<Work> =
|
||||||
|
serde_json::from_value(work_json.clone()).expect("Failed to parse work");
|
||||||
|
let mut work_str = String::new();
|
||||||
|
work_str.push('[');
|
||||||
|
for work in &work_data {
|
||||||
|
work_str.push_str("Work {");
|
||||||
|
work_str.push_str(&format!("name: {:?},", work.name));
|
||||||
|
work_str.push_str(&format!("position: {:?},", work.position));
|
||||||
|
work_str.push_str(&format!("start_date: {:?},", work.start_date));
|
||||||
|
|
||||||
|
work_str.push_str(&format!(
|
||||||
|
"end_date: {},",
|
||||||
|
match &work.end_date {
|
||||||
|
Some(end) => format!("Some({:?})", end),
|
||||||
|
None => "None".into(),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// Optional struct
|
||||||
|
// TODO: refacto
|
||||||
|
match &work.link {
|
||||||
|
// TODO: enum with 2 variants for unavailable link
|
||||||
|
Some(link) => match link.not_available {
|
||||||
|
true => work_str.push_str(&format!(
|
||||||
|
"link: Some(ResumeLink {{ uri: {:?}, label: \"Not available\", not_available: true }}),",
|
||||||
|
link.uri
|
||||||
|
)),
|
||||||
|
false => work_str.push_str(&format!(
|
||||||
|
"link: Some(ResumeLink {{ uri: {:?}, label: {:?}, not_available: false }}),",
|
||||||
|
link.uri, link.label
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
None => work_str.push_str("link: None,"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Struct
|
||||||
|
work_str.push_str(&format!(
|
||||||
|
"logo: Logo {{ file: {:?}, transparent_background: {} }},",
|
||||||
|
work.logo.file, work.logo.transparent_background
|
||||||
|
));
|
||||||
|
|
||||||
|
work_str.push_str(&format!("description: {:?},", work.description));
|
||||||
|
|
||||||
|
// Vector
|
||||||
|
let mut highlights = String::new();
|
||||||
|
highlights.push('[');
|
||||||
|
for high in &work.highlights {
|
||||||
|
highlights.push_str(&format!("{:?},", high));
|
||||||
|
}
|
||||||
|
highlights.push(']');
|
||||||
|
work_str.push_str(&format!("highlights: &{},", highlights));
|
||||||
|
|
||||||
|
// Vector
|
||||||
|
let mut technologies = String::new();
|
||||||
|
technologies.push('[');
|
||||||
|
for tech in &work.technologies {
|
||||||
|
technologies.push_str(&format!("{:?},", tech));
|
||||||
|
}
|
||||||
|
technologies.push(']');
|
||||||
|
work_str.push_str(&format!("technologies: &{},", technologies));
|
||||||
|
|
||||||
|
work_str.push_str("},\n");
|
||||||
|
}
|
||||||
|
work_str.push(']');
|
||||||
|
let work_ser = format!(
|
||||||
|
"pub const WORK: [Work; {}] = \n{};\n",
|
||||||
|
work_data.len(),
|
||||||
|
work_str
|
||||||
|
);
|
||||||
|
final_ser.push_str(&work_ser);
|
||||||
|
|
||||||
|
// Projects
|
||||||
|
let projects_json = &data["projects"];
|
||||||
|
let projects_data: Vec<Project> =
|
||||||
|
serde_json::from_value(projects_json.clone()).expect("Failed to parse projects");
|
||||||
|
let mut projects_str = String::new();
|
||||||
|
projects_str.push('[');
|
||||||
|
for project in &projects_data {
|
||||||
|
projects_str.push_str("Project {");
|
||||||
|
projects_str.push_str(&format!("name: {:?},", project.name));
|
||||||
|
projects_str.push_str(&format!("description: {:?},", project.description));
|
||||||
|
projects_str.push_str(&format!("start_date: {:?},", project.start_date));
|
||||||
|
|
||||||
|
projects_str.push_str(&format!(
|
||||||
|
"end_date: {},",
|
||||||
|
match &project.end_date {
|
||||||
|
Some(end) => format!("Some({:?})", end),
|
||||||
|
None => "None".into(),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// Optional struct
|
||||||
|
match &project.link {
|
||||||
|
// TODO: enum with 2 variants for unavailable link
|
||||||
|
Some(link) => match link.not_available {
|
||||||
|
true => projects_str.push_str(&format!(
|
||||||
|
"link: Some(ResumeLink {{ uri: {:?}, label: \"Not available\", not_available: true }}),",
|
||||||
|
link.uri
|
||||||
|
)),
|
||||||
|
false => projects_str.push_str(&format!(
|
||||||
|
"link: Some(ResumeLink {{ uri: {:?}, label: {:?}, not_available: false }}),",
|
||||||
|
link.uri, link.label
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
None => projects_str.push_str("link: None,"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vector
|
||||||
|
projects_str.push_str(&format!(
|
||||||
|
"presentation: &{},",
|
||||||
|
vec_strings(&project.presentation)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Vector
|
||||||
|
projects_str.push_str(&format!(
|
||||||
|
"highlights : &{},",
|
||||||
|
vec_strings(&project.highlights)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Vector
|
||||||
|
projects_str.push_str(&format!("keywords: &{},", vec_strings(&project.keywords)));
|
||||||
|
|
||||||
|
// Optional struct
|
||||||
|
match &project.logo {
|
||||||
|
Some(logo) => projects_str.push_str(&format!(
|
||||||
|
"logo: Some(Logo {{ file: {:?}, transparent_background: {} }}),",
|
||||||
|
logo.file, logo.transparent_background
|
||||||
|
)),
|
||||||
|
None => projects_str.push_str("logo: None,"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional struct
|
||||||
|
match &project.image {
|
||||||
|
Some(image) => projects_str.push_str(&format!(
|
||||||
|
"image: Some(Image {{ file: {:?}, position: {:?} }}),",
|
||||||
|
image.file, image.position
|
||||||
|
)),
|
||||||
|
None => projects_str.push_str("image: None,"),
|
||||||
|
}
|
||||||
|
|
||||||
|
projects_str.push_str("},\n");
|
||||||
|
}
|
||||||
|
projects_str.push(']');
|
||||||
|
let projects_ser = format!(
|
||||||
|
"pub const PROJECTS: [Project; {}] = \n{};\n",
|
||||||
|
projects_data.len(),
|
||||||
|
projects_str
|
||||||
|
);
|
||||||
|
final_ser.push_str(&projects_ser);
|
||||||
|
|
||||||
|
// Education
|
||||||
|
let education_json = &data["education"];
|
||||||
|
let education_data: Vec<Education> =
|
||||||
|
serde_json::from_value(education_json.clone()).expect("Failed to parse education");
|
||||||
|
let mut education_str = String::new();
|
||||||
|
education_str.push('[');
|
||||||
|
for education in &education_data {
|
||||||
|
education_str.push_str("Education {");
|
||||||
|
education_str.push_str(&format!("institution: {:?},", education.institution));
|
||||||
|
education_str.push_str(&format!("area: {:?},", education.area));
|
||||||
|
education_str.push_str(&format!("study_type: {:?},", education.study_type));
|
||||||
|
education_str.push_str(&format!("start_date: {:?},", education.start_date));
|
||||||
|
education_str.push_str(&format!(
|
||||||
|
"end_date: {},",
|
||||||
|
match &education.end_date {
|
||||||
|
Some(end) => format!("Some({:?})", end),
|
||||||
|
None => "None".into(),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// Optional struct
|
||||||
|
match &education.logo {
|
||||||
|
Some(logo) => education_str.push_str(&format!(
|
||||||
|
"logo: Some(Logo {{ file: {:?}, transparent_background: {} }}),",
|
||||||
|
logo.file, logo.transparent_background
|
||||||
|
)),
|
||||||
|
None => education_str.push_str("logo: None,"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vector
|
||||||
|
let mut courses = String::new();
|
||||||
|
courses.push('[');
|
||||||
|
for course in &education.courses {
|
||||||
|
courses.push_str(&format!("{:?},", course));
|
||||||
|
}
|
||||||
|
courses.push(']');
|
||||||
|
education_str.push_str(&format!("courses: &{},", courses));
|
||||||
|
education_str.push_str("},\n");
|
||||||
|
}
|
||||||
|
education_str.push(']');
|
||||||
|
let education_ser = format!(
|
||||||
|
"pub const EDUCATION: [Education; {}] = \n{};\n",
|
||||||
|
education_data.len(),
|
||||||
|
education_str
|
||||||
|
);
|
||||||
|
final_ser.push_str(&education_ser);
|
||||||
|
|
||||||
|
std::fs::write(dest, final_ser).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wallpapers(dest: &std::path::Path, source: std::fs::File) {
|
||||||
|
let reader = std::io::BufReader::new(source);
|
||||||
|
let wallpapers_data: Vec<Wallpaper> =
|
||||||
|
serde_json::from_reader(reader).expect("Failed to parse JSON");
|
||||||
|
let mut final_ser = String::new();
|
||||||
|
|
||||||
|
let mut wallpapers_str = String::new();
|
||||||
|
wallpapers_str.push('[');
|
||||||
|
|
||||||
|
for wallpaper in &wallpapers_data {
|
||||||
|
wallpapers_str.push_str("Wallpaper {");
|
||||||
|
wallpapers_str.push_str(&format!("filename: {:?},", wallpaper.filename));
|
||||||
|
wallpapers_str.push_str(&format!("date: {:?},", wallpaper.date));
|
||||||
|
wallpapers_str.push_str(&format!(
|
||||||
|
"gps: Gps {{ latitude: {}, longitude: {} }},",
|
||||||
|
wallpaper.gps.latitude, wallpaper.gps.longitude
|
||||||
|
));
|
||||||
|
wallpapers_str.push_str(&format!(
|
||||||
|
"location: Location {{ precise: {:?}, broad: {:?} }},",
|
||||||
|
wallpaper.location.precise, wallpaper.location.broad
|
||||||
|
));
|
||||||
|
wallpapers_str.push_str("},\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
wallpapers_str.push(']');
|
||||||
|
let wallpapers_ser = format!(
|
||||||
|
"pub const WALLPAPERS: [Wallpaper; {}] = \n{};\n",
|
||||||
|
wallpapers_data.len(),
|
||||||
|
wallpapers_str
|
||||||
|
);
|
||||||
|
final_ser.push_str(&wallpapers_ser);
|
||||||
|
|
||||||
|
std::fs::write(dest, final_ser).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo::rerun-if-changed=resume.json");
|
||||||
|
println!("cargo::rerun-if-changed=wallpapers.json");
|
||||||
|
|
||||||
|
let out_dir = std::env::var_os("OUT_DIR").unwrap();
|
||||||
|
|
||||||
|
let resume_dest = std::path::Path::new(&out_dir).join("resume.rs");
|
||||||
|
let resume_source = std::fs::File::open("resume.json").expect("Failed to open file");
|
||||||
|
resume(&resume_dest, resume_source);
|
||||||
|
|
||||||
|
let wallpapers_dest = std::path::Path::new(&out_dir).join("wallpapers.rs");
|
||||||
|
match std::fs::File::open("wallpapers.json") {
|
||||||
|
Ok(source) => wallpapers(&wallpapers_dest, source),
|
||||||
|
Err(_) => println!("cargo::warning=skipping wallpapers, file not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
24
crates/plcom/css/main.css
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
428
crates/plcom/resume.json
Normal file
|
|
@ -0,0 +1,428 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
|
||||||
|
"basics": {
|
||||||
|
"name": "Philippe Loctaux",
|
||||||
|
"url": "https://philippeloctaux.com",
|
||||||
|
"label": "Developer of all sorts",
|
||||||
|
"email": "www@philippeloctaux.com",
|
||||||
|
"location": {
|
||||||
|
"countryCode": "FR",
|
||||||
|
"city": "Rennes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"languages": [
|
||||||
|
{
|
||||||
|
"language": "French"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "English"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "Russian"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"education": [
|
||||||
|
{
|
||||||
|
"institution": "Epitech",
|
||||||
|
"studyType": "Master",
|
||||||
|
"area": "Computer software engineering",
|
||||||
|
"startDate": "2018-10",
|
||||||
|
"endDate": "2023-08",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/epitech.png",
|
||||||
|
"transparentBackground": true
|
||||||
|
},
|
||||||
|
"courses": [
|
||||||
|
"Many small projects in C to learn a technical concept during first year",
|
||||||
|
"my_teams: Microsoft Teams clone in C (protocol design, networking, server, client)",
|
||||||
|
"Arcade: Small gaming platform capable of loading dynamic libraries with a high abstraction level in C++",
|
||||||
|
"Epicture: Android client in Kotlin of the Imgur image service (OAuth2, listing, viewing, uploading images)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"institution": "UQAC",
|
||||||
|
"studyType": "Year abroad",
|
||||||
|
"area": "Computer software engineering",
|
||||||
|
"startDate": "2021-09",
|
||||||
|
"endDate": "2022-05",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/uqac.png",
|
||||||
|
"transparentBackground": true
|
||||||
|
},
|
||||||
|
"courses": [
|
||||||
|
"Android app development",
|
||||||
|
"Object Oriented Programming",
|
||||||
|
"Software engineering",
|
||||||
|
"Entreprise application development"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"work": [
|
||||||
|
{
|
||||||
|
"name": "Rubycat",
|
||||||
|
"position": "Software engineer",
|
||||||
|
"startDate": "2023-06",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/rubycat.png",
|
||||||
|
"transparentBackground": true
|
||||||
|
},
|
||||||
|
"description": "Maintenance and improvement of PROVE IT, a Privileged Access Management software platform.",
|
||||||
|
"highlights": [
|
||||||
|
"Rewrote an internal component in Rust, making it faster and simpler to use for the development team",
|
||||||
|
"Bug investigation and fixes",
|
||||||
|
"Public speaking about the product"
|
||||||
|
],
|
||||||
|
"technologies": [
|
||||||
|
"Rust",
|
||||||
|
"Angular",
|
||||||
|
"Python",
|
||||||
|
"PostgreSQL",
|
||||||
|
"GitLab"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://rubycat.eu",
|
||||||
|
"label": "Company website",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acklio",
|
||||||
|
"position": "Rust developer",
|
||||||
|
"startDate": "2023-03",
|
||||||
|
"endDate": "2023-05",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/acklio.png",
|
||||||
|
"transparentBackground": true
|
||||||
|
},
|
||||||
|
"description": "The first usage of the SCHC framework (RFC 8724) on Rust!",
|
||||||
|
"highlights": [
|
||||||
|
"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": [
|
||||||
|
"Rust",
|
||||||
|
"SCHC",
|
||||||
|
"STM32 controllers",
|
||||||
|
"LoRa",
|
||||||
|
"LoRaWAN"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://ackl.io",
|
||||||
|
"label": "Company website",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vélorail du Kreiz Breizh",
|
||||||
|
"position": "Freelance developer",
|
||||||
|
"startDate": "2021-08",
|
||||||
|
"endDate": "2022-04",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/velorail.png",
|
||||||
|
"transparentBackground": true
|
||||||
|
},
|
||||||
|
"description": "Creation of an online booking platform focused on the tourist activity of rail biking (vélorail).",
|
||||||
|
"highlights": [
|
||||||
|
"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",
|
||||||
|
"5 months after the initial deployment, 43% of the bookings were made with the online platform",
|
||||||
|
"Focus to use the least amount of external services, resulting in implementation of a invoice generation service, an image-based captcha, and a templating system for transactional emails"
|
||||||
|
],
|
||||||
|
"technologies": [
|
||||||
|
"Angular",
|
||||||
|
"NestJS",
|
||||||
|
"GraphQL",
|
||||||
|
"Rust",
|
||||||
|
"Stripe",
|
||||||
|
"Amazon SES",
|
||||||
|
"GitLab CI/CD"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://resa.velorail.bzh",
|
||||||
|
"label": "Booking platform",
|
||||||
|
"notAvailable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Yaakadev",
|
||||||
|
"position": "Full-Stack developer",
|
||||||
|
"startDate": "2021-04",
|
||||||
|
"endDate": "2021-07",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/yaakadev.png",
|
||||||
|
"transparentBackground": false
|
||||||
|
},
|
||||||
|
"description": "Design, development, deployment and maintenance of many projects for various clients.",
|
||||||
|
"highlights": [
|
||||||
|
"Admin dashboard of a local merchants solution",
|
||||||
|
"Calendar planning application with filtering and custom views for a SaaS for the agency",
|
||||||
|
"Intranet to upload and download documents for a client"
|
||||||
|
],
|
||||||
|
"technologies": [
|
||||||
|
"NodeJS",
|
||||||
|
"ExpressJS",
|
||||||
|
"Angular",
|
||||||
|
"MongoDB",
|
||||||
|
"CI/CD"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://yaakadev.com",
|
||||||
|
"label": "Agency website",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Epitech",
|
||||||
|
"position": "Teaching assistant",
|
||||||
|
"startDate": "2020-02",
|
||||||
|
"endDate": "2021-03",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/epitech.png",
|
||||||
|
"transparentBackground": true
|
||||||
|
},
|
||||||
|
"description": "Pedagogical supervision of three classes of students, conducting educational activites throughout the school year.",
|
||||||
|
"highlights": [
|
||||||
|
"Start of projects",
|
||||||
|
"Technical help and guidance",
|
||||||
|
"Proctoring exams",
|
||||||
|
"Grading students on their work"
|
||||||
|
],
|
||||||
|
"technologies": [
|
||||||
|
"C",
|
||||||
|
"C++",
|
||||||
|
"Haskell",
|
||||||
|
"Rust",
|
||||||
|
"Web and mobile development"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://www.epitech.eu/ecole-informatique-rennes",
|
||||||
|
"label": "School",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ubiscale",
|
||||||
|
"position": "Embedded developer",
|
||||||
|
"startDate": "2019-08",
|
||||||
|
"endDate": "2019-12",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/ubiscale.png",
|
||||||
|
"transparentBackground": true
|
||||||
|
},
|
||||||
|
"description": "Creation of a home Wifi gateway for a commercial IoT object.",
|
||||||
|
"highlights": [
|
||||||
|
"Research, reverse engineering of existing products",
|
||||||
|
"Design and implementation"
|
||||||
|
],
|
||||||
|
"technologies": [
|
||||||
|
"C on a ESP8266 controller",
|
||||||
|
"Wi-Fi",
|
||||||
|
"Bluetooth"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://ubiscale.com",
|
||||||
|
"label": "Company website",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"name": "ezidam",
|
||||||
|
"description": "Identity and Access Management system",
|
||||||
|
"presentation": [
|
||||||
|
"A simple identity and access management system for SMEs or personal use.",
|
||||||
|
"Low maintenance required, easy to deploy and to backup."
|
||||||
|
],
|
||||||
|
"highlights": [
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"Rust",
|
||||||
|
"SQLite",
|
||||||
|
"OAuth2 / OIDC",
|
||||||
|
"TOTP",
|
||||||
|
"SMTP",
|
||||||
|
"Docker"
|
||||||
|
],
|
||||||
|
"startDate": "2023-01",
|
||||||
|
"endDate": "2023-07",
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/ezidam.png",
|
||||||
|
"transparentBackground": true
|
||||||
|
},
|
||||||
|
"link": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pass4thewin",
|
||||||
|
"description": "Password manager",
|
||||||
|
"startDate": "2020-11",
|
||||||
|
"endDate": "2021-01",
|
||||||
|
"presentation": [
|
||||||
|
"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."
|
||||||
|
],
|
||||||
|
"highlights": [
|
||||||
|
"Creation of a store",
|
||||||
|
"List secrets",
|
||||||
|
"Decrypt secret",
|
||||||
|
"Insert or generate secrets",
|
||||||
|
"Edit existing secrets",
|
||||||
|
"Synchronisation with git",
|
||||||
|
"TOTP support"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"Windows",
|
||||||
|
"Rust",
|
||||||
|
"OpenPGP",
|
||||||
|
"libgit2"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://github.com/x4m3/pass4thewin",
|
||||||
|
"label": "Source code",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NaviaRent",
|
||||||
|
"description": "Epitech Innovative Project",
|
||||||
|
"startDate": "2020-09",
|
||||||
|
"endDate": "2023-01",
|
||||||
|
"presentation": [
|
||||||
|
"A B2B platform helping rentals of standup paddle boards."
|
||||||
|
],
|
||||||
|
"highlights": [
|
||||||
|
"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",
|
||||||
|
"Meetings managements spread across 3 timezones",
|
||||||
|
"Technical writing",
|
||||||
|
"Public presentations"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"NodeJS",
|
||||||
|
"Angular",
|
||||||
|
"Kotlin",
|
||||||
|
"SwiftUI",
|
||||||
|
"Docker",
|
||||||
|
"GitLab CI/CD",
|
||||||
|
"Raspberry Pi",
|
||||||
|
"ESP32"
|
||||||
|
],
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/naviarent.png",
|
||||||
|
"transparentBackground": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"file": "/images/naviarent.jpg",
|
||||||
|
"position": "right"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"uri": "https://naviarent.fr",
|
||||||
|
"label": "Website",
|
||||||
|
"notAvailable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "epitok",
|
||||||
|
"description": "Presence system at Epitech",
|
||||||
|
"startDate": "2020-06",
|
||||||
|
"endDate": "2020-09",
|
||||||
|
"presentation": [
|
||||||
|
"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."
|
||||||
|
],
|
||||||
|
"highlights": [
|
||||||
|
"Reverse engineering of a partially documented web API",
|
||||||
|
"Design, conception",
|
||||||
|
"User experience",
|
||||||
|
"Improvements based of usage of the application"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"Rust",
|
||||||
|
"HTML",
|
||||||
|
"Bootstrap",
|
||||||
|
"jQuery",
|
||||||
|
"Docker"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://github.com/x4m3/epitok",
|
||||||
|
"label": "Source code",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "epi.today",
|
||||||
|
"description": "Calendar for Epitech",
|
||||||
|
"startDate": "2019-11",
|
||||||
|
"endDate": "2019-02",
|
||||||
|
"presentation": [
|
||||||
|
"A viewer of the Epitech intranet calendar.",
|
||||||
|
"Students and teachers glance at their planning without the need to go on the school's intranet."
|
||||||
|
],
|
||||||
|
"highlights": [
|
||||||
|
"Reverse engineering of a web api",
|
||||||
|
"Design a web page",
|
||||||
|
"Mobile UX",
|
||||||
|
"Deployment on a server"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"TypeScript",
|
||||||
|
"HTML",
|
||||||
|
"Bootstrap",
|
||||||
|
"Docker"
|
||||||
|
],
|
||||||
|
"link": {
|
||||||
|
"uri": "https://github.com/x4m3/epi.today",
|
||||||
|
"label": "Source code",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "canvas.place",
|
||||||
|
"description": "Timelapse",
|
||||||
|
"startDate": "2017-04",
|
||||||
|
"endDate": "2020-01",
|
||||||
|
"presentation": [
|
||||||
|
"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."
|
||||||
|
],
|
||||||
|
"highlights": [],
|
||||||
|
"keywords": [
|
||||||
|
"FFmpeg",
|
||||||
|
"Shell scripting",
|
||||||
|
"nginx"
|
||||||
|
],
|
||||||
|
"logo": {
|
||||||
|
"file": "/icons/canvas.png",
|
||||||
|
"transparentBackground": false
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"file": "/images/canvas.png",
|
||||||
|
"position": "left"
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"uri": "https://timelapse.canvas.place",
|
||||||
|
"label": "Website",
|
||||||
|
"notAvailable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
71
crates/plcom/src/app.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
use crate::{
|
||||||
|
error_template::{AppError, ErrorTemplate},
|
||||||
|
Link, UnderlineLink,pages::*
|
||||||
|
};
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_meta::*;
|
||||||
|
use leptos_router::*;
|
||||||
|
use tailwind_fuse::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn App() -> impl IntoView {
|
||||||
|
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||||
|
provide_meta_context();
|
||||||
|
let formatter = |text| format!("{text} — Philippe Loctaux");
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Stylesheet id="leptos" href="/pkg/plcom.css"/>
|
||||||
|
|
||||||
|
// sets the document title
|
||||||
|
<Title formatter/>
|
||||||
|
|
||||||
|
<Meta name="viewport" content="width=device-width"/>
|
||||||
|
|
||||||
|
// favicon
|
||||||
|
<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"
|
||||||
|
attrs=vec![("color", Attribute::String(Oco::Borrowed("#0c4a6e")))]
|
||||||
|
/>
|
||||||
|
<Meta name="msapplication-TileColor" content="#0c4a6e"/>
|
||||||
|
<Meta name="theme-color" content="#0c4a6e"/>
|
||||||
|
|
||||||
|
// stats
|
||||||
|
<Script
|
||||||
|
defer="true"
|
||||||
|
src="https://plausible.y.z.x4m3.rocks/js/script.js"
|
||||||
|
attrs=vec![("data-domain", Attribute::String(Oco::Borrowed("philippeloctaux.com")))]
|
||||||
|
/>
|
||||||
|
|
||||||
|
// actual routes
|
||||||
|
<Router fallback=|| {
|
||||||
|
let mut outside_errors = Errors::default();
|
||||||
|
outside_errors.insert_with_default_key(AppError::NotFound);
|
||||||
|
view! { <ErrorTemplate outside_errors/> }.into_view()
|
||||||
|
}>
|
||||||
|
<Body class=tw_join!("flex", "flex-col", "min-h-screen", "bg-gray-900", "text-white")/>
|
||||||
|
<main class=tw_join!("flex-grow")>
|
||||||
|
<Routes>
|
||||||
|
<Route path="" view=RootPage ssr=SsrMode::Async/>
|
||||||
|
<Route path="email" view=EmailPage/>
|
||||||
|
<Route path="wallpapers" view=WallpapersPage/>
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
<footer class=tw_join!("bg-black")>
|
||||||
|
<div class=tw_join!("container", "mx-auto", "px-4", "py-8")>
|
||||||
|
<p>
|
||||||
|
"© 2015 - "{crate::get_year()}" Philippe Loctaux, made with "
|
||||||
|
<UnderlineLink link=Link::new("https://leptos.dev", "Leptos")/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</Router>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
137
crates/plcom/src/common.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
pub mod icon;
|
||||||
|
pub mod link;
|
||||||
|
|
||||||
|
pub fn get_year() -> i32 {
|
||||||
|
use chrono::Datelike;
|
||||||
|
chrono::Utc::now().year()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct Date {
|
||||||
|
pub year: u32,
|
||||||
|
pub month: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Date {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}-{:02}", self.year, self.month)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod resume {
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Work {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub position: &'static str,
|
||||||
|
pub start_date: &'static str,
|
||||||
|
pub end_date: Option<&'static str>,
|
||||||
|
pub logo: Logo,
|
||||||
|
pub description: &'static str,
|
||||||
|
pub highlights: &'static [&'static str],
|
||||||
|
pub technologies: &'static [&'static str],
|
||||||
|
pub link: Option<ResumeLink>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Logo {
|
||||||
|
pub file: &'static str,
|
||||||
|
pub transparent_background: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Project {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub description: &'static str,
|
||||||
|
pub start_date: &'static str,
|
||||||
|
pub end_date: Option<&'static str>,
|
||||||
|
pub presentation: &'static [&'static str],
|
||||||
|
pub highlights: &'static [&'static str],
|
||||||
|
pub keywords: &'static [&'static str],
|
||||||
|
pub link: Option<ResumeLink>,
|
||||||
|
pub logo: Option<Logo>,
|
||||||
|
pub image: Option<Image>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Image {
|
||||||
|
pub file: &'static str,
|
||||||
|
pub position: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ResumeLink {
|
||||||
|
pub uri: &'static str,
|
||||||
|
pub label: &'static str,
|
||||||
|
pub not_available: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::{Link, OutlineButtonLink};
|
||||||
|
use http::Uri;
|
||||||
|
use leptos::*;
|
||||||
|
use tailwind_fuse::tw_join;
|
||||||
|
impl IntoView for ResumeLink {
|
||||||
|
fn into_view(self) -> View {
|
||||||
|
if !self.not_available {
|
||||||
|
let link = Link {
|
||||||
|
label: self.label.into(),
|
||||||
|
uri: Uri::from_static(self.uri),
|
||||||
|
};
|
||||||
|
view! { <OutlineButtonLink link=link/> }.into_view()
|
||||||
|
} else {
|
||||||
|
view! {
|
||||||
|
<span class=tw_join!(
|
||||||
|
"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"
|
||||||
|
)>{self.label}</span>
|
||||||
|
}
|
||||||
|
.into_view()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Education {
|
||||||
|
pub institution: &'static str,
|
||||||
|
pub study_type: &'static str,
|
||||||
|
pub area: &'static str,
|
||||||
|
pub start_date: &'static str,
|
||||||
|
pub end_date: Option<&'static str>,
|
||||||
|
pub logo: Option<Logo>,
|
||||||
|
pub courses: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/resume.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod wallpapers {
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Wallpaper {
|
||||||
|
pub filename: &'static str,
|
||||||
|
pub date: &'static str,
|
||||||
|
pub gps: Gps,
|
||||||
|
pub location: Location,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Gps {
|
||||||
|
pub latitude: f32,
|
||||||
|
pub longitude: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Location {
|
||||||
|
pub precise: &'static str,
|
||||||
|
pub broad: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Wallpaper {
|
||||||
|
pub fn random() -> Option<&'static Wallpaper> {
|
||||||
|
let random_value = rand::Rng::gen_range(&mut rand::thread_rng(), 0..WALLPAPERS.len());
|
||||||
|
WALLPAPERS.get(random_value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/wallpapers.rs"));
|
||||||
|
}
|
||||||
140
crates/plcom/src/common/icon.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
use leptos::*;
|
||||||
|
use tailwind_fuse::tw_join;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum Icon {
|
||||||
|
Email,
|
||||||
|
Link,
|
||||||
|
Calendar,
|
||||||
|
Location,
|
||||||
|
Twitter,
|
||||||
|
Telegram,
|
||||||
|
Mastodon,
|
||||||
|
Github,
|
||||||
|
Linkedin,
|
||||||
|
Map,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoView for Icon {
|
||||||
|
fn into_view(self) -> View {
|
||||||
|
match self {
|
||||||
|
Self::Email => view! {
|
||||||
|
<svg
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
|
||||||
|
Self::Link => view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
|
||||||
|
Self::Calendar => view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
|
||||||
|
Self::Location => view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
Icon::Twitter => view! {
|
||||||
|
<svg
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
Icon::Telegram => view! {
|
||||||
|
<svg
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
Icon::Mastodon => view! {
|
||||||
|
<svg
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
Icon::Github => view! {
|
||||||
|
<svg
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
Icon::Linkedin => view! {
|
||||||
|
<svg
|
||||||
|
class=tw_join!("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>
|
||||||
|
}.into_view(),
|
||||||
|
Icon::Map => view! {
|
||||||
|
<svg
|
||||||
|
class=tw_join!("w-5", "h-5")
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8.157 2.175a1.5 1.5 0 00-1.147 0l-4.084 1.69A1.5 1.5 0 002 5.251v10.877a1.5 1.5 0 002.074 1.386l3.51-1.453 4.26 1.763a1.5 1.5 0 001.146 0l4.083-1.69A1.5 1.5 0 0018 14.748V3.873a1.5 1.5 0 00-2.073-1.386l-3.51 1.452-4.26-1.763zM7.58 5a.75.75 0 01.75.75v6.5a.75.75 0 01-1.5 0v-6.5A.75.75 0 017.58 5zm5.59 2.75a.75.75 0 00-1.5 0v6.5a.75.75 0 001.5 0v-6.5z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
}.into_view(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
crates/plcom/src/common/link.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
use http::Uri;
|
||||||
|
use leptos::*;
|
||||||
|
use tailwind_fuse::tw_join;
|
||||||
|
|
||||||
|
use crate::Icon;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct Link {
|
||||||
|
pub label: String,
|
||||||
|
pub uri: Uri,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Link {
|
||||||
|
pub fn new(uri: &'static str, label: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
uri: Uri::from_static(uri),
|
||||||
|
label: label.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn slides(uri: &'static str) -> Self {
|
||||||
|
Self::new(uri, "Slides")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn UnderlineLink(
|
||||||
|
#[prop(into)] link: MaybeSignal<Link>,
|
||||||
|
#[prop(into, optional)] class: MaybeSignal<String>,
|
||||||
|
#[prop(attrs)] attributes: Vec<(&'static str, Attribute)>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let class = tailwind_fuse::tw_merge!("underline", class.get());
|
||||||
|
view! {
|
||||||
|
<a href=link.get().uri.to_string() {..attributes} class=class target="_blank">
|
||||||
|
{link.get().label}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HideTextSmallDisplay = bool;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ButtonLink(
|
||||||
|
#[prop(into)] link: MaybeSignal<Link>,
|
||||||
|
#[prop(into, optional)] icon: Option<MaybeSignal<Icon>>,
|
||||||
|
#[prop(into, optional)] hide_text_small_display: Option<MaybeSignal<HideTextSmallDisplay>>,
|
||||||
|
#[prop(attrs)] attributes: Vec<(&'static str, Attribute)>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let text_css = hide_text_small_display
|
||||||
|
.and_then(|hide| {
|
||||||
|
if hide.get() {
|
||||||
|
Some(tw_join!("hidden", "sm:inline"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(tw_join!("ml-2", "sm:ml-0", "text-center"));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<a
|
||||||
|
href=link.get().uri.to_string()
|
||||||
|
{..attributes}
|
||||||
|
class=tw_join!(
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
>
|
||||||
|
|
||||||
|
{icon}
|
||||||
|
<div class=tw_join!("inline-flex", "items-center")>
|
||||||
|
<span class=text_css>{link.get().label}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn OutlineButtonLink(
|
||||||
|
#[prop(into)] link: MaybeSignal<Link>,
|
||||||
|
#[prop(attrs)] attributes: Vec<(&'static str, Attribute)>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<a
|
||||||
|
href=link.get().uri.to_string()
|
||||||
|
{..attributes}
|
||||||
|
class=tw_join!(
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
>
|
||||||
|
|
||||||
|
{Icon::Link}
|
||||||
|
<div class=tw_join!("inline-flex", "items-center")>
|
||||||
|
<span class=tw_join!("ml-2", "sm:ml-0", "text-center")>{link.get().label}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
83
crates/plcom/src/error_template.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
use crate::ContentPage;
|
||||||
|
use http::status::StatusCode;
|
||||||
|
use leptos::*;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("Not Found")]
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
pub fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn canonical_reason(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AppError::NotFound => StatusCode::NOT_FOUND.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AppError::NotFound => "This page could not be found.".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A basic function to display errors served by the error boundaries.
|
||||||
|
// Feel free to do more complicated things here than just displaying the error.
|
||||||
|
#[component]
|
||||||
|
pub fn ErrorTemplate(
|
||||||
|
#[prop(optional)] outside_errors: Option<Errors>,
|
||||||
|
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let errors = match outside_errors {
|
||||||
|
Some(e) => create_rw_signal(e),
|
||||||
|
None => match errors {
|
||||||
|
Some(e) => e,
|
||||||
|
None => panic!("No Errors found and we expected errors!"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Get Errors from Signal
|
||||||
|
let errors = errors.get_untracked();
|
||||||
|
|
||||||
|
// Downcast lets us take a type that implements `std::error::Error`
|
||||||
|
let errors: Vec<AppError> = errors
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||||
|
.collect();
|
||||||
|
println!("Errors: {errors:#?}");
|
||||||
|
|
||||||
|
// Only the response code for the first error is actually sent from the server
|
||||||
|
// this may be customized by the specific application
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
{
|
||||||
|
use leptos_axum::ResponseOptions;
|
||||||
|
let response = use_context::<ResponseOptions>();
|
||||||
|
if let Some(response) = response {
|
||||||
|
response.set_status(errors[0].status_code());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<For
|
||||||
|
// a function that returns the items we're iterating over; a signal is fine
|
||||||
|
each=move || { errors.clone().into_iter().enumerate() }
|
||||||
|
// a unique key for each item as a reference
|
||||||
|
key=|(index, _error)| *index
|
||||||
|
// renders each item to a view
|
||||||
|
children=move |error| {
|
||||||
|
view! {
|
||||||
|
<ContentPage title=error.1.canonical_reason()>
|
||||||
|
<p>{error.1.description()}</p>
|
||||||
|
</ContentPage>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
42
crates/plcom/src/fileserv.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::State,
|
||||||
|
response::IntoResponse,
|
||||||
|
http::{Request, Response, StatusCode, Uri},
|
||||||
|
};
|
||||||
|
use axum::response::Response as AxumResponse;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
use leptos::*;
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||||
|
let root = options.site_root.clone();
|
||||||
|
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||||
|
|
||||||
|
if res.status() == StatusCode::OK {
|
||||||
|
res.into_response()
|
||||||
|
} else {
|
||||||
|
let handler = leptos_axum::render_app_to_stream(options.to_owned(), App);
|
||||||
|
handler(req).await.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_static_file(
|
||||||
|
uri: Uri,
|
||||||
|
root: &str,
|
||||||
|
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||||
|
let req = Request::builder()
|
||||||
|
.uri(uri.clone())
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||||
|
// This path is relative to the cargo root
|
||||||
|
match ServeDir::new(root).oneshot(req).await {
|
||||||
|
Ok(res) => Ok(res.into_response()),
|
||||||
|
Err(err) => Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Something went wrong: {err}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
45
crates/plcom/src/lib.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use leptos::*;
|
||||||
|
use prelude::*;
|
||||||
|
|
||||||
|
pub mod app;
|
||||||
|
pub mod common;
|
||||||
|
pub mod error_template;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod fileserv;
|
||||||
|
pub mod pages;
|
||||||
|
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
|
pub fn hydrate() {
|
||||||
|
use crate::app::*;
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
mount_to_body(App);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::ContentPage;
|
||||||
|
pub use crate::common::icon::*;
|
||||||
|
pub use crate::common::link::*;
|
||||||
|
pub use crate::common::resume::*;
|
||||||
|
pub use crate::common::wallpapers::*;
|
||||||
|
pub use crate::common::*;
|
||||||
|
pub use leptos::*;
|
||||||
|
pub use leptos_meta::*;
|
||||||
|
pub use tailwind_fuse::tw_join;
|
||||||
|
pub use http::Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ContentPage(
|
||||||
|
#[prop(into, optional)] title: MaybeSignal<String>,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<leptos_meta::Title text=title.get()></leptos_meta::Title>
|
||||||
|
<div class=tw_join!("container", "mx-auto", "px-4", "py-16")>
|
||||||
|
<h1 class=tw_join!("text-3xl", "sm:text-4xl", "font-bold")>{title}</h1>
|
||||||
|
<UnderlineLink link=Link::new("/", "← Home")/>
|
||||||
|
<div class=tw_join!("mt-8")>{children()}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
38
crates/plcom/src/main.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
use axum::Router;
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
|
use plcom::app::*;
|
||||||
|
use plcom::fileserv::file_and_error_handler;
|
||||||
|
|
||||||
|
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
||||||
|
// For deployment these variables are:
|
||||||
|
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
||||||
|
// Alternately a file can be specified such as Some("Cargo.toml")
|
||||||
|
// The file would need to be included with the executable when moved to deployment
|
||||||
|
let conf = get_configuration(None).await.unwrap();
|
||||||
|
let leptos_options = conf.leptos_options;
|
||||||
|
let addr = leptos_options.site_addr;
|
||||||
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
|
// build our application with a route
|
||||||
|
let app = Router::new()
|
||||||
|
.leptos_routes(&leptos_options, routes, App)
|
||||||
|
.fallback(file_and_error_handler)
|
||||||
|
.with_state(leptos_options);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
|
logging::log!("listening on http://{}", &addr);
|
||||||
|
axum::serve(listener, app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
|
pub fn main() {
|
||||||
|
// no client-side main function
|
||||||
|
// unless we want this to work with e.g., Trunk for a purely client-side app
|
||||||
|
// see lib.rs for hydration function instead
|
||||||
|
}
|
||||||
7
crates/plcom/src/pages.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
pub mod root;
|
||||||
|
pub mod email;
|
||||||
|
pub mod wallpapers;
|
||||||
|
|
||||||
|
pub use root::RootPage;
|
||||||
|
pub use email::EmailPage;
|
||||||
|
pub use wallpapers::WallpapersPage;
|
||||||
37
crates/plcom/src/pages/email.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn EmailPage() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<ContentPage title="Email">
|
||||||
|
<p>
|
||||||
|
"Send an email if you want to work with me, propose a project idea, or just to say hi!"
|
||||||
|
</p>
|
||||||
|
<div class=tw_join!("my-4")>
|
||||||
|
<ButtonLink
|
||||||
|
link=Link::new(
|
||||||
|
"mailto:wwwATphilippeloctaux~DOT~com",
|
||||||
|
"www at philippeloctaux dot com",
|
||||||
|
)
|
||||||
|
|
||||||
|
icon=Icon::Email
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class=tw_join!(
|
||||||
|
"mb-2"
|
||||||
|
)>
|
||||||
|
"If you want to encrypt your message, I have a "
|
||||||
|
<UnderlineLink link=Link::new("/pub/pgp-0x69771CD04BA82EC0.txt", "pgp key")/>
|
||||||
|
" at your disposal."
|
||||||
|
</p>
|
||||||
|
<p class=tw_join!(
|
||||||
|
"mb-2"
|
||||||
|
)>
|
||||||
|
"I also have a " <UnderlineLink link=Link::new("/keybase.txt", "Keybase")/>
|
||||||
|
" account, but I do not check it often."
|
||||||
|
</p>
|
||||||
|
</ContentPage>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
76
crates/plcom/src/pages/root.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
mod hero;
|
||||||
|
mod www;
|
||||||
|
mod experience;
|
||||||
|
mod jobs;
|
||||||
|
mod projects;
|
||||||
|
mod education;
|
||||||
|
mod talks;
|
||||||
|
mod friends;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn RootPage() -> impl IntoView {
|
||||||
|
let random_wallpaper = Wallpaper::random();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Title text="Hello"/>
|
||||||
|
<hero::Hero wallpaper=random_wallpaper></hero::Hero>
|
||||||
|
|
||||||
|
<div class=tw_join!("container", "mx-auto", "px-4", "md:px-8", "lg:px-16", "py-16")>
|
||||||
|
<Whoami/>
|
||||||
|
<div class=tw_join!("my-16", "space-y-16", "md:space-y-32")>
|
||||||
|
<www::Www></www::Www>
|
||||||
|
<jobs::Jobs></jobs::Jobs>
|
||||||
|
<projects::Projects></projects::Projects>
|
||||||
|
<education::EducationList></education::EducationList>
|
||||||
|
<talks::Talks></talks::Talks>
|
||||||
|
<friends::Friends></friends::Friends>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Whoami() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!("md:flex", "md:flex-row-reverse", "items-center")>
|
||||||
|
<div class=tw_join!("md:w-1/2", "mb-4", "md:mb-0")>
|
||||||
|
<img
|
||||||
|
src="/phil.png"
|
||||||
|
alt="Phil"
|
||||||
|
class=tw_join!(
|
||||||
|
"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=tw_join!("md:w-1/2")>
|
||||||
|
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"About Phil"</h1>
|
||||||
|
<h2 class=tw_join!(
|
||||||
|
"text-2xl", "font-semibold", "mb-4"
|
||||||
|
)>"Developer of all sorts"</h2>
|
||||||
|
|
||||||
|
<div class=tw_join!("text-lg", "space-y-6")>
|
||||||
|
<p>
|
||||||
|
"I got into computer science by learning about the Linux kernel and administrating servers."
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
"After high school, I became a student at Epitech and learned to tackle technical concepts and apply 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>
|
||||||
|
}
|
||||||
|
}
|
||||||
51
crates/plcom/src/pages/root/education.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
use super::experience::*;
|
||||||
|
|
||||||
|
impl IntoView for Education {
|
||||||
|
fn into_view(self) -> View {
|
||||||
|
let subtitle = format!("{} in {}", self.study_type, self.area);
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!(
|
||||||
|
"rounded-2xl", "w-full", "bg-amber-950", "p-6"
|
||||||
|
)>
|
||||||
|
|
||||||
|
{ExperienceHeader::new(
|
||||||
|
self.start_date,
|
||||||
|
self.end_date,
|
||||||
|
self.institution,
|
||||||
|
&subtitle,
|
||||||
|
self.logo.as_ref(),
|
||||||
|
)}
|
||||||
|
<div class=tw_join!("space-y-2")>
|
||||||
|
<ul class=tw_join!(
|
||||||
|
"list-disc", "mt-6"
|
||||||
|
)>
|
||||||
|
{self
|
||||||
|
.courses
|
||||||
|
.iter()
|
||||||
|
.map(|h| {
|
||||||
|
view! { <li class=tw_join!("ml-5")>{*h}</li> }
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}.into_view()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn EducationList() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Education"</h1>
|
||||||
|
|
||||||
|
<div class=tw_join!(
|
||||||
|
"mt-4", "grid", "grid-cols-1", "md:grid-cols-2", "gap-6", "place-content-center"
|
||||||
|
)>{resume::EDUCATION.collect_view()}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
134
crates/plcom/src/pages/root/experience.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
use tailwind_fuse::*;
|
||||||
|
use leptos::*;
|
||||||
|
use crate::common::Date;
|
||||||
|
use crate::common::resume::Logo;
|
||||||
|
|
||||||
|
#[derive(TwClass, Clone, Copy, PartialEq)]
|
||||||
|
#[tw(class = r#"h-16 w-16 rounded-xl"#)]
|
||||||
|
struct LogoOptions {
|
||||||
|
background: ImageBackground,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TwVariant, PartialEq)]
|
||||||
|
enum ImageBackground {
|
||||||
|
#[tw(class = "p-2 bg-white")]
|
||||||
|
Transparent,
|
||||||
|
#[tw(default, class = "")]
|
||||||
|
Plain,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ExperienceLogo(
|
||||||
|
#[prop(into)] image: MaybeSignal<String>,
|
||||||
|
/// Name of the experience, used in the alt of the image
|
||||||
|
#[prop(into)]
|
||||||
|
name: MaybeSignal<String>,
|
||||||
|
#[prop(into, optional)] background: MaybeSignal<ImageBackground>,
|
||||||
|
#[prop(into, optional)] class: MaybeSignal<String>,
|
||||||
|
#[prop(attrs)] attributes: Vec<(&'static str, Attribute)>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let class = create_memo(move |_| {
|
||||||
|
let background = background.get();
|
||||||
|
let logo = LogoOptions { background };
|
||||||
|
logo.with_class(class.get())
|
||||||
|
});
|
||||||
|
let alt = format!("{} logo", name.get());
|
||||||
|
|
||||||
|
view! { <img {..attributes} loading="lazy" src=image.get() alt=alt class=class/> }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExperienceLogo {
|
||||||
|
file: String,
|
||||||
|
options: Option<LogoOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExperienceHeader {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
date_start: Date,
|
||||||
|
date_end: Option<Date>,
|
||||||
|
logo: Option<ExperienceLogo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoView for ExperienceHeader {
|
||||||
|
fn into_view(self) -> View {
|
||||||
|
let logo = match self.logo {
|
||||||
|
Some(logo) => view! {
|
||||||
|
<ExperienceLogo
|
||||||
|
image=logo.file
|
||||||
|
name=self.name.clone()
|
||||||
|
background=logo.options.map(|o| o.background).unwrap_or_default()
|
||||||
|
class=tw_join!("mr-4")
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
.into_view(),
|
||||||
|
None => view! {}.into_view(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let date = match self.date_end {
|
||||||
|
Some(end) => format!("{} - {}", self.date_start, end),
|
||||||
|
None => format!("Since {}", self.date_start),
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!("flex", "flex-col")>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"flex", "flex-row"
|
||||||
|
)>
|
||||||
|
{logo} <div class=tw_join!("flex", "flex-col", "justify-evenly")>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"text-xl", "md:text-2xl", "font-semibold"
|
||||||
|
)>{self.name}</div>
|
||||||
|
<div class=tw_join!("text-xs", "md:text-sm")>{date}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class=tw_join!(
|
||||||
|
"text-xl", "md:text-2xl", "font-semibold", "my-4"
|
||||||
|
)>{self.description}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_view()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExperienceHeader {
|
||||||
|
pub fn new(
|
||||||
|
start_date: &str,
|
||||||
|
end_date: Option<&str>,
|
||||||
|
name: &str,
|
||||||
|
description: &str,
|
||||||
|
logo: Option<&Logo>,
|
||||||
|
) -> Self {
|
||||||
|
let date_start: Vec<_> = start_date.split('-').collect();
|
||||||
|
|
||||||
|
let date_end = end_date.map(|end| {
|
||||||
|
let end: Vec<_> = end.split('-').collect();
|
||||||
|
Date {
|
||||||
|
year: end[0].parse().expect("not a number"),
|
||||||
|
month: end[1].parse().expect("not a number"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let logo = logo.map(|logo| ExperienceLogo {
|
||||||
|
file: logo.file.into(),
|
||||||
|
options: if logo.transparent_background {
|
||||||
|
Some(LogoOptions {
|
||||||
|
background: ImageBackground::Transparent,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
description: description.into(),
|
||||||
|
date_start: Date {
|
||||||
|
year: date_start[0].parse().expect("not a number"),
|
||||||
|
month: date_start[1].parse().expect("not a number"),
|
||||||
|
},
|
||||||
|
date_end,
|
||||||
|
logo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
crates/plcom/src/pages/root/friends.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct Name {
|
||||||
|
first: String,
|
||||||
|
last: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Name {
|
||||||
|
pub fn nick(nick: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
first: nick.into(),
|
||||||
|
last: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new(first: impl Into<String>, last: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
first: first.into(),
|
||||||
|
last: Some(last.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initials(&self) -> String {
|
||||||
|
let first = self
|
||||||
|
.first
|
||||||
|
.to_uppercase()
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.expect("Invalid first name");
|
||||||
|
let last = self
|
||||||
|
.last
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|last| last.to_uppercase().chars().next());
|
||||||
|
|
||||||
|
match last {
|
||||||
|
Some(last) => format!("{first}{last}"),
|
||||||
|
None => first.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Name {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match &self.last {
|
||||||
|
Some(last) => write!(f, "{} {}", self.first, last),
|
||||||
|
None => write!(f, "{}", self.first),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct Friend {
|
||||||
|
name: Name,
|
||||||
|
uri: Uri,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Friend {
|
||||||
|
pub fn nick(nick: impl Into<String>, uri: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: Name::nick(nick),
|
||||||
|
uri: Uri::from_static(uri),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(first: impl Into<String>, last: impl Into<String>, uri: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: Name::new(first, last),
|
||||||
|
uri: Uri::from_static(uri),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn domain_name(&self) -> String {
|
||||||
|
self.uri
|
||||||
|
.authority()
|
||||||
|
.map(|authority| authority.to_string())
|
||||||
|
.unwrap_or_else(|| self.uri.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Friend(#[prop(into)] friend: MaybeSignal<Friend>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<a
|
||||||
|
href=friend.get().uri.to_string()
|
||||||
|
target="_blank"
|
||||||
|
class=tw_join!(
|
||||||
|
"hover:bg-gray-500", "transition-all", "duration-200", "flex", "items-center",
|
||||||
|
"rounded-lg", "p-2"
|
||||||
|
)
|
||||||
|
>
|
||||||
|
|
||||||
|
<span class=tw_join!(
|
||||||
|
"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.get().name.initials()}</span>
|
||||||
|
<div>
|
||||||
|
<p class=tw_join!("font-bold")>{friend.get().name.to_string()}</p>
|
||||||
|
<p>{friend.get().domain_name()}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Friends() -> impl IntoView {
|
||||||
|
let friends = [
|
||||||
|
Friend::new("Paolo", "Rotolo", "https://rotolo.dev"),
|
||||||
|
Friend::new("Polly", "Bishop", "https://github.com/itspolly"),
|
||||||
|
Friend::new("Ayden", "Panhuyzen", "https://ayden.dev"),
|
||||||
|
Friend::new("Corbin", "Crutchley", "https://crutchcorn.dev"),
|
||||||
|
Friend::new("James", "Fenn", "https://jfenn.me"),
|
||||||
|
Friend::new("Alex", "Dueppen", "https://ajd.sh"),
|
||||||
|
Friend::new("Lyra", "Messier", "https://lyramsr.co"),
|
||||||
|
Friend::new("Peter", "Soboyejo", "https://twitter.com/pxtvr"),
|
||||||
|
Friend::nick("Millomaker", "https://youtube.com/millomaker"),
|
||||||
|
Friend::new("Alexandre", "Wagner", "https://wagnerwave.com"),
|
||||||
|
Friend::new("Aidan", "Follestad", "https://af.codes"),
|
||||||
|
Friend::new("Victor", "Simon", "https://simonvictor.com"),
|
||||||
|
];
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Friends"</h1>
|
||||||
|
<p class=tw_join!("text-lg")>"Folks I worked with, or I like what they do."</p>
|
||||||
|
|
||||||
|
<ul class=tw_join!(
|
||||||
|
"my-4", "grid", "grid-cols-1", "sm:grid-cols-2", "md:grid-cols-3", "lg:grid-cols-4",
|
||||||
|
"sm:gap-4"
|
||||||
|
)>
|
||||||
|
{friends
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| {
|
||||||
|
view! {
|
||||||
|
<li class=tw_join!("py-2")>
|
||||||
|
<Friend friend=f/>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>"If you do not appear here and we know each other, hit me up!"</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
100
crates/plcom/src/pages/root/hero.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn WallpaperInfo(#[prop(into)] wallpaper: &'static Wallpaper) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!(
|
||||||
|
"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"
|
||||||
|
)>
|
||||||
|
|
||||||
|
// See more
|
||||||
|
<div class=tw_join!("flex")>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"inline-flex", "items-center"
|
||||||
|
)>
|
||||||
|
{Icon::Map}
|
||||||
|
<a
|
||||||
|
class=tw_join!("ml-1", "text-sm", "underline")
|
||||||
|
href="/wallpapers"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
"See more!"
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Location
|
||||||
|
<div class=tw_join!("flex")>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"inline-flex", "items-center"
|
||||||
|
)>
|
||||||
|
{Icon::Location}
|
||||||
|
<span class=tw_join!(
|
||||||
|
"ml-1", "text-sm"
|
||||||
|
)>
|
||||||
|
{wallpaper.location.precise}
|
||||||
|
<span class=tw_join!(
|
||||||
|
"hidden", "md:inline"
|
||||||
|
)>", "{wallpaper.location.broad}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Date
|
||||||
|
<div class=tw_join!("flex")>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"inline-flex", "items-center"
|
||||||
|
)>
|
||||||
|
{Icon::Calendar} <span class=tw_join!("ml-1", "text-sm")>{wallpaper.date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Hero(#[prop(into)] wallpaper: Option<&'static Wallpaper>) -> impl IntoView {
|
||||||
|
let (wallpaper_info, background_image) = match wallpaper {
|
||||||
|
Some(wallpaper) => (
|
||||||
|
view! { <WallpaperInfo wallpaper=wallpaper/> }.into_view(),
|
||||||
|
format!("background-image: url(/wallpapers/{});", wallpaper.filename),
|
||||||
|
),
|
||||||
|
None => (view! {}.into_view(), "".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!("bg-gradient-to-r", "from-red-900", "via-teal-900", "to-fuchsia-900")>
|
||||||
|
<div
|
||||||
|
id="wallpaper"
|
||||||
|
class=tw_join!(
|
||||||
|
"relative", "text-white", "w-full", "h-almostscreen", "bg-center", "bg-cover"
|
||||||
|
)
|
||||||
|
|
||||||
|
style=background_image
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class=tw_join!(
|
||||||
|
"container", "mx-auto", "px-8", "py-16", "w-full", "h-full", "justify-center",
|
||||||
|
"items-center", "flex", "flex-col"
|
||||||
|
)>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"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=tw_join!(
|
||||||
|
"text-3xl", "sm:text-4xl", "font-bold"
|
||||||
|
)>"Philippe Loctaux"</h1>
|
||||||
|
<h2 class=tw_join!(
|
||||||
|
"sm:text-xl", "font-semibold"
|
||||||
|
)>"Developer of all sorts. Epitech alumni, class of 2023."</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{wallpaper_info}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
76
crates/plcom/src/pages/root/jobs.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
use super::experience::*;
|
||||||
|
|
||||||
|
impl IntoView for Work {
|
||||||
|
fn into_view(self) -> View {
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!("w-full", "rounded-2xl", "bg-sky-950")>
|
||||||
|
|
||||||
|
<div class=tw_join!(
|
||||||
|
"p-6", "justify-between", "h-full"
|
||||||
|
)>
|
||||||
|
|
||||||
|
{ExperienceHeader::new(
|
||||||
|
self.start_date,
|
||||||
|
self.end_date,
|
||||||
|
self.name,
|
||||||
|
self.position,
|
||||||
|
Some(&self.logo),
|
||||||
|
)} <div class=tw_join!("space-y-2")>
|
||||||
|
|
||||||
|
<p>{self.description}</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul class=tw_join!(
|
||||||
|
"list-disc", "mt-6"
|
||||||
|
)>
|
||||||
|
{self
|
||||||
|
.highlights
|
||||||
|
.iter()
|
||||||
|
.map(|h| {
|
||||||
|
view! { <li class=tw_join!("ml-5")>{*h}</li> }
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"mt-6 flex flex-wrap gap-x-6 gap-y-4"
|
||||||
|
)>
|
||||||
|
{self
|
||||||
|
.technologies
|
||||||
|
.iter()
|
||||||
|
.map(|t| {
|
||||||
|
view! {
|
||||||
|
<span class=tw_join!(
|
||||||
|
"inline-flex", "items-center", "rounded-md", "bg-blue-100",
|
||||||
|
"px-2", "py-1", "font-medium", "text-blue-700"
|
||||||
|
)>{*t}</span>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div> {self.link}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}.into_view()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Jobs() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Professional Experiences"</h1>
|
||||||
|
|
||||||
|
<div class=tw_join!(
|
||||||
|
"mt-4", "grid", "grid-cols-1", "sm:grid-cols-2", "gap-4"
|
||||||
|
)>{resume::WORK.collect_view()}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
163
crates/plcom/src/pages/root/projects.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
use super::experience::*;
|
||||||
|
|
||||||
|
impl IntoView for Project {
|
||||||
|
fn into_view(self) -> View {
|
||||||
|
let (css_image_position, css_image_position_corner) = match self.image {
|
||||||
|
Some(image) => {
|
||||||
|
let position = match image.position {
|
||||||
|
"left" => tw_join!("2xl:flex items-stretch justify-between"),
|
||||||
|
"right" => {
|
||||||
|
tw_join!("2xl:flex items-stretch justify-between 2xl:flex-row-reverse")
|
||||||
|
}
|
||||||
|
_ => todo!("match on an enum instead of raw strings"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let corner = match image.position {
|
||||||
|
"left" => tw_join!("2xl:rounded-tr-none", "2xl:rounded-l-2xl"),
|
||||||
|
"right" => tw_join!("2xl:rounded-tl-none", "2xl:rounded-r-2xl"),
|
||||||
|
_ => todo!("match on an enum instead of raw strings"),
|
||||||
|
};
|
||||||
|
let corner = tw_join!(
|
||||||
|
"flex",
|
||||||
|
"w-full",
|
||||||
|
"2xl:w-1/2",
|
||||||
|
"grow",
|
||||||
|
"rounded-t-2xl",
|
||||||
|
"object-cover",
|
||||||
|
corner
|
||||||
|
);
|
||||||
|
|
||||||
|
(position, corner)
|
||||||
|
}
|
||||||
|
None => ("".into(), "".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!(
|
||||||
|
"w-full", "rounded-2xl", "bg-pink-950", css_image_position
|
||||||
|
)>
|
||||||
|
|
||||||
|
{if let Some(image) = self.image {
|
||||||
|
view! {
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
src=image.file
|
||||||
|
alt=format!("{} image", self.name)
|
||||||
|
class=css_image_position_corner
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
.into_view()
|
||||||
|
} else {
|
||||||
|
view! {}.into_view()
|
||||||
|
}}
|
||||||
|
<div class=tw_join!(
|
||||||
|
"p-6", "justify-between", "h-full"
|
||||||
|
)>
|
||||||
|
|
||||||
|
{ExperienceHeader::new(
|
||||||
|
self.start_date,
|
||||||
|
self.end_date,
|
||||||
|
self.name,
|
||||||
|
self.description,
|
||||||
|
self.logo.as_ref(),
|
||||||
|
)}
|
||||||
|
<div class=tw_join!(
|
||||||
|
"space-y-2"
|
||||||
|
)>
|
||||||
|
|
||||||
|
{self
|
||||||
|
.presentation
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
view! { <p>{*p}</p> }
|
||||||
|
})
|
||||||
|
.collect_view()} <div>
|
||||||
|
<ul class=tw_join!(
|
||||||
|
"list-disc", "mt-6"
|
||||||
|
)>
|
||||||
|
{self
|
||||||
|
.highlights
|
||||||
|
.iter()
|
||||||
|
.map(|h| {
|
||||||
|
view! { <li class=tw_join!("ml-5")>{*h}</li> }
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</ul>
|
||||||
|
</div> <div>
|
||||||
|
|
||||||
|
<div class=tw_join!(
|
||||||
|
"mt-6", "grid", "grid-cols-2", "sm:grid-cols-3", "gap-x-6",
|
||||||
|
"gap-y-4"
|
||||||
|
)>
|
||||||
|
{self
|
||||||
|
.keywords
|
||||||
|
.iter()
|
||||||
|
.map(|t| {
|
||||||
|
view! {
|
||||||
|
<span class=tw_join!(
|
||||||
|
"items-center", "rounded-md", "bg-blue-100", "px-2", "py-1",
|
||||||
|
"font-medium", "text-blue-700",
|
||||||
|
)>{*t}</span>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div> {self.link}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}.into_view()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageProject = Project;
|
||||||
|
type TextProject = Project;
|
||||||
|
|
||||||
|
enum DisplayProject {
|
||||||
|
Text(Box<(TextProject, Option<TextProject>)>),
|
||||||
|
Image(ImageProject)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoView for DisplayProject {
|
||||||
|
fn into_view(self) -> View {
|
||||||
|
match self {
|
||||||
|
Self::Image(image) => image.into_view(),
|
||||||
|
Self::Text(boxy) => {
|
||||||
|
let (text1, text2) = *boxy;
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!(
|
||||||
|
"my-4", "grid", "grid-cols-1", "sm:grid-cols-2", "gap-4"
|
||||||
|
)>{text1} {text2}</div>
|
||||||
|
}.into_view()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Projects() -> impl IntoView {
|
||||||
|
let mut projects = vec![];
|
||||||
|
let mut iter = resume::PROJECTS.iter();
|
||||||
|
|
||||||
|
while let Some(cur_proj) = iter.next() {
|
||||||
|
if cur_proj.image.is_some() {
|
||||||
|
projects.push(DisplayProject::Image(*cur_proj));
|
||||||
|
} else {
|
||||||
|
let next_text = iter.next();
|
||||||
|
projects.push(DisplayProject::Text(Box::new((*cur_proj, next_text.copied()))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Projects"</h1>
|
||||||
|
|
||||||
|
{projects}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
137
crates/plcom/src/pages/root/talks.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct Talk {
|
||||||
|
title: String,
|
||||||
|
date: Date,
|
||||||
|
location: String,
|
||||||
|
link: Link,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Talk {
|
||||||
|
pub fn new(
|
||||||
|
title: impl Into<String>,
|
||||||
|
date: Date,
|
||||||
|
location: impl Into<String>,
|
||||||
|
link: Link,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
title: title.into(),
|
||||||
|
date,
|
||||||
|
location: location.into(),
|
||||||
|
link,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Talk(#[prop(into)] talk: MaybeSignal<Talk>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!("rounded-2xl", "w-full", "bg-teal-950", "p-6")>
|
||||||
|
|
||||||
|
<h3 class=tw_join!("text-xl", "font-semibold", "mb-4")>{talk.get().title}</h3>
|
||||||
|
|
||||||
|
<div class=tw_join!("flex")>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"inline-flex", "items-center"
|
||||||
|
)>
|
||||||
|
{Icon::Calendar}
|
||||||
|
<span class=tw_join!("ml-2")>{talk.get().date.to_string()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=tw_join!("flex")>
|
||||||
|
<div class=tw_join!(
|
||||||
|
"inline-flex", "items-center"
|
||||||
|
)>{Icon::Location} <span class=tw_join!("ml-2")>{talk.get().location}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OutlineButtonLink link=talk.get().link/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Talks() -> impl IntoView {
|
||||||
|
let talks = [
|
||||||
|
Talk::new(
|
||||||
|
"Vim",
|
||||||
|
Date {
|
||||||
|
year: 2023,
|
||||||
|
month: 2,
|
||||||
|
},
|
||||||
|
"Epitech Rennes",
|
||||||
|
Link::slides("/pub/talks/vim.pdf"),
|
||||||
|
),
|
||||||
|
Talk::new(
|
||||||
|
"CLion",
|
||||||
|
Date {
|
||||||
|
year: 2021,
|
||||||
|
month: 3,
|
||||||
|
},
|
||||||
|
"Epitech Rennes",
|
||||||
|
Link::slides("/pub/talks/clion.pdf"),
|
||||||
|
),
|
||||||
|
Talk::new(
|
||||||
|
"git & devops 2",
|
||||||
|
Date {
|
||||||
|
year: 2021,
|
||||||
|
month: 2,
|
||||||
|
},
|
||||||
|
"Epitech Rennes",
|
||||||
|
Link::slides("/pub/talks/git-devops2.pdf"),
|
||||||
|
),
|
||||||
|
Talk::new(
|
||||||
|
"pass4thewin",
|
||||||
|
Date {
|
||||||
|
year: 2021,
|
||||||
|
month: 2,
|
||||||
|
},
|
||||||
|
"Epitech Rennes",
|
||||||
|
Link::slides("/pub/talks/pass4thewin.pdf"),
|
||||||
|
),
|
||||||
|
Talk::new(
|
||||||
|
"git & devops",
|
||||||
|
Date {
|
||||||
|
year: 2020,
|
||||||
|
month: 5,
|
||||||
|
},
|
||||||
|
"Epitech Rennes",
|
||||||
|
Link::slides("/pub/talks/git-devops.pdf"),
|
||||||
|
),
|
||||||
|
Talk::new(
|
||||||
|
"git gud",
|
||||||
|
Date {
|
||||||
|
year: 2019,
|
||||||
|
month: 5,
|
||||||
|
},
|
||||||
|
"Epitech Rennes",
|
||||||
|
Link::slides("/pub/talks/git-tek.pdf"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Talks"</h1>
|
||||||
|
<p class=tw_join!(
|
||||||
|
"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=tw_join!(
|
||||||
|
"mt-4", "grid", "grid-cols-1", "sm:grid-cols-2", "lg:grid-cols-3", "gap-6",
|
||||||
|
"place-content-center"
|
||||||
|
)>
|
||||||
|
{talks
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| {
|
||||||
|
view! { <Talk talk=t/> }
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
65
crates/plcom/src/pages/root/www.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct Www {
|
||||||
|
link: Link,
|
||||||
|
icon: Icon,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Www() -> impl IntoView {
|
||||||
|
let www = [
|
||||||
|
Www {
|
||||||
|
link: Link::new("https://twitter.com/philippeloctaux", "Twitter"),
|
||||||
|
icon: Icon::Twitter,
|
||||||
|
},
|
||||||
|
Www {
|
||||||
|
link: Link::new("https://t.me/philippeloctaux", "Telegram"),
|
||||||
|
icon: Icon::Telegram,
|
||||||
|
},
|
||||||
|
Www {
|
||||||
|
link: Link::new("https://mastodon.social/@philt3r", "Mastodon"),
|
||||||
|
icon: Icon::Mastodon,
|
||||||
|
},
|
||||||
|
Www {
|
||||||
|
link: Link::new("https://github.com/x4m3", "GitHub"),
|
||||||
|
icon: Icon::Github,
|
||||||
|
},
|
||||||
|
Www {
|
||||||
|
link: Link::new("https://linkedin.com/in/philippeloctaux", "LinkedIn"),
|
||||||
|
icon: Icon::Linkedin,
|
||||||
|
},
|
||||||
|
Www {
|
||||||
|
link: Link::new("/email", "Email"),
|
||||||
|
icon: Icon::Email,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!(
|
||||||
|
"grid", "grid-cols-3", "lg:grid-cols-6", "gap-4", "place-content-center"
|
||||||
|
)>
|
||||||
|
|
||||||
|
{www
|
||||||
|
.into_iter()
|
||||||
|
.map(|w| {
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!("w-full", "h-auto", "md:w-auto")>
|
||||||
|
<div class=tw_join!("text-center")>
|
||||||
|
<ButtonLink
|
||||||
|
link=w.link
|
||||||
|
icon=w.icon
|
||||||
|
hide_text_small_display=true
|
||||||
|
attributes=vec![
|
||||||
|
("target", Attribute::String(Oco::Borrowed("_blank"))),
|
||||||
|
]
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view()}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
49
crates/plcom/src/pages/wallpapers.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn WallpapersPage() -> impl IntoView {
|
||||||
|
use leptos_leaflet::{TileLayer, MapContainer, Position, Marker, position, Popup};
|
||||||
|
|
||||||
|
let wallpapers = WALLPAPERS;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<ContentPage title="Wallpapers">
|
||||||
|
<Stylesheet href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||||
|
<Script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"/>
|
||||||
|
<p class=tw_join!("mb-2")>"Pictures I took around the world"</p>
|
||||||
|
<MapContainer
|
||||||
|
center=Position::new(0.0, 0.0)
|
||||||
|
zoom=1.0
|
||||||
|
class=tw_join!("w-full", "h-halfscreen")
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
attribution="© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors"
|
||||||
|
/>
|
||||||
|
<For
|
||||||
|
each=move || wallpapers
|
||||||
|
key=|w| w.filename
|
||||||
|
children=move |w: Wallpaper| {
|
||||||
|
let uri = format!("/wallpapers/{}", w.filename);
|
||||||
|
view! {
|
||||||
|
<Marker position=position!(
|
||||||
|
w.gps.latitude.into(), w.gps.longitude.into()
|
||||||
|
)>
|
||||||
|
<Popup>
|
||||||
|
<a target="_blank" href=uri>
|
||||||
|
{w.location.precise}
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
|
{w.location.broad}
|
||||||
|
<br/>
|
||||||
|
<b>{w.date}</b>
|
||||||
|
</Popup>
|
||||||
|
</Marker>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</MapContainer>
|
||||||
|
</ContentPage>
|
||||||
|
}
|
||||||
|
}
|
||||||
16
crates/plcom/tailwind.config.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: {
|
||||||
|
relative: true,
|
||||||
|
files: ["./src/**/*.rs"],
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
height: {
|
||||||
|
almostscreen: "90vh",
|
||||||
|
halfscreen: "60vh",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
BIN
public/icons/rubycat.png
Normal file
|
After Width: | Height: | Size: 496 KiB |
BIN
public/icons/uqac.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/phil.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
|
@ -6,7 +6,7 @@ http://philippeloctaux.com
|
||||||
|
|
||||||
## tech
|
## tech
|
||||||
|
|
||||||
- https://rocket.rs
|
- https://leptos.dev
|
||||||
- https://tailwindcss.com
|
- https://tailwindcss.com
|
||||||
|
|
||||||
## colors
|
## colors
|
||||||
|
|
@ -16,9 +16,10 @@ 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. run `cargo run --bin gen-wallpapers` to generate wallpaper metadata
|
2. run `cargo run -p gen-wallpapers --example cli -- ./public/wallpapers > ./crates/plcom/wallpapers.json` to generate wallpaper metadata
|
||||||
|
|
||||||
## icons
|
## icons
|
||||||
|
|
||||||
- https://simpleicons.org for brand icons
|
- https://simpleicons.org for brand icons
|
||||||
- https://heroicons.com for the rest
|
- https://heroicons.com for the rest
|
||||||
|
|
||||||
|
|
|
||||||
54
src/cache.rs
|
|
@ -1,54 +0,0 @@
|
||||||
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,11 +0,0 @@
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
78
src/main.rs
|
|
@ -1,78 +0,0 @@
|
||||||
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() -> _ {
|
|
||||||
let server = rocket::build()
|
|
||||||
.mount("/", FileServer::from("public"))
|
|
||||||
.mount("/", routes![root, email, wallpapers_route])
|
|
||||||
.register("/", catchers![not_found]);
|
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
server
|
|
||||||
} else {
|
|
||||||
server
|
|
||||||
.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("/?<wallpaper>")]
|
|
||||||
fn root(wallpaper: Option<&str>) -> templates::Root<'static> {
|
|
||||||
templates::Root {
|
|
||||||
title: "Philippe Loctaux",
|
|
||||||
year: chrono::Utc::now().year(),
|
|
||||||
wallpaper: wallpaper
|
|
||||||
.and_then(Wallpaper::find)
|
|
||||||
.or_else(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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/wallpapers")]
|
|
||||||
fn wallpapers_route() -> templates::Wallpapers<'static> {
|
|
||||||
templates::Wallpapers {
|
|
||||||
title: "Wallpapers",
|
|
||||||
year: chrono::Utc::now().year(),
|
|
||||||
wallpapers: WALLPAPERS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
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,57 +0,0 @@
|
||||||
use crate::filters;
|
|
||||||
use crate::types::*;
|
|
||||||
|
|
||||||
#[derive(askama::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(askama::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(askama::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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "pages/wallpapers.html")]
|
|
||||||
pub struct Wallpapers<'a> {
|
|
||||||
pub title: &'a str,
|
|
||||||
pub year: i32,
|
|
||||||
pub wallpapers: &'static [&'static Wallpaper],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(rocket::Responder)]
|
|
||||||
struct WallpapersResponder<'a> {
|
|
||||||
template: Wallpapers<'a>,
|
|
||||||
}
|
|
||||||
65
src/types.rs
|
|
@ -1,65 +0,0 @@
|
||||||
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 gps: Gps,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Gps {
|
|
||||||
pub latitude: f32,
|
|
||||||
pub longitude: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Gps {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "[{}, {}]", self.latitude, self.longitude)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find(filename: &str) -> Option<&'static &'static Wallpaper> {
|
|
||||||
WALLPAPERS.iter().find(|w| w.file.contains(filename))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
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
|
|
@ -1,111 +0,0 @@
|
||||||
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"],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,247 +0,0 @@
|
||||||
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: "January - July 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."
|
|
||||||
],
|
|
||||||
accomplishments: vec![],
|
|
||||||
technologies: vec!["FFmpeg", "Shell scripting", "nginx"],
|
|
||||||
|
|
||||||
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",
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
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,15 +0,0 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: {
|
|
||||||
files: ["./templates/**/*.html"],
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
height: {
|
|
||||||
almostscreen: '90vh',
|
|
||||||
halfscreen: '60vh',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
{% 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 %}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 547 B |
|
|
@ -1,14 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 556 B |
|
|
@ -1,9 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 887 B |
|
|
@ -1,13 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 596 B |
|
|
@ -1,9 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 673 B |
|
|
@ -1,11 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 613 B |
|
|
@ -1,10 +0,0 @@
|
||||||
<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="M8.157 2.175a1.5 1.5 0 00-1.147 0l-4.084 1.69A1.5 1.5 0 002 5.251v10.877a1.5 1.5 0 002.074 1.386l3.51-1.453 4.26 1.763a1.5 1.5 0 001.146 0l4.083-1.69A1.5 1.5 0 0018 14.748V3.873a1.5 1.5 0 00-2.073-1.386l-3.51 1.452-4.26-1.763zM7.58 5a.75.75 0 01.75.75v6.5a.75.75 0 01-1.5 0v-6.5A.75.75 0 017.58 5zm5.59 2.75a.75.75 0 00-1.5 0v6.5a.75.75 0 001.5 0v-6.5z"
|
|
||||||
clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 566 B |
|
|
@ -1,9 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,9 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 805 B |
|
|
@ -1,9 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 666 B |
|
|
@ -1,5 +0,0 @@
|
||||||
{% extends "content.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<p>This page could not be found.</p>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{% 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 %}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{% 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 %}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<!-- See more -->
|
|
||||||
<div class="flex">
|
|
||||||
<div class="inline-flex items-center">
|
|
||||||
{% include "icons/map.html" %}
|
|
||||||
<a class="ml-1 text-sm underline" href="/wallpapers">See more!</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
{% 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>
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<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,39 +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="/pub/phil.png"
|
|
||||||
alt="Phil"
|
|
||||||
class="rounded-3xl bg-sky-900 h-36 w-36 md:mx-auto md:h-56 md:w-56 lg:h-64 lg:w-64 mb-2 md:mb-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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 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 apply 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>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<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,27 +0,0 @@
|
||||||
{% extends "content.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<p class="mb-2">Pictures I took around the world</p>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
||||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
||||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
|
||||||
|
|
||||||
<div id="map" class="w-full h-halfscreen"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let map = L.map('map').setView([0, 0], 1);
|
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
maxZoom: 19,
|
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
||||||
}).addTo(map);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% for wallpaper in wallpapers %}
|
|
||||||
<script>
|
|
||||||
var marker = L.marker({{ wallpaper.location.gps }}).addTo(map);
|
|
||||||
marker.bindPopup(`<a target="_blank" href="{{ wallpaper.file }}">{{ wallpaper.location.precise }}</a><br>{{ wallpaper.location.broad }}<br><b>{{ wallpaper.date }}</b>`);
|
|
||||||
</script>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
|
||||||