frontend backend works without docker
This commit is contained in:
parent
ba3fb012b5
commit
829093e64a
13 changed files with 135 additions and 76 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -31,3 +31,5 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
scripts/
|
||||
|
|
|
|||
53
README.md
53
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) → <http://localhost:3000>
|
||||
- Frontend (Vite) → <http://localhost:5173>
|
||||
|
||||
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 <http://localhost:3000>.
|
||||
|
||||
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 <http://localhost:3001>. The SQLite database is persisted
|
||||
The app is then available at <http://localhost:3000>. The SQLite database is persisted
|
||||
in the `media-kit-data` Docker volume.
|
||||
|
||||
## API
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
BIN
frontend/public/fonts/Antonio-Bold.ttf
Normal file
BIN
frontend/public/fonts/Antonio-Bold.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/Fredoka-Regular.ttf
Normal file
BIN
frontend/public/fonts/Fredoka-Regular.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/Montserrat-Bold.ttf
Normal file
BIN
frontend/public/fonts/Montserrat-Bold.ttf
Normal file
Binary file not shown.
BIN
frontend/public/fonts/Montserrat-Regular.ttf
Normal file
BIN
frontend/public/fonts/Montserrat-Regular.ttf
Normal file
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,6 +187,20 @@
|
|||
rows="4"
|
||||
bind:value={content[key]}
|
||||
></textarea>
|
||||
{:else if key === 'profile_image_url'}
|
||||
<div class="flex items-start gap-4">
|
||||
<input
|
||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||
bind:value={content[key]}
|
||||
/>
|
||||
{#if content[key]}
|
||||
<img
|
||||
class="h-16 w-16 shrink-0 rounded-full border border-gray-200 object-cover dark:border-zinc-800"
|
||||
src={content[key]}
|
||||
alt="Profile preview"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||
|
|
|
|||
|
|
@ -34,32 +34,38 @@
|
|||
</script>
|
||||
|
||||
{#if error}
|
||||
<main class="mx-auto w-full max-w-[920px] px-6 pt-12 pb-[72px]">
|
||||
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
||||
<div class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
</main>
|
||||
{:else if !data}
|
||||
<main class="mx-auto w-full max-w-[920px] px-6 pt-12 pb-[72px]">
|
||||
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
||||
<div class="py-8 text-gray-500 dark:text-gray-400">Loading…</div>
|
||||
</main>
|
||||
{:else}
|
||||
<main class="mx-auto w-full max-w-[920px] px-6 pt-12 pb-[72px]">
|
||||
<header class="pt-6 pb-2">
|
||||
<p class="mb-3 text-xs font-semibold tracking-[0.14em] uppercase text-gray-500 dark:text-gray-400">
|
||||
Media kit
|
||||
</p>
|
||||
<h1 class="text-[clamp(36px,8vw,52px)] leading-[1.05] font-semibold tracking-tight">
|
||||
{content.brand_name ?? 'RythonDev'}
|
||||
</h1>
|
||||
{#if content.tagline}
|
||||
<p class="mt-4 text-xl text-gray-500 dark:text-gray-400">{content.tagline}</p>
|
||||
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
||||
<header class="flex flex-col items-start gap-6 pt-6 pb-2 sm:flex-row sm:items-center">
|
||||
{#if content.profile_image_url}
|
||||
<img
|
||||
class="h-24 w-24 shrink-0 rounded-md object-cover"
|
||||
src={content.profile_image_url}
|
||||
alt={content.brand_name ?? 'Profile image'}
|
||||
/>
|
||||
{/if}
|
||||
<div>
|
||||
<h1 class="text-[clamp(36px,8vw,52px)] leading-[1.05] font-semibold tracking-tight">
|
||||
{content.brand_name ?? 'RythonDev'}
|
||||
</h1>
|
||||
{#if content.tagline}
|
||||
<p class="mt-4 text-xl text-gray-500 dark:text-gray-400">{content.tagline}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="mt-14 border-t border-gray-200 pt-8 dark:border-zinc-800">
|
||||
<h2 class="text-xl font-semibold tracking-tight">About</h2>
|
||||
<p class="mt-4 max-w-[640px] text-lg leading-relaxed text-gray-500 dark:text-gray-400">
|
||||
<p class="mt-4 max-w-160 text-lg leading-relaxed text-gray-500 dark:text-gray-400">
|
||||
{content.about ?? ''}
|
||||
</p>
|
||||
{#if content.email}
|
||||
|
|
@ -80,45 +86,7 @@
|
|||
<span class="text-sm text-gray-500 dark:text-gray-400">Followers</span>
|
||||
<span class="text-3xl font-semibold tracking-tight">{formatCount(twitch?.followers)}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-neutral-50 p-5 dark:border-zinc-800 dark:bg-neutral-900"
|
||||
>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">Status</span>
|
||||
<span
|
||||
class="text-3xl font-semibold tracking-tight {twitch?.live
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: ''}"
|
||||
>
|
||||
{twitch?.live ? 'LIVE' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
{#if twitch?.live}
|
||||
<div
|
||||
class="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-neutral-50 p-5 dark:border-zinc-800 dark:bg-neutral-900"
|
||||
>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">Viewers</span>
|
||||
<span class="text-3xl font-semibold tracking-tight">{formatCount(twitch?.viewer_count)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if twitch?.live && twitch?.title}
|
||||
<p class="mt-5">
|
||||
{twitch.title}
|
||||
{#if twitch.game_name}
|
||||
<span class="text-gray-500 dark:text-gray-400">— {twitch.game_name}</span>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
class="mt-6 inline-block rounded-[10px] bg-neutral-900 px-[18px] py-2.5 text-sm font-medium text-white dark:bg-neutral-50 dark:text-neutral-950"
|
||||
href="https://twitch.tv/{twitchLogin}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Watch on Twitch
|
||||
</a>
|
||||
{:else}
|
||||
<p class="text-gray-500 dark:text-gray-400">{twitch?.message ?? 'Twitch stats are unavailable.'}</p>
|
||||
{/if}
|
||||
|
|
@ -145,9 +113,5 @@
|
|||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="mt-16 border-t border-gray-200 pt-6 text-sm text-gray-500 dark:border-zinc-800 dark:text-gray-400">
|
||||
<a href="#/admin" class="hover:text-neutral-900 dark:hover:text-neutral-50">Admin</a>
|
||||
</footer>
|
||||
</main>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue