summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore5
-rw-r--r--.sqlx/query-07b6199b50db665e1a31ded4a8612d16f230448c97a8eb301948e9f3f0955763.json12
-rw-r--r--.sqlx/query-2dc5e20d5cc793cc639466b2a4177bb090792b553b951f524ee6f5939f992b7a.json12
-rw-r--r--.sqlx/query-312cace7ec199361a6df0b9dc6fd308a8eaf1b9f7d0dab531164961142ba041f.json44
-rw-r--r--.sqlx/query-40e5b26d2df5aa2cc92c1a45c47560092b4cbb50dd1aa24320afc6a729f16cfe.json44
-rw-r--r--.sqlx/query-710176c919e6c108a8c0c982821a9c17e71ea118e5b67265136e04e2a59a5df9.json32
-rw-r--r--.sqlx/query-bcf2aee61cbec68a6d3d964ec5de803061f81e8a36002812e9eaacee4bd2a47a.json44
-rw-r--r--.sqlx/query-c924cd379b966e7ca47f2c8a6c97d9d641db0e8be85f6776d34c59801329f27c.json12
-rw-r--r--.sqlx/query-cb6484175e6e0a7207510351f735379a42653230f17abf8649d944cf5e06f9ab.json44
-rw-r--r--Cargo.toml11
-rw-r--r--Dockerfile76
-rw-r--r--README.md4
-rw-r--r--documentation/er-model.pdfbin142284 -> 0 bytes
-rw-r--r--input.css3
-rw-r--r--migrations/20241026164427_init.sql67
-rw-r--r--migrations/20241106161637_tagredesign.sql13
-rw-r--r--migrations/20241124111952_uniquetags.sql19
-rw-r--r--migrations/20251121124741_remove_story_epub_file.sql1
-rw-r--r--src/db/game.rs14
-rw-r--r--src/db/mod.rs32
-rw-r--r--src/db/product.rs110
-rw-r--r--src/db/producttag.rs5
-rw-r--r--src/db/story.rs129
-rw-r--r--src/db/tag.rs5
-rw-r--r--src/db/tool.rs14
-rw-r--r--src/error.rs23
-rw-r--r--src/handlers/admin.rs143
-rw-r--r--src/handlers/frontend_builder.rs121
-rw-r--r--src/handlers/mod.rs53
-rw-r--r--src/main.rs36
-rw-r--r--src/static/styles.css1
-rw-r--r--tailwind.config.js9
32 files changed, 1 insertions, 1137 deletions
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index fc04251..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,5 +0,0 @@
-target/
-.git/
-.env
-*.db
-documentation/
diff --git a/.sqlx/query-07b6199b50db665e1a31ded4a8612d16f230448c97a8eb301948e9f3f0955763.json b/.sqlx/query-07b6199b50db665e1a31ded4a8612d16f230448c97a8eb301948e9f3f0955763.json
deleted file mode 100644
index 96a16a8..0000000
--- a/.sqlx/query-07b6199b50db665e1a31ded4a8612d16f230448c97a8eb301948e9f3f0955763.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "DELETE FROM story WHERE pid = $1",
- "describe": {
- "columns": [],
- "parameters": {
- "Right": 1
- },
- "nullable": []
- },
- "hash": "07b6199b50db665e1a31ded4a8612d16f230448c97a8eb301948e9f3f0955763"
-}
diff --git a/.sqlx/query-2dc5e20d5cc793cc639466b2a4177bb090792b553b951f524ee6f5939f992b7a.json b/.sqlx/query-2dc5e20d5cc793cc639466b2a4177bb090792b553b951f524ee6f5939f992b7a.json
deleted file mode 100644
index 9d2d57b..0000000
--- a/.sqlx/query-2dc5e20d5cc793cc639466b2a4177bb090792b553b951f524ee6f5939f992b7a.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "DELETE FROM product WHERE id = $1",
- "describe": {
- "columns": [],
- "parameters": {
- "Right": 1
- },
- "nullable": []
- },
- "hash": "2dc5e20d5cc793cc639466b2a4177bb090792b553b951f524ee6f5939f992b7a"
-}
diff --git a/.sqlx/query-312cace7ec199361a6df0b9dc6fd308a8eaf1b9f7d0dab531164961142ba041f.json b/.sqlx/query-312cace7ec199361a6df0b9dc6fd308a8eaf1b9f7d0dab531164961142ba041f.json
deleted file mode 100644
index 073f76b..0000000
--- a/.sqlx/query-312cace7ec199361a6df0b9dc6fd308a8eaf1b9f7d0dab531164961142ba041f.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT * FROM story",
- "describe": {
- "columns": [
- {
- "name": "id",
- "ordinal": 0,
- "type_info": "Integer"
- },
- {
- "name": "language",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "pdf",
- "ordinal": 2,
- "type_info": "Blob"
- },
- {
- "name": "epub",
- "ordinal": 3,
- "type_info": "Blob"
- },
- {
- "name": "pid",
- "ordinal": 4,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 0
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "312cace7ec199361a6df0b9dc6fd308a8eaf1b9f7d0dab531164961142ba041f"
-}
diff --git a/.sqlx/query-40e5b26d2df5aa2cc92c1a45c47560092b4cbb50dd1aa24320afc6a729f16cfe.json b/.sqlx/query-40e5b26d2df5aa2cc92c1a45c47560092b4cbb50dd1aa24320afc6a729f16cfe.json
deleted file mode 100644
index 331604f..0000000
--- a/.sqlx/query-40e5b26d2df5aa2cc92c1a45c47560092b4cbb50dd1aa24320afc6a729f16cfe.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT * FROM product",
- "describe": {
- "columns": [
- {
- "name": "id",
- "ordinal": 0,
- "type_info": "Integer"
- },
- {
- "name": "name",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "description",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "uploaddate",
- "ordinal": 3,
- "type_info": "Integer"
- },
- {
- "name": "updatedate",
- "ordinal": 4,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 0
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "40e5b26d2df5aa2cc92c1a45c47560092b4cbb50dd1aa24320afc6a729f16cfe"
-}
diff --git a/.sqlx/query-710176c919e6c108a8c0c982821a9c17e71ea118e5b67265136e04e2a59a5df9.json b/.sqlx/query-710176c919e6c108a8c0c982821a9c17e71ea118e5b67265136e04e2a59a5df9.json
deleted file mode 100644
index c0d5519..0000000
--- a/.sqlx/query-710176c919e6c108a8c0c982821a9c17e71ea118e5b67265136e04e2a59a5df9.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "SELECT p.name, s.pdf, s.epub FROM story s INNER JOIN product p ON s.pid = p.id",
- "describe": {
- "columns": [
- {
- "name": "name",
- "ordinal": 0,
- "type_info": "Text"
- },
- {
- "name": "pdf",
- "ordinal": 1,
- "type_info": "Blob"
- },
- {
- "name": "epub",
- "ordinal": 2,
- "type_info": "Blob"
- }
- ],
- "parameters": {
- "Right": 0
- },
- "nullable": [
- false,
- false,
- false
- ]
- },
- "hash": "710176c919e6c108a8c0c982821a9c17e71ea118e5b67265136e04e2a59a5df9"
-}
diff --git a/.sqlx/query-bcf2aee61cbec68a6d3d964ec5de803061f81e8a36002812e9eaacee4bd2a47a.json b/.sqlx/query-bcf2aee61cbec68a6d3d964ec5de803061f81e8a36002812e9eaacee4bd2a47a.json
deleted file mode 100644
index ad7fec3..0000000
--- a/.sqlx/query-bcf2aee61cbec68a6d3d964ec5de803061f81e8a36002812e9eaacee4bd2a47a.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "INSERT INTO product (name, description, uploaddate, updatedate) VALUES ($1, $2, $3, $4) RETURNING id, name, description, uploaddate, updatedate",
- "describe": {
- "columns": [
- {
- "name": "id",
- "ordinal": 0,
- "type_info": "Integer"
- },
- {
- "name": "name",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "description",
- "ordinal": 2,
- "type_info": "Text"
- },
- {
- "name": "uploaddate",
- "ordinal": 3,
- "type_info": "Integer"
- },
- {
- "name": "updatedate",
- "ordinal": 4,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 4
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "bcf2aee61cbec68a6d3d964ec5de803061f81e8a36002812e9eaacee4bd2a47a"
-}
diff --git a/.sqlx/query-c924cd379b966e7ca47f2c8a6c97d9d641db0e8be85f6776d34c59801329f27c.json b/.sqlx/query-c924cd379b966e7ca47f2c8a6c97d9d641db0e8be85f6776d34c59801329f27c.json
deleted file mode 100644
index 727a89d..0000000
--- a/.sqlx/query-c924cd379b966e7ca47f2c8a6c97d9d641db0e8be85f6776d34c59801329f27c.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "DELETE FROM producttag WHERE pid = $1",
- "describe": {
- "columns": [],
- "parameters": {
- "Right": 1
- },
- "nullable": []
- },
- "hash": "c924cd379b966e7ca47f2c8a6c97d9d641db0e8be85f6776d34c59801329f27c"
-}
diff --git a/.sqlx/query-cb6484175e6e0a7207510351f735379a42653230f17abf8649d944cf5e06f9ab.json b/.sqlx/query-cb6484175e6e0a7207510351f735379a42653230f17abf8649d944cf5e06f9ab.json
deleted file mode 100644
index bb7efcc..0000000
--- a/.sqlx/query-cb6484175e6e0a7207510351f735379a42653230f17abf8649d944cf5e06f9ab.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "INSERT INTO story (language, pdf, epub, pid) VALUES ($1, $2, $3, $4) RETURNING id, language, pdf, epub, pid",
- "describe": {
- "columns": [
- {
- "name": "id",
- "ordinal": 0,
- "type_info": "Integer"
- },
- {
- "name": "language",
- "ordinal": 1,
- "type_info": "Text"
- },
- {
- "name": "pdf",
- "ordinal": 2,
- "type_info": "Blob"
- },
- {
- "name": "epub",
- "ordinal": 3,
- "type_info": "Blob"
- },
- {
- "name": "pid",
- "ordinal": 4,
- "type_info": "Integer"
- }
- ],
- "parameters": {
- "Right": 4
- },
- "nullable": [
- false,
- false,
- false,
- false,
- false
- ]
- },
- "hash": "cb6484175e6e0a7207510351f735379a42653230f17abf8649d944cf5e06f9ab"
-}
diff --git a/Cargo.toml b/Cargo.toml
index daab210..2c4dbf0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,14 +8,5 @@ edition = "2021"
[dependencies]
serde = {version = "1.0", features = ["derive"]}
anyhow = "1.0"
-tokio = { version = "1.39.2", features = ["full"] }
-maud = { version = "0.26.0", features = ["axum"] }
-axum = { version = "0.7.5", features = ["multipart"] }
-tower-http = { version = "0.5.2", features = ["full", "fs"] }
-sqlx = { version = "0.8.2", features = ["runtime-tokio", "tls-native-tls", "sqlite", "macros"] }
-dotenvy = "0.15.7"
-subtle = "2.6.1"
-axum-extra = { version = "0.9.4", features = ["query"] }
-async-trait = "0.1.83"
-rand = "0.8.5"
+maud = { version = "0.27.0" }
chrono = "0.4.38"
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 765fd1a..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,76 +0,0 @@
-# Planner stage - Compute dependencies and create recipe
-FROM lukemathwalker/cargo-chef:latest-rust-1.82 AS planner
-WORKDIR /app
-# Copy the minimal files needed for dependency calculation
-COPY Cargo.* ./
-COPY src/ src/
-RUN cargo chef prepare --recipe-path recipe.json
-
-# Cache dependencies stage
-FROM lukemathwalker/cargo-chef:latest-rust-1.82 AS cacher
-WORKDIR /app
-COPY --from=planner /app/recipe.json recipe.json
-RUN cargo chef cook --release --recipe-path recipe.json
-
-# Builder stage
-FROM lukemathwalker/cargo-chef:latest-rust-1.82 AS builder
-WORKDIR /app
-
-# Copy the entire project context
-COPY . .
-
-# Copy cached dependencies
-COPY --from=cacher /app/target target
-COPY --from=cacher /usr/local/cargo /usr/local/cargo
-
-# Set sqlx offline mode to enable building without database connection
-ENV SQLX_OFFLINE=true
-
-# Build the application
-RUN cargo build --release
-
-# Runtime stage
-FROM debian:bookworm-slim AS runtime
-
-# Install SSL certificates and sqlite3
-RUN apt-get update && \
- apt-get install -y ca-certificates sqlite3 && \
- rm -rf /var/lib/apt/lists/*
-
-WORKDIR /app
-
-# Copy the compiled binary
-COPY --from=builder /app/target/release/portfolio-website /app/portfolio-website
-
-# Copy migrations folder for SQLX
-COPY --from=builder /app/migrations /app/migrations
-
-# Copy static directory
-COPY --from=builder /app/static /app/static
-
-# Create data directory and set up permissions
-RUN mkdir -p /app/data && \
- # Create a specific group and user for the application
- groupadd -r appgroup && \
- useradd -r -g appgroup appuser && \
- # Give ownership to the new user
- chown -R appuser:appgroup /app && \
- # Set directory permissions
- chmod -R 755 /app && \
- chmod 777 /app/data && \
- # Create an empty database file with proper permissions
- touch /app/data/production.db && \
- chmod 666 /app/data/production.db
-
-# Create .env file with production settings
-RUN echo "DATABASE_URL=sqlite:/app/data/production.db" > /app/.env && \
- chown appuser:appgroup /app/.env
-
-# Switch to application user
-USER appuser
-
-# Expose the port your application runs on
-EXPOSE 3000
-
-# Use JSON format for CMD to handle signals properly
-CMD ["sh", "-c", "DATABASE_URL=sqlite:/app/data/production.db /app/portfolio-website"]
diff --git a/README.md b/README.md
deleted file mode 100644
index 35f5816..0000000
--- a/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Portfolio page - writerv2.github.io
-
-My portfolio page should ideally give a visitor a little impression of me and my work/interests.
-
diff --git a/documentation/er-model.pdf b/documentation/er-model.pdf
deleted file mode 100644
index 2a6d49c..0000000
--- a/documentation/er-model.pdf
+++ /dev/null
Binary files differ
diff --git a/input.css b/input.css
deleted file mode 100644
index b5c61c9..0000000
--- a/input.css
+++ /dev/null
@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
diff --git a/migrations/20241026164427_init.sql b/migrations/20241026164427_init.sql
deleted file mode 100644
index be6ffca..0000000
--- a/migrations/20241026164427_init.sql
+++ /dev/null
@@ -1,67 +0,0 @@
-CREATE TABLE IF NOT EXISTS product
-(
- id INTEGER PRIMARY KEY NOT NULL,
- name TEXT NOT NULL UNIQUE,
- description TEXT NOT NULL UNIQUE,
- uploaddate INTEGER NOT NULL,
- updatedate INTEGER NOT NULL
-);
-
-CREATE TABLE IF NOT EXISTS story
-(
- id INTEGER PRIMARY KEY NOT NULL,
- language TEXT NOT NULL,
- pdf BLOB NOT NULL,
- epub BLOB NOT NULL,
- pid INTEGER NOT NULL,
- FOREIGN KEY (pid) REFERENCES product(id)
-);
-
-CREATE TABLE IF NOT EXISTS tool
-(
- id INTEGER PRIMARY KEY NOT NULL,
- repolink TEXT NOT NULL,
- pid INTEGER NOT NULL,
- FOREIGN KEY (pid) REFERENCES product(id)
-);
-
-CREATE TABLE IF NOT EXISTS game
-(
- id INTEGER PRIMARY KEY NOT NULL,
- repolink TEXT NOT NULL,
- pid INTEGER NOT NULL,
- FOREIGN KEY (pid) REFERENCES product(id)
-);
-
-CREATE TABLE IF NOT EXISTS tag
-(
- id INTEGER PRIMARY KEY NOT NULL,
- name TEXT NOT NULL
-);
-
-CREATE TABLE IF NOT EXISTS storytag
-(
- sid INTEGER NOT NULL,
- tgid INTEGER NOT NULL,
- FOREIGN KEY (sid) REFERENCES story(id),
- FOREIGN KEY (tgid) REFERENCES tag(id),
- PRIMARY KEY (sid, tgid)
-);
-
-CREATE TABLE IF NOT EXISTS tooltag
-(
- tid INTEGER NOT NULL,
- tgid INTEGER NOT NULL,
- FOREIGN KEY (tid) REFERENCES tool(id),
- FOREIGN KEY (tgid) REFERENCES tag(id),
- PRIMARY KEY (tid, tgid)
-);
-
-CREATE TABLE IF NOT EXISTS gametag
-(
- gid INTEGER NOT NULL,
- tgid INTEGER NOT NULL,
- FOREIGN KEY (gid) REFERENCES game(id),
- FOREIGN KEY (tgid) REFERENCES tag(id),
- PRIMARY KEY (gid, tgid)
-);
diff --git a/migrations/20241106161637_tagredesign.sql b/migrations/20241106161637_tagredesign.sql
deleted file mode 100644
index c33cec3..0000000
--- a/migrations/20241106161637_tagredesign.sql
+++ /dev/null
@@ -1,13 +0,0 @@
-DROP TABLE IF EXISTS storytag;
-DROP TABLE IF EXISTS gametag;
-DROP TABLE IF EXISTS tooltag;
-
-CREATE TABLE IF NOT EXISTS producttag
-(
- pid INTEGER NOT NULL,
- tid INTEGER NOT NULL,
- FOREIGN KEY (pid) REFERENCES product(id),
- FOREIGN KEY (tid) REFERENCES tag(id),
- PRIMARY KEY (pid, tid)
-);
-
diff --git a/migrations/20241124111952_uniquetags.sql b/migrations/20241124111952_uniquetags.sql
deleted file mode 100644
index ff8b6ef..0000000
--- a/migrations/20241124111952_uniquetags.sql
+++ /dev/null
@@ -1,19 +0,0 @@
-DELETE FROM producttag;
-CREATE TABLE IF NOT EXISTS newtag
-(
- id INTEGER PRIMARY KEY NOT NULL,
- name TEXT NOT NULL UNIQUE
-);
-
-WITH uniquetags AS (
- SELECT id, name, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id) AS row_num
- FROM tag
-)
-
-INSERT INTO newtag (id, name)
-SELECT id, name
-FROM uniquetags
-WHERE row_num = 1;
-
-DROP TABLE tag;
-ALTER TABLE newtag RENAME TO tag;
diff --git a/migrations/20251121124741_remove_story_epub_file.sql b/migrations/20251121124741_remove_story_epub_file.sql
deleted file mode 100644
index 77dd00c..0000000
--- a/migrations/20251121124741_remove_story_epub_file.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE story DROP COLUMN epub;
diff --git a/src/db/game.rs b/src/db/game.rs
deleted file mode 100644
index e10006c..0000000
--- a/src/db/game.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use super::product::ProductMarker;
-
-#[derive(Debug, serde::Serialize, sqlx::FromRow)]
-pub struct Game {
- pub id: i64,
- pub repolink: String,
- pub pid: i64,
-}
-
-impl ProductMarker for Game {
- fn product_id(&self) -> i64 {
- self.pid
- }
-}
diff --git a/src/db/mod.rs b/src/db/mod.rs
deleted file mode 100644
index f1baf44..0000000
--- a/src/db/mod.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-use sqlx::SqlitePool;
-
-use crate::error::AppError;
-use async_trait::async_trait;
-
-pub mod game;
-pub mod product;
-pub mod producttag;
-pub mod story;
-pub mod tag;
-pub mod tool;
-
-#[async_trait]
-pub trait ProductDatabaseHandler {
- async fn get_all(pool: &SqlitePool) -> Result<Vec<Self>, AppError>
- where
- Self: Sized;
-
- async fn post(
- &self,
- pool: &SqlitePool,
- name: &str,
- description: &str,
- tags: Vec<&str>,
- ) -> Result<Self, AppError>
- where
- Self: Sized;
-
- async fn delete(pool: &SqlitePool, id: i64) -> Result<(), AppError>
- where
- Self: Sized;
-}
diff --git a/src/db/product.rs b/src/db/product.rs
deleted file mode 100644
index e5e797f..0000000
--- a/src/db/product.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-use std::collections::HashMap;
-use std::time::SystemTime;
-
-use sqlx::{QueryBuilder, Sqlite, SqlitePool, Transaction};
-
-use crate::error::AppError;
-
-use super::tag::Tag;
-use super::ProductDatabaseHandler;
-
-#[derive(Debug, serde::Serialize, sqlx::FromRow)]
-pub struct Product {
- pub id: i64,
- pub name: String,
- pub description: String,
- pub uploaddate: i64,
- pub updatedate: i64,
-}
-
-impl Product {
- pub async fn post(tx: &mut Transaction<'static, Sqlite>, name: &str, description: &str, tags: Vec<&str>) -> Result<Product, AppError> {
- let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_millis() as i64;
- let product = sqlx::query_as!(
- Product,
- "INSERT INTO product (name, description, uploaddate, updatedate) VALUES ($1, $2, $3, $4) RETURNING id, name, description, uploaddate, updatedate",
- name,
- description,
- now,
- now)
- .fetch_one(&mut **tx)
- .await?;
-
- if !tags.is_empty() {
- // insert tags that do not exist yet
- let mut tags_query_builder: QueryBuilder<Sqlite> = QueryBuilder::new("INSERT INTO tag (name) ");
- tags_query_builder.push_values(tags.iter(), | mut b, tag | { b.push_bind(tag); });
- tags_query_builder.push(" ON CONFLICT(name) DO NOTHING;");
- tags_query_builder.build().execute(&mut **tx).await?;
-
- // fetch all tags of story
- let added_tags: Vec<Tag> = {
- let mut tags_fetch_query_builder = QueryBuilder::new("WITH input(name) AS (");
- tags_fetch_query_builder.push_values(tags.iter(), |mut b, tag| {
- b.push_bind(tag);
- });
- tags_fetch_query_builder.push(
- ") \
- SELECT t.id, t.name \
- FROM tag t \
- JOIN input i ON i.name = t.name;"
- );
- tags_fetch_query_builder.build_query_as::<Tag>().fetch_all(&mut **tx).await?
- };
- println!("{:?}", added_tags);
-
- // bind tags to story if they are not bound yet
- let mut producttags_query_builder: QueryBuilder<Sqlite> = QueryBuilder::new("INSERT INTO producttag (pid, tid) ");
- producttags_query_builder.push_values(added_tags.iter(), | mut b, added_tag | {
- b.push_bind(product.id).push_bind(added_tag.id);
- });
- producttags_query_builder.push(" ON CONFLICT(pid, tid) DO NOTHING;");
- producttags_query_builder
- .build().execute(&mut **tx).await?;
-
- }
- Ok(product)
- }
-
- pub async fn delete(tx: &mut Transaction<'static, Sqlite>, pid: i64) -> Result<(), AppError> {
- sqlx::query!("DELETE FROM producttag WHERE pid = $1", pid).execute(&mut **tx).await?;
- sqlx::query!("DELETE FROM product WHERE id = $1", pid).execute(&mut **tx).await?;
- Ok(())
- }
-}
-
-pub trait ProductMarker {
- fn product_id(&self) -> i64;
-}
-
-pub struct SpecificProduct<T: ProductMarker> {
- pub product: Product,
- pub detail: T,
-}
-
-pub async fn get_all_products(pool: &SqlitePool) -> Result<Vec<Product>, AppError> {
- Ok(sqlx::query_as!(Product, "SELECT * FROM product")
- .fetch_all(pool)
- .await?)
-}
-
-pub async fn get_all_specificproducts<T: ProductMarker + ProductDatabaseHandler>(
- pool: &SqlitePool,
-) -> Result<Vec<SpecificProduct<T>>, AppError> {
- let product_details = T::get_all(pool).await?;
- let products = get_all_products(pool).await?;
- let mut details_map: HashMap<i64, T> = product_details
- .into_iter()
- .map(|detail| (detail.product_id(), detail))
- .collect();
-
- let specific_products = products
- .into_iter()
- .filter_map(|product| {
- details_map
- .remove(&product.id)
- .map(|detail| SpecificProduct { product, detail })
- })
- .collect();
- Ok(specific_products)
-}
diff --git a/src/db/producttag.rs b/src/db/producttag.rs
deleted file mode 100644
index 160e720..0000000
--- a/src/db/producttag.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-#[derive(Debug, serde::Serialize, sqlx::FromRow)]
-pub struct ProductTag {
- pub pid: i64,
- pub tid: i64,
-}
diff --git a/src/db/story.rs b/src/db/story.rs
deleted file mode 100644
index 1566d22..0000000
--- a/src/db/story.rs
+++ /dev/null
@@ -1,129 +0,0 @@
-use std::ffi::OsStr;
-use std::fs::{read_dir, remove_file};
-use std::io::{BufWriter, Write};
-
-use anyhow::Context;
-use axum::async_trait;
-use maud::html;
-use sqlx::SqlitePool;
-
-use crate::db::product::Product;
-use crate::error::AppError;
-use crate::handlers::frontend_builder::PageBuilder;
-
-use super::product::ProductMarker;
-use super::ProductDatabaseHandler;
-
-#[derive(Debug, serde::Serialize, sqlx::FromRow)]
-pub struct Story {
- pub id: i64,
- pub language: String,
- pub pdf: Vec<u8>,
- pub pid: i64,
-}
-
-impl ProductMarker for Story {
- fn product_id(&self) -> i64 {
- self.pid
- }
-}
-
-pub async fn synchronize_story_files(pool: &SqlitePool) -> Result<(), anyhow::Error> {
- for entry in read_dir("static").with_context(|| "Failed to read directory")? {
- let path = entry.with_context(|| "Failed to get entry")?.path();
- if path.extension().is_some_and(|ext| ext == OsStr::new("pdf")) {
- remove_file(path).with_context(|| "Failed to remove file")?;
- }
- }
- let results =
- sqlx::query!("SELECT p.name, s.pdf FROM story s INNER JOIN product p ON s.pid = p.id")
- .fetch_all(pool)
- .await?;
- for result in results.iter() {
- let pdf_filename = format_filepath(&result.name, "pdf");
- let file = std::fs::File::create_new(&pdf_filename)
- .with_context(|| format!("Failed to create {}", &pdf_filename))?;
- BufWriter::new(file)
- .write_all(&result.pdf.to_vec())
- .with_context(|| format!("Failed to write {}", &pdf_filename))?;
- }
- Ok(())
-}
-
-#[async_trait]
-impl ProductDatabaseHandler for Story {
- async fn get_all(pool: &SqlitePool) -> Result<Vec<Self>, AppError>
- where
- Self: Sized,
- {
- Ok(sqlx::query_as!(Story, "SELECT * FROM story")
- .fetch_all(pool)
- .await?)
- }
-
- async fn post(
- &self,
- pool: &SqlitePool,
- name: &str,
- description: &str,
- tags: Vec<&str>,
- ) -> Result<Self, AppError>
- where
- Self: Sized,
- {
- let mut tx = pool.begin().await?;
- let product = Product::post(&mut tx, name, description, tags).await?;
- let story = sqlx::query_as!(
- Story,
- "INSERT INTO story (language, pdf, pid) VALUES ($1, $2, $3) RETURNING id, language, pdf, pid",
- self.language,
- self.pdf,
- product.id)
- .fetch_one(&mut *tx)
- .await?;
-
- tx.commit().await?;
-
- Ok(story)
- }
-
- async fn delete(pool: &SqlitePool, pid: i64) -> Result<(), AppError>
- where
- Self: Sized,
- {
- let mut tx = pool.begin().await?;
- sqlx::query!("DELETE FROM story WHERE pid = $1", pid)
- .execute(&mut *tx)
- .await?;
- Product::delete(&mut tx, pid).await?;
- tx.commit().await?;
- Ok(())
- }
-}
-
-impl<'a> PageBuilder<'a> for Story {
- fn page_title() -> &'a str {
- "Stories"
- }
-
- // TODO: Add pdf icon
- fn product_specific_card_content<T: PageBuilder<'a> + ProductMarker>(
- &self,
- specific_product: &super::product::SpecificProduct<T>,
- ) -> maud::Markup {
- html!(
- div class="flex justify-start mt-5 text-sm" {
- span class="mr-10" { (&self.language) }
- div class="flex justify-between" {
- a href=(format_filepath(&specific_product.product.name, "pdf")) { "PDF" }
- }
- }
- )
- }
-}
-
-pub fn format_filepath(name: &str, extension: &str) -> String {
- let mut filename = format!("static/{}.{}", name, extension).to_lowercase();
- filename.retain(|c| !c.is_whitespace());
- filename
-}
diff --git a/src/db/tag.rs b/src/db/tag.rs
deleted file mode 100644
index df55a8a..0000000
--- a/src/db/tag.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-#[derive(Debug, serde::Serialize, sqlx::FromRow)]
-pub struct Tag {
- pub id: i64,
- pub name: String,
-}
diff --git a/src/db/tool.rs b/src/db/tool.rs
deleted file mode 100644
index c53dc1e..0000000
--- a/src/db/tool.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use super::product::ProductMarker;
-
-#[derive(Debug, serde::Serialize, sqlx::FromRow)]
-pub struct Tool {
- pub id: i64,
- pub repolink: String,
- pub pid: i64,
-}
-
-impl ProductMarker for Tool {
- fn product_id(&self) -> i64 {
- self.id
- }
-}
diff --git a/src/error.rs b/src/error.rs
deleted file mode 100644
index 72378ae..0000000
--- a/src/error.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-use axum::http::StatusCode;
-use axum::response::IntoResponse;
-
-pub struct AppError(anyhow::Error);
-
-impl IntoResponse for AppError {
- fn into_response(self) -> axum::response::Response {
- (
- StatusCode::INTERNAL_SERVER_ERROR,
- format!("Something went wrong: {}", self.0),
- )
- .into_response()
- }
-}
-
-impl<E> From<E> for AppError
-where
- E: Into<anyhow::Error>,
-{
- fn from(err: E) -> Self {
- Self(err.into())
- }
-}
diff --git a/src/handlers/admin.rs b/src/handlers/admin.rs
deleted file mode 100644
index b0863af..0000000
--- a/src/handlers/admin.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-use std::collections::HashMap;
-use std::io::{BufWriter, Write};
-
-use anyhow::Context;
-use axum::body::Bytes;
-use axum::extract::{Multipart, Path, Request, State};
-use axum::http::{HeaderMap, StatusCode};
-use axum::middleware::Next;
-use axum::response::{IntoResponse, Response};
-use axum::{Extension, Json};
-use rand::Rng;
-use subtle::ConstantTimeEq;
-
-use crate::db::product::get_all_products;
-use crate::db::story::{format_filepath, Story};
-use crate::db::ProductDatabaseHandler;
-use crate::error::AppError;
-
-use super::{ApiContext, AppState};
-
-pub fn generate_token() -> String {
- const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
- abcdefghijklmnopqrstuvwxyz\
- 0123456789)(*&^%$#@!~";
- let mut rng = rand::thread_rng();
- let token: String = (0..32)
- .map(|_| {
- let idx = rng.gen_range(0..CHARSET.len());
- CHARSET[idx] as char
- })
- .collect();
- println!("New Token generated: {}", token);
- token
-}
-
-pub async fn admin_middleware(
- State(state): State<AppState>,
- headers: HeaderMap,
- request: Request,
- next: Next,
-) -> Result<Response, StatusCode> {
- let mut expected_token = state.admin_token.lock().await;
-
- let token = headers
- .get("X-Admin-Token")
- .and_then(|header| header.to_str().ok())
- .ok_or(StatusCode::UNAUTHORIZED)?;
-
- if expected_token
- .as_bytes()
- .ct_ne(token.as_bytes())
- .unwrap_u8()
- == 1
- {
- eprintln!("Warning: Unauthorized access attempt with invalid token");
- *expected_token = generate_token();
- return Err(StatusCode::UNAUTHORIZED);
- }
- *expected_token = generate_token();
-
- Ok(next.run(request).await)
-}
-
-pub async fn admin_healthcheck() -> impl IntoResponse {
- StatusCode::OK
-}
-
-pub async fn upload_story(
- ctx: Extension<ApiContext>,
- mut multipart: Multipart,
-) -> Result<impl IntoResponse, AppError> {
- let mut form: HashMap<String, Bytes> = HashMap::new();
- while let Some(field) = multipart.next_field().await? {
- if let Some(label) = field.name() {
- form.insert(label.to_string(), field.bytes().await?);
- }
- }
-
- let name = if let Some(name_value) = form.get("name") {
- std::str::from_utf8(name_value).with_context(|| "Failed to parse name")?
- } else {
- return Ok(StatusCode::BAD_REQUEST.into_response());
- };
-
- let description = if let Some(description_value) = form.get("description") {
- std::str::from_utf8(description_value).with_context(|| "Failed to parse description")?
- } else {
- return Ok(StatusCode::BAD_REQUEST.into_response());
- };
-
- let language = if let Some(language_value) = form.get("language") {
- String::from_utf8(language_value.to_vec()).with_context(|| "Failed to parse language")?
- } else {
- return Ok(StatusCode::BAD_REQUEST.into_response());
- };
-
- let pdf = if let Some(pdf_value) = form.get("pdf") {
- let filename = format_filepath(name, "pdf");
- let file = std::fs::File::create_new(filename)?;
- BufWriter::new(file).write_all(pdf_value)?;
- pdf_value.to_vec()
- } else {
- return Ok(StatusCode::BAD_REQUEST.into_response());
- };
-
- let mut tags: Vec<&str> = Vec::new();
- if let Some(tags_value) = form.get("tag") {
- let tagstring = std::str::from_utf8(tags_value)?;
- for tag in tagstring.split(",") {
- tags.push(tag.trim());
- }
- }
-
- let story_template = Story {
- id: 0,
- pid: 0,
- language,
- pdf,
- };
-
- let story = story_template
- .post(&ctx.pool, name, description, tags)
- .await?;
- Ok((StatusCode::CREATED, Json(story)).into_response())
-}
-
-pub async fn delete_story(
- ctx: Extension<ApiContext>,
- Path(story_id): Path<i64>,
-) -> Result<impl IntoResponse, AppError> {
- Story::delete(&ctx.pool, story_id).await?;
- Ok((StatusCode::NO_CONTENT).into_response())
-}
-
-pub async fn get_stories(ctx: Extension<ApiContext>) -> Result<impl IntoResponse, AppError> {
- let stories = Story::get_all(&ctx.pool).await?;
- Ok((StatusCode::OK, Json(stories)).into_response())
-}
-
-pub async fn get_products(ctx: Extension<ApiContext>) -> Result<impl IntoResponse, AppError> {
- let products = get_all_products(&ctx.pool).await?;
- Ok((StatusCode::OK, Json(products)).into_response())
-}
diff --git a/src/handlers/frontend_builder.rs b/src/handlers/frontend_builder.rs
deleted file mode 100644
index dd49854..0000000
--- a/src/handlers/frontend_builder.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use axum::response::IntoResponse;
-use axum::Extension;
-use chrono::DateTime;
-use maud::{html, Markup, DOCTYPE};
-
-use crate::db::product::{get_all_specificproducts, ProductMarker, SpecificProduct};
-use crate::db::ProductDatabaseHandler;
-use crate::error::AppError;
-
-pub async fn home() -> Result<impl IntoResponse, AppError> {
- Ok(html!((page("Home", Markup::default()))))
-}
-
-fn page(page_title: &str, content: Markup) -> Markup {
- html!(
- (head(page_title))
- body class="bg-[#eae3d2] text-[#333] dark:bg-[#333] dark:text-[#eae3d2] font-mono text-base" {
- (header())
- (navbar())
- (content)
- }
- )
-}
-
-fn header() -> Markup {
- html!(
- header {
- h1 class="underline decoration-[#601B69] dark:decoration-[#e1b1e7] text-center tracking-widest font-serif p-3 text-xl" {"Vithuran Vishnuthas"}
- }
- )
-}
-
-fn navbar() -> Markup {
- html!({
- navbar class="p-3 text-sm flex flex-row flex-wrap content-center justify-around decoration-[#eae3d2] tracking-wide" {
- a class="hover:underline" href="/" { "Home" }
- a class="hover:underline" href="/stories" { "Stories" }
- a class="hover:underline" href="/" { "Tools" }
- a class="hover:underline" href="/" { "Games" }
- }
- })
-}
-
-fn cards<'a, T: PageBuilder<'a> + ProductMarker>(
- products: Vec<SpecificProduct<T>>,
- category: &str,
-) -> Markup {
- html!(
- div class="m-12" {
- h1 class="p-6 font-serif text-xl tracking-wider underline decoration-[#601B69] dark:decoration-[#E1B1E7]" { (category) }
- div class="grid grid-cols-2 gap-8" {
- @for product in products {
- (card(product))
- }
- }
- }
- )
-}
-
-fn card<'a, T: PageBuilder<'a> + ProductMarker>(specific_product: SpecificProduct<T>) -> Markup {
- let date = |i: i64| -> String {
- if let Some(datetime) = DateTime::from_timestamp(i / 1_000, 0) {
- return format!("{}", datetime.format("%Y/%m/%d")).to_string();
- }
- "Unknown".to_string()
- };
- let upload_date = date(specific_product.product.uploaddate);
- let update_date = date(specific_product.product.updatedate);
- html!(
- div class="p-9 shadow" {
- h2 class="text-[#601B69] dark:text-[#E1B1E7]" {
- (specific_product.product.name)
- }
- div {
- (specific_product.product.description)
- }
- div {
- (specific_product.detail.product_specific_card_content(&specific_product))
- }
- div class="flex flex-row space-x-5 mt-9 text-xs" {
- span {"Upload: " (upload_date)}
- @if upload_date != update_date {
- span {"Update: " (update_date)}
- }
- }
- }
- )
-}
-
-fn head(page_title: &str) -> Markup {
- html! {
- head {
- (DOCTYPE)
- meta charset="utf-8";
- link rel="stylesheet" type="text/css" href="/static/styles.css";
- title { (page_title) }
- }
- }
-}
-
-pub async fn build_page<'a, T: Sized + ProductMarker + PageBuilder<'a> + ProductDatabaseHandler>(
- ctx: Extension<super::ApiContext>,
-) -> Result<impl IntoResponse, AppError> {
- Ok(html!(
- (page(
- T::page_title(),
- cards(
- get_all_specificproducts::<T>(&ctx.pool).await?,
- T::page_title()
- )
- ))
- ))
-}
-
-pub trait PageBuilder<'a> {
- fn page_title() -> &'a str;
- fn product_specific_card_content<T: PageBuilder<'a> + ProductMarker>(
- &self,
- specific_product: &SpecificProduct<T>,
- ) -> Markup;
-}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
deleted file mode 100644
index 3235f24..0000000
--- a/src/handlers/mod.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-use std::sync::Arc;
-use tokio::sync::Mutex;
-
-use axum::extract::DefaultBodyLimit;
-use axum::middleware::{self};
-use axum::routing::{delete, get, post};
-use axum::Router;
-use sqlx::SqlitePool;
-use tower_http::add_extension::AddExtensionLayer;
-use tower_http::services::ServeDir;
-
-use admin::{admin_healthcheck, admin_middleware, delete_story, generate_token, upload_story};
-use frontend_builder::home;
-
-use crate::db::story::Story;
-
-use self::admin::{get_products, get_stories};
-use self::frontend_builder::build_page;
-
-mod admin;
-pub mod frontend_builder;
-
-#[derive(Debug, Clone)]
-pub struct ApiContext {
- pool: SqlitePool,
-}
-
-#[derive(Clone)]
-struct AppState {
- admin_token: Arc<Mutex<String>>,
-}
-
-pub async fn router(pool: SqlitePool) -> Router {
- let state = AppState {
- admin_token: Arc::new(Mutex::new(generate_token())),
- };
-
- Router::new()
- .route("/admin/story/:story_id", delete(delete_story))
- .route("/admin/story", post(upload_story).get(get_stories))
- .route("/admin/product", get(get_products))
- .route("/admin", get(admin_healthcheck))
- .layer(middleware::from_fn_with_state(
- state.clone(),
- admin_middleware,
- ))
- .with_state(state)
- .route("/", get(home))
- .route("/stories", get(build_page::<Story>))
- .nest_service("/static", ServeDir::new("static"))
- .layer(DefaultBodyLimit::max(1074000000))
- .layer(AddExtensionLayer::new(ApiContext { pool }))
-}
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index dfe1447..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-use std::env;
-
-use dotenvy::dotenv;
-use sqlx::sqlite::SqlitePoolOptions;
-
-use self::db::story;
-
-pub mod db;
-mod error;
-mod handlers;
-
-#[tokio::main]
-async fn main() {
- dotenv().ok();
- let db_url =
- &env::var("DATABASE_URL").expect("Failed to get environment variable DATABASE_URL");
-
- let pool = SqlitePoolOptions::new()
- .connect(db_url)
- .await
- .expect("Failed to connect to database");
-
- sqlx::migrate!()
- .run(&pool)
- .await
- .expect("Failed to run migrations");
-
- story::synchronize_story_files(&pool)
- .await
- .expect("Failed to synchronize files");
-
- let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
- axum::serve(listener, handlers::router(pool).await.into_make_service())
- .await
- .unwrap();
-}
diff --git a/src/static/styles.css b/src/static/styles.css
deleted file mode 100644
index 99f6305..0000000
--- a/src/static/styles.css
+++ /dev/null
@@ -1 +0,0 @@
-/*! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.static{position:static}.m-9{margin:2.25rem}.m-3{margin:.75rem}.m-12{margin:3rem}.m-5{margin:1.25rem}.mt-9{margin-top:2.25rem}.flex{display:flex}.grid{display:grid}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-center{align-content:center}.justify-around{justify-content:space-around}.gap-4{gap:1rem}.gap-8{gap:2rem}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1.25rem*var(--tw-space-x-reverse));margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)))}.border{border-width:1px}.border-y{border-top-width:1px}.border-b,.border-y{border-bottom-width:1px}.border-r{border-right-width:1px}.border-black{--tw-border-opacity:1;border-color:rgb(0 0 0/var(--tw-border-opacity))}.bg-\[\#eae3d2\]{--tw-bg-opacity:1;background-color:rgb(234 227 210/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-\[\#0D0D0D\]{--tw-bg-opacity:1;background-color:rgb(13 13 13/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(147 197 253/var(--tw-bg-opacity))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity))}.bg-\[\#fff\]{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-\[\#eae3d2\]{--tw-gradient-from:#eae3d2 var(--tw-gradient-from-position);--tw-gradient-to:#eae3d200 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-black{--tw-gradient-from:#000 var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-purple-950{--tw-gradient-from:#3b0764 var(--tw-gradient-from-position);--tw-gradient-to:#3b076400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-stone-950{--tw-gradient-from:#0c0a09 var(--tw-gradient-from-position);--tw-gradient-to:#0c0a0900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-stone-900{--tw-gradient-from:#1c1917 var(--tw-gradient-from-position);--tw-gradient-to:#1c191700 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-stone-600{--tw-gradient-from:#57534e var(--tw-gradient-from-position);--tw-gradient-to:#57534e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-stone-700{--tw-gradient-from:#44403c var(--tw-gradient-from-position);--tw-gradient-to:#44403c00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-\[\#D7C49E\]{--tw-gradient-from:#d7c49e var(--tw-gradient-from-position);--tw-gradient-to:#d7c49e00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-\[\#1C1C1C\]{--tw-gradient-from:#1c1c1c var(--tw-gradient-from-position);--tw-gradient-to:#1c1c1c00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-\[\#AFAFAF\]{--tw-gradient-from:#afafaf var(--tw-gradient-from-position);--tw-gradient-to:#afafaf00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-\[\#C8BFB4\],.from-\[\#C8BFb4\]{--tw-gradient-from:#c8bfb4 var(--tw-gradient-from-position);--tw-gradient-to:#c8bfb400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-\[\#eae3d2\]{--tw-gradient-to:#eae3d2 var(--tw-gradient-to-position)}.bg-fixed{background-attachment:fixed}.p-3{padding:.75rem}.p-9{padding:2.25rem}.p-6{padding:1.5rem}.pt-3{padding-top:.75rem}.pt-9{padding-top:2.25rem}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-serif{font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-xs{font-size:.75rem;line-height:1rem}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-\[\#333\]{--tw-text-opacity:1;color:rgb(51 51 51/var(--tw-text-opacity))}.text-\[\#a29d7f\]{--tw-text-opacity:1;color:rgb(162 157 127/var(--tw-text-opacity))}.text-\[\#c7c7b2\]{--tw-text-opacity:1;color:rgb(199 199 178/var(--tw-text-opacity))}.text-\[\#E1B1E7\]{--tw-text-opacity:1;color:rgb(225 177 231/var(--tw-text-opacity))}.text-\[\#4c1655\]{--tw-text-opacity:1;color:rgb(76 22 85/var(--tw-text-opacity))}.text-\[\#601B69\]{--tw-text-opacity:1;color:rgb(96 27 105/var(--tw-text-opacity))}.text-fuchsia-200{--tw-text-opacity:1;color:rgb(245 208 254/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.decoration-\[\#eae3d2\]{text-decoration-color:#eae3d2}.decoration-zinc-800{text-decoration-color:#27272a}.decoration-\[\#a29d7f\]{text-decoration-color:#a29d7f}.decoration-\[\#E1B1E7\]{text-decoration-color:#e1b1e7}.decoration-\[\#601B69\]{text-decoration-color:#601b69}.bg-blend-darken{background-blend-mode:darken}.bg-blend-color-dodge{background-blend-mode:color-dodge}.bg-blend-difference{background-blend-mode:difference}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-\[\#eae3d2\]{--tw-shadow-color:#eae3d2;--tw-shadow:var(--tw-shadow-colored)}.shadow-\[\#a29d7f\]{--tw-shadow-color:#a29d7f;--tw-shadow:var(--tw-shadow-colored)}.shadow-\[\#E1B1E7\]{--tw-shadow-color:#e1b1e7;--tw-shadow:var(--tw-shadow-colored)}.outline{outline-style:solid}.outline-0{outline-width:0}.outline-1{outline-width:1px}.outline-offset-1{outline-offset:1px}.outline-offset-0{outline-offset:0}.outline-purple-50{outline-color:#faf5ff}.outline-\[\#eae3d2\]{outline-color:#eae3d2}.outline-black{outline-color:#000}.outline-slate-950{outline-color:#020617}.outline-slate-900{outline-color:#0f172a}.outline-zinc-800{outline-color:#27272a}.contrast-200{--tw-contrast:contrast(2);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-brightness-100{--tw-backdrop-brightness:brightness(1)}.backdrop-brightness-100,.backdrop-hue-rotate-90{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-hue-rotate-90{--tw-backdrop-hue-rotate:hue-rotate(90deg)}.backdrop-hue-rotate-30{--tw-backdrop-hue-rotate:hue-rotate(30deg)}.backdrop-hue-rotate-15,.backdrop-hue-rotate-30{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-hue-rotate-15{--tw-backdrop-hue-rotate:hue-rotate(15deg)}.hover\:underline:hover{text-decoration-line:underline}@media (prefers-color-scheme:dark){.dark\:bg-\[\#333\]{--tw-bg-opacity:1;background-color:rgb(51 51 51/var(--tw-bg-opacity))}.dark\:bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.dark\:text-\[\#eae3d2\]{--tw-text-opacity:1;color:rgb(234 227 210/var(--tw-text-opacity))}.dark\:text-\[\#6A2173\]{--tw-text-opacity:1;color:rgb(106 33 115/var(--tw-text-opacity))}.dark\:text-\[\#4c1655\]{--tw-text-opacity:1;color:rgb(76 22 85/var(--tw-text-opacity))}.dark\:text-\[\#E1B1E7\],.dark\:text-\[\#e1b1e7\]{--tw-text-opacity:1;color:rgb(225 177 231/var(--tw-text-opacity))}.dark\:decoration-\[\#E1B1E7\],.dark\:decoration-\[\#e1b1e7\]{text-decoration-color:#e1b1e7}} \ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index d8a3a8c..0000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./src/**/**.rs"],
- theme: {
- extend: {},
- },
- plugins: [],
-}
-