improve some things
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
target
|
target
|
||||||
.idea
|
.idea
|
||||||
assets/index.css
|
assets/index.css
|
||||||
|
services.toml
|
||||||
|
|
816
Cargo.lock
generated
|
@ -10,12 +10,13 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7.4"
|
axum = "0.7.4"
|
||||||
# fluent = "0.16.0"
|
|
||||||
minify-html = "0.15.0"
|
minify-html = "0.15.0"
|
||||||
minijinja = { version = "1.0.12", features = ["loader"] }
|
minijinja = { version = "1.0.12", features = ["loader"] }
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
config = { version = "0.14.0", features = ["toml"] }
|
||||||
tokio = { version = "1.36.0", features = ["full"] }
|
tokio = { version = "1.36.0", features = ["full"] }
|
||||||
toml = "0.8.10"
|
toml = "0.8.10"
|
||||||
tower-http = { version = "0.5.1", features = ["fs", "timeout", "trace"] }
|
tower-http = { version = "0.5.1", features = ["fs", "timeout", "trace"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
serde_derive = "1.0.199"
|
||||||
|
serde = "1.0.199"
|
||||||
|
|
23
Dockerfile
|
@ -1,8 +1,8 @@
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable as build_amd64
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:x86_64-musl-stable AS build_amd64
|
||||||
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable as build_arm64
|
FROM --platform=$BUILDPLATFORM ghcr.io/blackdex/rust-musl:aarch64-musl-stable AS build_arm64
|
||||||
|
|
||||||
# Build image
|
# Build image
|
||||||
FROM --platform=$BUILDPLATFORM build_${TARGETARCH} as build
|
FROM --platform=$BUILDPLATFORM build_${TARGETARCH} AS build
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
|
@ -31,13 +31,16 @@ RUN curl -Lo tailwindcss https://github.com/tailwindlabs/tailwindcss/releases/la
|
||||||
|
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
COPY templates ./templates
|
COPY config.toml ./config.toml
|
||||||
|
|
||||||
# Build the actual app
|
# Build the actual app
|
||||||
RUN touch src/main.rs && \
|
RUN touch src/main.rs && \
|
||||||
source /env-cargo && \
|
source /env-cargo && \
|
||||||
cargo install --path . --target "${CARGO_TARGET}"
|
cargo install --path . --target "${CARGO_TARGET}"
|
||||||
|
|
||||||
|
# Required for Tailwind to know what classes are used
|
||||||
|
COPY templates ./templates
|
||||||
|
|
||||||
COPY tailwind.* ./
|
COPY tailwind.* ./
|
||||||
|
|
||||||
RUN ./tailwindcss -i tailwind.css -o index.css --minify
|
RUN ./tailwindcss -i tailwind.css -o index.css --minify
|
||||||
|
@ -45,27 +48,27 @@ RUN ./tailwindcss -i tailwind.css -o index.css --minify
|
||||||
|
|
||||||
|
|
||||||
# Runtime image
|
# Runtime image
|
||||||
FROM docker.io/alpine
|
FROM docker.io/alpine:latest
|
||||||
|
|
||||||
WORKDIR /homepage
|
WORKDIR /etc/homepage
|
||||||
|
|
||||||
ARG GID=8686
|
ARG GID=8686
|
||||||
ARG UID=8686
|
ARG UID=8686
|
||||||
|
|
||||||
RUN addgroup -g ${GID} homepage && \
|
RUN addgroup -g ${GID} homepage && \
|
||||||
adduser -u ${UID} -D -H -G homepage homepage
|
adduser -u ${UID} -D -H -G homepage homepage
|
||||||
|
|
||||||
USER homepage:homepage
|
USER homepage:homepage
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8686
|
||||||
|
|
||||||
COPY --from=build /root/.cargo/bin/homepage /usr/local/bin/homepage
|
COPY --from=build /root/.cargo/bin/homepage /usr/local/bin/homepage
|
||||||
|
|
||||||
|
COPY templates ./templates
|
||||||
|
|
||||||
COPY assets ./assets
|
COPY assets ./assets
|
||||||
|
|
||||||
# Copy CSS generated by Tailwind
|
# Copy CSS generated by Tailwind
|
||||||
COPY --from=build /usr/src/homepage/index.css assets/
|
COPY --from=build /usr/src/homepage/index.css assets/
|
||||||
|
|
||||||
COPY services.toml .
|
|
||||||
|
|
||||||
ENTRYPOINT ["homepage"]
|
ENTRYPOINT ["homepage"]
|
||||||
|
|
42
README.md
|
@ -1,3 +1,45 @@
|
||||||
# homepage
|
# homepage
|
||||||
|
|
||||||
Source for my [homepage](https://viyurz.fr/), built with Axum & Tailwind (CSS gives me nightmares).
|
Source for my [homepage](https://viyurz.fr/), built with Axum & Tailwind (CSS gives me nightmares).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Change the default configuration by creating a file at `/etc/homepage/config.toml`
|
||||||
|
or setting the environment variable `HP_CONFIG_FILE` if using another path.
|
||||||
|
|
||||||
|
Configuration options can also be set using environment variables by
|
||||||
|
preceding the variable name by `HP_` (ex. `HP_LISTEN_ADDRESS`).
|
||||||
|
|
||||||
|
## Example service
|
||||||
|
|
||||||
|
```
|
||||||
|
services.toml
|
||||||
|
|
||||||
|
[[services]]
|
||||||
|
name = "Vaultwarden"
|
||||||
|
description = "Rust rewrite of the Bitwarden server, a password management service."
|
||||||
|
domain = "vw.viyurz.fr"
|
||||||
|
language = "Rust"
|
||||||
|
repository_url = "https://github.com/dani-garcia/vaultwarden"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create & push multi-platform image
|
||||||
|
|
||||||
|
Create a builder that use the `docker-container`` driver, which supports multi-platform builds:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker buildx create --name multiarch --bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the multi-platform image using this new builder:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker buildx build --builder multiarch --load --platform linux/amd64,linux/arm64 --tag git.ahur.ac/viyurz/homepage:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Publish the image:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker login git.ahur.ac
|
||||||
|
docker push git.ahur.ac/viyurz/homepage:latest
|
||||||
|
```
|
||||||
|
|
9
assets/fa/all.min.css
vendored
Normal file
BIN
assets/fa/fa-brands-400.woff2
Normal file
BIN
assets/fa/fa-solid-900.woff2
Normal file
Before Width: | Height: | Size: 978 B |
BIN
assets/favicon-180x180.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 34 KiB |
BIN
assets/favicon.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/git.png
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.3 KiB |
BIN
assets/images/element.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
assets/images/etebase.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
BIN
assets/images/hedgedoc.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/images/matrix-dark.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
assets/images/matrix.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/images/searxng-dark.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/images/searxng.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/images/stalwart mail server-dark.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/images/stalwart mail server.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
assets/images/stump.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/images/vaultwarden.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
assets/logo.jpg
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 43 KiB |
BIN
assets/logo.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 576 KiB |
|
@ -1,2 +1,4 @@
|
||||||
ip = "0.0.0.0"
|
# Default configuration interpreted at compile time
|
||||||
port = 8080
|
LISTEN_ADDRESS = "0.0.0.0"
|
||||||
|
LISTEN_PORT = "8686"
|
||||||
|
SERVICES_FILE = "/etc/homepage/services.toml"
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
name = "Element"
|
name = "Element"
|
||||||
description = "Web client of Element, an instant messaging client implementing the Matrix protocol."
|
description = "Web client of Element, an instant messaging client implementing the Matrix protocol."
|
||||||
domain = "element.viyurz.fr"
|
domain = "element.viyurz.fr"
|
||||||
image = "/assets/element.png"
|
|
||||||
language = "TypeScript"
|
language = "TypeScript"
|
||||||
repository_url = "https://github.com/element-hq/element-web"
|
repository_url = "https://github.com/element-hq/element-web"
|
||||||
|
|
||||||
|
@ -10,23 +9,13 @@ repository_url = "https://github.com/element-hq/element-web"
|
||||||
name = "EteBase"
|
name = "EteBase"
|
||||||
description = "Server for EteSync, an end-to-end encrypted contacts, calendars, tasks and notes provider."
|
description = "Server for EteSync, an end-to-end encrypted contacts, calendars, tasks and notes provider."
|
||||||
domain = "etebase.viyurz.fr"
|
domain = "etebase.viyurz.fr"
|
||||||
image = "/assets/etesync.png"
|
|
||||||
language = "Python"
|
language = "Python"
|
||||||
repository_url = "https://github.com/etesync/server"
|
repository_url = "https://github.com/etesync/server"
|
||||||
|
|
||||||
[[services]]
|
|
||||||
name = "EteSync (Soon™)"
|
|
||||||
description = "Web client of EteSync, an end-to-end encrypted contacts, calendars, tasks and notes provider."
|
|
||||||
domain = "etesync.viyurz.fr"
|
|
||||||
image = "/assets/etesync.png"
|
|
||||||
language = "TypeScript"
|
|
||||||
repository_url = "https://github.com/etesync/etesync-web"
|
|
||||||
|
|
||||||
[[services]]
|
[[services]]
|
||||||
name = "HedgeDoc"
|
name = "HedgeDoc"
|
||||||
description = "A real-time collaborative markdown editor."
|
description = "A real-time collaborative markdown editor."
|
||||||
domain = "hedgedoc.viyurz.fr"
|
domain = "hedgedoc.viyurz.fr"
|
||||||
image = "/assets/hedgedoc.png"
|
|
||||||
language = "TypeScript"
|
language = "TypeScript"
|
||||||
repository_url = "https://github.com/hedgedoc/hedgedoc"
|
repository_url = "https://github.com/hedgedoc/hedgedoc"
|
||||||
|
|
||||||
|
@ -34,7 +23,6 @@ repository_url = "https://github.com/hedgedoc/hedgedoc"
|
||||||
name = "Matrix"
|
name = "Matrix"
|
||||||
description = "Synapse homeserver implemeting the Matrix protocol, an open standard for real-time communication supporting encryption and VoIP."
|
description = "Synapse homeserver implemeting the Matrix protocol, an open standard for real-time communication supporting encryption and VoIP."
|
||||||
domain = "matrix.viyurz.fr"
|
domain = "matrix.viyurz.fr"
|
||||||
image = "/assets/matrix.png"
|
|
||||||
language = "Python"
|
language = "Python"
|
||||||
repository_url = "https://github.com/element-hq/synapse"
|
repository_url = "https://github.com/element-hq/synapse"
|
||||||
|
|
||||||
|
@ -42,23 +30,20 @@ repository_url = "https://github.com/element-hq/synapse"
|
||||||
name = "SearXNG"
|
name = "SearXNG"
|
||||||
description = "A privacy-respecting, hackable metasearch engine."
|
description = "A privacy-respecting, hackable metasearch engine."
|
||||||
domain = "searx.viyurz.fr"
|
domain = "searx.viyurz.fr"
|
||||||
image = "/assets/searxng.png"
|
|
||||||
language = "Python"
|
language = "Python"
|
||||||
repository_url = "https://github.com/searxng/searxng"
|
repository_url = "https://github.com/searxng/searxng"
|
||||||
|
|
||||||
[[services]]
|
[[services]]
|
||||||
name = "Stalwart Mail Server (Soon™)"
|
name = "Stalwart Mail Server"
|
||||||
description = "Secure & Modern All-in-One Mail Server (IMAP, JMAP, SMTP)."
|
description = "Secure & Modern All-in-One Mail Server (IMAP, JMAP, SMTP)."
|
||||||
domain = "smtp.viyurz.fr"
|
domain = "mail.viyurz.fr"
|
||||||
image = "/assets/mail-server.png"
|
|
||||||
language = "Rust"
|
language = "Rust"
|
||||||
repository_url = "https://github.com/stalwartlabs/mail-server"
|
repository_url = "https://github.com/stalwartlabs/mail-server"
|
||||||
|
|
||||||
[[services]]
|
[[services]]
|
||||||
name = "Stump (Soon™)"
|
name = "Stump"
|
||||||
description = "A comics, manga and digital book server with OPDS support (WIP)."
|
description = "A comics, manga and digital book server with OPDS support."
|
||||||
domain = "stump.viyurz.fr"
|
domain = "stump.viyurz.fr"
|
||||||
image = "/assets/stump.png"
|
|
||||||
language = "Rust / TypeScript"
|
language = "Rust / TypeScript"
|
||||||
repository_url = "https://github.com/stumpapp/stump"
|
repository_url = "https://github.com/stumpapp/stump"
|
||||||
|
|
||||||
|
@ -66,6 +51,5 @@ repository_url = "https://github.com/stumpapp/stump"
|
||||||
name = "Vaultwarden"
|
name = "Vaultwarden"
|
||||||
description = "Rust rewrite of the Bitwarden server, a password management service."
|
description = "Rust rewrite of the Bitwarden server, a password management service."
|
||||||
domain = "vw.viyurz.fr"
|
domain = "vw.viyurz.fr"
|
||||||
image = "/assets/vaultwarden.png"
|
|
||||||
language = "Rust"
|
language = "Rust"
|
||||||
repository_url = "https://github.com/dani-garcia/vaultwarden"
|
repository_url = "https://github.com/dani-garcia/vaultwarden"
|
34
src/config.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use config::{Config, Environment, File, FileFormat};
|
||||||
|
use serde_derive::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub listen_address: String,
|
||||||
|
pub listen_port: u16,
|
||||||
|
pub services_file: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get() -> AppConfig {
|
||||||
|
// Interpreted at compile time
|
||||||
|
static DEFAULT: &str = include_str!("../config.toml");
|
||||||
|
|
||||||
|
let mut config_file = String::from("/etc/homepage/config.toml");
|
||||||
|
match env::var("HP_CONFIG_FILE") {
|
||||||
|
Ok(val) => config_file = val,
|
||||||
|
Err(_e) => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config: Config = Config::builder()
|
||||||
|
// Load default configuration stored in DEFAULT.
|
||||||
|
.add_source(File::from_str(DEFAULT, FileFormat::Toml))
|
||||||
|
// Add configuration file.
|
||||||
|
.add_source(File::with_name(&*config_file).required(false))
|
||||||
|
// Add environment variables.
|
||||||
|
.add_source(Environment::with_prefix("HP"))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
return config.try_deserialize().unwrap();
|
||||||
|
}
|
33
src/handlers.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::Html;
|
||||||
|
use minijinja::context;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
pub async fn home(State(state): State<Arc<AppState>>) -> Result<Html<String>, StatusCode> {
|
||||||
|
let template = state.env.get_template("home").unwrap();
|
||||||
|
|
||||||
|
let rendered = template
|
||||||
|
.render(context! {
|
||||||
|
title => "Home"
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(Html(rendered))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn services(State(state): State<Arc<AppState>>) -> Result<Html<String>, StatusCode> {
|
||||||
|
let template = state.env.get_template("services").unwrap();
|
||||||
|
|
||||||
|
let rendered = template
|
||||||
|
.render(context! {
|
||||||
|
title => "Services",
|
||||||
|
services => state.services
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(Html(rendered))
|
||||||
|
}
|
104
src/main.rs
|
@ -1,30 +1,24 @@
|
||||||
use std::fs;
|
use std::{
|
||||||
use std::fs::{read, read_to_string};
|
fs::{read, read_dir, read_to_string},
|
||||||
use std::time::Duration;
|
sync::Arc,
|
||||||
use axum::extract::State;
|
time::Duration,
|
||||||
use axum::http::StatusCode;
|
};
|
||||||
use axum::{response::Html, routing::get, Router};
|
|
||||||
use minijinja::{context, Environment};
|
use axum::{routing::get, Router};
|
||||||
use std::sync::Arc;
|
use minijinja::Environment;
|
||||||
use tower_http::services::ServeDir;
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tower_http::timeout::TimeoutLayer;
|
use tower_http::{services::ServeDir, timeout::TimeoutLayer, trace::TraceLayer};
|
||||||
use tower_http::trace::TraceLayer;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod handlers;
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
env: Environment<'static>,
|
env: Environment<'static>,
|
||||||
services: Vec<Service>,
|
services: Vec<Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Config {
|
|
||||||
ip: Option<String>,
|
|
||||||
port: Option<u16>,
|
|
||||||
services: Option<Vec<Service>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ServiceVec {
|
struct ServiceVec {
|
||||||
services: Vec<Service>,
|
services: Vec<Service>,
|
||||||
|
@ -35,36 +29,32 @@ struct Service {
|
||||||
name: String,
|
name: String,
|
||||||
description: String,
|
description: String,
|
||||||
domain: String,
|
domain: String,
|
||||||
image: String,
|
|
||||||
language: String,
|
language: String,
|
||||||
repository_url: String,
|
repository_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_config() -> Config {
|
|
||||||
let config_str = read_to_string("config.toml").expect("Failed to read config.toml file.");
|
|
||||||
let services_str = read_to_string("services.toml").expect("Failed to read services.toml file.");
|
|
||||||
|
|
||||||
let mut config: Config = toml::from_str(&config_str).unwrap();
|
|
||||||
|
|
||||||
let service_vec: ServiceVec = toml::from_str(&services_str).unwrap();
|
|
||||||
config.services = Option::from(service_vec.services);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minify templates before loading them
|
// Minify templates before loading them
|
||||||
fn load_templates() -> Environment<'static> {
|
fn load_templates() -> Environment<'static> {
|
||||||
let mut env = Environment::new();
|
let mut env = Environment::new();
|
||||||
let cfg = minify_html::Cfg::spec_compliant();
|
let cfg = minify_html::Cfg::spec_compliant();
|
||||||
|
|
||||||
for template_entry in fs::read_dir("./templates").expect("Failed to read directory ./templates.") {
|
for template_entry in read_dir("./templates").expect("Failed to read directory ./templates.") {
|
||||||
let template_file_path = template_entry.unwrap().path();
|
let template_file_path = template_entry.unwrap().path();
|
||||||
let template_name = template_file_path.file_stem().unwrap().to_os_string().into_string().unwrap();
|
let template_name = template_file_path
|
||||||
|
.file_stem()
|
||||||
|
.unwrap()
|
||||||
|
.to_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let template_code = read(template_file_path).unwrap();
|
let template_code = read(template_file_path).unwrap();
|
||||||
let template_code_minified = minify_html::minify(&template_code, &cfg);
|
let template_code_minified = minify_html::minify(&template_code, &cfg);
|
||||||
|
|
||||||
env.add_template_owned(template_name, String::from_utf8(template_code_minified).unwrap()).unwrap();
|
env.add_template_owned(
|
||||||
|
template_name,
|
||||||
|
String::from_utf8(template_code_minified).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
|
@ -72,7 +62,7 @@ fn load_templates() -> Environment<'static> {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let config: Config = load_config();
|
let config = config::get();
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(
|
.with(
|
||||||
|
@ -88,12 +78,22 @@ async fn main() {
|
||||||
// init template engine and add templates
|
// init template engine and add templates
|
||||||
let env: Environment = load_templates();
|
let env: Environment = load_templates();
|
||||||
|
|
||||||
|
// load services files
|
||||||
|
let services_str = read_to_string(&config.services_file)
|
||||||
|
.expect(&format!("Failed to read {}.", &config.services_file));
|
||||||
|
let services_vec: ServiceVec = toml::from_str(&services_str).expect(&format!(
|
||||||
|
"Failed to interpret services file {}.",
|
||||||
|
&config.services_file
|
||||||
|
));
|
||||||
|
let services = services_vec.services;
|
||||||
|
|
||||||
// pass env & services to handlers via state
|
// pass env & services to handlers via state
|
||||||
let app_state = Arc::new(AppState { env, services: config.services.unwrap() });
|
let app_state = Arc::new(AppState { env, services });
|
||||||
|
|
||||||
// define routes
|
// define routes
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(handler_home))
|
.route("/", get(handlers::home))
|
||||||
|
.route("/services", get(handlers::services))
|
||||||
.nest_service("/assets", ServeDir::new("assets"))
|
.nest_service("/assets", ServeDir::new("assets"))
|
||||||
.layer((
|
.layer((
|
||||||
TraceLayer::new_for_http(),
|
TraceLayer::new_for_http(),
|
||||||
|
@ -105,25 +105,15 @@ async fn main() {
|
||||||
|
|
||||||
// run it
|
// run it
|
||||||
let listener = tokio::net::TcpListener::bind(
|
let listener = tokio::net::TcpListener::bind(
|
||||||
config.ip.expect("Missing 'ip' config parameter.") +
|
config.listen_address + ":" + &config.listen_port.to_string(),
|
||||||
":" + &config.port.expect("Missing 'port' config parameter.").to_string())
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("listening on http://{}", listener.local_addr().unwrap());
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(shutdown_signal())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("listening on {}", listener.local_addr().unwrap());
|
|
||||||
axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handler_home(State(state): State<Arc<AppState>>) -> Result<Html<String>, StatusCode> {
|
|
||||||
let template = state.env.get_template("home").unwrap();
|
|
||||||
|
|
||||||
let rendered = template
|
|
||||||
.render(context! {
|
|
||||||
title => "Home",
|
|
||||||
services => state.services
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(Html(rendered))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shutdown_signal() {
|
async fn shutdown_signal() {
|
||||||
|
@ -134,7 +124,7 @@ async fn shutdown_signal() {
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let terminate = async {
|
let terminate = async {
|
||||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||||
.expect("failed to install signal handler")
|
.expect("failed to install signal handler")
|
||||||
.recv()
|
.recv()
|
||||||
|
@ -142,7 +132,7 @@ async fn shutdown_signal() {
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
let terminate = std::future::pending::<()>();
|
let terminate = std::future::pending::<()>();
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = ctrl_c => {},
|
_ = ctrl_c => {},
|
||||||
|
|
|
@ -1,16 +1,40 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./templates/**/*.html"],
|
content: ["./templates/**/*.html"],
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
extend: {
|
||||||
transparent: 'transparent',
|
colors: {
|
||||||
current: 'currentColor',
|
transparent: "transparent",
|
||||||
white: '#ffffff',
|
current: "currentColor",
|
||||||
primary: "#601237",
|
base: {
|
||||||
background: "#290718",
|
DEFAULT: "#ffffff",
|
||||||
|
dark: "#26010c",
|
||||||
},
|
},
|
||||||
|
surface: {
|
||||||
|
DEFAULT: "#fffcfd",
|
||||||
|
dark: "#4d041a",
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
DEFAULT: "#d85079",
|
||||||
|
dark: "#800d2f",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "#4d3239",
|
||||||
|
dark: "#807e7f",
|
||||||
|
},
|
||||||
|
subtle: {
|
||||||
|
DEFAULT: "#331a21",
|
||||||
|
dark: "#d9d7d7",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
DEFAULT: "#1a030a",
|
||||||
|
dark: "#fcfafb",
|
||||||
|
},
|
||||||
|
orange: "#BA562A",
|
||||||
|
rose: "#ba2a56",
|
||||||
|
turquoise: "#2ABABA",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
63
tailwind.css
|
@ -3,24 +3,55 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
h1, h2 {
|
@font-face {
|
||||||
@apply p-4;
|
font-family: "JetBrains Mono";
|
||||||
}
|
src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2")
|
||||||
|
format("woff2"),
|
||||||
|
url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff")
|
||||||
|
format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: auto;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2 {
|
html {
|
||||||
@apply font-bold;
|
font-family: "JetBrains Mono", sans-serif, system-ui;
|
||||||
@apply text-center;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1,
|
||||||
@apply text-3xl;
|
h2,
|
||||||
}
|
h3 {
|
||||||
|
@apply font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h1,
|
||||||
@apply text-2xl;
|
h2 {
|
||||||
}
|
@apply text-center;
|
||||||
|
}
|
||||||
|
|
||||||
h3 {
|
h1 {
|
||||||
@apply text-xl;
|
@apply text-2xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@apply text-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a,
|
||||||
|
footer a {
|
||||||
|
@apply hover:text-turquoise;
|
||||||
|
@apply hover:text-opacity-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-page {
|
||||||
|
@apply text-turquoise;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,33 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>{{ title }} - Viyurz</title>
|
<title>{{ title }} - Viyurz</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
|
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/favicon-180x180.png">
|
||||||
<meta property="og:title" content="{{ title }} - Viyurz">
|
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://viyurz.fr">
|
<meta property="og:title" content="{{ title }} - Viyurz" />
|
||||||
<meta property="og:description" content="{% block description %}Home of Viyurz.{% endblock %}">
|
<meta property="og:description" content="{% block description %}Viyurz's website.{% endblock %}" />
|
||||||
<meta property="og:image" content="https://viyurz.fr/assets/logo.jpg">
|
<meta property="og:url" content="https://viyurz.fr/" />
|
||||||
<link href="/assets/index.css" rel="stylesheet">
|
<meta property="og:image" content="https://viyurz.fr/assets/logo.jpg" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/index.css">
|
||||||
|
<link rel="stylesheet" href="/assets/fa/all.min.css">
|
||||||
|
<link rel="preload" href="/assets/index.css" as="style">
|
||||||
|
<link rel="preload" href="/assets/fa/all.min.css" as="style">
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-background text-white">
|
|
||||||
{% include "_navbar" %}
|
<body class="flex flex-col min-h-screen bg-base dark:bg-base-dark text-sm text-text dark:text-text-dark">
|
||||||
<main class="flex flex-col items-center p-6">
|
{% include "_navbar" %}
|
||||||
{% block main %}{% endblock %}
|
<main class="mb-auto flex flex-col items-center p-3 md:p-6">
|
||||||
</main>
|
{% block main %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
{% include "_footer" %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
13
templates/_footer.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<footer class="flex flex-row place-content-center space-x-12 md:space-x-6 p-2 w-full bg-overlay-dark text-text-dark">
|
||||||
|
<div class="max-md:basis-1/2 flex flex-col md:flex-row md:space-x-6 max-md:space-y-2 text-right">
|
||||||
|
<p><i class="fa-brands fa-discord"></i> viyurz</p>
|
||||||
|
<a href="https://www.reddit.com/user/Viyurz/" target="_blank"><i class="fa-brands fa-reddit-alien"></i>
|
||||||
|
u/Viyurz</a>
|
||||||
|
</div>
|
||||||
|
<div class="max-md:basis-1/2 flex flex-col md:flex-row md:space-x-6 max-md:space-y-2 text-left">
|
||||||
|
<a href="https://x.com/Viyurz" target="_blank"><i class="fa-brands fa-x-twitter"></i> @Viyurz</a>
|
||||||
|
<a href="https://www.youtube.com/@Viyurz" target="_blank"><i class="fa-brands fa-youtube"></i>
|
||||||
|
@Viyurz</a>
|
||||||
|
<a href="mailto:viyurz@viyurz.fr"><i class="fa-solid fa-envelope"></i> viyurz@viyurz.fr</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
|
@ -1,19 +1,40 @@
|
||||||
<nav class="flex flex-row place-content-around h-20 bg-primary px-12">
|
<nav class="flex flex-row place-content-around lg:px-12 md:h-14 bg-overlay-dark text-text-dark">
|
||||||
<div class="basis-1/3 flex flex-row items-center">
|
<div class="basis-1/6 flex flex-row items-center">
|
||||||
<a href="/" class="flex flex-row items-center p-0">
|
<a href="/" class="flex flex-row items-center p-0">
|
||||||
<img src="/assets/logo.jpg" alt="logo.jpg" class="size-16 rounded-lg p-3">
|
<img src="/assets/logo.png" alt="logo.png" class="size-16 p-2">
|
||||||
<h2>Viyurz</h2>
|
<h2 class="max-md:hidden">Viyurz</h2>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ul class="basis-1/3 flex flex-row justify-center items-center">
|
<div
|
||||||
<li><h2><a href="/">Home</a></h2></li>
|
class="basis-3/6 flex flex-col md:flex-row place-content-center place-items-center max-md:p-2 max-md:space-y-2 md:space-x-10">
|
||||||
<li><h2><a href="https://status.viyurz.fr/">Status</a></h2></li>
|
<h2><a class="{% if title == 'Home' %} active-page {% endif %}" href="/">Home</a></h2>
|
||||||
<li><h2><a href="mailto:viyurz@viyurz.fr">Contact</a></h2></li>
|
<h2><a class="{% if title == 'Services' %} active-page {% endif %}" href="/services">Services</a></h2>
|
||||||
</ul>
|
<h2><a href="https://status.viyurz.fr/" target="_blank">Status</a></h2>
|
||||||
<div class="basis-1/3 flex flex-row justify-end items-center">
|
<h2><a href="https://auth.viyurz.fr/" target="_blank">Account</a></h2>
|
||||||
<div class="grid grid-cols-2 divide-x-2 rounded-lg bg-background p-2">
|
</div>
|
||||||
<button class="px-2">English</button>
|
<div class="basis-1/6 flex flex-row justify-end items-center">
|
||||||
<button class="opacity-50 px-2">Français</button>
|
<div class="hidden relative inline-block opacity-10">
|
||||||
|
<div>
|
||||||
|
<button type="button"
|
||||||
|
class="md:before:content-['Language'] inline-flex w-full justify-center gap-x-1.5 px-3 py-2"
|
||||||
|
id="menu-button" aria-expanded="true" aria-haspopup="true">
|
||||||
|
<svg class="-mr-1 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="hidden absolute right-0 z-10 mt-2 text-right origin-top-right rounded-lg border border-overlay dark:border-overlay-dark bg-surface dark:bg-surface-dark focus:outline-none"
|
||||||
|
role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||||
|
<div class="py-1" role="none">
|
||||||
|
<a href="#" class="text-gray-700 block px-4 py-2 text-sm" role="menuitem" tabindex="-1"
|
||||||
|
id="menu-item-0">English</a>
|
||||||
|
<a href="#" class="text-gray-700 block px-4 py-2 text-sm" role="menuitem" tabindex="-1"
|
||||||
|
id="menu-item-1">Français</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
|
@ -1,23 +1,8 @@
|
||||||
{% extends "_base" %}
|
{% extends "_base" %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<h1>Services</h1>
|
<h1>Home</h1>
|
||||||
<div class="flex flex-row flex-wrap justify-center p-4">
|
<p
|
||||||
{% for service in services %}
|
class="text-center rounded-lg p-4 m-4 bg-overlay dark:bg-overlay-dark border border-base-dark dark:border-overlay-dark">
|
||||||
<a href="https://{{ service.domain }}/"
|
Personal website of someone who spends too much time watching Anime and playing video games.<br>
|
||||||
class="flex flex-col basis-3/12 bg-primary rounded-lg p-4 m-4 h-60 border-transparent border-2 hover:border-white">
|
Also I had nothing better to do so I built this pointless website.</p>
|
||||||
<div class="flex flex-row flex-center basis-1/3">
|
|
||||||
<img src="{{ service.image }}" alt="{{ service.name }} logo" class="w-16">
|
|
||||||
<div class="flex flex-col justify-center ml-4">
|
|
||||||
<h3>{{ service.name }}</h3>
|
|
||||||
<p class="opacity-75">{{ service.domain }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="mt-4 basis-1/3">{{ service.description }}</p>
|
|
||||||
<div class="flex flex-row items-end basis-1/3">
|
|
||||||
<button class="rounded-lg bg-background h-7 px-4 text-center"></> {{ service.language }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
27
templates/services.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "_base" %}
|
||||||
|
{% block main %}
|
||||||
|
<h1>Services</h1>
|
||||||
|
<div class="flex flex-row flex-wrap place-content-center place-items-center">
|
||||||
|
{% for service in services %}
|
||||||
|
<a href="https://{{ service.domain }}/"
|
||||||
|
class="flex flex-col basis-11/12 md:basis-5/12 xl:basis-3/12 rounded-lg p-4 m-4 h-60 hover:scale-105 transition bg-surface dark:bg-surface-dark border hover:border-2 border-base-dark dark:border-surface-dark hover:border-surface-dark dark:hover:border-surface">
|
||||||
|
<div class="flex flex-row flex-center basis-1/3">
|
||||||
|
<img src="/assets/images/{{ service.name | lower }}.png" alt="{{ service.name }} logo"
|
||||||
|
class="w-16 dark:hidden">
|
||||||
|
<img src="/assets/images/{{ service.name | lower }}-dark.png" alt="{{ service.name }} logo"
|
||||||
|
class="w-16 hidden dark:inline-block">
|
||||||
|
<div class="flex flex-col justify-center ml-4">
|
||||||
|
<h3>{{ service.name }}</h3>
|
||||||
|
<p class="text-subtle dark:text-subtle-dark">{{ service.domain }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 basis-1/3">{{ service.description }}</p>
|
||||||
|
<div class="flex flex-row items-end basis-1/3">
|
||||||
|
<button class="rounded-lg h-7 px-4 text-center text-text-dark bg-overlay dark:bg-overlay-dark"><i
|
||||||
|
class="fa-solid fa-code"></i> {{ service.language }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|