Compare commits
27 commits
a770e3e209
...
3f1abb9956
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f1abb9956 | |||
| 97ee77f9bb | |||
| 958fa9baed | |||
| 3924b4845d | |||
| 097185ce4f | |||
| b95c92cfb2 | |||
| e109a942f2 | |||
| dfd521bb21 | |||
| e1f561d6af | |||
| fd7f6e02db | |||
| cb8e992f27 | |||
| a1f5d93a56 | |||
| 891965c113 | |||
| 83a7d6de14 | |||
| 821ddba3b7 | |||
| 3471ccdfbf | |||
| 2673c183a5 | |||
| b0ecc8060c | |||
| 0108e64c48 | |||
| db66f74900 | |||
| 18f4e7d958 | |||
| 6bda740440 | |||
| af5ab5b197 | |||
| 1d1b7db7b7 | |||
| feba0ad668 | |||
| a573a1ab7b | |||
| 74bcb1dcd0 |
43 changed files with 4529 additions and 2426 deletions
|
|
@ -1,10 +0,0 @@
|
||||||
target/
|
|
||||||
wallpapers.json
|
|
||||||
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
.DS_Store
|
|
||||||
.gitea/
|
|
||||||
readme.md
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
|
|
@ -4,7 +4,7 @@ root = true
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
|
|
||||||
[*.js]
|
[*.{js, nix}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,12 +1,10 @@
|
||||||
# built css
|
# built css
|
||||||
/public/style.css
|
/public/style.css
|
||||||
|
|
||||||
# wallpapers
|
|
||||||
wallpapers.json
|
|
||||||
|
|
||||||
# build output
|
# build output
|
||||||
target/
|
target/
|
||||||
pkg
|
pkg
|
||||||
|
/result
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
|
|
@ -20,3 +18,6 @@ pkg
|
||||||
|
|
||||||
# rustfmt
|
# rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# vim
|
||||||
|
[._]*.sw[a-p]
|
||||||
|
|
|
||||||
15
Cargo.toml
15
Cargo.toml
|
|
@ -1,15 +0,0 @@
|
||||||
[workspace]
|
|
||||||
resolver = "2"
|
|
||||||
members = ["crates/gen-wallpapers", "crates/plcom"]
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
serde = "1.0"
|
|
||||||
|
|
||||||
# Defines a size-optimized profile for the WASM bundle in release mode
|
|
||||||
[profile.wasm-release]
|
|
||||||
inherits = "release"
|
|
||||||
opt-level = 'z'
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
panic = "abort"
|
|
||||||
|
|
||||||
51
Dockerfile
51
Dockerfile
|
|
@ -1,51 +0,0 @@
|
||||||
ARG RUST_VERSION=1.77
|
|
||||||
FROM rust:${RUST_VERSION}-bookworm as builder
|
|
||||||
|
|
||||||
# Install cargo-binstall, which makes it easier to install other
|
|
||||||
# cargo extensions like cargo-leptos
|
|
||||||
RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
|
|
||||||
RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
|
|
||||||
RUN cp cargo-binstall /usr/local/cargo/bin
|
|
||||||
|
|
||||||
# Install cargo-leptos
|
|
||||||
RUN cargo binstall cargo-leptos -y
|
|
||||||
|
|
||||||
# Add the WASM target
|
|
||||||
RUN rustup target add wasm32-unknown-unknown
|
|
||||||
|
|
||||||
# Make an /app dir, which everything will eventually live in
|
|
||||||
RUN mkdir -p /app
|
|
||||||
WORKDIR /app
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Generate wallpapers metadata
|
|
||||||
RUN cargo run -p gen-wallpapers --example cli -- ./public/wallpapers > crates/plcom/wallpapers.json
|
|
||||||
|
|
||||||
# Build the app
|
|
||||||
RUN cargo leptos build --release -vv
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim as runtime
|
|
||||||
WORKDIR /app
|
|
||||||
RUN apt-get update -y \
|
|
||||||
&& apt-get install -y --no-install-recommends openssl ca-certificates \
|
|
||||||
&& apt-get autoremove -y \
|
|
||||||
&& apt-get clean -y \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy the server binary to the /app directory
|
|
||||||
COPY --from=builder /app/target/release/plcom /app/
|
|
||||||
|
|
||||||
# /target/site contains our JS/WASM/CSS, etc.
|
|
||||||
COPY --from=builder /app/target/site /app/site
|
|
||||||
|
|
||||||
# Copy Cargo.toml if it’s needed at runtime
|
|
||||||
COPY --from=builder /app/Cargo.toml /app/
|
|
||||||
|
|
||||||
# Set any required env variables and
|
|
||||||
ENV RUST_LOG="info"
|
|
||||||
ENV LEPTOS_SITE_ADDR="0.0.0.0:8000"
|
|
||||||
ENV LEPTOS_SITE_ROOT="site"
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Run the server
|
|
||||||
CMD ["/app/plcom"]
|
|
||||||
1725
crates/gen-wallpapers/Cargo.lock
generated
Normal file
1725
crates/gen-wallpapers/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,79 +1,27 @@
|
||||||
use exif::{DateTime, Exif, In, Tag};
|
mod location;
|
||||||
|
|
||||||
|
use location::Gps;
|
||||||
|
use location::Location;
|
||||||
|
use location::parse_coordinates;
|
||||||
|
|
||||||
|
use exif::{DateTime, In, Tag};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fs::ReadDir;
|
use std::fs::ReadDir;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
|
|
||||||
fn parse_coordinates(exif: &Exif, tag: Tag, r#ref: Tag) -> Option<f32> {
|
|
||||||
let mut coord = None;
|
|
||||||
let mut coord_ref = None;
|
|
||||||
|
|
||||||
// Parse DMS coordinates
|
#[derive(Debug, Serialize)]
|
||||||
if let Some(field) = exif.get_field(tag, In::PRIMARY) {
|
pub struct Metadata {
|
||||||
match field.value {
|
pub filename: String,
|
||||||
exif::Value::Rational(ref vec) if !vec.is_empty() => {
|
pub date: String,
|
||||||
let deg = vec[0].to_f64() as u16;
|
pub width: u32,
|
||||||
let min = vec[1].to_f64() as u8;
|
pub height: u32,
|
||||||
let sec = vec[2].to_f64();
|
pub gps: Gps,
|
||||||
|
pub location: Option<Location>,
|
||||||
coord = Some((deg, min, sec));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get bearing
|
|
||||||
if let Some(field) = exif.get_field(r#ref, In::PRIMARY) {
|
|
||||||
coord_ref = Some(field.display_value().to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
match (coord, coord_ref) {
|
|
||||||
(Some((deg, min, sec)), Some(r#ref)) => {
|
|
||||||
use dms_coordinates::Cardinal;
|
|
||||||
use dms_coordinates::Cardinal::*;
|
|
||||||
let bearing: Option<Cardinal> = match r#ref.as_str() {
|
|
||||||
"N" => Some(North),
|
|
||||||
"NE" => Some(NorthEast),
|
|
||||||
"NW" => Some(NorthWest),
|
|
||||||
"S" => Some(South),
|
|
||||||
"SE" => Some(SouthEast),
|
|
||||||
"SW" => Some(SouthWest),
|
|
||||||
"E" => Some(East),
|
|
||||||
"W" => Some(West),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let dms = dms_coordinates::DMS::new(deg, min, sec, bearing);
|
|
||||||
|
|
||||||
Some(dms.to_ddeg_angle() as f32)
|
|
||||||
}
|
|
||||||
(_, _) => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct Metadata {
|
pub struct MetadataList(pub Vec<Metadata>);
|
||||||
filename: String,
|
|
||||||
date: String,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
gps: Gps,
|
|
||||||
location: Option<Location>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct Location {
|
|
||||||
precise: String,
|
|
||||||
broad: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct Gps {
|
|
||||||
latitude: f32,
|
|
||||||
longitude: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct MetadataList(Vec<Metadata>);
|
|
||||||
|
|
||||||
impl MetadataList {
|
impl MetadataList {
|
||||||
pub fn process_folder(dir: ReadDir, get_location: bool) -> Self {
|
pub fn process_folder(dir: ReadDir, get_location: bool) -> Self {
|
||||||
|
|
@ -105,7 +53,7 @@ impl MetadataList {
|
||||||
|
|
||||||
// Get date
|
// Get date
|
||||||
let mut date = None;
|
let mut date = None;
|
||||||
if let Some(field) = exif.get_field(Tag::DateTime, In::PRIMARY) {
|
if let Some(field) = exif.get_field(Tag::DateTimeOriginal, In::PRIMARY) {
|
||||||
match field.value {
|
match field.value {
|
||||||
exif::Value::Ascii(ref vec) if !vec.is_empty() => {
|
exif::Value::Ascii(ref vec) if !vec.is_empty() => {
|
||||||
if let Ok(datetime) = DateTime::from_ascii(&vec[0]) {
|
if let Ok(datetime) = DateTime::from_ascii(&vec[0]) {
|
||||||
|
|
@ -118,6 +66,8 @@ impl MetadataList {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO: get OffsetTimeOriginal as well
|
||||||
|
// TODO: use crate `jiff` to store dates with timezone offsets
|
||||||
|
|
||||||
// Get image width
|
// Get image width
|
||||||
let mut width = None;
|
let mut width = None;
|
||||||
|
|
@ -137,10 +87,7 @@ impl MetadataList {
|
||||||
|
|
||||||
match (date, latitude, longitude, width, height) {
|
match (date, latitude, longitude, width, height) {
|
||||||
(Some(date), Some(latitude), Some(longitude), Some(width), Some(height)) => {
|
(Some(date), Some(latitude), Some(longitude), Some(width), Some(height)) => {
|
||||||
let gps = Gps {
|
let gps = Gps::new(latitude,longitude);
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
};
|
|
||||||
let location = if get_location {
|
let location = if get_location {
|
||||||
eprintln!("Getting location for `{}`", filename);
|
eprintln!("Getting location for `{}`", filename);
|
||||||
gps.get_location()
|
gps.get_location()
|
||||||
|
|
@ -151,7 +98,7 @@ impl MetadataList {
|
||||||
filename,
|
filename,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
date,
|
date: date.to_string(),
|
||||||
gps,
|
gps,
|
||||||
location,
|
location,
|
||||||
});
|
});
|
||||||
|
|
@ -169,54 +116,3 @@ impl MetadataList {
|
||||||
serde_json::to_string_pretty(&self.0)
|
serde_json::to_string_pretty(&self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gps {
|
|
||||||
fn get_location(&self) -> Option<Location> {
|
|
||||||
match reqwest::blocking::Client::new()
|
|
||||||
.get(format!(
|
|
||||||
// Documentation: https://nominatim.org/release-docs/develop/api/Reverse/
|
|
||||||
"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={}&lon={}&zoom=8",
|
|
||||||
self.latitude, self.longitude,
|
|
||||||
))
|
|
||||||
.header("User-Agent", "https://philippeloctaux.com")
|
|
||||||
.send()
|
|
||||||
.and_then(|data| data.json::<serde_json::Value>())
|
|
||||||
{
|
|
||||||
Ok(data) => {
|
|
||||||
let location = &data["display_name"];
|
|
||||||
|
|
||||||
if location.is_string() {
|
|
||||||
let location = location.to_string();
|
|
||||||
// Remove first and last characters (the string is wrapped in double quotes '"')
|
|
||||||
let location = {
|
|
||||||
let mut chars = location.chars();
|
|
||||||
chars.next();
|
|
||||||
chars.next_back();
|
|
||||||
chars.as_str()
|
|
||||||
};
|
|
||||||
eprintln!("Raw location is `{}`", location);
|
|
||||||
|
|
||||||
let mut location = location.split(',');
|
|
||||||
let precise = location.next().unwrap_or("?").to_string();
|
|
||||||
|
|
||||||
let mut broad: String =
|
|
||||||
location.collect::<Vec<&str>>().join(",").trim().to_string();
|
|
||||||
if broad.is_empty() {
|
|
||||||
broad.push('?');
|
|
||||||
}
|
|
||||||
|
|
||||||
let location = Location { precise, broad };
|
|
||||||
eprintln!("Location is `{:?}`", location);
|
|
||||||
Some(location)
|
|
||||||
} else {
|
|
||||||
eprintln!("Failed to find location.");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
eprintln!("Failed to make API call to get location.");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
116
crates/gen-wallpapers/src/location.rs
Normal file
116
crates/gen-wallpapers/src/location.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
use exif::{Exif, In, Tag};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Location {
|
||||||
|
precise: String,
|
||||||
|
broad: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Gps {
|
||||||
|
latitude: f32,
|
||||||
|
longitude: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gps {
|
||||||
|
pub fn new(latitude: f32, longitude: f32) -> Self {
|
||||||
|
Self { latitude, longitude }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_location(&self) -> Option<Location> {
|
||||||
|
match reqwest::blocking::Client::new()
|
||||||
|
.get(format!(
|
||||||
|
// Documentation: https://nominatim.org/release-docs/develop/api/Reverse/
|
||||||
|
"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={}&lon={}&zoom=8",
|
||||||
|
self.latitude, self.longitude,
|
||||||
|
))
|
||||||
|
.header("User-Agent", "https://philippeloctaux.com")
|
||||||
|
.send()
|
||||||
|
.and_then(|data| data.json::<serde_json::Value>())
|
||||||
|
{
|
||||||
|
Ok(data) => {
|
||||||
|
let location = &data["display_name"];
|
||||||
|
|
||||||
|
if location.is_string() {
|
||||||
|
let location = location.to_string();
|
||||||
|
// Remove first and last characters (the string is wrapped in double quotes '"')
|
||||||
|
let location = {
|
||||||
|
let mut chars = location.chars();
|
||||||
|
chars.next();
|
||||||
|
chars.next_back();
|
||||||
|
chars.as_str()
|
||||||
|
};
|
||||||
|
eprintln!("Raw location is `{}`", location);
|
||||||
|
|
||||||
|
let mut location = location.split(',');
|
||||||
|
let precise = location.next().unwrap_or("?").to_string();
|
||||||
|
|
||||||
|
let mut broad: String =
|
||||||
|
location.collect::<Vec<&str>>().join(",").trim().to_string();
|
||||||
|
if broad.is_empty() {
|
||||||
|
broad.push('?');
|
||||||
|
}
|
||||||
|
|
||||||
|
let location = Location { precise, broad };
|
||||||
|
eprintln!("Location is `{:?}`", location);
|
||||||
|
Some(location)
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to find location.");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Failed to make API call to get location.");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_coordinates(exif: &Exif, tag: Tag, r#ref: Tag) -> Option<f32> {
|
||||||
|
let mut coord = None;
|
||||||
|
let mut coord_ref = None;
|
||||||
|
|
||||||
|
// Parse DMS coordinates
|
||||||
|
if let Some(field) = exif.get_field(tag, In::PRIMARY) {
|
||||||
|
match field.value {
|
||||||
|
exif::Value::Rational(ref vec) if !vec.is_empty() => {
|
||||||
|
let deg = vec[0].to_f64() as u16;
|
||||||
|
let min = vec[1].to_f64() as u8;
|
||||||
|
let sec = vec[2].to_f64();
|
||||||
|
|
||||||
|
coord = Some((deg, min, sec));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bearing
|
||||||
|
if let Some(field) = exif.get_field(r#ref, In::PRIMARY) {
|
||||||
|
coord_ref = Some(field.display_value().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match (coord, coord_ref) {
|
||||||
|
(Some((deg, min, sec)), Some(r#ref)) => {
|
||||||
|
use dms_coordinates::Cardinal;
|
||||||
|
use dms_coordinates::Cardinal::*;
|
||||||
|
let bearing: Option<Cardinal> = match r#ref.as_str() {
|
||||||
|
"N" => Some(North),
|
||||||
|
"NE" => Some(NorthEast),
|
||||||
|
"NW" => Some(NorthWest),
|
||||||
|
"S" => Some(South),
|
||||||
|
"SE" => Some(SouthEast),
|
||||||
|
"SW" => Some(SouthWest),
|
||||||
|
"E" => Some(East),
|
||||||
|
"W" => Some(West),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let dms = dms_coordinates::DMS::new(deg, min, sec, bearing);
|
||||||
|
|
||||||
|
Some(dms.to_ddeg_angle() as f32)
|
||||||
|
}
|
||||||
|
(_, _) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
2869
Cargo.lock → crates/plcom/Cargo.lock
generated
2869
Cargo.lock → crates/plcom/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -3,116 +3,15 @@ name = "plcom"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "rlib"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# leptos + axum
|
rocket = "0.5"
|
||||||
axum = { version = "0.7", optional = true }
|
leptos = {version = "0.7.0-rc2" , features = ["ssr"]}
|
||||||
console_error_panic_hook = "0.1"
|
jiff = "0.1"
|
||||||
leptos = { version = "0.6" }
|
rocket_async_compression = "0.6"
|
||||||
leptos_axum = { version = "0.6", optional = true }
|
nanorand = { version = "0.7", features = ["chacha"] }
|
||||||
leptos_meta = { version = "0.6" }
|
tailwind_fuse = { version = "0.3", features = ["variant"] }
|
||||||
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.93"
|
|
||||||
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]
|
[build-dependencies]
|
||||||
serde = { workspace = true }
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
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"
|
|
||||||
|
|
|
||||||
26
crates/plcom/assets.nix
Normal file
26
crates/plcom/assets.nix
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
stdenvNoCC,
|
||||||
|
tailwindcss,
|
||||||
|
tailwindProjectRoot,
|
||||||
|
src,
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
tailwindStylesheet = import ./tailwind.nix {
|
||||||
|
stdenvNoCC = stdenvNoCC;
|
||||||
|
tailwindcss = tailwindcss;
|
||||||
|
src = tailwindProjectRoot;
|
||||||
|
inputFile = "css/main.css";
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
stdenvNoCC.mkDerivation {
|
||||||
|
name = "plcom-assets";
|
||||||
|
src = src;
|
||||||
|
buildInputs = [ tailwindStylesheet ];
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
cp -r $src/* $out/
|
||||||
|
cp ${tailwindStylesheet}/output.css $out/style.css
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
|
@ -38,12 +38,12 @@ struct Image {
|
||||||
position: String,
|
position: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
// #[derive(Debug, Deserialize)]
|
||||||
// TODO: possible implementation: https://www.reddit.com/r/rust/comments/10bab4v/serdejson_how_to_deserialize_and_serialize_an/
|
// TODO: possible implementation: https://www.reddit.com/r/rust/comments/10bab4v/serdejson_how_to_deserialize_and_serialize_an/
|
||||||
enum ImagePosition {
|
// enum ImagePosition {
|
||||||
Left,
|
// Left,
|
||||||
Right,
|
// Right,
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
@ -360,6 +360,9 @@ fn main() {
|
||||||
let wallpapers_dest = std::path::Path::new(&out_dir).join("wallpapers.rs");
|
let wallpapers_dest = std::path::Path::new(&out_dir).join("wallpapers.rs");
|
||||||
match std::fs::File::open("wallpapers.json") {
|
match std::fs::File::open("wallpapers.json") {
|
||||||
Ok(source) => wallpapers(&wallpapers_dest, source),
|
Ok(source) => wallpapers(&wallpapers_dest, source),
|
||||||
Err(_) => println!("cargo::warning=skipping wallpapers, file not found"),
|
Err(_) => {
|
||||||
|
std::fs::write(wallpapers_dest, "pub const WALLPAPERS: [Wallpaper; 0] = [];").unwrap();
|
||||||
|
println!("cargo::warning=skipping wallpapers, file not found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
59
crates/plcom/default.nix
Normal file
59
crates/plcom/default.nix
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
libiconv,
|
||||||
|
lib,
|
||||||
|
pkg-config,
|
||||||
|
stdenv,
|
||||||
|
craneLib,
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
commonArgs = {
|
||||||
|
src = lib.cleanSourceWith {
|
||||||
|
src = craneLib.path ./.; # The original, unfiltered source
|
||||||
|
filter =
|
||||||
|
path: type:
|
||||||
|
# Assets for codegen
|
||||||
|
(lib.hasSuffix ".json" path)
|
||||||
|
||
|
||||||
|
# Default filter from crane (allow .rs files)
|
||||||
|
(craneLib.filterCargoSources path type);
|
||||||
|
};
|
||||||
|
|
||||||
|
strictDeps = true;
|
||||||
|
|
||||||
|
buildInputs =
|
||||||
|
[
|
||||||
|
# Add additional build inputs here
|
||||||
|
]
|
||||||
|
++ lib.optionals stdenv.isDarwin [
|
||||||
|
libiconv
|
||||||
|
];
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
# Add extra native build inputs here, etc.
|
||||||
|
pkg-config
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Build *just* the cargo dependencies
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
|
|
||||||
|
# Clippy
|
||||||
|
clippyArtifacts = craneLib.cargoClippy (
|
||||||
|
commonArgs
|
||||||
|
// {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
# Again we apply some extra arguments only to this derivation
|
||||||
|
# and not every where else. In this case we add some clippy flags
|
||||||
|
# cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
in
|
||||||
|
craneLib.buildPackage (
|
||||||
|
commonArgs
|
||||||
|
// {
|
||||||
|
cargoExtraArgs = "-p plcom";
|
||||||
|
cargoArtifacts = clippyArtifacts;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
use crate::{
|
|
||||||
error_template::{AppError, ErrorTemplate},
|
|
||||||
pages::*,
|
|
||||||
Link, UnderlineLink,
|
|
||||||
};
|
|
||||||
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! {
|
|
||||||
<Html lang="en"/>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
crates/plcom/src/cache.rs
Normal file
36
crates/plcom/src/cache.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use rocket::fairing::{self, Fairing};
|
||||||
|
use rocket::http::{Header, Method, Status};
|
||||||
|
use rocket::{Request, Response};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CacheControl {
|
||||||
|
duration_secs: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CacheControl {
|
||||||
|
fn default() -> Self {
|
||||||
|
CacheControl {
|
||||||
|
duration_secs: 60 * 60, // 60 secs * 60 minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>) {
|
||||||
|
// Aggressive caching
|
||||||
|
if request.method() == Method::Get && response.status() == Status::Ok {
|
||||||
|
response.set_header(Header::new(
|
||||||
|
"Cache-Control",
|
||||||
|
format!("public, max-age={}", self.duration_secs),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
pub mod icon;
|
pub mod icon;
|
||||||
pub mod link;
|
pub mod link;
|
||||||
|
|
||||||
pub fn get_year() -> i32 {
|
|
||||||
use chrono::Datelike;
|
|
||||||
chrono::Utc::now().year()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct Date {
|
pub struct Date {
|
||||||
pub year: u32,
|
pub year: u32,
|
||||||
|
|
@ -14,7 +9,22 @@ pub struct Date {
|
||||||
|
|
||||||
impl std::fmt::Display for Date {
|
impl std::fmt::Display for Date {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}-{:02}", self.year, self.month)
|
let month = match self.month {
|
||||||
|
1 => "January",
|
||||||
|
2 => "Februrary",
|
||||||
|
3 => "March",
|
||||||
|
4 => "April",
|
||||||
|
5 => "May",
|
||||||
|
6 => "June",
|
||||||
|
7 => "July",
|
||||||
|
8 => "August",
|
||||||
|
9 => "September",
|
||||||
|
10 => "October",
|
||||||
|
11 => "November",
|
||||||
|
12 => "December",
|
||||||
|
_ => panic!("wtf not a month"),
|
||||||
|
};
|
||||||
|
write!(f, "{month} {}", self.year)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,18 +75,14 @@ pub mod resume {
|
||||||
pub not_available: bool,
|
pub not_available: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::{Link, OutlineButtonLink};
|
use crate::common::link::{Link, outline_button_link};
|
||||||
use http::Uri;
|
use leptos::prelude::*;
|
||||||
use leptos::*;
|
|
||||||
use tailwind_fuse::tw_join;
|
use tailwind_fuse::tw_join;
|
||||||
impl IntoView for ResumeLink {
|
impl IntoAny for ResumeLink {
|
||||||
fn into_view(self) -> View {
|
fn into_any(self) -> AnyView {
|
||||||
if !self.not_available {
|
if !self.not_available {
|
||||||
let link = Link {
|
let link = Link::parse(self.uri, self.label);
|
||||||
label: self.label.into(),
|
outline_button_link(link).into_any()
|
||||||
uri: Uri::from_static(self.uri),
|
|
||||||
};
|
|
||||||
view! { <OutlineButtonLink link=link/> }.into_view()
|
|
||||||
} else {
|
} else {
|
||||||
view! {
|
view! {
|
||||||
<span class=tw_join!(
|
<span class=tw_join!(
|
||||||
|
|
@ -85,7 +91,7 @@ pub mod resume {
|
||||||
"items-center"
|
"items-center"
|
||||||
)>{self.label}</span>
|
)>{self.label}</span>
|
||||||
}
|
}
|
||||||
.into_view()
|
.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +126,12 @@ pub mod wallpapers {
|
||||||
pub longitude: 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Location {
|
pub struct Location {
|
||||||
pub precise: &'static str,
|
pub precise: &'static str,
|
||||||
|
|
@ -128,8 +140,19 @@ pub mod wallpapers {
|
||||||
|
|
||||||
impl Wallpaper {
|
impl Wallpaper {
|
||||||
pub fn random() -> Option<&'static Wallpaper> {
|
pub fn random() -> Option<&'static Wallpaper> {
|
||||||
let random_value = rand::Rng::gen_range(&mut rand::thread_rng(), 0..WALLPAPERS.len());
|
use nanorand::{ChaCha20, Rng};
|
||||||
WALLPAPERS.get(random_value)
|
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 Wallpaper> {
|
||||||
|
WALLPAPERS.iter().find(|w| w.filename.contains(filename))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use leptos::*;
|
use leptos::prelude::*;
|
||||||
use tailwind_fuse::tw_join;
|
use tailwind_fuse::tw_join;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
|
@ -15,8 +15,8 @@ pub enum Icon {
|
||||||
Map,
|
Map,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoView for Icon {
|
impl IntoAny for Icon {
|
||||||
fn into_view(self) -> View {
|
fn into_any(self) -> AnyView {
|
||||||
match self {
|
match self {
|
||||||
Self::Email => view! {
|
Self::Email => view! {
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -33,7 +33,7 @@ impl IntoView for Icon {
|
||||||
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"
|
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>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
|
|
||||||
Self::Link => view! {
|
Self::Link => view! {
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -45,7 +45,7 @@ impl IntoView for Icon {
|
||||||
<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="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>
|
<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>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
|
|
||||||
Self::Calendar => view! {
|
Self::Calendar => view! {
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -60,7 +60,7 @@ impl IntoView for Icon {
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
|
|
||||||
Self::Location => view! {
|
Self::Location => view! {
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -75,7 +75,7 @@ impl IntoView for Icon {
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
Icon::Twitter => view! {
|
Icon::Twitter => view! {
|
||||||
<svg
|
<svg
|
||||||
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
||||||
|
|
@ -84,7 +84,7 @@ impl IntoView for Icon {
|
||||||
>
|
>
|
||||||
<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>
|
<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>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
Icon::Telegram => view! {
|
Icon::Telegram => view! {
|
||||||
<svg
|
<svg
|
||||||
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
||||||
|
|
@ -93,7 +93,7 @@ impl IntoView for Icon {
|
||||||
>
|
>
|
||||||
<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>
|
<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>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
Icon::Mastodon => view! {
|
Icon::Mastodon => view! {
|
||||||
<svg
|
<svg
|
||||||
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
||||||
|
|
@ -102,7 +102,7 @@ impl IntoView for Icon {
|
||||||
>
|
>
|
||||||
<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>
|
<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>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
Icon::Github => view! {
|
Icon::Github => view! {
|
||||||
<svg
|
<svg
|
||||||
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
||||||
|
|
@ -111,7 +111,7 @@ impl IntoView for Icon {
|
||||||
>
|
>
|
||||||
<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>
|
<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>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
Icon::Linkedin => view! {
|
Icon::Linkedin => view! {
|
||||||
<svg
|
<svg
|
||||||
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
class=tw_join!("w-6", "h-6", "fill-current", "mr-0", "sm:mr-2")
|
||||||
|
|
@ -120,7 +120,7 @@ impl IntoView for Icon {
|
||||||
>
|
>
|
||||||
<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>
|
<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>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
Icon::Map => view! {
|
Icon::Map => view! {
|
||||||
<svg
|
<svg
|
||||||
class=tw_join!("w-5", "h-5")
|
class=tw_join!("w-5", "h-5")
|
||||||
|
|
@ -134,7 +134,7 @@ impl IntoView for Icon {
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
}.into_view(),
|
}.into_any(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,55 @@
|
||||||
use http::Uri;
|
use crate::prelude::*;
|
||||||
use leptos::*;
|
use rocket::http::uri::Uri;
|
||||||
use tailwind_fuse::tw_join;
|
use tailwind_fuse::tw_merge;
|
||||||
|
|
||||||
use crate::Icon;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct Link {
|
pub struct Link<'a> {
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub uri: Uri,
|
pub uri: Uri<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Link {
|
impl<'a> Link<'a> {
|
||||||
pub fn new(uri: &'static str, label: impl Into<String>) -> Self {
|
pub fn new(uri: Uri<'a>, label: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uri: Uri::from_static(uri),
|
uri,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn slides(uri: &'static str) -> Self {
|
pub fn parse(uri: &'static str, label: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
uri: Uri::parse_any(uri).expect("not a real uri"),
|
||||||
|
label: label.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn slides(uri: Uri<'a>) -> Self {
|
||||||
Self::new(uri, "Slides")
|
Self::new(uri, "Slides")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
pub fn underline_link(
|
||||||
pub fn UnderlineLink(
|
link: Link,
|
||||||
#[prop(into)] link: MaybeSignal<Link>,
|
class: Option<String>,
|
||||||
#[prop(into, optional)] class: MaybeSignal<String>,
|
|
||||||
#[prop(attrs)] attributes: Vec<(&'static str, Attribute)>,
|
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let class = tailwind_fuse::tw_merge!("underline", class.get());
|
let class = class.unwrap_or_default();
|
||||||
|
let class = tw_merge!("underline", class);
|
||||||
view! {
|
view! {
|
||||||
<a href=link.get().uri.to_string() {..attributes} class=class target="_blank">
|
<a href=link.uri.to_string() class=class>
|
||||||
{link.get().label}
|
{link.label}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
.into_view()
|
||||||
}
|
}
|
||||||
|
|
||||||
type HideTextSmallDisplay = bool;
|
type HideTextSmallDisplay = bool;
|
||||||
|
|
||||||
#[component]
|
pub fn button_link(
|
||||||
pub fn ButtonLink(
|
link: Link,
|
||||||
#[prop(into)] link: MaybeSignal<Link>,
|
icon: Option<Icon>,
|
||||||
#[prop(into, optional)] icon: Option<MaybeSignal<Icon>>,
|
hide_text_small_display: Option<HideTextSmallDisplay>,
|
||||||
#[prop(into, optional)] hide_text_small_display: Option<MaybeSignal<HideTextSmallDisplay>>,
|
) -> impl IntoAny {
|
||||||
#[prop(attrs)] attributes: Vec<(&'static str, Attribute)>,
|
|
||||||
) -> impl IntoView {
|
|
||||||
let text_css = hide_text_small_display
|
let text_css = hide_text_small_display
|
||||||
.and_then(|hide| {
|
.and_then(|hide| {
|
||||||
if hide.get() {
|
if hide {
|
||||||
Some(tw_join!("hidden", "sm:inline"))
|
Some(tw_join!("hidden", "sm:inline"))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -55,11 +57,14 @@ pub fn ButtonLink(
|
||||||
})
|
})
|
||||||
.unwrap_or(tw_join!("ml-2", "sm:ml-0", "text-center"));
|
.unwrap_or(tw_join!("ml-2", "sm:ml-0", "text-center"));
|
||||||
|
|
||||||
|
let icon = icon
|
||||||
|
.map(|icon| icon.into_any())
|
||||||
|
.unwrap_or_else(|| ().into_any());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<a
|
<a
|
||||||
href=link.get().uri.to_string()
|
href=link.uri.to_string()
|
||||||
{..attributes}
|
aria-label=link.label.to_string()
|
||||||
aria-label=link.get().label
|
|
||||||
class=tw_join!(
|
class=tw_join!(
|
||||||
"inline-flex", "bg-sky-900", "hover:bg-sky-700", "transition-all", "duration-200",
|
"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"
|
"text-white", "font-bold", "py-2", "px-4", "rounded-xl", "items-center"
|
||||||
|
|
@ -68,21 +73,19 @@ pub fn ButtonLink(
|
||||||
|
|
||||||
{icon}
|
{icon}
|
||||||
<div class=tw_join!("inline-flex", "items-center")>
|
<div class=tw_join!("inline-flex", "items-center")>
|
||||||
<span class=text_css>{link.get().label}</span>
|
<span class=text_css>{link.label.to_string()}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
pub fn outline_button_link(
|
||||||
pub fn OutlineButtonLink(
|
link: Link,
|
||||||
#[prop(into)] link: MaybeSignal<Link>,
|
) -> impl IntoAny {
|
||||||
#[prop(attrs)] attributes: Vec<(&'static str, Attribute)>,
|
|
||||||
) -> impl IntoView {
|
|
||||||
view! {
|
view! {
|
||||||
<a
|
<a
|
||||||
href=link.get().uri.to_string()
|
href=link.uri.to_string()
|
||||||
{..attributes}
|
|
||||||
class=tw_join!(
|
class=tw_join!(
|
||||||
"mt-4", "inline-flex", "bg-transparent", "hover:bg-sky-700", "text-white",
|
"mt-4", "inline-flex", "bg-transparent", "hover:bg-sky-700", "text-white",
|
||||||
"font-semibold", "py-1.5", "px-4", "rounded-xl", "items-center", "border",
|
"font-semibold", "py-1.5", "px-4", "rounded-xl", "items-center", "border",
|
||||||
|
|
@ -90,10 +93,11 @@ pub fn OutlineButtonLink(
|
||||||
)
|
)
|
||||||
>
|
>
|
||||||
|
|
||||||
{Icon::Link}
|
{Icon::Link.into_any()}
|
||||||
<div class=tw_join!("inline-flex", "items-center")>
|
<div class=tw_join!("inline-flex", "items-center")>
|
||||||
<span class=tw_join!("ml-2", "sm:ml-0", "text-center")>{link.get().label}</span>
|
<span class=tw_join!("ml-2", "sm:ml-0", "text-center")>{link.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
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}"),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +1,46 @@
|
||||||
#[cfg(feature = "ssr")]
|
mod prelude {
|
||||||
#[tokio::main]
|
pub use crate::common::icon::Icon;
|
||||||
async fn main() {
|
pub use crate::common::link::*;
|
||||||
use axum::Router;
|
pub use crate::common::Date;
|
||||||
use leptos::*;
|
pub use crate::views::*;
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
pub use leptos::prelude::*;
|
||||||
use plcom::app::*;
|
pub use rocket::uri;
|
||||||
use plcom::fileserv::file_and_error_handler;
|
pub use tailwind_fuse::tw_join;
|
||||||
|
|
||||||
// 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"))]
|
mod cache;
|
||||||
pub fn main() {
|
mod common;
|
||||||
// no client-side main function
|
mod pages;
|
||||||
// unless we want this to work with e.g., Trunk for a purely client-side app
|
mod views;
|
||||||
// see lib.rs for hydration function instead
|
|
||||||
|
use pages::*;
|
||||||
|
|
||||||
|
#[rocket::launch]
|
||||||
|
fn rocket() -> _ {
|
||||||
|
let assets = std::env::var("PLCOM_ASSETS_PATH").unwrap_or("../../public".into());
|
||||||
|
|
||||||
|
let server = rocket::build()
|
||||||
|
.mount("/", rocket::fs::FileServer::from(assets))
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
rocket::routes![root_route, email_route, wallpapers_route],
|
||||||
|
)
|
||||||
|
.register("/", rocket::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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,47 @@
|
||||||
pub mod root;
|
mod email;
|
||||||
pub mod email;
|
mod root;
|
||||||
pub mod wallpapers;
|
mod wallpapers;
|
||||||
|
|
||||||
pub use root::RootPage;
|
use crate::common::wallpapers::Wallpaper;
|
||||||
pub use email::EmailPage;
|
use crate::prelude::*;
|
||||||
pub use wallpapers::WallpapersPage;
|
use rocket::get;
|
||||||
|
|
||||||
|
#[derive(rocket::Responder)]
|
||||||
|
#[response(content_type = "text/html")]
|
||||||
|
pub struct LeptosResponder(String);
|
||||||
|
|
||||||
|
impl From<AnyView> for LeptosResponder {
|
||||||
|
fn from(value: AnyView) -> Self {
|
||||||
|
Self(value.to_html())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::catch(404)]
|
||||||
|
pub fn not_found() -> LeptosResponder {
|
||||||
|
content_page(
|
||||||
|
"404 Not Found",
|
||||||
|
view! {
|
||||||
|
<div>"This page could not be found."</div>
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/?<wallpaper>")]
|
||||||
|
pub fn root_route(wallpaper: Option<&str>) -> LeptosResponder {
|
||||||
|
let wallpaper = wallpaper
|
||||||
|
.and_then(Wallpaper::find)
|
||||||
|
.or_else(Wallpaper::random);
|
||||||
|
|
||||||
|
shell("Philippe Loctaux", root::root_page(wallpaper)).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/email")]
|
||||||
|
pub fn email_route() -> LeptosResponder {
|
||||||
|
content_page("Email", email::email_page()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/wallpapers")]
|
||||||
|
pub fn wallpapers_route() -> LeptosResponder {
|
||||||
|
content_page("Wallpapers", wallpapers::wallpapers_page()).into()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,29 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[component]
|
pub fn email_page() -> impl IntoAny {
|
||||||
pub fn EmailPage() -> impl IntoView {
|
|
||||||
view! {
|
view! {
|
||||||
<ContentPage title="Email">
|
|
||||||
<p>
|
<p>
|
||||||
"Send an email if you want to work with me, propose a project idea, or just to say hi!"
|
"Send an email if you want to work with me, propose a project idea, or just to say hi!"
|
||||||
</p>
|
</p>
|
||||||
<div class=tw_join!("my-4")>
|
<div class=tw_join!("my-4")>
|
||||||
<ButtonLink
|
{button_link(Link::new(
|
||||||
link=Link::new(
|
uri!("mailto:wwwATphilippeloctaux~DOT~com").into(),
|
||||||
"mailto:wwwATphilippeloctaux~DOT~com",
|
|
||||||
"www at philippeloctaux dot com",
|
"www at philippeloctaux dot com",
|
||||||
)
|
), Some(Icon::Email), None).into_any()}
|
||||||
|
|
||||||
icon=Icon::Email
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class=tw_join!(
|
<p class=tw_join!(
|
||||||
"mb-2"
|
"mb-2"
|
||||||
)>
|
)>
|
||||||
"If you want to encrypt your message, I have a "
|
"If you want to encrypt your message, I have a "
|
||||||
<UnderlineLink link=Link::new("/pub/pgp-0x69771CD04BA82EC0.txt", "pgp key")/>
|
{underline_link(Link::new(uri!("/pub/pgp-0x69771CD04BA82EC0.txt").into(), "pgp key"), None).into_any()}
|
||||||
" at your disposal."
|
" at your disposal."
|
||||||
</p>
|
</p>
|
||||||
<p class=tw_join!(
|
<p class=tw_join!(
|
||||||
"mb-2"
|
"mb-2"
|
||||||
)>
|
)>
|
||||||
"I also have a " <UnderlineLink link=Link::new("/keybase.txt", "Keybase")/>
|
"I also have a "{underline_link(Link::new(uri!("/keybase.txt").into(), "Keybase"), None).into_any()}
|
||||||
" account, but I do not check it often."
|
" account, but I do not check it often."
|
||||||
</p>
|
</p>
|
||||||
</ContentPage>
|
}.into_any()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,35 @@
|
||||||
use crate::prelude::*;
|
mod education;
|
||||||
|
|
||||||
mod hero;
|
|
||||||
mod www;
|
|
||||||
mod experience;
|
mod experience;
|
||||||
|
mod friends;
|
||||||
|
mod hero;
|
||||||
mod jobs;
|
mod jobs;
|
||||||
mod projects;
|
mod projects;
|
||||||
mod education;
|
|
||||||
mod talks;
|
mod talks;
|
||||||
mod friends;
|
mod www;
|
||||||
|
|
||||||
#[component]
|
use crate::common::wallpapers::Wallpaper;
|
||||||
pub fn RootPage() -> impl IntoView {
|
use crate::prelude::*;
|
||||||
let random_wallpaper = Wallpaper::random();
|
|
||||||
|
|
||||||
|
pub fn root_page(wallpaper: Option<&'static Wallpaper>) -> impl IntoAny {
|
||||||
view! {
|
view! {
|
||||||
<Title text="Hello"/>
|
{hero::hero(wallpaper).into_any()}
|
||||||
<hero::Hero wallpaper=random_wallpaper></hero::Hero>
|
|
||||||
|
|
||||||
<div class=tw_join!("container", "mx-auto", "px-4", "md:px-8", "lg:px-16", "py-16")>
|
<div class=tw_join!("container", "mx-auto", "px-4", "md:px-8", "lg:px-16", "py-16")>
|
||||||
<Whoami/>
|
{whoami}
|
||||||
<div class=tw_join!("my-16", "space-y-16", "md:space-y-32")>
|
<div class=tw_join!("my-16", "space-y-16", "md:space-y-32")>
|
||||||
<www::Www></www::Www>
|
{www::list().into_any()}
|
||||||
<jobs::Jobs></jobs::Jobs>
|
{jobs::jobs().into_any()}
|
||||||
<projects::Projects></projects::Projects>
|
{projects::projects().into_any()}
|
||||||
<education::EducationList></education::EducationList>
|
{education::education_list().into_any()}
|
||||||
<talks::Talks></talks::Talks>
|
{talks::talks().into_any()}
|
||||||
<friends::Friends></friends::Friends>
|
{friends::friends().into_any()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
fn whoami() -> impl IntoView {
|
||||||
fn Whoami() -> impl IntoView {
|
|
||||||
view! {
|
view! {
|
||||||
<div class=tw_join!("md:flex", "md:flex-row-reverse", "items-center")>
|
<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")>
|
<div class=tw_join!("md:w-1/2", "mb-4", "md:mb-0")>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::prelude::*;
|
|
||||||
use super::experience::*;
|
use super::experience::*;
|
||||||
|
use crate::common::resume::{self, Education};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
impl IntoView for Education {
|
impl IntoAny for Education {
|
||||||
fn into_view(self) -> View {
|
fn into_any(self) -> AnyView {
|
||||||
let subtitle = format!("{} in {}", self.study_type, self.area);
|
let subtitle = format!("{} in {}", self.study_type, self.area);
|
||||||
view! {
|
view! {
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
|
|
@ -15,7 +16,7 @@ impl IntoView for Education {
|
||||||
self.institution,
|
self.institution,
|
||||||
&subtitle,
|
&subtitle,
|
||||||
self.logo.as_ref(),
|
self.logo.as_ref(),
|
||||||
)}
|
).into_any()}
|
||||||
<div class=tw_join!("space-y-2")>
|
<div class=tw_join!("space-y-2")>
|
||||||
<ul class=tw_join!(
|
<ul class=tw_join!(
|
||||||
"list-disc", "mt-6"
|
"list-disc", "mt-6"
|
||||||
|
|
@ -31,12 +32,12 @@ impl IntoView for Education {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}.into_view()
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
pub fn education_list() -> impl IntoView {
|
||||||
pub fn EducationList() -> impl IntoView {
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
|
|
@ -44,8 +45,7 @@ pub fn EducationList() -> impl IntoView {
|
||||||
|
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"mt-4", "grid", "grid-cols-1", "md:grid-cols-2", "gap-6", "place-content-center"
|
"mt-4", "grid", "grid-cols-1", "md:grid-cols-2", "gap-6", "place-content-center"
|
||||||
)>{resume::EDUCATION.collect_view()}</div>
|
)>{resume::EDUCATION.map(|education| education.into_any()).collect_view()}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use tailwind_fuse::*;
|
|
||||||
use leptos::*;
|
|
||||||
use crate::common::Date;
|
|
||||||
use crate::common::resume::Logo;
|
use crate::common::resume::Logo;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use tailwind_fuse::*;
|
||||||
|
|
||||||
#[derive(TwClass, Clone, Copy, PartialEq)]
|
#[derive(TwClass, Clone, Copy, PartialEq)]
|
||||||
#[tw(class = r#"h-16 w-16 rounded-xl"#)]
|
#[tw(class = r#"h-16 w-16 rounded-xl"#)]
|
||||||
|
|
@ -17,24 +16,17 @@ enum ImageBackground {
|
||||||
Plain,
|
Plain,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
fn experience_logo(
|
||||||
fn ExperienceLogo(
|
image: String,
|
||||||
#[prop(into)] image: MaybeSignal<String>,
|
// Name of the experience, used in the alt of the image
|
||||||
/// Name of the experience, used in the alt of the image
|
name: String,
|
||||||
#[prop(into)]
|
background: ImageBackground,
|
||||||
name: MaybeSignal<String>,
|
class: String,
|
||||||
#[prop(into, optional)] background: MaybeSignal<ImageBackground>,
|
|
||||||
#[prop(into, optional)] class: MaybeSignal<String>,
|
|
||||||
#[prop(attrs)] attributes: Vec<(&'static str, Attribute)>,
|
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let class = create_memo(move |_| {
|
let class = LogoOptions { background }.with_class(class);
|
||||||
let background = background.get();
|
let alt = format!("{} logo", name);
|
||||||
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/> }
|
view! { <img loading="lazy" src=image alt=alt class=class/> }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExperienceLogo {
|
struct ExperienceLogo {
|
||||||
|
|
@ -50,19 +42,17 @@ pub struct ExperienceHeader {
|
||||||
logo: Option<ExperienceLogo>,
|
logo: Option<ExperienceLogo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoView for ExperienceHeader {
|
impl IntoAny for ExperienceHeader {
|
||||||
fn into_view(self) -> View {
|
fn into_any(self) -> AnyView {
|
||||||
let logo = match self.logo {
|
let logo = match self.logo {
|
||||||
Some(logo) => view! {
|
Some(logo) => experience_logo(
|
||||||
<ExperienceLogo
|
logo.file,
|
||||||
image=logo.file
|
self.name.clone(),
|
||||||
name=self.name.clone()
|
logo.options.map(|o| o.background).unwrap_or_default(),
|
||||||
background=logo.options.map(|o| o.background).unwrap_or_default()
|
tw_join!("mr-4"),
|
||||||
class=tw_join!("mr-4")
|
)
|
||||||
/>
|
.into_any(),
|
||||||
}
|
None => ().into_any(),
|
||||||
.into_view(),
|
|
||||||
None => view! {}.into_view(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let date = match self.date_end {
|
let date = match self.date_end {
|
||||||
|
|
@ -87,7 +77,7 @@ impl IntoView for ExperienceHeader {
|
||||||
)>{self.description}</p>
|
)>{self.description}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
.into_view()
|
.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use rocket::http::uri::Absolute;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
struct Name {
|
struct Name {
|
||||||
|
|
@ -51,21 +52,21 @@ impl std::fmt::Display for Name {
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
struct Friend {
|
struct Friend {
|
||||||
name: Name,
|
name: Name,
|
||||||
uri: Uri,
|
uri: Absolute<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Friend {
|
impl Friend {
|
||||||
pub fn nick(nick: impl Into<String>, uri: &'static str) -> Self {
|
pub fn nick(nick: impl Into<String>, uri: Absolute<'static>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: Name::nick(nick),
|
name: Name::nick(nick),
|
||||||
uri: Uri::from_static(uri),
|
uri,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(first: impl Into<String>, last: impl Into<String>, uri: &'static str) -> Self {
|
pub fn new(first: impl Into<String>, last: impl Into<String>, uri: Absolute<'static>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: Name::new(first, last),
|
name: Name::new(first, last),
|
||||||
uri: Uri::from_static(uri),
|
uri,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,12 +78,11 @@ impl Friend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
impl IntoAny for Friend {
|
||||||
fn Friend(#[prop(into)] friend: MaybeSignal<Friend>) -> impl IntoView {
|
fn into_any(self) -> AnyView {
|
||||||
view! {
|
view! {
|
||||||
<a
|
<a
|
||||||
href=friend.get().uri.to_string()
|
href=self.uri.to_string()
|
||||||
target="_blank"
|
|
||||||
class=tw_join!(
|
class=tw_join!(
|
||||||
"hover:bg-gray-500", "transition-all", "duration-200", "flex", "items-center",
|
"hover:bg-gray-500", "transition-all", "duration-200", "flex", "items-center",
|
||||||
"rounded-lg", "p-2"
|
"rounded-lg", "p-2"
|
||||||
|
|
@ -92,30 +92,31 @@ fn Friend(#[prop(into)] friend: MaybeSignal<Friend>) -> impl IntoView {
|
||||||
<span class=tw_join!(
|
<span class=tw_join!(
|
||||||
"rounded-full", "flex-shrink-0", "mr-4", "w-10", "h-10", "bg-sky-900", "text-white",
|
"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"
|
"flex", "items-center", "justify-center", "text-lg", "font-medium"
|
||||||
)>{friend.get().name.initials()}</span>
|
)>{self.name.initials()}</span>
|
||||||
<div>
|
<div>
|
||||||
<p class=tw_join!("font-bold")>{friend.get().name.to_string()}</p>
|
<p class=tw_join!("font-bold")>{self.name.to_string()}</p>
|
||||||
<p>{friend.get().domain_name()}</p>
|
<p>{self.domain_name()}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
pub fn friends() -> impl IntoView {
|
||||||
pub fn Friends() -> impl IntoView {
|
|
||||||
let friends = [
|
let friends = [
|
||||||
Friend::new("Paolo", "Rotolo", "https://rotolo.dev"),
|
Friend::new("Paolo", "Rotolo", uri!("https://rotolo.dev")),
|
||||||
Friend::new("Polly", "Bishop", "https://github.com/itspolly"),
|
Friend::new("Polly", "Bishop", uri!("https://github.com/itspolly")),
|
||||||
Friend::new("Ayden", "Panhuyzen", "https://ayden.dev"),
|
Friend::new("Ayden", "Panhuyzen", uri!("https://ayden.dev")),
|
||||||
Friend::new("Corbin", "Crutchley", "https://crutchcorn.dev"),
|
Friend::new("Corbin", "Crutchley", uri!("https://crutchcorn.dev")),
|
||||||
Friend::new("James", "Fenn", "https://jfenn.me"),
|
Friend::new("James", "Fenn", uri!("https://jfenn.me")),
|
||||||
Friend::new("Alex", "Dueppen", "https://ajd.sh"),
|
Friend::new("Alex", "Dueppen", uri!("https://ajd.sh")),
|
||||||
Friend::new("Lyra", "Messier", "https://lyramsr.co"),
|
Friend::new("Lyra", "Messier", uri!("https://lyramsr.co")),
|
||||||
Friend::new("Peter", "Soboyejo", "https://twitter.com/pxtvr"),
|
Friend::new("Peter", "Soboyejo", uri!("https://twitter.com/pxtvr")),
|
||||||
Friend::nick("Millomaker", "https://youtube.com/millomaker"),
|
Friend::nick("Millomaker", uri!("https://youtube.com/millomaker")),
|
||||||
Friend::new("Alexandre", "Wagner", "https://dev4people.fr"),
|
Friend::new("Alexandre", "Wagner", uri!("https://dev4people.fr")),
|
||||||
Friend::new("Aidan", "Follestad", "https://af.codes"),
|
Friend::new("Aidan", "Follestad", uri!("https://af.codes")),
|
||||||
Friend::new("Victor", "Simon", "https://simonvictor.com"),
|
Friend::new("Victor", "Simon", uri!("https://simonvictor.com")),
|
||||||
];
|
];
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
|
@ -129,10 +130,10 @@ pub fn Friends() -> impl IntoView {
|
||||||
)>
|
)>
|
||||||
{friends
|
{friends
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|f| {
|
.map(|friend| {
|
||||||
view! {
|
view! {
|
||||||
<li class=tw_join!("py-2")>
|
<li class=tw_join!("py-2")>
|
||||||
<Friend friend=f/>
|
{friend.into_any()}
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
use crate::common::wallpapers::Wallpaper;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[component]
|
fn wallpaper_info(wallpaper: &'static Wallpaper) -> impl IntoAny {
|
||||||
fn WallpaperInfo(#[prop(into)] wallpaper: &'static Wallpaper) -> impl IntoView {
|
|
||||||
view! {
|
view! {
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"absolute", "bottom-3", "sm:bottom-5", "left-2", "sm:left-5", "inline-block",
|
"absolute", "bottom-3", "sm:bottom-5", "left-2", "sm:left-5", "inline-block",
|
||||||
|
|
@ -14,11 +14,10 @@ fn WallpaperInfo(#[prop(into)] wallpaper: &'static Wallpaper) -> impl IntoView {
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"inline-flex", "items-center"
|
"inline-flex", "items-center"
|
||||||
)>
|
)>
|
||||||
{Icon::Map}
|
{Icon::Map.into_any()}
|
||||||
<a
|
<a
|
||||||
class=tw_join!("ml-1", "text-sm", "underline")
|
class=tw_join!("ml-1", "text-sm", "underline")
|
||||||
href="/wallpapers"
|
href="/wallpapers"
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
"See more!"
|
"See more!"
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -30,7 +29,7 @@ fn WallpaperInfo(#[prop(into)] wallpaper: &'static Wallpaper) -> impl IntoView {
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"inline-flex", "items-center"
|
"inline-flex", "items-center"
|
||||||
)>
|
)>
|
||||||
{Icon::Location}
|
{Icon::Location.into_any()}
|
||||||
<span class=tw_join!(
|
<span class=tw_join!(
|
||||||
"ml-1", "text-sm"
|
"ml-1", "text-sm"
|
||||||
)>
|
)>
|
||||||
|
|
@ -47,22 +46,21 @@ fn WallpaperInfo(#[prop(into)] wallpaper: &'static Wallpaper) -> impl IntoView {
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"inline-flex", "items-center"
|
"inline-flex", "items-center"
|
||||||
)>
|
)>
|
||||||
{Icon::Calendar} <span class=tw_join!("ml-1", "text-sm")>{wallpaper.date}</span>
|
{Icon::Calendar.into_any()} <span class=tw_join!("ml-1", "text-sm")>{wallpaper.date}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
pub fn hero(wallpaper: Option<&'static Wallpaper>) -> impl IntoAny {
|
||||||
pub fn Hero(#[prop(into)] wallpaper: Option<&'static Wallpaper>) -> impl IntoView {
|
|
||||||
let (wallpaper_info, background_image) = match wallpaper {
|
let (wallpaper_info, background_image) = match wallpaper {
|
||||||
Some(wallpaper) => (
|
Some(wallpaper) => (
|
||||||
view! { <WallpaperInfo wallpaper=wallpaper/> }.into_view(),
|
wallpaper_info(wallpaper).into_any(),
|
||||||
format!("background-image: url(/wallpapers/{});", wallpaper.filename),
|
format!("background-image: url(/wallpapers/{});", wallpaper.filename),
|
||||||
),
|
),
|
||||||
None => (view! {}.into_view(), "".into()),
|
None => (().into_any(), "".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
use crate::prelude::*;
|
|
||||||
use super::experience::*;
|
use super::experience::*;
|
||||||
|
use crate::common::resume::{self, Work};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
impl IntoAny for Work {
|
||||||
|
fn into_any(self) -> AnyView {
|
||||||
|
let link = match self.link {
|
||||||
|
Some(link) => link.into_any(),
|
||||||
|
None => ().into_any(),
|
||||||
|
};
|
||||||
|
|
||||||
impl IntoView for Work {
|
|
||||||
fn into_view(self) -> View {
|
|
||||||
view! {
|
view! {
|
||||||
<div class=tw_join!("w-full", "rounded-2xl", "bg-sky-950")>
|
<div class=tw_join!("w-full", "rounded-2xl", "bg-sky-950")>
|
||||||
|
|
||||||
|
|
@ -16,7 +22,7 @@ impl IntoView for Work {
|
||||||
self.name,
|
self.name,
|
||||||
self.position,
|
self.position,
|
||||||
Some(&self.logo),
|
Some(&self.logo),
|
||||||
)} <div class=tw_join!("space-y-2")>
|
).into_any()} <div class=tw_join!("space-y-2")>
|
||||||
|
|
||||||
<p>{self.description}</p>
|
<p>{self.description}</p>
|
||||||
|
|
||||||
|
|
@ -53,24 +59,22 @@ impl IntoView for Work {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div> {self.link}
|
</div> {link}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}.into_view()
|
}.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
pub fn jobs() -> impl IntoView {
|
||||||
pub fn Jobs() -> impl IntoView {
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Professional Experiences"</h1>
|
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Professional Experiences"</h1>
|
||||||
|
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"mt-4", "grid", "grid-cols-1", "sm:grid-cols-2", "gap-4"
|
"mt-4", "grid", "grid-cols-1", "sm:grid-cols-2", "gap-4"
|
||||||
)>{resume::WORK.collect_view()}</div>
|
)>{resume::WORK.map(|work| work.into_any()).collect_view()}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::prelude::*;
|
|
||||||
use super::experience::*;
|
use super::experience::*;
|
||||||
|
use crate::common::resume::{self, Project};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
impl IntoView for Project {
|
impl IntoAny for Project {
|
||||||
fn into_view(self) -> View {
|
fn into_any(self) -> AnyView {
|
||||||
let (css_image_position, css_image_position_corner) = match self.image {
|
let (css_image_position, css_image_position_corner) = match self.image {
|
||||||
Some(image) => {
|
Some(image) => {
|
||||||
let position = match image.position {
|
let position = match image.position {
|
||||||
|
|
@ -33,6 +34,11 @@ impl IntoView for Project {
|
||||||
None => ("".into(), "".into()),
|
None => ("".into(), "".into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let link = match self.link {
|
||||||
|
Some(link) => link.into_any(),
|
||||||
|
None => ().into_any(),
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"w-full", "rounded-2xl", "bg-pink-950", css_image_position
|
"w-full", "rounded-2xl", "bg-pink-950", css_image_position
|
||||||
|
|
@ -47,9 +53,9 @@ impl IntoView for Project {
|
||||||
class=css_image_position_corner
|
class=css_image_position_corner
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
.into_view()
|
.into_any()
|
||||||
} else {
|
} else {
|
||||||
view! {}.into_view()
|
().into_any()
|
||||||
}}
|
}}
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"p-6", "justify-between", "h-full"
|
"p-6", "justify-between", "h-full"
|
||||||
|
|
@ -61,7 +67,7 @@ impl IntoView for Project {
|
||||||
self.name,
|
self.name,
|
||||||
self.description,
|
self.description,
|
||||||
self.logo.as_ref(),
|
self.logo.as_ref(),
|
||||||
)}
|
).into_any()}
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"space-y-2"
|
"space-y-2"
|
||||||
)>
|
)>
|
||||||
|
|
@ -70,7 +76,7 @@ impl IntoView for Project {
|
||||||
.presentation
|
.presentation
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
view! { <p>{*p}</p> }
|
view! { <p>{*p}</p> }.into_any()
|
||||||
})
|
})
|
||||||
.collect_view()} <div>
|
.collect_view()} <div>
|
||||||
<ul class=tw_join!(
|
<ul class=tw_join!(
|
||||||
|
|
@ -80,7 +86,7 @@ impl IntoView for Project {
|
||||||
.highlights
|
.highlights
|
||||||
.iter()
|
.iter()
|
||||||
.map(|h| {
|
.map(|h| {
|
||||||
view! { <li class=tw_join!("ml-5")>{*h}</li> }
|
view! { <li class=tw_join!("ml-5")>{*h}</li> }.into_any()
|
||||||
})
|
})
|
||||||
.collect_view()}
|
.collect_view()}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -99,47 +105,51 @@ impl IntoView for Project {
|
||||||
"items-center", "rounded-md", "bg-blue-100", "px-2", "py-1",
|
"items-center", "rounded-md", "bg-blue-100", "px-2", "py-1",
|
||||||
"font-medium", "text-blue-700",
|
"font-medium", "text-blue-700",
|
||||||
)>{*t}</span>
|
)>{*t}</span>
|
||||||
}
|
}.into_any()
|
||||||
})
|
})
|
||||||
.collect_view()}
|
.collect_view()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div> {self.link}
|
</div> {link}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}.into_view()
|
}.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageProject = Project;
|
type ImageProject = Project;
|
||||||
type TextProject = Project;
|
type TextProject = Project;
|
||||||
|
|
||||||
enum DisplayProject {
|
enum DisplayProject {
|
||||||
Text(Box<(TextProject, Option<TextProject>)>),
|
Text(Box<(TextProject, Option<TextProject>)>),
|
||||||
Image(ImageProject)
|
Image(ImageProject),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoView for DisplayProject {
|
impl IntoAny for DisplayProject {
|
||||||
fn into_view(self) -> View {
|
fn into_any(self) -> AnyView {
|
||||||
match self {
|
match self {
|
||||||
Self::Image(image) => image.into_view(),
|
Self::Image(image) => image.into_any(),
|
||||||
Self::Text(boxy) => {
|
Self::Text(boxy) => {
|
||||||
let (text1, text2) = *boxy;
|
let (text1, text2) = *boxy;
|
||||||
|
let text2 = match text2 {
|
||||||
|
Some(text2) => text2.into_any(),
|
||||||
|
None => ().into_any(),
|
||||||
|
};
|
||||||
view! {
|
view! {
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"my-4", "grid", "grid-cols-1", "sm:grid-cols-2", "gap-4"
|
"my-4", "grid", "grid-cols-1", "sm:grid-cols-2", "gap-4"
|
||||||
)>{text1} {text2}</div>
|
)>{text1.into_any()} {text2}</div>
|
||||||
}.into_view()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
pub fn projects() -> impl IntoView {
|
||||||
pub fn Projects() -> impl IntoView {
|
|
||||||
let mut projects = vec![];
|
let mut projects = vec![];
|
||||||
let mut iter = resume::PROJECTS.iter();
|
let mut iter = resume::PROJECTS.iter();
|
||||||
|
|
||||||
|
|
@ -148,7 +158,10 @@ pub fn Projects() -> impl IntoView {
|
||||||
projects.push(DisplayProject::Image(*cur_proj));
|
projects.push(DisplayProject::Image(*cur_proj));
|
||||||
} else {
|
} else {
|
||||||
let next_text = iter.next();
|
let next_text = iter.next();
|
||||||
projects.push(DisplayProject::Text(Box::new((*cur_proj, next_text.copied()))));
|
projects.push(DisplayProject::Text(Box::new((
|
||||||
|
*cur_proj,
|
||||||
|
next_text.copied(),
|
||||||
|
))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,7 +170,7 @@ pub fn Projects() -> impl IntoView {
|
||||||
|
|
||||||
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Projects"</h1>
|
<h1 class=tw_join!("text-4xl", "font-bold", "mb-4")>"Projects"</h1>
|
||||||
|
|
||||||
{projects}
|
{projects.into_iter().map(|project| project.into_any()).collect_view()}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ struct Talk {
|
||||||
title: String,
|
title: String,
|
||||||
date: Date,
|
date: Date,
|
||||||
location: String,
|
location: String,
|
||||||
link: Link,
|
link: Link<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Talk {
|
impl Talk {
|
||||||
|
|
@ -13,7 +13,7 @@ impl Talk {
|
||||||
title: impl Into<String>,
|
title: impl Into<String>,
|
||||||
date: Date,
|
date: Date,
|
||||||
location: impl Into<String>,
|
location: impl Into<String>,
|
||||||
link: Link,
|
link: Link<'static>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: title.into(),
|
title: title.into(),
|
||||||
|
|
@ -24,35 +24,35 @@ impl Talk {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
impl IntoAny for Talk {
|
||||||
fn Talk(#[prop(into)] talk: MaybeSignal<Talk>) -> impl IntoView {
|
fn into_any(self) -> AnyView {
|
||||||
view! {
|
view! {
|
||||||
<div class=tw_join!("rounded-2xl", "w-full", "bg-teal-950", "p-6")>
|
<div class=tw_join!("rounded-2xl", "w-full", "bg-teal-950", "p-6")>
|
||||||
|
|
||||||
<div class=tw_join!("text-xl", "font-semibold", "mb-4")>{talk.get().title}</div>
|
<div class=tw_join!("text-xl", "font-semibold", "mb-4")>{self.title}</div>
|
||||||
|
|
||||||
<div class=tw_join!("flex")>
|
<div class=tw_join!("flex")>
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"inline-flex", "items-center"
|
"inline-flex", "items-center"
|
||||||
)>
|
)>
|
||||||
{Icon::Calendar}
|
{Icon::Calendar.into_any()}
|
||||||
<span class=tw_join!("ml-2")>{talk.get().date.to_string()}</span>
|
<span class=tw_join!("ml-2")>{self.date.to_string()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=tw_join!("flex")>
|
<div class=tw_join!("flex")>
|
||||||
<div class=tw_join!(
|
<div class=tw_join!(
|
||||||
"inline-flex", "items-center"
|
"inline-flex", "items-center"
|
||||||
)>{Icon::Location} <span class=tw_join!("ml-2")>{talk.get().location}</span></div>
|
)>{Icon::Location.into_any()} <span class=tw_join!("ml-2")>{self.location}</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OutlineButtonLink link=talk.get().link/>
|
{outline_button_link(self.link).into_any()}
|
||||||
</div>
|
</div>
|
||||||
|
}.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
pub fn talks() -> impl IntoAny {
|
||||||
pub fn Talks() -> impl IntoView {
|
|
||||||
let talks = [
|
let talks = [
|
||||||
Talk::new(
|
Talk::new(
|
||||||
"Vim",
|
"Vim",
|
||||||
|
|
@ -61,7 +61,7 @@ pub fn Talks() -> impl IntoView {
|
||||||
month: 2,
|
month: 2,
|
||||||
},
|
},
|
||||||
"Epitech Rennes",
|
"Epitech Rennes",
|
||||||
Link::slides("/pub/talks/vim.pdf"),
|
Link::slides(uri!("/pub/talks/vim.pdf").into()),
|
||||||
),
|
),
|
||||||
Talk::new(
|
Talk::new(
|
||||||
"CLion",
|
"CLion",
|
||||||
|
|
@ -70,7 +70,7 @@ pub fn Talks() -> impl IntoView {
|
||||||
month: 3,
|
month: 3,
|
||||||
},
|
},
|
||||||
"Epitech Rennes",
|
"Epitech Rennes",
|
||||||
Link::slides("/pub/talks/clion.pdf"),
|
Link::slides(uri!("/pub/talks/clion.pdf").into()),
|
||||||
),
|
),
|
||||||
Talk::new(
|
Talk::new(
|
||||||
"git & devops 2",
|
"git & devops 2",
|
||||||
|
|
@ -79,7 +79,7 @@ pub fn Talks() -> impl IntoView {
|
||||||
month: 2,
|
month: 2,
|
||||||
},
|
},
|
||||||
"Epitech Rennes",
|
"Epitech Rennes",
|
||||||
Link::slides("/pub/talks/git-devops2.pdf"),
|
Link::slides(uri!("/pub/talks/git-devops2.pdf").into()),
|
||||||
),
|
),
|
||||||
Talk::new(
|
Talk::new(
|
||||||
"pass4thewin",
|
"pass4thewin",
|
||||||
|
|
@ -88,7 +88,7 @@ pub fn Talks() -> impl IntoView {
|
||||||
month: 2,
|
month: 2,
|
||||||
},
|
},
|
||||||
"Epitech Rennes",
|
"Epitech Rennes",
|
||||||
Link::slides("/pub/talks/pass4thewin.pdf"),
|
Link::slides(uri!("/pub/talks/pass4thewin.pdf").into()),
|
||||||
),
|
),
|
||||||
Talk::new(
|
Talk::new(
|
||||||
"git & devops",
|
"git & devops",
|
||||||
|
|
@ -97,7 +97,7 @@ pub fn Talks() -> impl IntoView {
|
||||||
month: 5,
|
month: 5,
|
||||||
},
|
},
|
||||||
"Epitech Rennes",
|
"Epitech Rennes",
|
||||||
Link::slides("/pub/talks/git-devops.pdf"),
|
Link::slides(uri!("/pub/talks/git-devops.pdf").into()),
|
||||||
),
|
),
|
||||||
Talk::new(
|
Talk::new(
|
||||||
"git gud",
|
"git gud",
|
||||||
|
|
@ -106,7 +106,7 @@ pub fn Talks() -> impl IntoView {
|
||||||
month: 5,
|
month: 5,
|
||||||
},
|
},
|
||||||
"Epitech Rennes",
|
"Epitech Rennes",
|
||||||
Link::slides("/pub/talks/git-tek.pdf"),
|
Link::slides(uri!("/pub/talks/git-tek.pdf").into()),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -123,15 +123,9 @@ pub fn Talks() -> impl IntoView {
|
||||||
"mt-4", "grid", "grid-cols-1", "sm:grid-cols-2", "lg:grid-cols-3", "gap-6",
|
"mt-4", "grid", "grid-cols-1", "sm:grid-cols-2", "lg:grid-cols-3", "gap-6",
|
||||||
"place-content-center"
|
"place-content-center"
|
||||||
)>
|
)>
|
||||||
{talks
|
{talks.into_iter().map(|talk| talk.into_any()).collect_view()}
|
||||||
.into_iter()
|
|
||||||
.map(|t| {
|
|
||||||
view! { <Talk talk=t/> }
|
|
||||||
})
|
|
||||||
.collect_view()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,54 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
struct Www {
|
pub struct Www {
|
||||||
link: Link,
|
link: Link<'static>,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
impl IntoAny for Www {
|
||||||
pub fn Www() -> impl IntoView {
|
fn into_any(self) -> AnyView {
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!("w-full", "h-auto", "md:w-auto")>
|
||||||
|
<div class=tw_join!("text-center")>
|
||||||
|
{button_link(self.link, Some(self.icon), Some(true)).into_any()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list() -> impl IntoAny {
|
||||||
let www = [
|
let www = [
|
||||||
Www {
|
Www {
|
||||||
link: Link::new("https://twitter.com/philippeloctaux", "Twitter"),
|
link: Link::new(
|
||||||
|
uri!("https://twitter.com/philippeloctaux").into(),
|
||||||
|
"Twitter",
|
||||||
|
),
|
||||||
icon: Icon::Twitter,
|
icon: Icon::Twitter,
|
||||||
},
|
},
|
||||||
Www {
|
Www {
|
||||||
link: Link::new("https://t.me/philippeloctaux", "Telegram"),
|
link: Link::new(uri!("https://t.me/philippeloctaux").into(), "Telegram"),
|
||||||
icon: Icon::Telegram,
|
icon: Icon::Telegram,
|
||||||
},
|
},
|
||||||
Www {
|
Www {
|
||||||
link: Link::new("https://mastodon.social/@philt3r", "Mastodon"),
|
link: Link::new(uri!("https://mastodon.social/@philt3r").into(), "Mastodon"),
|
||||||
icon: Icon::Mastodon,
|
icon: Icon::Mastodon,
|
||||||
},
|
},
|
||||||
Www {
|
Www {
|
||||||
link: Link::new("https://github.com/deadbaed", "GitHub"),
|
link: Link::new(uri!("https://github.com/deadbaed").into(), "GitHub"),
|
||||||
icon: Icon::Github,
|
icon: Icon::Github,
|
||||||
},
|
},
|
||||||
Www {
|
Www {
|
||||||
link: Link::new("https://linkedin.com/in/philippeloctaux", "LinkedIn"),
|
link: Link::new(
|
||||||
|
uri!("https://linkedin.com/in/philippeloctaux").into(),
|
||||||
|
"LinkedIn",
|
||||||
|
),
|
||||||
icon: Icon::Linkedin,
|
icon: Icon::Linkedin,
|
||||||
},
|
},
|
||||||
Www {
|
Www {
|
||||||
link: Link::new("/email", "Email"),
|
link: Link::new(uri!("/email").into(), "Email"),
|
||||||
icon: Icon::Email,
|
icon: Icon::Email,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -39,27 +57,9 @@ pub fn Www() -> impl IntoView {
|
||||||
"grid", "grid-cols-3", "lg:grid-cols-6", "gap-4", "place-content-center"
|
"grid", "grid-cols-3", "lg:grid-cols-6", "gap-4", "place-content-center"
|
||||||
)>
|
)>
|
||||||
|
|
||||||
{www
|
{www.into_iter().map(|w| {w.into_any()}).collect_view()}
|
||||||
.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>
|
</div>
|
||||||
}
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,37 @@
|
||||||
|
use crate::common::wallpapers::WALLPAPERS;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[component]
|
pub fn wallpapers_page() -> impl IntoAny {
|
||||||
pub fn WallpapersPage() -> impl IntoView {
|
let wallpaper_markers = WALLPAPERS
|
||||||
use leptos_leaflet::{TileLayer, MapContainer, Position, Marker, position, Popup};
|
.iter()
|
||||||
|
.map(|wallpaper| {
|
||||||
let wallpapers = WALLPAPERS;
|
format!(
|
||||||
|
r#"L.marker({}).addTo(map).bindPopup(`<p>{}<br>{}<br><b>{}</b></p><a href="/?wallpaper={}">Use as wallpaper</a>`);"#,
|
||||||
|
wallpaper.gps,
|
||||||
|
wallpaper.location.precise,
|
||||||
|
wallpaper.location.broad,
|
||||||
|
wallpaper.date,
|
||||||
|
wallpaper.filename,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
view! {
|
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>
|
<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>
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||||
</ContentPage>
|
<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 inner_html=r#"
|
||||||
|
let map = L.map('map').setView([48.858288, 2.294442], 2);
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
"#></script>
|
||||||
|
|
||||||
|
<script inner_html=wallpaper_markers></script>
|
||||||
|
}.into_any()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
crates/plcom/src/views.rs
Normal file
63
crates/plcom/src/views.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub fn shell(title: &str, children: impl IntoAny) -> AnyView {
|
||||||
|
const SUFFIX: &str = "Philippe Loctaux";
|
||||||
|
let title = if title != SUFFIX {
|
||||||
|
format!("{title} - {SUFFIX}")
|
||||||
|
} else {
|
||||||
|
title.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let year = jiff::Zoned::now().year();
|
||||||
|
view! {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width"/>
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
<title>{title}</title>
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
color="#0c4a6e"
|
||||||
|
/>
|
||||||
|
<meta name="msapplication-TileColor" content="#0c4a6e"/>
|
||||||
|
<meta name="theme-color" content="#0c4a6e"/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class=tw_join!("flex", "flex-col", "min-h-screen", "bg-gray-900", "text-white")>
|
||||||
|
<main class=tw_join!("flex-grow")>
|
||||||
|
{children.into_any()}
|
||||||
|
</main>
|
||||||
|
<footer class=tw_join!("bg-black")>
|
||||||
|
<div class=tw_join!("container", "mx-auto", "px-4", "py-8")>
|
||||||
|
<p>"© 2015 - "{year}" Philippe Loctaux, made with "{underline_link(Link::new(uri!("https://leptos.dev").into(), "Leptos"), None).into_any()}"."</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
}.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content_page(title: &str, children: impl IntoAny) -> AnyView {
|
||||||
|
shell(
|
||||||
|
title,
|
||||||
|
view! {
|
||||||
|
<div class=tw_join!("container", "mx-auto", "px-4", "py-16")>
|
||||||
|
<h1 class=tw_join!("text-3xl", "sm:text-4xl", "font-bold")>{title.to_string()}</h1>
|
||||||
|
{underline_link(Link::new(uri!("/").into(), "← Home"), None).into_any()}
|
||||||
|
<div class=tw_join!("mt-8")>{children.into_any()}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
@ -13,4 +13,4 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
crates/plcom/tailwind.nix
Normal file
14
crates/plcom/tailwind.nix
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
stdenvNoCC,
|
||||||
|
tailwindcss,
|
||||||
|
src,
|
||||||
|
inputFile,
|
||||||
|
}:
|
||||||
|
|
||||||
|
stdenvNoCC.mkDerivation {
|
||||||
|
name = "plcom-css-tailwind";
|
||||||
|
inherit src;
|
||||||
|
buildInputs = [ tailwindcss ];
|
||||||
|
dontUnpack = true;
|
||||||
|
buildPhase = "${tailwindcss}/bin/tailwindcss --config ${src}/tailwind.config.js --input ${src}/${inputFile} --output $out/output.css --minify";
|
||||||
|
}
|
||||||
422
crates/plcom/wallpapers.json
Normal file
422
crates/plcom/wallpapers.json
Normal file
|
|
@ -0,0 +1,422 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"filename": "IMG_2287.jpeg",
|
||||||
|
"date": "2021-09-11",
|
||||||
|
"width": 2048,
|
||||||
|
"height": 1536,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.398243,
|
||||||
|
"longitude": -71.04341
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Saguenay–Lac-Saint-Jean",
|
||||||
|
"broad": "Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2950.jpeg",
|
||||||
|
"date": "2022-02-26",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 24.82987,
|
||||||
|
"longitude": -80.9947
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Florida",
|
||||||
|
"broad": "United States"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2879.jpeg",
|
||||||
|
"date": "2022-02-23",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.42597,
|
||||||
|
"longitude": -71.05559
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Saguenay–Lac-Saint-Jean",
|
||||||
|
"broad": "Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2361.jpeg",
|
||||||
|
"date": "2021-08-27",
|
||||||
|
"width": 2048,
|
||||||
|
"height": 1536,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.430798,
|
||||||
|
"longitude": -71.06215
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Saguenay–Lac-Saint-Jean",
|
||||||
|
"broad": "Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_1395.jpeg",
|
||||||
|
"date": "2024-05-19",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.104378,
|
||||||
|
"longitude": 7.07935
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Haut-Rhin",
|
||||||
|
"broad": "Grand Est, France métropolitaine, France"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_1301.jpeg",
|
||||||
|
"date": "2021-12-11",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.580082,
|
||||||
|
"longitude": -70.87979
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Le Fjord-du-Saguenay",
|
||||||
|
"broad": "Saguenay–Lac-Saint-Jean, Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_4359.jpeg",
|
||||||
|
"date": "2022-05-10",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 34.056507,
|
||||||
|
"longitude": -118.236946
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Los Angeles County",
|
||||||
|
"broad": "California, United States"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_0360.jpeg",
|
||||||
|
"date": "2021-10-10",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 45.498695,
|
||||||
|
"longitude": -73.57866
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Agglomération de Montréal",
|
||||||
|
"broad": "Montréal (région administrative), Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_0946.jpeg",
|
||||||
|
"date": "2021-10-24",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.43079,
|
||||||
|
"longitude": -71.062904
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Saguenay–Lac-Saint-Jean",
|
||||||
|
"broad": "Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2392.jpeg",
|
||||||
|
"date": "2024-09-10",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 50.08757,
|
||||||
|
"longitude": 14.4074135
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Hlavní město Praha",
|
||||||
|
"broad": "Praha, Česko"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_7469.jpeg",
|
||||||
|
"date": "2023-04-16",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 43.73368,
|
||||||
|
"longitude": 3.5493166
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Hérault",
|
||||||
|
"broad": "Occitanie, France métropolitaine, France"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_1801.jpeg",
|
||||||
|
"date": "2021-12-27",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 40.576057,
|
||||||
|
"longitude": -73.97656
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Kings County",
|
||||||
|
"broad": "City of New York, New York, United States"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_9011.jpeg",
|
||||||
|
"date": "2023-09-16",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 38.71581,
|
||||||
|
"longitude": -9.129289
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Lisboa",
|
||||||
|
"broad": "Portugal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_3076.jpeg",
|
||||||
|
"date": "2024-11-02",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 45.977516,
|
||||||
|
"longitude": 5.4183946
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Ain",
|
||||||
|
"broad": "Auvergne-Rhône-Alpes, France métropolitaine, France"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_1292.jpeg",
|
||||||
|
"date": "2021-12-11",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.582912,
|
||||||
|
"longitude": -70.87781
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Le Fjord-du-Saguenay",
|
||||||
|
"broad": "Saguenay–Lac-Saint-Jean, Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_3186.jpeg",
|
||||||
|
"date": "2022-03-01",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 23.087925,
|
||||||
|
"longitude": -81.44968
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Matanzas",
|
||||||
|
"broad": "Cuba"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_9163.jpeg",
|
||||||
|
"date": "2023-09-18",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 38.679382,
|
||||||
|
"longitude": -9.171367
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Setúbal",
|
||||||
|
"broad": "Portugal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_9030.jpeg",
|
||||||
|
"date": "2023-09-16",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 38.710762,
|
||||||
|
"longitude": -9.143733
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Lisboa",
|
||||||
|
"broad": "Portugal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_3485.jpeg",
|
||||||
|
"date": "2022-03-03",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 23.135822,
|
||||||
|
"longitude": -81.28973
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Matanzas",
|
||||||
|
"broad": "Cuba"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2980.jpeg",
|
||||||
|
"date": "2024-10-26",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.218185,
|
||||||
|
"longitude": -3.1597612
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Côtes-d'Armor",
|
||||||
|
"broad": "Bretagne, France métropolitaine, France"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2134.jpeg",
|
||||||
|
"date": "2021-09-19",
|
||||||
|
"width": 2048,
|
||||||
|
"height": 1536,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.427017,
|
||||||
|
"longitude": -72.16885
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Le Domaine-du-Roy",
|
||||||
|
"broad": "Saguenay–Lac-Saint-Jean, Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2319.jpeg",
|
||||||
|
"date": "2024-09-10",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 50.088455,
|
||||||
|
"longitude": 14.393522
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Hlavní město Praha",
|
||||||
|
"broad": "Praha, Česko"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_3174.jpeg",
|
||||||
|
"date": "2022-03-01",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 23.038994,
|
||||||
|
"longitude": -81.49568
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Matanzas",
|
||||||
|
"broad": "Cuba"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2289.jpeg",
|
||||||
|
"date": "2021-12-30",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 40.705616,
|
||||||
|
"longitude": -73.92375
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Kings County",
|
||||||
|
"broad": "City of New York, New York, United States"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_3284.jpeg",
|
||||||
|
"date": "2022-03-01",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 23.13444,
|
||||||
|
"longitude": -81.285706
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Cárdenas",
|
||||||
|
"broad": "Matanzas, Cuba"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_0492.jpeg",
|
||||||
|
"date": "2021-10-12",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 43.079403,
|
||||||
|
"longitude": -79.07802
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Niagara Region",
|
||||||
|
"broad": "Golden Horseshoe, Ontario, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_2552.jpeg",
|
||||||
|
"date": "2024-09-11",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 50.088455,
|
||||||
|
"longitude": 14.449577
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Hlavní město Praha",
|
||||||
|
"broad": "Praha, Česko"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_0938.jpeg",
|
||||||
|
"date": "2021-10-24",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.43053,
|
||||||
|
"longitude": -71.05728
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Saguenay–Lac-Saint-Jean",
|
||||||
|
"broad": "Québec, Canada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_9427.jpeg",
|
||||||
|
"date": "2023-09-20",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 38.792416,
|
||||||
|
"longitude": -9.382112
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Lisboa",
|
||||||
|
"broad": "Portugal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "IMG_7634.jpeg",
|
||||||
|
"date": "2023-05-28",
|
||||||
|
"width": 4032,
|
||||||
|
"height": 3024,
|
||||||
|
"gps": {
|
||||||
|
"latitude": 48.05029,
|
||||||
|
"longitude": 6.745989
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"precise": "Vosges",
|
||||||
|
"broad": "Grand Est, France métropolitaine, France"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
98
flake.lock
generated
Normal file
98
flake.lock
generated
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1732407143,
|
||||||
|
"narHash": "sha256-qJOGDT6PACoX+GbNH2PPx2ievlmtT1NVeTB80EkRLys=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "f2b4b472983817021d9ffb60838b2b36b9376b20",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1732238832,
|
||||||
|
"narHash": "sha256-sQxuJm8rHY20xq6Ah+GwIUkF95tWjGRd1X8xF+Pkk38=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "8edf06bea5bcbee082df1b7369ff973b91618b8d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1732328983,
|
||||||
|
"narHash": "sha256-RHt12f/slrzDpSL7SSkydh8wUE4Nr4r23HlpWywed9E=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "ed8aa5b64f7d36d9338eb1d0a3bb60cf52069a72",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
69
flake.nix
Normal file
69
flake.nix
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
description = "philippeloctaux dot com";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
crane.url = "github:ipetkov/crane";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
rust-overlay = {
|
||||||
|
url = "github:oxalica/rust-overlay";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
nixpkgs,
|
||||||
|
crane,
|
||||||
|
flake-utils,
|
||||||
|
rust-overlay,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ (import rust-overlay) ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Gather assets
|
||||||
|
plcomAssets = import ./crates/plcom/assets.nix {
|
||||||
|
stdenvNoCC = pkgs.stdenvNoCC;
|
||||||
|
tailwindcss = pkgs.tailwindcss;
|
||||||
|
tailwindProjectRoot = ./crates/plcom;
|
||||||
|
src = ./public;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Which rust toolchain to use
|
||||||
|
craneLib = (crane.mkLib pkgs).overrideToolchain (p: p.rust-bin.stable.latest.default);
|
||||||
|
|
||||||
|
# Build binary
|
||||||
|
plcomBinary = pkgs.callPackage ./crates/plcom/default.nix {
|
||||||
|
libiconv = pkgs.libiconv;
|
||||||
|
lib = pkgs.lib;
|
||||||
|
pkg-config = pkgs.pkg-config;
|
||||||
|
stdenv = pkgs.stdenv;
|
||||||
|
craneLib = craneLib;
|
||||||
|
};
|
||||||
|
|
||||||
|
# How to launch binary
|
||||||
|
plcom = pkgs.writeShellScriptBin "plcom" ''
|
||||||
|
PLCOM_ASSETS_PATH=${plcomAssets} ${plcomBinary}/bin/plcom
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = {
|
||||||
|
inherit plcom;
|
||||||
|
default = plcom;
|
||||||
|
};
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
# Build the crate as part of `nix flake check` for convenience
|
||||||
|
inherit plcomBinary;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
public/wallpapers/IMG_2980.jpeg
Normal file
BIN
public/wallpapers/IMG_2980.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 MiB |
BIN
public/wallpapers/IMG_3076.jpeg
Normal file
BIN
public/wallpapers/IMG_3076.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
|
|
@ -16,7 +16,7 @@ http://philippeloctaux.com
|
||||||
## wallpapers
|
## wallpapers
|
||||||
|
|
||||||
1. place **JPEG** files in `public/wallpapers` and make sure they have exif data (GPS + date)
|
1. place **JPEG** files in `public/wallpapers` and make sure they have exif data (GPS + date)
|
||||||
2. run `cargo run -p gen-wallpapers --example cli -- ./public/wallpapers > ./crates/plcom/wallpapers.json` to generate wallpaper metadata
|
2. inside `crates/gen-wallpapers`, run `cargo run --example cli -- ./public/wallpapers > ./crates/plcom/wallpapers.json` to generate wallpaper metadata
|
||||||
|
|
||||||
## icons
|
## icons
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue