1
Fork 0
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:
RGBCube 2023-12-31 13:34:38 +03:00
parent c1af80979d
commit c3d45b6ba4
No known key found for this signature in database
20 changed files with 926 additions and 29 deletions

7
.gitignore vendored
View file

@ -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
View 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
View 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)
}
}

View 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
View 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,
)
}

View 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(),
))
}

View 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;
}

View file

@ -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,12 +35,17 @@ async fn main() -> anyhow::Result<()> {
.target(env_logger::Target::Stdout)
.init();
HttpServer::new(App::new) //|| App::new().route(routes::index))
.bind(("0.0.0.0", args.port))
.with_context(|| format!("Failed to bind to 0.0.0.0:{port}", port = args.port))?
.run()
.await
.with_context(|| "Failed to run HttpServer")?;
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:{}", args.port))?
.run()
.await
.with_context(|| "Failed to run HttpServer")?;
Ok(())
}

73
src/minify.rs Normal file
View 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()
}

Binary file not shown.

108
src/page/cube/cube.css Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -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
View 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()
}
}

View 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
View 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
View 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)
}