feat: add mock page
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user