98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import { useState } from 'react';
|
|||
|
|
|
||
|
|
import { Button } from '@/shared/components/Button';
|
||
|
|
import { Modal } from '@/shared/components/Modal';
|
||
|
|
import { useLogout } from '../hooks/useLogout';
|
||
|
|
import { useAuthStore } from '../store/authStore';
|
||
|
|
|
||
|
|
// ponytail: Gravatar's default-image endpoint seeds off whatever string sits in the
|
||
|
|
// path — no real MD5 needed for a mocked identicon. Swap in a proper md5(email) hash
|
||
|
|
// when real Gravatar photos (not just the generated identicon) are wanted.
|
||
|
|
function gravatarUrl(email: string, size: number) {
|
||
|
|
return `https://www.gravatar.com/avatar/${encodeURIComponent(email.trim().toLowerCase())}?d=identicon&s=${size}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
const PLACEHOLDER = 'Not provided';
|
||
|
|
|
||
|
|
// ponytail: hardcoded until learner/progress feature exposes a real API; swap then.
|
||
|
|
const MOCK_PROGRESS = { level: 4, xp: 320, streak: 7 };
|
||
|
|
|
||
|
|
export function UserMenu() {
|
||
|
|
const user = useAuthStore((s) => s.user);
|
||
|
|
const logout = useLogout();
|
||
|
|
const [open, setOpen] = useState(false);
|
||
|
|
|
||
|
|
if (!user) return null;
|
||
|
|
|
||
|
|
const fields = [
|
||
|
|
{ label: 'Full name', value: user.name },
|
||
|
|
{ label: 'Date of birth', value: PLACEHOLDER },
|
||
|
|
{ label: 'Email', value: user.email },
|
||
|
|
{ label: 'Phone', value: PLACEHOLDER },
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<button type="button" onClick={() => setOpen(true)} className="flex items-center gap-1.5">
|
||
|
|
<span className="flex h-10 items-center justify-center rounded-full bg-accent px-3 text-xs font-extrabold text-ink-inverse">
|
||
|
|
🔥{MOCK_PROGRESS.streak}
|
||
|
|
</span>
|
||
|
|
<span className="flex h-10 items-center justify-center rounded-full bg-secondary px-3 text-xs font-extrabold text-ink">
|
||
|
|
⭐{MOCK_PROGRESS.xp}
|
||
|
|
</span>
|
||
|
|
<div className="relative h-10 w-10">
|
||
|
|
<img
|
||
|
|
src={gravatarUrl(user.email, 80)}
|
||
|
|
alt=""
|
||
|
|
className="h-10 w-10 rounded-lg border border-border"
|
||
|
|
/>
|
||
|
|
<span className="absolute -bottom-1 -right-1 inline-flex items-center rounded-full bg-primary px-1.5 py-0.5 text-xs font-extrabold text-ink-inverse">
|
||
|
|
{MOCK_PROGRESS.level}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<Modal open={open} onClose={() => setOpen(false)} title="User profile">
|
||
|
|
<div className="relative flex flex-col items-center gap-4 px-6 py-8">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setOpen(false)}
|
||
|
|
aria-label="Close"
|
||
|
|
className="absolute top-3 right-3 rounded-full p-1.5 text-ink-muted hover:bg-surface-soft hover:text-ink"
|
||
|
|
>
|
||
|
|
✕
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<img
|
||
|
|
src={gravatarUrl(user.email, 160)}
|
||
|
|
alt=""
|
||
|
|
className="h-20 w-20 rounded-full border-4 border-primary-light"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<dl className="w-full space-y-3 text-sm">
|
||
|
|
{fields.map((f) => (
|
||
|
|
<div key={f.label} className="flex items-center justify-between gap-4 border-b border-border pb-2">
|
||
|
|
<dt className="text-ink-secondary">{f.label}</dt>
|
||
|
|
<dd className="truncate font-semibold text-ink">{f.value}</dd>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</dl>
|
||
|
|
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => {
|
||
|
|
setOpen(false);
|
||
|
|
logout.mutate();
|
||
|
|
}}
|
||
|
|
disabled={logout.isPending}
|
||
|
|
className="min-h-11 w-full text-sm"
|
||
|
|
>
|
||
|
|
{logout.isPending ? 'Signing out…' : 'Sign out'}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</Modal>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|