Home billboard shows latest titles; shelves sort by newest files; Films/Series get shelf subnav with search; player overlay closer to Netflix. Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
4 KiB
TypeScript
142 lines
4 KiB
TypeScript
"use client";
|
|
|
|
import { FormEvent, useEffect, useState } from "react";
|
|
|
|
export default function LinkDevicePage() {
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [code, setCode] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
const [ok, setOk] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const q = new URLSearchParams(window.location.search);
|
|
const c = q.get("code");
|
|
if (c) setCode(c.toUpperCase());
|
|
}, []);
|
|
|
|
async function onSubmit(e: FormEvent) {
|
|
e.preventDefault();
|
|
setBusy(true);
|
|
setErr(null);
|
|
setOk(null);
|
|
try {
|
|
const res = await fetch("/api/v1/link/claim", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, password, code }),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
setErr(data.error?.message ?? "Koppelen mislukt");
|
|
return;
|
|
}
|
|
setOk(`Apparaat “${data.deviceName ?? "TV"}” is gekoppeld. Je kunt dit venster sluiten.`);
|
|
setPassword("");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="link-page">
|
|
<div className="link-card">
|
|
<p className="page-kicker">VMC</p>
|
|
<h1>Apparaat koppelen</h1>
|
|
<p className="muted">
|
|
Log in met je kijker-account en voer de code van je TV of telefoon in.
|
|
</p>
|
|
<form onSubmit={onSubmit} className="link-form">
|
|
<label>
|
|
E-mail
|
|
<input
|
|
type="email"
|
|
autoComplete="username"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
/>
|
|
</label>
|
|
<label>
|
|
Wachtwoord
|
|
<input
|
|
type="password"
|
|
autoComplete="current-password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
/>
|
|
</label>
|
|
<label>
|
|
Code
|
|
<input
|
|
type="text"
|
|
inputMode="text"
|
|
autoCapitalize="characters"
|
|
autoComplete="one-time-code"
|
|
placeholder="ABCD-EFGH"
|
|
value={code}
|
|
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
required
|
|
/>
|
|
</label>
|
|
{err && <p className="form-error">{err}</p>}
|
|
{ok && <p className="form-ok">{ok}</p>}
|
|
<button type="submit" className="btn-primary" disabled={busy}>
|
|
{busy ? "Bezig…" : "Koppelen"}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
<style jsx>{`
|
|
.link-page {
|
|
min-height: 100vh;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 2rem 1rem;
|
|
background:
|
|
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(56, 120, 255, 0.25), transparent),
|
|
#04060a;
|
|
}
|
|
.link-card {
|
|
width: min(420px, 100%);
|
|
background: rgba(12, 16, 24, 0.92);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 16px;
|
|
padding: 1.75rem;
|
|
}
|
|
h1 {
|
|
margin: 0.25rem 0 0.5rem;
|
|
font-size: 1.75rem;
|
|
}
|
|
.link-form {
|
|
display: grid;
|
|
gap: 0.9rem;
|
|
margin-top: 1.25rem;
|
|
}
|
|
label {
|
|
display: grid;
|
|
gap: 0.35rem;
|
|
font-size: 0.85rem;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
input {
|
|
padding: 0.7rem 0.8rem;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
background: #0a0e16;
|
|
color: #fff;
|
|
font-size: 1rem;
|
|
}
|
|
.form-error {
|
|
color: #ff8b8b;
|
|
margin: 0;
|
|
}
|
|
.form-ok {
|
|
color: #8dffb0;
|
|
margin: 0;
|
|
}
|
|
`}</style>
|
|
</main>
|
|
);
|
|
}
|