156 lines
5.2 KiB
TypeScript
156 lines
5.2 KiB
TypeScript
import { useState } from "react";
|
|
import { useNavigate } from "react-router";
|
|
import { Lock, Plus } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
import type { Profile } from "@/api/types";
|
|
import { ProfileEditorDialog } from "@/components/profiles/ProfileEditorDialog";
|
|
import { ProfilePinDialog } from "@/components/profiles/ProfilePinDialog";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { useDocumentTitle } from "@/hooks/useDocumentTitle";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import { useAvailableUserLibraries } from "@/hooks/queries/libraries";
|
|
import { useProfiles } from "@/hooks/queries/profiles";
|
|
|
|
export default function Profiles() {
|
|
const { data: profiles = [], isLoading: profilesLoading, avatarUploadEnabled } = useProfiles();
|
|
const { data: libraries = [], isLoading: librariesLoading } = useAvailableUserLibraries();
|
|
const [editorOpen, setEditorOpen] = useState(false);
|
|
const [pinProfile, setPinProfile] = useState<Profile | null>(null);
|
|
const { selectProfile, verifyProfilePin, logout } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
useDocumentTitle("Profiles");
|
|
|
|
const isLoading = profilesLoading || librariesLoading;
|
|
|
|
function handleSelect(profile: Profile) {
|
|
if (profile.has_pin) {
|
|
setPinProfile(profile);
|
|
return;
|
|
}
|
|
|
|
selectProfile(profile);
|
|
navigate("/");
|
|
}
|
|
|
|
async function handleCreateSuccess(
|
|
profile: Profile,
|
|
context: {
|
|
pin: string;
|
|
},
|
|
) {
|
|
if (context.pin === "") {
|
|
selectProfile(profile);
|
|
navigate("/");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await verifyProfilePin(profile.id, context.pin);
|
|
if (response.valid && response.profile_token) {
|
|
selectProfile(profile, response.profile_token);
|
|
navigate("/");
|
|
return;
|
|
}
|
|
} catch {
|
|
// fall through to the toast below
|
|
}
|
|
|
|
toast.error("Profile created, but PIN verification failed");
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="auth-shell flex-col gap-8">
|
|
<Skeleton className="h-12 w-64" />
|
|
<div className="flex flex-wrap justify-center gap-5">
|
|
{Array.from({ length: 3 }).map((_, index) => (
|
|
<div key={index} className="flex w-[148px] flex-col items-center gap-3 p-5">
|
|
<Skeleton className="h-20 w-20 rounded-full" />
|
|
<Skeleton className="h-4 w-16 rounded" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (profiles.length === 0) {
|
|
return (
|
|
<div className="auth-shell flex-col gap-6">
|
|
<div className="w-full max-w-md space-y-3 text-center">
|
|
<h1 className="page-title text-[clamp(2.1rem,6vw,4rem)]">Create your first profile</h1>
|
|
<p className="text-muted-foreground text-sm">
|
|
You need a profile before you can enter the app.
|
|
</p>
|
|
</div>
|
|
|
|
<Button onClick={() => setEditorOpen(true)}>
|
|
<Plus className="h-4 w-4" />
|
|
Create profile
|
|
</Button>
|
|
|
|
<Button variant="outline" size="sm" onClick={logout}>
|
|
Sign out
|
|
</Button>
|
|
|
|
<ProfileEditorDialog
|
|
open={editorOpen}
|
|
libraries={libraries}
|
|
avatarUploadEnabled={avatarUploadEnabled}
|
|
onOpenChange={setEditorOpen}
|
|
onSaveSuccess={(profile, context) => void handleCreateSuccess(profile, context)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="auth-shell flex-col gap-8">
|
|
<div className="relative z-10 text-center">
|
|
<h1 className="page-title text-[clamp(2.2rem,7vw,4.5rem)]">Who's watching?</h1>
|
|
</div>
|
|
|
|
<div className="relative z-10 flex max-w-5xl flex-wrap justify-center gap-5">
|
|
{profiles.map((profile) => (
|
|
<div key={profile.id} className="group relative">
|
|
<button
|
|
onClick={() => handleSelect(profile)}
|
|
className="surface-panel hover:border-primary flex w-[148px] flex-col items-center gap-3 rounded-[1.75rem] p-5 transition-all duration-150 hover:-translate-y-1"
|
|
>
|
|
<Avatar className="ring-border group-hover:ring-primary/30 h-20 w-20 ring-2 transition-colors">
|
|
{profile.avatar_url ? (
|
|
<AvatarImage src={profile.avatar_url} alt={profile.name} />
|
|
) : null}
|
|
<AvatarFallback className="bg-surface text-primary text-2xl font-bold">
|
|
{profile.name.charAt(0).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span className="text-sm font-medium">{profile.name}</span>
|
|
{profile.has_pin ? <Lock className="text-muted-foreground h-3 w-3" /> : null}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Button variant="outline" size="sm" onClick={logout} className="relative z-10 mt-2">
|
|
Sign out
|
|
</Button>
|
|
|
|
<ProfilePinDialog
|
|
profile={pinProfile}
|
|
onClose={() => setPinProfile(null)}
|
|
onVerified={(profile, token) => {
|
|
setPinProfile(null);
|
|
selectProfile(profile, token);
|
|
navigate("/");
|
|
}}
|
|
verifyPin={verifyProfilePin}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|