M0: scaffold SPA + API monorepo
Backend (Django 6.1 + DRF): - Token auth with a custom User model (register/login/logout/me) - Library models (MediaItem, MediaLocation) and REST endpoints - File list/detail with search, rating filter, sorting, pagination - Multipart upload with optional rating/tags/notes - Range-aware media serving (video seeking) and ffmpeg thumbnails - scan_files management command for the watched folder Frontend (React 19 + Vite + TypeScript): - Catppuccin Mocha design tokens from the design docs - App shell, token persistence, protected routes - Library grid with filters, file detail with custom data editor - Upload page with per-file progress via XHR - Dev proxy to the Django API
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button, Field, inputClass } from "@/components/ui";
|
||||
import { errorMessage } from "@/lib/api";
|
||||
import { useAuth } from "@/store/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
const login = useAuth((state) => state.login);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const from = (location.state as { from?: string } | null)?.from ?? "/";
|
||||
|
||||
async function handleSubmit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await login(username, password);
|
||||
navigate(from, { replace: true });
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center px-4">
|
||||
<div className="w-full max-w-sm rounded-xl border border-ctp-surface0 bg-ctp-mantle p-6">
|
||||
<div className="mb-6 flex flex-col items-center gap-3">
|
||||
<span className="rounded bg-ctp-mauve px-3 py-1 font-mono text-sm font-bold text-ctp-crust">
|
||||
J621
|
||||
</span>
|
||||
<h1 className="text-lg font-semibold">Log in</h1>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Field label="Username">
|
||||
<input
|
||||
className={inputClass}
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Password">
|
||||
<input
|
||||
className={inputClass}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
{error ? (
|
||||
<p className="rounded-md border border-ctp-red/30 bg-ctp-red/10 px-3 py-2 text-sm text-ctp-red">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<Button type="submit" disabled={busy}>
|
||||
{busy ? "Signing in…" : "Sign in"}
|
||||
</Button>
|
||||
</form>
|
||||
<p className="mt-4 text-center text-sm text-ctp-overlay0">
|
||||
No account?{" "}
|
||||
<Link to="/register" className="text-ctp-blue hover:underline">
|
||||
Register
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button, Field, inputClass } from "@/components/ui";
|
||||
import { errorMessage } from "@/lib/api";
|
||||
import { useAuth } from "@/store/auth";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const register = useAuth((state) => state.register);
|
||||
const navigate = useNavigate();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function handleSubmit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (password !== confirm) {
|
||||
setError("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await register(username, password);
|
||||
navigate("/", { replace: true });
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center px-4">
|
||||
<div className="w-full max-w-sm rounded-xl border border-ctp-surface0 bg-ctp-mantle p-6">
|
||||
<div className="mb-6 flex flex-col items-center gap-3">
|
||||
<span className="rounded bg-ctp-mauve px-3 py-1 font-mono text-sm font-bold text-ctp-crust">
|
||||
J621
|
||||
</span>
|
||||
<h1 className="text-lg font-semibold">Create account</h1>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<Field label="Username">
|
||||
<input
|
||||
className={inputClass}
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Password">
|
||||
<input
|
||||
className={inputClass}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Confirm password">
|
||||
<input
|
||||
className={inputClass}
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={(event) => setConfirm(event.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
{error ? (
|
||||
<p className="rounded-md border border-ctp-red/30 bg-ctp-red/10 px-3 py-2 text-sm text-ctp-red">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<Button type="submit" disabled={busy}>
|
||||
{busy ? "Creating account…" : "Register"}
|
||||
</Button>
|
||||
</form>
|
||||
<p className="mt-4 text-center text-sm text-ctp-overlay0">
|
||||
Already have an account?{" "}
|
||||
<Link to="/login" className="text-ctp-blue hover:underline">
|
||||
Log in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user