Spaces:
Sleeping
Sleeping
| import { useEffect, useRef, useState } from "react"; | |
| import { Camera, FolderOpen, Save, UserRound } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { roleLabel, statusLabel, updateProfile, type StoredUser } from "@/lib/auth-service"; | |
| import { formatStamp, listProjects, type StoredProject } from "@/lib/studio-storage"; | |
| type Props = { | |
| open: boolean; | |
| onOpenChange: (v: boolean) => void; | |
| user: StoredUser; | |
| onUserChange: (u: StoredUser) => void; | |
| onOpenProject: (p: StoredProject) => void; | |
| }; | |
| export default function ProfileDialog({ open, onOpenChange, user, onUserChange, onOpenProject }: Props) { | |
| const fileRef = useRef<HTMLInputElement | null>(null); | |
| const [displayName, setDisplayName] = useState(user.displayName ?? user.username); | |
| const [about, setAbout] = useState(user.about ?? ""); | |
| const [avatar, setAvatar] = useState(user.avatar ?? ""); | |
| const [items, setItems] = useState<StoredProject[]>([]); | |
| const [savedAt, setSavedAt] = useState(0); | |
| useEffect(() => { | |
| if (!open) return; | |
| setDisplayName(user.displayName ?? user.username); | |
| setAbout(user.about ?? ""); | |
| setAvatar(user.avatar ?? ""); | |
| setItems(listProjects(user.id)); | |
| }, [open, user]); | |
| const pickAvatar = (file: File) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => setAvatar(String(reader.result ?? "")); | |
| reader.readAsDataURL(file); | |
| }; | |
| const save = () => { | |
| const next = updateProfile(user.id, { displayName: displayName.trim(), about: about.trim(), avatar }); | |
| if (next) onUserChange(next); | |
| setSavedAt(Date.now()); | |
| }; | |
| return ( | |
| <Dialog open={open} onOpenChange={onOpenChange}> | |
| <DialogContent className="max-h-[88vh] max-w-3xl overflow-y-auto"> | |
| <DialogHeader> | |
| <DialogTitle className="font-display text-2xl">Профиль</DialogTitle> | |
| <DialogDescription>Данные аккаунта, аватар и сохранённые работы.</DialogDescription> | |
| </DialogHeader> | |
| <input | |
| ref={fileRef} | |
| type="file" | |
| accept="image/*" | |
| className="hidden" | |
| onChange={(e) => { | |
| const f = e.target.files?.[0]; | |
| if (f) pickAvatar(f); | |
| e.target.value = ""; | |
| }} | |
| /> | |
| <div className="grid gap-4 sm:grid-cols-[auto_minmax(0,1fr)]"> | |
| <div className="flex flex-col items-center gap-2"> | |
| <div className="grid h-24 w-24 place-items-center overflow-hidden rounded-full border border-border bg-secondary"> | |
| {avatar ? ( | |
| <img src={avatar} alt="Аватар пользователя" className="h-full w-full object-cover" /> | |
| ) : ( | |
| <UserRound className="h-10 w-10 text-muted-foreground" /> | |
| )} | |
| </div> | |
| <Button variant="outline" size="sm" className="h-9 gap-1.5" onClick={() => fileRef.current?.click()}> | |
| <Camera className="h-4 w-4" /> Аватар | |
| </Button> | |
| </div> | |
| <div className="space-y-3"> | |
| <div className="grid gap-2 sm:grid-cols-2"> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="pf-name">Отображаемое имя</Label> | |
| <Input id="pf-name" value={displayName} onChange={(e) => setDisplayName(e.target.value)} /> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <Label>Логин</Label> | |
| <Input value={user.username} readOnly className="text-muted-foreground" /> | |
| </div> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="pf-about">О себе</Label> | |
| <Textarea | |
| id="pf-about" | |
| rows={3} | |
| value={about} | |
| placeholder="Например: плету салфетки и мандалы" | |
| onChange={(e) => setAbout(e.target.value)} | |
| /> | |
| </div> | |
| <dl className="grid grid-cols-2 gap-2 text-xs sm:grid-cols-4"> | |
| {[ | |
| { k: "Роль", v: roleLabel(user.role) }, | |
| { k: "Статус", v: statusLabel(user.status) }, | |
| { k: "Создан", v: formatStamp(user.createdAt) }, | |
| { k: "Схем", v: String(items.length) }, | |
| ].map((s) => ( | |
| <div key={s.k} className="rounded-lg border border-border bg-secondary px-2 py-1.5"> | |
| <dt className="text-muted-foreground">{s.k}</dt> | |
| <dd className="truncate font-medium">{s.v}</dd> | |
| </div> | |
| ))} | |
| </dl> | |
| <div className="flex items-center gap-2"> | |
| <Button size="sm" className="h-10 gap-1.5" onClick={save}> | |
| <Save className="h-4 w-4" /> Сохранить профиль | |
| </Button> | |
| {savedAt > 0 && <span className="text-xs text-muted-foreground">Изменения сохранены</span>} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="border-t border-border pt-3"> | |
| <h3 className="font-display text-lg">Мои работы</h3> | |
| {items.length === 0 ? ( | |
| <p className="py-6 text-center text-sm text-muted-foreground">Пока нет сохранённых схем.</p> | |
| ) : ( | |
| <div className="mt-2 grid gap-3 sm:grid-cols-3"> | |
| {items.map((p) => ( | |
| <article key={p.id} className="overflow-hidden rounded-xl border border-border bg-card"> | |
| <img | |
| src={p.thumb} | |
| alt={`Превью схемы «${p.title}»`} | |
| className="aspect-[4/3] w-full bg-secondary object-cover" | |
| /> | |
| <div className="space-y-1.5 p-2"> | |
| <h4 className="truncate text-sm font-medium">{p.title}</h4> | |
| <p className="text-[11px] text-muted-foreground">{formatStamp(p.updatedAt)}</p> | |
| <Button | |
| size="sm" | |
| className="h-8 w-full gap-1.5" | |
| onClick={() => { | |
| onOpenProject(p); | |
| onOpenChange(false); | |
| }} | |
| > | |
| <FolderOpen className="h-4 w-4" /> Открыть | |
| </Button> | |
| </div> | |
| </article> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } | |