From beec58ae6358dc7c7ddd88521f135b1d4bd8a126 Mon Sep 17 00:00:00 2001 From: liyunze <50455574+liyunze-coding@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:15:07 +0800 Subject: [PATCH] added preview and collaborations cms --- backend/src/api.rs | 50 +++++++- backend/src/db.rs | 86 +++++++++++++ backend/src/main.rs | 1 + frontend/index.html | 39 +++--- frontend/public/images/preview.webp | Bin 0 -> 24566 bytes frontend/src/components/Collabs.svelte | 32 +---- frontend/src/lib/api.ts | 26 +++- frontend/src/lib/types.ts | 9 ++ frontend/src/routes/Admin.svelte | 148 ++++++++++++++++++++++- frontend/src/routes/MediaKit.svelte | 27 +++-- frontend/src/routes/MediaKit_copy.svelte | 147 ---------------------- 11 files changed, 362 insertions(+), 203 deletions(-) create mode 100644 frontend/public/images/preview.webp delete mode 100644 frontend/src/routes/MediaKit_copy.svelte diff --git a/backend/src/api.rs b/backend/src/api.rs index e05a73b..0438247 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -7,7 +7,7 @@ use serde_json::{Value, json}; use std::collections::HashMap; use crate::auth::is_authorized; -use crate::db::{self, PlatformInput}; +use crate::db::{self, CollabInput, PlatformInput}; use crate::state::AppState; use crate::twitch; @@ -30,6 +30,10 @@ fn unauthorized() -> ApiError { ) } +fn bad_request(message: &str) -> ApiError { + (StatusCode::BAD_REQUEST, Json(json!({ "error": message }))) +} + fn require_admin(state: &AppState, headers: &HeaderMap) -> Result<(), ApiError> { if is_authorized(state, headers) { Ok(()) @@ -53,6 +57,11 @@ pub async fn get_media_kit(State(state): State) -> Result, db::get_platforms(&conn).map_err(internal_error)? }; + let collabs = { + let conn = state.db.lock().unwrap(); + db::get_collabs(&conn).map_err(internal_error)? + }; + let twitch = match twitch::get_twitch(&state).await { Ok(value) => value, Err(message) => json!({ @@ -66,6 +75,7 @@ pub async fn get_media_kit(State(state): State) -> Result, let payload = json!({ "content": content, "platforms": platforms, + "collabs": collabs, "twitch": twitch, }); @@ -93,10 +103,12 @@ pub async fn admin_get_state( let conn = state.db.lock().unwrap(); let content = db::get_content(&conn).map_err(internal_error)?; let platforms = db::get_platforms(&conn).map_err(internal_error)?; + let collabs = db::get_collabs(&conn).map_err(internal_error)?; Ok(Json(json!({ "content": content, "platforms": platforms, + "collabs": collabs, }))) } @@ -142,3 +154,39 @@ pub async fn admin_update_platforms( Ok(Json(json!({ "platforms": saved }))) } +pub async fn admin_update_collabs( + State(state): State, + headers: HeaderMap, + Json(collabs): Json>, +) -> Result, ApiError> { + require_admin(&state, &headers)?; + + let mut cleaned = Vec::with_capacity(collabs.len()); + for collab in collabs { + let name = collab.name.trim().to_string(); + let image = collab.image.trim().to_string(); + let url = collab.url.trim().to_string(); + + if name.is_empty() && image.is_empty() && url.is_empty() { + continue; + } + if name.is_empty() { + return Err(bad_request("Each collaboration needs a name")); + } + + cleaned.push(CollabInput { name, image, url }); + } + + { + let mut conn = state.db.lock().unwrap(); + db::replace_collabs(&mut conn, &cleaned).map_err(internal_error)?; + } + + state.cache.invalidate(MEDIA_KIT_CACHE_KEY); + + let conn = state.db.lock().unwrap(); + let saved = db::get_collabs(&conn).map_err(internal_error)?; + + Ok(Json(json!({ "collabs": saved }))) +} + diff --git a/backend/src/db.rs b/backend/src/db.rs index bba6332..766837c 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -11,6 +11,13 @@ pub struct PlatformInput { pub followers: i64, } +#[derive(Debug, Deserialize)] +pub struct CollabInput { + pub name: String, + pub image: String, + pub url: String, +} + pub fn init(conn: &Connection) -> rusqlite::Result<()> { conn.execute_batch( r#" @@ -28,6 +35,15 @@ pub fn init(conn: &Connection) -> rusqlite::Result<()> { updated_at INTEGER NOT NULL ); + CREATE TABLE IF NOT EXISTS collabs ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + image TEXT NOT NULL DEFAULT '', + url TEXT NOT NULL DEFAULT '', + sort_order INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL + ); + "#, )?; seed(conn) @@ -72,6 +88,35 @@ fn seed(conn: &Connection) -> rusqlite::Result<()> { )?; } + let collab_count: i64 = conn.query_row("SELECT COUNT(*) FROM collabs", [], |row| row.get(0))?; + if collab_count == 0 { + let collab_defaults = [ + ( + "ProtonPass", + "/images/collaborations/proton-pass.webp", + "https://proton.me/pass", + ), + ( + "ProtonVPN", + "/images/collaborations/proton-vpn.webp", + "https://protonvpn.com/", + ), + ("Brick", "/images/collaborations/brick.webp", "https://getbrick.com/"), + ( + "Focumon", + "/images/collaborations/focumon.webp", + "https://www.focumon.com/landing", + ), + ]; + + for (index, (name, image, url)) in collab_defaults.iter().enumerate() { + conn.execute( + "INSERT INTO collabs (name, image, url, sort_order, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", + params![name, image, url, index as i64, now], + )?; + } + } + Ok(()) } @@ -120,6 +165,47 @@ pub fn upsert_content(conn: &Connection, entries: &[(String, String)]) -> rusqli Ok(()) } +pub fn get_collabs(conn: &Connection) -> rusqlite::Result> { + let mut stmt = + conn.prepare("SELECT id, name, image, url FROM collabs ORDER BY sort_order, id")?; + + let rows = stmt.query_map([], |row| { + Ok(json!({ + "id": row.get::<_, i64>(0)?, + "name": row.get::<_, String>(1)?, + "image": row.get::<_, String>(2)?, + "url": row.get::<_, String>(3)?, + })) + })?; + + rows.collect() +} + +pub fn replace_collabs(conn: &mut Connection, collabs: &[CollabInput]) -> rusqlite::Result<()> { + let tx = conn.transaction()?; + tx.execute("DELETE FROM collabs", [])?; + + let now = now_secs(); + { + let mut stmt = tx.prepare( + "INSERT INTO collabs (name, image, url, sort_order, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", + )?; + + for (index, collab) in collabs.iter().enumerate() { + stmt.execute(params![ + collab.name, + collab.image, + collab.url, + index as i64, + now + ])?; + } + } + + tx.commit()?; + Ok(()) +} + pub fn upsert_platforms(conn: &Connection, platforms: &[PlatformInput]) -> rusqlite::Result<()> { let now = now_secs(); let mut stmt = conn.prepare( diff --git a/backend/src/main.rs b/backend/src/main.rs index dbf4416..c0b16db 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -72,6 +72,7 @@ async fn main() { .route("/api/admin/state", get(api::admin_get_state)) .route("/api/admin/content", put(api::admin_update_content)) .route("/api/admin/platforms", put(api::admin_update_platforms)) + .route("/api/admin/collabs", put(api::admin_update_collabs)) .fallback_service(ServeDir::new(&static_dir).append_index_html_on_directories(true)) .with_state(app_state); diff --git a/frontend/index.html b/frontend/index.html index eda6b2d..7906731 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,19 +1,26 @@ - - - - - - RythonDev — Media Kit - - -
- - + + + + + + + + + + RythonDev's Media Kit + + +
+ + diff --git a/frontend/public/images/preview.webp b/frontend/public/images/preview.webp new file mode 100644 index 0000000000000000000000000000000000000000..f9ab1c87185d3722d36c787092f96bfe5aea7ab4 GIT binary patch literal 24566 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/Collabs.svelte b/frontend/src/components/Collabs.svelte index 729640d..8461705 100644 --- a/frontend/src/components/Collabs.svelte +++ b/frontend/src/components/Collabs.svelte @@ -1,32 +1,8 @@ - + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index c89f328..86da148 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,4 +1,4 @@ -import type { AdminState, MediaKit, Platform } from "./types"; +import type { AdminState, Collab, MediaKit, Platform } from "./types"; const TOKEN_KEY = "rythondev_admin_token"; @@ -84,3 +84,27 @@ export async function updatePlatforms( return data.platforms; } +async function readError(res: Response, fallback: string): Promise { + const data = await res.json().catch(() => null); + return typeof data?.error === "string" ? data.error : fallback; +} + +export async function updateCollabs(collabs: Collab[]): Promise { + const res = await fetch("/api/admin/collabs", { + method: "PUT", + headers: { "Content-Type": "application/json", ...authHeaders() }, + body: JSON.stringify( + collabs.map(({ name, image, url }) => ({ name, image, url })), + ), + }); + + if (res.status === 401) { + clearStoredToken(); + throw new Error("unauthorized"); + } + if (!res.ok) throw new Error(await readError(res, "Failed to save collaborations")); + + const data = await res.json(); + return data.collabs; +} + diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 8137711..905be9f 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -5,6 +5,13 @@ export interface Platform { followers: number; } +export interface Collab { + id: number; + name: string; + image: string; + url: string; +} + export interface TwitchData { configured?: boolean; found?: boolean; @@ -23,10 +30,12 @@ export interface TwitchData { export interface MediaKit { content: Record; platforms: Platform[]; + collabs: Collab[]; twitch: TwitchData | null; } export interface AdminState { content: Record; platforms: Platform[]; + collabs: Collab[]; } diff --git a/frontend/src/routes/Admin.svelte b/frontend/src/routes/Admin.svelte index 4be3b63..1ba2f11 100644 --- a/frontend/src/routes/Admin.svelte +++ b/frontend/src/routes/Admin.svelte @@ -4,11 +4,12 @@ clearStoredToken, fetchAdminState, getStoredToken, + updateCollabs, updateContent, updatePlatforms, verifyAdmin, } from "../lib/api"; - import type { Platform } from "../lib/types"; + import type { Collab, Platform } from "../lib/types"; let authed = $state(false); let loading = $state(true); @@ -19,6 +20,8 @@ let content = $state>({}); let platforms = $state([]); + let collabs = $state([]); + let nextCollabId = -1; onMount(async () => { if (getStoredToken()) { @@ -42,6 +45,7 @@ const state = await fetchAdminState(); content = state.content; platforms = state.platforms; + collabs = state.collabs ?? []; authed = true; } catch (err) { authed = false; @@ -72,6 +76,7 @@ authed = false; content = {}; platforms = []; + collabs = []; message = ""; error = ""; } @@ -88,6 +93,39 @@ } } + function addCollab() { + collabs = [ + ...collabs, + { id: nextCollabId--, name: "", image: "", url: "" }, + ]; + } + + function removeCollab(index: number) { + collabs = collabs.filter((_, i) => i !== index); + } + + function moveCollab(index: number, direction: -1 | 1) { + const target = index + direction; + if (target < 0 || target >= collabs.length) return; + const next = [...collabs]; + [next[index], next[target]] = [next[target], next[index]]; + collabs = next; + } + + async function handleSaveCollabs() { + error = ""; + message = ""; + try { + collabs = await updateCollabs(collabs); + message = "Collaborations saved"; + } catch (err) { + error = + err instanceof Error + ? err.message + : "Unable to save collaborations"; + } + } + async function handleSavePlatforms() { error = ""; message = ""; @@ -296,6 +334,114 @@ {/each} + +
+
+

+ Collaborations +

+
+ + +
+
+ + {#if collabs.length === 0} +

+ No collaborations yet. +

+ {:else} +
+ {#each collabs as collab, index (collab.id)} +
+
+
+ {collab.name || "New collaboration"} +
+
+ + + +
+
+
+ + + +
+
+ {/each} +
+ {/if} +
{#if message} diff --git a/frontend/src/routes/MediaKit.svelte b/frontend/src/routes/MediaKit.svelte index 9a30b5b..02aefba 100644 --- a/frontend/src/routes/MediaKit.svelte +++ b/frontend/src/routes/MediaKit.svelte @@ -18,6 +18,7 @@ let content = $derived(data?.content ?? {}); let platforms = $derived(data?.platforms ?? []); + let collabs = $derived(data?.collabs ?? []); let services = $derived( (content.services ?? "") .split("\n") @@ -185,7 +186,9 @@ -
+
@@ -202,23 +205,29 @@ {/if}
-
About Me
-

+

+ About Me +
+

{content.about ?? ""}

-
-
- Collaborations + {#if collabs.length} +
+
+ Collaborations +
+
- -
+ {/if} {#if services.length}
Services
-
+