diff --git a/.env.example b/.env.example index c548e43..53fcc98 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,8 @@ TWITCH_CLIENT_SECRET= TWITCH_LOGIN=rythondev # CMS authentication +# Values containing `$` or other special characters must be single-quoted, +# e.g. ADMIN_PASSWORD='p@$$w0rd!' ADMIN_USER=admin ADMIN_PASSWORD=change-me diff --git a/.gitignore b/.gitignore index 53fc912..a3fdd47 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ dist-ssr *.njsproj *.sln *.sw? + +scripts/ diff --git a/README.md b/README.md index 6097c98..4decad5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Copy `.env.example` to `.env` and fill in your values: cp .env.example .env ``` +> **Note:** values containing `$` must be single-quoted, otherwise the backend's +> dotenv loader will try to expand them as variables. For example: +> `ADMIN_PASSWORD='p@$$w0rd!'` + | Variable | Description | Default | | --- | --- | --- | | `TWITCH_CLIENT_ID` | Twitch Helix API client ID | empty | @@ -39,40 +43,51 @@ cp .env.example .env | `DATABASE_PATH` | SQLite database file path | `data/media_kit.db` | | `STATIC_DIR` | Directory containing the built frontend | `frontend/dist` | -## Local development +## Local development (no Docker) -### 1. Backend +The quickest way is the root `dev` script, which starts the backend and the Vite +dev server together: ```bash -cd backend -cargo run -``` - -### 2. Frontend - -```bash -cd frontend npm install npm run dev ``` +- Backend (Rust/Axum) → +- Frontend (Vite) → + The Vite dev server proxies `/api` requests to `http://localhost:3000`. -## Production build +If you prefer two terminals, run them separately: ```bash -cd frontend -npm install -npm run build +# terminal 1 +npm run dev:backend -cd ../backend -cargo build --release +# terminal 2 +npm run dev:frontend ``` -Then run the backend with the frontend build directory: +> Rust is required for the backend. If `cargo` is not found, add +> `%USERPROFILE%\.cargo\bin` to your `PATH` (Windows) or `~/.cargo/bin` +> (macOS/Linux). + +## Production build (no Docker) ```bash -STATIC_DIR=../frontend/dist ./backend/target/release/backend +npm install +npm run build +npm start +``` + +This builds the frontend to `frontend/dist`, compiles the backend, and serves the +whole app (static site + API) from . + +The SQLite database is written to `data/media_kit.db` and static files are read +from `frontend/dist` by default. Override them if needed: + +```bash +DATABASE_PATH=./data/media_kit.db STATIC_DIR=./frontend/dist npm start ``` ## Docker @@ -81,7 +96,7 @@ STATIC_DIR=../frontend/dist ./backend/target/release/backend docker compose up --build ``` -The app is then available at . The SQLite database is persisted +The app is then available at . The SQLite database is persisted in the `media-kit-data` Docker volume. ## API diff --git a/backend/src/db.rs b/backend/src/db.rs index c11218d..feaabb7 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -39,6 +39,7 @@ fn seed(conn: &Connection) -> rusqlite::Result<()> { ("tagline", "Game developer & live streamer"), ("about", "I build games and stream the process on Twitch."), ("email", "hello@rython.dev"), + ("profile_image_url", "/images/pfp_new_medium.webp"), ]; for (key, value) in content_defaults { diff --git a/backend/src/main.rs b/backend/src/main.rs index 09948a3..dbf4416 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -19,9 +19,26 @@ fn env_or(key: &str, default: &str) -> String { std::env::var(key).unwrap_or_else(|_| default.to_string()) } +/// Load `.env` from the current directory, then walk up parent directories +/// until one is found. This lets `cargo run` from `backend/` pick up the +/// repository-root `.env` without Docker. +fn load_env_file() { + let mut dir = std::env::current_dir().unwrap_or_default(); + loop { + let candidate = dir.join(".env"); + if candidate.exists() { + dotenvy::from_path(&candidate).ok(); + return; + } + if !dir.pop() { + return; + } + } +} + #[tokio::main] async fn main() { - dotenvy::dotenv().ok(); + load_env_file(); let db_path = env_or("DATABASE_PATH", "data/media_kit.db"); if let Some(parent) = std::path::Path::new(&db_path).parent() { diff --git a/frontend/public/fonts/Antonio-Bold.ttf b/frontend/public/fonts/Antonio-Bold.ttf new file mode 100644 index 0000000..595f1ea Binary files /dev/null and b/frontend/public/fonts/Antonio-Bold.ttf differ diff --git a/frontend/public/fonts/Fredoka-Regular.ttf b/frontend/public/fonts/Fredoka-Regular.ttf new file mode 100644 index 0000000..23618d7 Binary files /dev/null and b/frontend/public/fonts/Fredoka-Regular.ttf differ diff --git a/frontend/public/fonts/Montserrat-Bold.ttf b/frontend/public/fonts/Montserrat-Bold.ttf new file mode 100644 index 0000000..efddc83 Binary files /dev/null and b/frontend/public/fonts/Montserrat-Bold.ttf differ diff --git a/frontend/public/fonts/Montserrat-Regular.ttf b/frontend/public/fonts/Montserrat-Regular.ttf new file mode 100644 index 0000000..aa9033a Binary files /dev/null and b/frontend/public/fonts/Montserrat-Regular.ttf differ diff --git a/frontend/src/app.css b/frontend/src/app.css index f1d8c73..b188655 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1 +1,38 @@ @import "tailwindcss"; + +@font-face { + font-family: "Fredoka"; + src: url("/fonts/Fredoka-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Antonio"; + src: url("/fonts/Antonio-Bold.ttf") format("truetype"); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +/* Montserrat Fonts */ +@font-face { + font-family: "Montserrat"; + src: url("/fonts/Montserrat-Bold.ttf") format("truetype"); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Montserrat"; + src: url("/fonts/Montserrat-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +html, body { + font-family: Montserrat; +} diff --git a/frontend/src/routes/Admin.svelte b/frontend/src/routes/Admin.svelte index 909ca7e..71c5b22 100644 --- a/frontend/src/routes/Admin.svelte +++ b/frontend/src/routes/Admin.svelte @@ -187,6 +187,20 @@ rows="4" bind:value={content[key]} > + {:else if key === 'profile_image_url'} +
+ + {#if content[key]} + Profile preview + {/if} +
{:else} {#if error} -
+
{error}
{:else if !data} -
+
Loading…
{:else} -
-
-

- Media kit -

-

- {content.brand_name ?? 'RythonDev'} -

- {#if content.tagline} -

{content.tagline}

+
+
+ {#if content.profile_image_url} + {content.brand_name {/if} +
+

+ {content.brand_name ?? 'RythonDev'} +

+ {#if content.tagline} +

{content.tagline}

+ {/if} +

About

-

+

{content.about ?? ''}

{#if content.email} @@ -80,45 +86,7 @@ Followers {formatCount(twitch?.followers)} -
- Status - - {twitch?.live ? 'LIVE' : 'Offline'} - -
- {#if twitch?.live} -
- Viewers - {formatCount(twitch?.viewer_count)} -
- {/if} - - {#if twitch?.live && twitch?.title} -

- {twitch.title} - {#if twitch.game_name} - — {twitch.game_name} - {/if} -

- {/if} - - - Watch on Twitch - {:else}

{twitch?.message ?? 'Twitch stats are unavailable.'}

{/if} @@ -145,9 +113,5 @@ {/each}
- -
{/if} diff --git a/package.json b/package.json index bc9fc17..3809934 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,13 @@ { + "scripts": { + "dev": "node scripts/dev.mjs", + "dev:frontend": "npm --prefix frontend run dev", + "dev:backend": "cargo run --manifest-path backend/Cargo.toml", + "build": "npm --prefix frontend run build && cargo build --manifest-path backend/Cargo.toml", + "start": "cargo run --manifest-path backend/Cargo.toml" + }, "dependencies": { "@tailwindcss/vite": "^4.3.4", "tailwindcss": "^4.3.4" } -} \ No newline at end of file +}