mirror of
https://github.com/RGBCube/Site
synced 2025-08-01 13:37:49 +00:00
Add routes, minification, asset serving (BROKEN), error handlers
This commit is contained in:
parent
c1af80979d
commit
c3d45b6ba4
20 changed files with 926 additions and 29 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,14 +1,19 @@
|
|||
*
|
||||
|
||||
!src/
|
||||
!src/errors/
|
||||
!src/errors/internal_server_error/
|
||||
!src/errors/not_found/
|
||||
!src/page/
|
||||
!src/page/cube/
|
||||
!src/routes/
|
||||
!src/routes/_404/
|
||||
!src/routes/index/
|
||||
|
||||
!.gitignore
|
||||
|
||||
!*.gif
|
||||
!*.woff2
|
||||
|
||||
!*.css
|
||||
!*.js
|
||||
!*.rs
|
||||
|
|
12
build.rs
Normal file
12
build.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=src");
|
||||
println!("cargo:rerun-if-changed=src.tar");
|
||||
|
||||
Command::new("tar")
|
||||
.args(["-czf", "src.tar", "src"])
|
||||
.output()
|
||||
.expect("Failed to create tar archive");
|
||||
}
|
94
src/asset.rs
Normal file
94
src/asset.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use maud::{
|
||||
html,
|
||||
Markup,
|
||||
PreEscaped,
|
||||
Render,
|
||||
};
|
||||
|
||||
use crate::minify;
|
||||
|
||||
pub fn extension_of(path: &str) -> Option<&str> {
|
||||
path.rsplit_once(".").map(|(_base, extension)| extension)
|
||||
}
|
||||
|
||||
pub enum Js {
|
||||
Shared(&'static str),
|
||||
Owned(String),
|
||||
}
|
||||
|
||||
impl Render for Js {
|
||||
fn render(&self) -> Markup {
|
||||
match self {
|
||||
Self::Shared(path) => {
|
||||
html! {
|
||||
script src=(format!("/assets/{}", minify::insert_min(path))) {}
|
||||
}
|
||||
},
|
||||
Self::Owned(content) => {
|
||||
html! {
|
||||
script {
|
||||
(minify::js(content))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod js {
|
||||
macro_rules! owned {
|
||||
($path:literal) => {
|
||||
crate::asset::Js::Owned(::embed::string!($path))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use owned;
|
||||
}
|
||||
|
||||
pub enum Css {
|
||||
Shared(&'static str),
|
||||
Owned(String),
|
||||
}
|
||||
|
||||
impl Render for Css {
|
||||
fn render(&self) -> Markup {
|
||||
match self {
|
||||
Self::Shared(path) => {
|
||||
html! {
|
||||
link rel="stylesheet" type="text/css" href=(format!("/assets/{}", minify::insert_min(path)));
|
||||
}
|
||||
},
|
||||
Self::Owned(content) => {
|
||||
html! {
|
||||
style {
|
||||
(minify::css(content))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod css {
|
||||
macro_rules! owned {
|
||||
($path:literal) => {
|
||||
crate::asset::Css::Owned(::embed::string!($path).to_string())
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use owned;
|
||||
}
|
||||
|
||||
pub struct File(pub &'static str);
|
||||
|
||||
impl Render for File {
|
||||
fn render(&self) -> Markup {
|
||||
PreEscaped(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for File {
|
||||
fn to_string(&self) -> String {
|
||||
format!("/assets/{}", self.0)
|
||||
}
|
||||
}
|
41
src/errors/internal_server_error/mod.rs
Normal file
41
src/errors/internal_server_error/mod.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use std::array;
|
||||
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
middleware::ErrorHandlerResponse,
|
||||
};
|
||||
use maud::html;
|
||||
|
||||
use crate::{
|
||||
asset,
|
||||
page::cube,
|
||||
};
|
||||
|
||||
pub fn handler<B: 'static>(
|
||||
response: ServiceResponse<B>,
|
||||
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
let (request, response) = response.into_parts();
|
||||
|
||||
let response = response.set_body(
|
||||
cube::create(
|
||||
asset::Css::Shared("not-found.css"),
|
||||
array::from_fn(|_| {
|
||||
(html! {
|
||||
div class="frame" { "error" }
|
||||
div class="square black" {}
|
||||
div class="square red" {}
|
||||
div class="square red" {}
|
||||
div class="square black" {}
|
||||
})
|
||||
.clone()
|
||||
}),
|
||||
)
|
||||
.into_string(),
|
||||
);
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(
|
||||
ServiceResponse::new(request, response)
|
||||
.map_into_boxed_body()
|
||||
.map_into_right_body(),
|
||||
))
|
||||
}
|
16
src/errors/mod.rs
Normal file
16
src/errors/mod.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
mod internal_server_error;
|
||||
mod not_found;
|
||||
|
||||
use actix_web::{
|
||||
http::StatusCode,
|
||||
middleware::ErrorHandlers,
|
||||
};
|
||||
|
||||
pub fn handler<B: 'static>() -> ErrorHandlers<B> {
|
||||
ErrorHandlers::new()
|
||||
.handler(StatusCode::NOT_FOUND, not_found::handler)
|
||||
.handler(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
internal_server_error::handler,
|
||||
)
|
||||
}
|
41
src/errors/not_found/mod.rs
Normal file
41
src/errors/not_found/mod.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use std::array;
|
||||
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
middleware::ErrorHandlerResponse,
|
||||
};
|
||||
use maud::html;
|
||||
|
||||
use crate::{
|
||||
asset,
|
||||
page::cube,
|
||||
};
|
||||
|
||||
pub fn handler<B: 'static>(
|
||||
response: ServiceResponse<B>,
|
||||
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
let (request, response) = response.into_parts();
|
||||
|
||||
let response = response.set_body(
|
||||
cube::create(
|
||||
asset::Css::Shared("not-found.css"),
|
||||
array::from_fn(|_| {
|
||||
(html! {
|
||||
div class="frame" { "404" }
|
||||
div class="square black" {}
|
||||
div class="square magenta" {}
|
||||
div class="square magenta" {}
|
||||
div class="square black" {}
|
||||
})
|
||||
.clone()
|
||||
}),
|
||||
)
|
||||
.into_string(),
|
||||
);
|
||||
|
||||
Ok(ErrorHandlerResponse::Response(
|
||||
ServiceResponse::new(request, response)
|
||||
.map_into_boxed_body()
|
||||
.map_into_right_body(),
|
||||
))
|
||||
}
|
33
src/errors/not_found/not-found.css
Normal file
33
src/errors/not_found/not-found.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
.face {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
box-shadow: 0 0 10px #AAAAAA;
|
||||
}
|
||||
|
||||
.square {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.black {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.magenta {
|
||||
background-color: magenta;
|
||||
}
|
||||
|
||||
.red {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: absolute;
|
||||
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
color: black;
|
||||
}
|
15
src/main.rs
15
src/main.rs
|
@ -1,10 +1,14 @@
|
|||
#![feature(lazy_cell)]
|
||||
|
||||
mod asset;
|
||||
mod errors;
|
||||
mod minify;
|
||||
mod page;
|
||||
// mod routes;
|
||||
mod routes;
|
||||
|
||||
use actix_web::{
|
||||
main as async_main,
|
||||
middleware,
|
||||
App,
|
||||
HttpServer,
|
||||
};
|
||||
|
@ -31,9 +35,14 @@ async fn main() -> anyhow::Result<()> {
|
|||
.target(env_logger::Target::Stdout)
|
||||
.init();
|
||||
|
||||
HttpServer::new(App::new) //|| App::new().route(routes::index))
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.wrap(errors::handler())
|
||||
.service(routes::handler())
|
||||
})
|
||||
.bind(("0.0.0.0", args.port))
|
||||
.with_context(|| format!("Failed to bind to 0.0.0.0:{port}", port = args.port))?
|
||||
.with_context(|| format!("Failed to bind to 0.0.0.0:{}", args.port))?
|
||||
.run()
|
||||
.await
|
||||
.with_context(|| "Failed to run HttpServer")?;
|
||||
|
|
73
src/minify.rs
Normal file
73
src/minify.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use std::{
|
||||
env::temp_dir,
|
||||
fs::File,
|
||||
hash::{
|
||||
BuildHasher,
|
||||
RandomState,
|
||||
},
|
||||
io::Write,
|
||||
};
|
||||
|
||||
use minify_js::{
|
||||
Session,
|
||||
TopLevelMode,
|
||||
};
|
||||
|
||||
use crate::asset::extension_of;
|
||||
|
||||
pub const MINIFIABLE: &[&str] = &[".js", ".css"];
|
||||
|
||||
pub fn is_minifiable(path: &str) -> bool {
|
||||
MINIFIABLE.iter().any(|extension| path.ends_with(extension))
|
||||
}
|
||||
|
||||
pub fn insert_min(path: &str) -> String {
|
||||
match path.rsplit_once(".") {
|
||||
Some((base, extension)) => format!("{base}.min.{extension}"),
|
||||
None => format!("{path}.min"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generic(path: &str, content: &[u8]) -> Vec<u8> {
|
||||
match extension_of(path) {
|
||||
Some("js") => js(&String::from_utf8(content.to_vec()).unwrap()).into_bytes(),
|
||||
Some("css") => css(&String::from_utf8(content.to_vec()).unwrap()).into_bytes(),
|
||||
_ => content.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn js(content: &str) -> String {
|
||||
let mut output = Vec::new();
|
||||
|
||||
minify_js::minify(
|
||||
&Session::new(),
|
||||
TopLevelMode::Module,
|
||||
content.as_bytes(),
|
||||
&mut output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
String::from_utf8(output)
|
||||
.map_err(|error| {
|
||||
let hash = RandomState::new()
|
||||
.hash_one(error.clone().into_bytes())
|
||||
.to_string();
|
||||
|
||||
let path = temp_dir().join(hash);
|
||||
|
||||
let mut file = File::create(&path).unwrap();
|
||||
file.write_all(&error.into_bytes()).unwrap();
|
||||
|
||||
format!(
|
||||
"Failed to create a String from minified JavaScript code. The minified code has \
|
||||
been written to {}",
|
||||
path.display()
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn css(content: &str) -> String {
|
||||
// TODO
|
||||
content.to_string()
|
||||
}
|
BIN
src/page/BaiJamjuree700.woff2
Normal file
BIN
src/page/BaiJamjuree700.woff2
Normal file
Binary file not shown.
108
src/page/cube/cube.css
Normal file
108
src/page/cube/cube.css
Normal file
|
@ -0,0 +1,108 @@
|
|||
@font-face {
|
||||
font-family: "Bai Jamjuree";
|
||||
font-weight: 700;
|
||||
src: url("/assets/BaiJamjuree700.woff2") format("woff2");
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: #000000;
|
||||
font-family: "Bai Jamjuree", sans;
|
||||
font-size: 450%;
|
||||
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
html {
|
||||
font-size: 200%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) and (min-width: 400px) {
|
||||
html {
|
||||
font-size: 300%;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000000;
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
.frame {
|
||||
background-color: #FFFFFF;
|
||||
|
||||
width: min-content;
|
||||
|
||||
padding: 0 .3em;
|
||||
border-radius: 1em;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.scene {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
perspective: 15em;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cube {
|
||||
height: 5em;
|
||||
width: 5em;
|
||||
|
||||
position: relative;
|
||||
|
||||
transform: translateZ(-2.498em);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.face {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.front {
|
||||
transform: rotateY(0deg) translateZ(2.498em);
|
||||
}
|
||||
|
||||
.top {
|
||||
/* Guess what? Yeah, you guessed right. Safari can't render shit. */
|
||||
transform: rotateX(89.99999999999999deg) translateZ(2.498em);
|
||||
}
|
||||
|
||||
.back {
|
||||
transform: rotateY(180deg) translateZ(2.498em);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
transform: rotateX(-89.99999999999999deg) translateZ(2.498em);
|
||||
}
|
||||
|
||||
.right {
|
||||
transform: rotateY(89.99999999999999deg) translateZ(2.498em);
|
||||
}
|
||||
|
||||
.left {
|
||||
transform: rotateY(-89.99999999999999deg) translateZ(2.498em);
|
||||
}
|
228
src/page/cube/cube.js
Normal file
228
src/page/cube/cube.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
"use strict";
|
||||
|
||||
class Vec3 {
|
||||
constructor(x, y, z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
static zero() {
|
||||
return new Vec3(0, 0, 0);
|
||||
}
|
||||
|
||||
length() {
|
||||
return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
|
||||
}
|
||||
|
||||
scale(factor) {
|
||||
this.x *= factor;
|
||||
this.y *= factor;
|
||||
this.z *= factor;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
normalize() {
|
||||
const length = this.length();
|
||||
|
||||
if (length != 0) {
|
||||
this.x /= length;
|
||||
this.y /= length;
|
||||
this.z /= length;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
static sub(v, t) {
|
||||
return new Vec3(
|
||||
v.x - t.x,
|
||||
v.y - t.y,
|
||||
v.z - t.z,
|
||||
)
|
||||
}
|
||||
|
||||
static sum(v, t) {
|
||||
return new Vec3(
|
||||
v.x + t.x,
|
||||
v.y + t.y,
|
||||
v.z + t.z,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Quat {
|
||||
constructor(x, y, z, w) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
static fromAxis(axis) {
|
||||
const angle = axis.length();
|
||||
|
||||
axis.normalize();
|
||||
|
||||
const half = angle / 2;
|
||||
|
||||
const sinHalf = Math.sin(half);
|
||||
const cosHalf = Math.cos(half);
|
||||
|
||||
const x = axis.x * sinHalf;
|
||||
const y = axis.y * sinHalf;
|
||||
const z = axis.z * sinHalf;
|
||||
const w = cosHalf;
|
||||
|
||||
return new Quat(x, y, z, w);
|
||||
}
|
||||
|
||||
static mul(q, r) {
|
||||
return new Quat(
|
||||
q.w * r.x + q.x * r.w + q.y * r.z - q.z * r.y,
|
||||
q.w * r.y - q.x * r.z + q.y * r.w + q.z * r.x,
|
||||
q.w * r.z + q.x * r.y - q.y * r.x + q.z * r.w,
|
||||
q.w * r.w - q.x * r.x - q.y * r.y - q.z * r.z,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let friction = 3;
|
||||
let sensitivity = 0.01;
|
||||
let velocity = Vec3.zero();
|
||||
|
||||
const orientation = {
|
||||
__cube: document.querySelector(".cube"),
|
||||
__value: new Quat(0, 0, 0, 1),
|
||||
|
||||
set(value) {
|
||||
this.__value = value;
|
||||
|
||||
const q = this.__value;
|
||||
this.__cube.style.transform = `rotate3d(${q.x}, ${q.y}, ${q.z}, ${Math.acos(q.w) * 2}rad)`;
|
||||
},
|
||||
|
||||
get() {
|
||||
return this.__value;
|
||||
},
|
||||
};
|
||||
|
||||
{
|
||||
const mouse = {
|
||||
down: false,
|
||||
lastMove: -10000,
|
||||
previous: null,
|
||||
};
|
||||
|
||||
let impulseThisFrame = Vec3.zero();
|
||||
|
||||
const handleUp = () => {
|
||||
mouse.down = false;
|
||||
};
|
||||
|
||||
document.addEventListener("mouseup", handleUp);
|
||||
document.addEventListener("touchend", handleUp);
|
||||
|
||||
const handleDown = (event) => {
|
||||
// Disables link dragging that occurs when spinning.
|
||||
event.preventDefault();
|
||||
|
||||
mouse.down = true;
|
||||
|
||||
velocity = Vec3.zero();
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleDown);
|
||||
document.addEventListener("touchstart", handleDown);
|
||||
|
||||
const handleMove = (event) => {
|
||||
// Disables scrolling.
|
||||
event.preventDefault();
|
||||
|
||||
if (!mouse.down) return;
|
||||
|
||||
const newMouse = new Vec3(event.clientX, event.clientY, 0);
|
||||
|
||||
const timeDelta = (window.performance.now() - mouse.lastMove) / 1000;
|
||||
|
||||
if (timeDelta > 0.1) {
|
||||
// This is a fresh scroll.
|
||||
mouse.previous = newMouse;
|
||||
}
|
||||
|
||||
const delta = Vec3.sub(newMouse, mouse.previous);
|
||||
|
||||
mouse.previous = newMouse;
|
||||
mouse.lastMove = window.performance.now();
|
||||
|
||||
const axis = new Vec3(-delta.y, delta.x, 0)
|
||||
.normalize()
|
||||
.scale(delta.length() * sensitivity);
|
||||
|
||||
impulseThisFrame = Vec3.sum(impulseThisFrame, axis);
|
||||
|
||||
const rotation = Quat.fromAxis(axis);
|
||||
|
||||
orientation.set(Quat.mul(rotation, orientation.get()));
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMove);
|
||||
document.addEventListener("touchmove", (event) => {
|
||||
const delta = event.changedTouches[0];
|
||||
|
||||
event.clientX = delta.clientX;
|
||||
event.clientY = delta.clientY;
|
||||
|
||||
handleMove(event);
|
||||
});
|
||||
|
||||
let lastUpdate = 0;
|
||||
|
||||
const updateFrame = (timestamp) => {
|
||||
if (lastUpdate == 0) lastUpdate = timestamp;
|
||||
|
||||
const delta = (timestamp - lastUpdate) / 1000;
|
||||
lastUpdate = timestamp;
|
||||
|
||||
if (mouse.down) {
|
||||
velocity = impulseThisFrame.scale(1 / delta);
|
||||
impulseThisFrame = Vec3.zero();
|
||||
} else {
|
||||
const decay = Math.exp(-delta * friction);
|
||||
|
||||
const effectiveDelta = friction > 0 ? (1 - decay) / friction : delta;
|
||||
|
||||
let theta = effectiveDelta * velocity.length();
|
||||
|
||||
velocity.x *= decay;
|
||||
velocity.y *= decay;
|
||||
velocity.z *= decay;
|
||||
|
||||
if (friction > 0 && velocity.length() < 0.00001) {
|
||||
theta += velocity.length() / friction;
|
||||
|
||||
velocity.x = 0;
|
||||
velocity.y = 0;
|
||||
velocity.z = 0;
|
||||
}
|
||||
|
||||
if (window.performance.now() - mouse.lastMove > 10000) {
|
||||
const impulse = new Vec3(1, 1, -1);
|
||||
velocity = Vec3.sum(impulse.scale(effectiveDelta * 3), velocity);
|
||||
}
|
||||
|
||||
const axis = new Vec3(velocity.x, velocity.y, velocity.z)
|
||||
.normalize()
|
||||
.scale(theta);
|
||||
|
||||
const rotation = Quat.fromAxis(axis);
|
||||
|
||||
orientation.set(Quat.mul(rotation, orientation.get()));
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateFrame);
|
||||
};
|
||||
|
||||
updateFrame(0);
|
||||
}
|
35
src/page/cube/mod.rs
Normal file
35
src/page/cube/mod.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use maud::{
|
||||
html,
|
||||
Markup,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
asset::Css,
|
||||
page::asset,
|
||||
};
|
||||
|
||||
/// Creates a pure HTML CSS and JS cube with 6 faces, the
|
||||
/// order of the faces are as so:
|
||||
///
|
||||
/// front, top, back, bottom, right, left.
|
||||
pub fn create(css: Css, faces: [Markup; 6]) -> Markup {
|
||||
crate::page::create(
|
||||
html! {
|
||||
(asset::Css::Shared("cube.css"))
|
||||
(css)
|
||||
},
|
||||
html! {
|
||||
div class="scene" {
|
||||
div class="cube" {
|
||||
@for (name, content) in ["front", "top", "back", "bottom", "right", "left"].iter().zip(faces) {
|
||||
div class=(format!("face {name}")) {
|
||||
(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(asset::Js::Shared("cube.js"))
|
||||
},
|
||||
)
|
||||
}
|
18
src/page/elements.rs
Normal file
18
src/page/elements.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use maud::{
|
||||
html,
|
||||
Markup,
|
||||
};
|
||||
|
||||
/// Creates a meta tag with property and content.
|
||||
pub fn property(name: &str, content: &str) -> Markup {
|
||||
html! {
|
||||
meta property=(name) content=(content);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a meta tag with name and content.
|
||||
pub fn pname(name: &str, content: &str) -> Markup {
|
||||
html! {
|
||||
meta name=(name) content=(content);
|
||||
}
|
||||
}
|
BIN
src/page/icon.gif
Normal file
BIN
src/page/icon.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
|
@ -1,42 +1,31 @@
|
|||
pub mod cube;
|
||||
mod elements;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Context;
|
||||
use cargo_toml::Manifest;
|
||||
pub use elements::*;
|
||||
use maud::{
|
||||
html,
|
||||
Markup,
|
||||
DOCTYPE,
|
||||
};
|
||||
|
||||
use crate::asset;
|
||||
|
||||
static MANIFEST: LazyLock<Manifest> = LazyLock::new(|| {
|
||||
Manifest::from_str(&embed::string!("../../Cargo.toml"))
|
||||
.with_context(|| "Failed to deserialize Cargo manifest.")
|
||||
.with_context(|| "Failed to deserialize Cargo manifest")
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
fn property(name: &str, content: &str) -> Markup {
|
||||
html! {
|
||||
meta property=(name) content=(content);
|
||||
}
|
||||
}
|
||||
|
||||
fn pname(name: &str, content: &str) -> Markup {
|
||||
html! {
|
||||
meta name=(name) content=(content);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an asset URL from the given asset path.
|
||||
pub(crate) fn asset(path: &str) -> String {
|
||||
format!("/assets/{path}")
|
||||
}
|
||||
|
||||
/// Creates a page with the given head and body.
|
||||
///
|
||||
/// This is the most low level function for page creation
|
||||
/// as all pages use this, as this function provides the
|
||||
/// page title, OpenGraph and other information.
|
||||
pub(crate) fn create(head: Markup, body: Markup) -> Markup {
|
||||
pub fn create(head: Markup, body: Markup) -> Markup {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
|
||||
|
@ -58,9 +47,9 @@ pub(crate) fn create(head: Markup, body: Markup) -> Markup {
|
|||
(pname("description", description))
|
||||
(property("og:description", description))
|
||||
|
||||
link rel="icon" href=(asset("icon.gif")) type="image/gif";
|
||||
link rel="icon" href=(asset::File("icon.gif")) type="image/gif";
|
||||
|
||||
(property("og:image", &asset("thumbnail.png")))
|
||||
(property("og:image", &asset::File("thumbnail.png").to_string()))
|
||||
(property("og:image:type", "image/png"))
|
||||
(property("og:image:height", "1080"))
|
||||
(property("og:image:width", "600"))
|
||||
|
|
77
src/routes/assets.rs
Normal file
77
src/routes/assets.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
io::{
|
||||
Cursor,
|
||||
Read,
|
||||
},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use actix_web::{
|
||||
get,
|
||||
web,
|
||||
HttpResponse,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use tar::Archive;
|
||||
|
||||
use crate::minify;
|
||||
|
||||
const ASSET_EXTENSIONS: &[&str] = &[".js", ".css", ".woff2", ".gif"];
|
||||
|
||||
static ASSETS: LazyLock<HashMap<String, Bytes>> = LazyLock::new(|| {
|
||||
let contents = embed::bytes!("../../src.tar");
|
||||
let mut archive = Archive::new(Cursor::new(contents));
|
||||
|
||||
let mut assets = HashMap::new();
|
||||
|
||||
for entry in archive.entries().unwrap() {
|
||||
let mut entry = if let Ok(entry) = entry {
|
||||
entry
|
||||
} else {
|
||||
log::error!("fail");
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = entry.path_bytes();
|
||||
let path = String::from_utf8(path.to_vec()).unwrap();
|
||||
|
||||
if path.ends_with("/") || !ASSET_EXTENSIONS.iter().any(|ext| path.ends_with(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = path.rsplit_once("/").unwrap_or(("", &path)).1;
|
||||
|
||||
let mut content = Vec::new();
|
||||
entry.read_to_end(&mut content).unwrap();
|
||||
|
||||
if minify::is_minifiable(&path) {
|
||||
let content = minify::generic(&path, &content);
|
||||
|
||||
log::info!("Minifying asset {path}");
|
||||
assets.insert(minify::insert_min(path), Bytes::from(content));
|
||||
}
|
||||
|
||||
log::info!("Adding asset {path}");
|
||||
assets.insert(path.to_string(), Bytes::from(content));
|
||||
}
|
||||
|
||||
assets
|
||||
});
|
||||
|
||||
#[get("/assets/{name}")]
|
||||
pub async fn handler(name: web::Path<String>) -> HttpResponse {
|
||||
let name = name.into_inner();
|
||||
|
||||
if let Some(body) = ASSETS.get(&name) {
|
||||
HttpResponse::Ok()
|
||||
.content_type(
|
||||
mime_guess::from_path(&name)
|
||||
.first_or_octet_stream()
|
||||
.essence_str(),
|
||||
)
|
||||
.body(Bytes::clone(body))
|
||||
} else {
|
||||
HttpResponse::NotFound().into()
|
||||
}
|
||||
}
|
68
src/routes/index/index.css
Normal file
68
src/routes/index/index.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
.frame:hover {
|
||||
background-color: #FFFF00;
|
||||
}
|
||||
|
||||
.face::after {
|
||||
z-index: -1;
|
||||
|
||||
content: "";
|
||||
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.front {
|
||||
background: linear-gradient(to bottom, cyan, blue);
|
||||
}
|
||||
|
||||
.front::after {
|
||||
background: linear-gradient(to bottom, white, magenta);
|
||||
mask-image: linear-gradient(to left, magenta, transparent);
|
||||
}
|
||||
|
||||
.top {
|
||||
background: linear-gradient(to bottom, lime, cyan);
|
||||
}
|
||||
|
||||
.top::after {
|
||||
background: linear-gradient(to bottom, yellow, white);
|
||||
mask-image: linear-gradient(to left, white, transparent);
|
||||
}
|
||||
|
||||
.back {
|
||||
background: linear-gradient(to bottom, yellow, red);
|
||||
}
|
||||
|
||||
.back::after {
|
||||
background: linear-gradient(to bottom, lime, black);
|
||||
mask-image: linear-gradient(to left, black, transparent);
|
||||
}
|
||||
|
||||
.bottom {
|
||||
background: linear-gradient(to bottom, blue, black);
|
||||
}
|
||||
|
||||
.bottom::after {
|
||||
background: linear-gradient(to bottom, magenta, red);
|
||||
mask-image: linear-gradient(to left, red, transparent);
|
||||
}
|
||||
|
||||
.right {
|
||||
background: linear-gradient(to bottom, white, magenta);
|
||||
}
|
||||
|
||||
.right::after {
|
||||
background: linear-gradient(to bottom, yellow, red);
|
||||
mask-image: linear-gradient(to left, red, transparent);
|
||||
}
|
||||
|
||||
.left {
|
||||
background: linear-gradient(to bottom, lime, black);
|
||||
}
|
||||
|
||||
.left::after {
|
||||
background: linear-gradient(to bottom, cyan, blue);
|
||||
mask-image: linear-gradient(to left, blue, transparent);
|
||||
}
|
37
src/routes/index/mod.rs
Normal file
37
src/routes/index/mod.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use actix_web::get;
|
||||
use maud::{
|
||||
html,
|
||||
Markup,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
asset,
|
||||
page::cube,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
pub async fn handler() -> actix_web::Result<Markup> {
|
||||
Ok(cube::create(
|
||||
asset::css::owned!("index.css"),
|
||||
[
|
||||
html! {
|
||||
a href="/contact" {
|
||||
div class="frame" {
|
||||
"contact"
|
||||
}
|
||||
}
|
||||
},
|
||||
html! {
|
||||
a href="/github" {
|
||||
div class="frame" {
|
||||
"github"
|
||||
}
|
||||
}
|
||||
},
|
||||
html! {},
|
||||
html! {},
|
||||
html! {},
|
||||
html! {},
|
||||
],
|
||||
))
|
||||
}
|
13
src/routes/mod.rs
Normal file
13
src/routes/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
mod assets;
|
||||
mod index;
|
||||
|
||||
use actix_web::{
|
||||
web,
|
||||
Scope,
|
||||
};
|
||||
|
||||
pub fn handler() -> Scope {
|
||||
web::scope("")
|
||||
.service(index::handler)
|
||||
.service(assets::handler)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue