chore : move all to root

This commit is contained in:
Thibault Pouch
2026-02-26 16:16:44 +01:00
parent 308a758e79
commit c2d94a349c
44 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
import { useState, useCallback } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { MOCK_THREADS, MOCK_BUGS } from '../../data/mockData';
import { formatDate } from '../../utils/format';
import { Link } from 'react-router-dom';
type Tab = 'profile' | 'threads' | 'bugs' | 'password';
export default function AccountPage() {
const { user, updateUsername } = useAuth();
const [activeTab, setActiveTab] = useState<Tab>('profile');
const userThreads = MOCK_THREADS.filter((t) => t.authorId === user?.id);
const userBugs = MOCK_BUGS.filter((b) => b.submittedById === user?.id);
const tabs: { id: Tab; label: string }[] = [
{ id: 'profile', label: 'Profile' },
{ id: 'threads', label: `Threads (${userThreads.length})` },
{ id: 'bugs', label: `Bug Reports (${userBugs.length})` },
{ id: 'password', label: 'Change Password' },
];
if (!user) return null;
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '4rem 1.5rem' }}>
<div className="section-label">My Account</div>
<h1 style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-text)', fontSize: 'clamp(2rem, 5vw, 3rem)', marginTop: '0.5rem', marginBottom: '2rem' }}>
{user.username}
</h1>
{/* Tabs */}
<div style={{ display: 'flex', borderBottom: '1px solid var(--color-border)', marginBottom: '2rem', gap: '0', flexWrap: 'wrap' }}>
{tabs.map(({ id, label }) => (
<button
key={id}
onClick={() => setActiveTab(id)}
style={{
background: 'transparent',
border: 'none',
borderBottom: activeTab === id ? '2px solid var(--color-green)' : '2px solid transparent',
color: activeTab === id ? 'var(--color-green)' : 'var(--color-text-muted)',
fontFamily: 'var(--font-mono)',
fontSize: '0.78rem',
padding: '0.6rem 1rem',
cursor: 'pointer',
letterSpacing: '0.05em',
textTransform: 'uppercase',
transition: 'all 0.2s',
}}
>
{label}
</button>
))}
</div>
{/* Profile Tab */}
{activeTab === 'profile' && (
<ProfileTab user={user} updateUsername={updateUsername} />
)}
{/* Threads Tab */}
{activeTab === 'threads' && (
<div>
{userThreads.length === 0 ? (
<div className="crt-box" style={{ padding: '2rem', textAlign: 'center', color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)' }}>
You haven't posted any threads yet.{' '}
<Link to="/forum" style={{ color: 'var(--color-green)' }}>Go to Forum</Link>
</div>
) : (
userThreads.map((t) => (
<div key={t.id} className="crt-box" style={{ padding: '1rem 1.25rem', marginBottom: '0.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem', flexWrap: 'wrap' }}>
<Link to={`/forum/thread/${t.id}`} style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text)', fontSize: '0.87rem' }}>
{t.title}
</Link>
<span style={{ color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', flexShrink: 0 }}>
{formatDate(t.createdAt)}
</span>
</div>
<div style={{ color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', marginTop: '0.25rem' }}>
{t.categoryName} &mdash; {t.replyCount} replies
</div>
</div>
))
)}
</div>
)}
{/* Bug Reports Tab */}
{activeTab === 'bugs' && (
<div>
{userBugs.length === 0 ? (
<div className="crt-box" style={{ padding: '2rem', textAlign: 'center', color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)' }}>
You haven't submitted any bug reports.{' '}
<Link to="/bugs" style={{ color: 'var(--color-green)' }}>Report a Bug</Link>
</div>
) : (
userBugs.map((b) => (
<div key={b.id} className="crt-box" style={{ padding: '1rem 1.25rem', marginBottom: '0.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem', flexWrap: 'wrap', marginBottom: '0.25rem' }}>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text)', fontSize: '0.87rem' }}>{b.title}</span>
<span style={{ color: 'var(--color-text-muted)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', flexShrink: 0 }}>{formatDate(b.createdAt)}</span>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.7rem' }}>{b.uniqueCode}</span>
<span className={`badge badge-${b.status === 'in_progress' ? 'progress' : b.status}`}>{b.status}</span>
<span className={`badge badge-${b.severity}`}>{b.severity}</span>
</div>
</div>
))
)}
</div>
)}
{/* Password Tab */}
{activeTab === 'password' && <ChangePasswordForm />}
</div>
);
}
// ── Profile Tab ────────────────────────────────────────────────────────────────
function ProfileTab({ user, updateUsername }: { user: NonNullable<ReturnType<typeof useAuth>['user']>; updateUsername: (u: string) => void }) {
const [editing, setEditing] = useState(false);
const [username, setUsername] = useState(user.username);
const [error, setError] = useState('');
const [saved, setSaved] = useState(false);
const handleSave = useCallback(() => {
if (!username.trim()) { setError('Username cannot be empty.'); return; }
if (username.length < 3) { setError('Must be at least 3 characters.'); return; }
updateUsername(username.trim());
setEditing(false);
setSaved(true);
setTimeout(() => setSaved(false), 3000);
}, [username, updateUsername]);
return (
<div className="crt-box" style={{ padding: '2rem' }}>
{saved && (
<div style={{ color: 'var(--color-green)', fontFamily: 'var(--font-mono)', fontSize: '0.78rem', marginBottom: '1rem', background: 'rgba(0,255,65,0.07)', border: '1px solid rgba(0,255,65,0.2)', padding: '0.6rem 0.75rem' }}>
[OK] Username updated successfully.
</div>
)}
<div style={{ display: 'grid', gap: '1rem' }}>
{/* Username */}
<div style={{ display: 'grid', gridTemplateColumns: '140px 1fr', gap: '0.5rem', alignItems: 'center' }}>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.75rem', letterSpacing: '0.1em' }}>USERNAME</span>
{editing ? (
<div style={{ display: 'flex', gap: '0.5rem' }}>
<input
className={`input-terminal${error ? ' error' : ''}`}
value={username}
onChange={(e) => { setUsername(e.target.value); setError(''); }}
style={{ flex: 1 }}
autoFocus
/>
<button className="btn-terminal" onClick={handleSave} style={{ padding: '0.35rem 0.75rem', fontSize: '0.75rem' }}>Save</button>
<button className="btn-terminal btn-danger" onClick={() => { setEditing(false); setUsername(user.username); setError(''); }} style={{ padding: '0.35rem 0.75rem', fontSize: '0.75rem' }}>Cancel</button>
</div>
) : (
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text)', fontSize: '0.87rem' }}>{user.username}</span>
<button
className="btn-terminal"
onClick={() => setEditing(true)}
style={{ padding: '0.2rem 0.6rem', fontSize: '0.65rem' }}
>
Edit
</button>
</div>
)}
</div>
{error && <div style={{ color: 'var(--color-red)', fontSize: '0.72rem', gridColumn: '2' }}>{error}</div>}
{/* Static fields */}
{[
{ label: 'EMAIL', value: user.email },
{ label: 'ROLE', value: user.role.toUpperCase() },
{ label: 'MEMBER SINCE', value: formatDate(user.createdAt) },
{ label: 'ADMIN', value: user.isAdmin ? 'Yes' : 'No' },
].map(({ label, value }) => (
<div key={label} style={{ display: 'grid', gridTemplateColumns: '140px 1fr', gap: '0.5rem', alignItems: 'center' }}>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.75rem', letterSpacing: '0.1em' }}>{label}</span>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-dim)', fontSize: '0.87rem' }}>{value}</span>
</div>
))}
</div>
</div>
);
}
// ── Change Password Form ───────────────────────────────────────────────────────
function ChangePasswordForm() {
const [form, setForm] = useState({ current: '', next: '', confirm: '' });
const [errors, setErrors] = useState<Partial<typeof form & { success: string }>>({});
const [loading, setLoading] = useState(false);
const handleSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault();
const next: typeof errors = {};
if (!form.current) next.current = 'Current password required.';
if (!form.next) next.next = 'New password required.';
else if (form.next.length < 8) next.next = 'Must be at least 8 characters.';
if (form.next !== form.confirm) next.confirm = 'Passwords do not match.';
setErrors(next);
if (Object.keys(next).length > 0) return;
setLoading(true);
await new Promise((r) => setTimeout(r, 400));
setLoading(false);
setForm({ current: '', next: '', confirm: '' });
setErrors({ success: 'Password changed successfully.' });
}, [form]);
return (
<form onSubmit={handleSubmit} noValidate className="crt-box" style={{ padding: '2rem' }}>
{errors.success && (
<div style={{ color: 'var(--color-green)', fontFamily: 'var(--font-mono)', fontSize: '0.78rem', marginBottom: '1.25rem', background: 'rgba(0,255,65,0.07)', border: '1px solid rgba(0,255,65,0.2)', padding: '0.6rem 0.75rem' }}>
[OK] {errors.success}
</div>
)}
{[
{ key: 'current' as const, label: 'Current Password', auto: 'current-password' },
{ key: 'next' as const, label: 'New Password', auto: 'new-password' },
{ key: 'confirm' as const, label: 'Confirm New Password', auto: 'new-password' },
].map(({ key, label, auto }) => (
<div key={key} style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.75rem', marginBottom: '0.4rem' }}>{label}</label>
<input
className={`input-terminal${errors[key] ? ' error' : ''}`}
type="password"
autoComplete={auto}
value={form[key]}
onChange={(e) => { setForm((p) => ({ ...p, [key]: e.target.value })); setErrors((p) => ({ ...p, [key]: undefined })); }}
/>
{errors[key] && <div style={{ color: 'var(--color-red)', fontSize: '0.72rem', marginTop: '0.25rem' }}>{errors[key]}</div>}
</div>
))}
<button type="submit" className="btn-terminal" disabled={loading} style={{ marginTop: '0.5rem', opacity: loading ? 0.7 : 1 }}>
{loading ? 'Saving...' : '> Update Password'}
</button>
</form>
);
}