mirror of
https://github.com/myronblair/kino-app
synced 2026-06-30 17:50:16 -05:00
158 lines
7.7 KiB
React
158 lines
7.7 KiB
React
import { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import api from "../lib/api";
|
|
import { useProfile } from "../lib/profile";
|
|
import { useAuth } from "../lib/auth";
|
|
import { Plus, Pencil, Check, X, Trash2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
const COLORS = ["#D9381E", "#EAB308", "#22C55E", "#3B82F6", "#A855F7", "#EC4899"];
|
|
|
|
export default function ProfileSelect() {
|
|
const { user, logout } = useAuth();
|
|
const { profiles, refresh, switchTo } = useProfile();
|
|
const nav = useNavigate();
|
|
const [editing, setEditing] = useState(false);
|
|
const [creating, setCreating] = useState(false);
|
|
const [draft, setDraft] = useState({ name: "", avatar_color: COLORS[0], is_kids: false, max_rating: "NR" });
|
|
|
|
useEffect(() => { refresh(); }, [refresh]);
|
|
|
|
const choose = (p) => {
|
|
switchTo(p);
|
|
nav("/browse", { replace: true });
|
|
};
|
|
|
|
const submitNew = async (e) => {
|
|
e.preventDefault();
|
|
if (!draft.name.trim()) return;
|
|
try {
|
|
await api.post("/profiles", draft);
|
|
toast.success("Profile created");
|
|
setCreating(false);
|
|
setDraft({ name: "", avatar_color: COLORS[0], is_kids: false, max_rating: "NR" });
|
|
refresh();
|
|
} catch (err) {
|
|
toast.error(err.response?.data?.detail || "Could not create");
|
|
}
|
|
};
|
|
|
|
const remove = async (id) => {
|
|
if (!window.confirm("Delete this profile? Watchlist & history will be removed.")) return;
|
|
try { await api.delete(`/profiles/${id}`); refresh(); } catch (err) {
|
|
toast.error(err.response?.data?.detail || "Could not delete");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#050505] flex flex-col items-center justify-center px-6 py-16" data-testid="profile-select-page">
|
|
<span className="text-xs uppercase tracking-[0.3em] text-[#D9381E] mb-6">Welcome back, {user?.name}</span>
|
|
<h1 className="font-display text-5xl md:text-6xl font-black tracking-tighter text-white text-center">
|
|
Who's watching?
|
|
</h1>
|
|
|
|
<div className="mt-16 flex flex-wrap justify-center gap-8 max-w-4xl">
|
|
{profiles.map((p) => (
|
|
<div key={p.id} className="flex flex-col items-center gap-3 group" data-testid={`profile-${p.id}`}>
|
|
<button
|
|
onClick={() => editing ? null : choose(p)}
|
|
className="w-28 h-28 md:w-36 md:h-36 flex items-center justify-center text-3xl font-bold text-white transition-all duration-300 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-[#D9381E]"
|
|
style={{ backgroundColor: p.avatar_color }}
|
|
data-testid={`select-profile-${p.id}`}
|
|
>
|
|
{p.name?.[0]?.toUpperCase() || "?"}
|
|
</button>
|
|
<span className="font-display text-lg text-white">{p.name}</span>
|
|
<div className="flex gap-2 text-[10px] uppercase tracking-[0.2em] text-[#8A8A8A]">
|
|
{p.is_kids && <span>Kids</span>}
|
|
{p.max_rating !== "NR" && <span>· Up to {p.max_rating}</span>}
|
|
</div>
|
|
{editing && (
|
|
<button onClick={() => remove(p.id)} className="text-[#8A8A8A] hover:text-[#fca5a5] transition-colors mt-1" data-testid={`delete-profile-${p.id}`}>
|
|
<Trash2 size={14} strokeWidth={1.5} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{profiles.length < 5 && (
|
|
<button
|
|
onClick={() => setCreating(true)}
|
|
className="w-28 h-28 md:w-36 md:h-36 flex flex-col items-center justify-center border border-dashed border-[#333] hover:border-[#D9381E] text-[#8A8A8A] hover:text-white transition-colors duration-300"
|
|
data-testid="add-profile-button"
|
|
>
|
|
<Plus size={32} strokeWidth={1.5} />
|
|
<span className="text-xs uppercase tracking-[0.2em] mt-2">Add</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-16 flex gap-4">
|
|
<button onClick={() => setEditing(!editing)}
|
|
className="flex items-center gap-2 border border-[#222] hover:border-white text-[#8A8A8A] hover:text-white px-5 py-2 text-xs uppercase tracking-[0.2em] transition-colors duration-300"
|
|
data-testid="manage-profiles-button">
|
|
{editing ? <Check size={14} strokeWidth={1.5} /> : <Pencil size={14} strokeWidth={1.5} />}
|
|
{editing ? "Done" : "Manage Profiles"}
|
|
</button>
|
|
<button onClick={() => { logout(); nav("/login"); }}
|
|
className="text-[#8A8A8A] hover:text-white text-xs uppercase tracking-[0.2em] transition-colors duration-300"
|
|
data-testid="profile-logout-button">
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
|
|
{creating && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center" data-testid="new-profile-modal">
|
|
<div className="absolute inset-0 bg-black/85 backdrop-blur-md" onClick={() => setCreating(false)} />
|
|
<form onSubmit={submitNew} className="relative bg-[#0F0F0F] border border-[#222] p-8 w-full max-w-md mx-4 fade-up">
|
|
<button type="button" onClick={() => setCreating(false)} className="absolute top-4 right-4 text-[#8A8A8A] hover:text-white"><X size={18} /></button>
|
|
<h2 className="font-display text-3xl font-bold tracking-tight text-white">New profile</h2>
|
|
|
|
<label className="block mt-6">
|
|
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">Name</span>
|
|
<input value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })}
|
|
required className="mt-2 w-full bg-[#0F0F0F] border border-[#222] focus:border-[#D9381E] focus:outline-none text-white px-4 py-3"
|
|
data-testid="new-profile-name" />
|
|
</label>
|
|
|
|
<div className="mt-5">
|
|
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">Color</span>
|
|
<div className="mt-2 flex gap-2">
|
|
{COLORS.map((c) => (
|
|
<button type="button" key={c} onClick={() => setDraft({ ...draft, avatar_color: c })}
|
|
className={`w-10 h-10 ${draft.avatar_color === c ? "ring-2 ring-white" : ""}`}
|
|
style={{ backgroundColor: c }} aria-label={`Color ${c}`}
|
|
data-testid={`color-${c.replace('#', '')}`} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<label className="flex items-center gap-3 mt-5 cursor-pointer">
|
|
<input type="checkbox" checked={draft.is_kids}
|
|
onChange={(e) => setDraft({ ...draft, is_kids: e.target.checked, max_rating: e.target.checked ? "PG" : draft.max_rating })}
|
|
className="accent-[#D9381E]" data-testid="new-profile-kids" />
|
|
<span className="text-sm text-[#C8C8C8]">Kids profile (limits content to PG)</span>
|
|
</label>
|
|
|
|
<label className="block mt-5">
|
|
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">Max rating</span>
|
|
<select value={draft.max_rating} onChange={(e) => setDraft({ ...draft, max_rating: e.target.value })}
|
|
className="mt-2 w-full bg-[#0F0F0F] border border-[#222] text-white px-4 py-3 focus:border-[#D9381E] focus:outline-none"
|
|
data-testid="new-profile-rating">
|
|
<option value="G">G</option>
|
|
<option value="PG">PG</option>
|
|
<option value="PG-13">PG-13</option>
|
|
<option value="R">R</option>
|
|
<option value="NR">No restriction</option>
|
|
</select>
|
|
</label>
|
|
|
|
<button type="submit" className="mt-8 w-full bg-[#D9381E] hover:bg-[#ED4B32] text-white py-3 text-sm uppercase tracking-[0.2em]"
|
|
data-testid="new-profile-submit">Create profile</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|