Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/(account)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Avatar } from '@/components/ui/Avatar';
import { Skeleton } from '@/components/ui/Skeleton';
import { CopyButton } from '@/components/ui/CopyButton';
import { User, Shield, Grid, Activity, AlertCircle, ArrowRight, Trash2 } from 'lucide-react';
import { toast } from 'sonner';

Expand Down Expand Up @@ -139,6 +140,25 @@ export default function OverviewPage() {
</div>
)}

{/* SAMKIEL ID */}
<section>
<Card className="bg-surface/70">
<div className="flex items-start justify-between gap-4">
<div className="space-y-2 min-w-0">
<p className="text-xs uppercase tracking-wider text-muted font-medium">Your SAMKIEL ID</p>
<div className="flex items-center gap-3 flex-wrap">
<span className="font-mono text-2xl sm:text-3xl font-bold text-accent tracking-wider">
{user?.samkielId ?? '—'}
</span>
{user?.samkielId && <CopyButton value={user.samkielId} label="Copy SAMKIEL ID" />}
</div>
<p className="text-sm text-muted">This is your permanent identifier. It never changes.</p>
{user?.username && <p className="text-sm text-white font-medium">@{user.username}</p>}
</div>
</div>
</Card>
</section>

{/* Summary grid */}
<section className="space-y-5">
<div className="space-y-1">
Expand Down
114 changes: 112 additions & 2 deletions app/(account)/personal-info/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import React, { useRef, useState } from 'react';
import { useAuth } from '@samkiel/authsdk/react';
import { SamkielAuthError } from '@samkiel/authsdk';
import { SamkielAuthError, UsernameChangeCooldownError } from '@samkiel/authsdk';
import { Card } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Avatar } from '@/components/ui/Avatar';
import { Badge } from '@/components/ui/Badge';
import { Skeleton } from '@/components/ui/Skeleton';
import { CopyButton } from '@/components/ui/CopyButton';
import { Camera, Loader2 } from 'lucide-react';
import { toast } from 'sonner';

Expand All @@ -21,12 +22,25 @@ function formatJoinDate(iso?: string) {
}
}

function formatFullDate(iso?: string) {
if (!iso) return 'a later date';
try {
return new Date(iso).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
} catch {
return 'a later date';
}
}

export default function PersonalInfoPage() {
const { user, isLoading: authLoading, updateName, updateEmail, uploadAvatar } = useAuth();
const { user, isLoading: authLoading, updateName, updateEmail, uploadAvatar, changeUsername } = useAuth();

const [name, setName] = useState(user?.name ?? '');
const [isSavingName, setIsSavingName] = useState(false);

const [editingUsername, setEditingUsername] = useState(false);
const [usernameInput, setUsernameInput] = useState('');
const [isSavingUsername, setIsSavingUsername] = useState(false);

const [newEmail, setNewEmail] = useState('');
const [currentPassword, setCurrentPassword] = useState('');
const [isSavingEmail, setIsSavingEmail] = useState(false);
Expand Down Expand Up @@ -74,6 +88,36 @@ export default function PersonalInfoPage() {
}
};

const startEditUsername = () => {
setUsernameInput(user?.username ?? '');
setEditingUsername(true);
};

const handleChangeUsername = async (e: React.FormEvent) => {
e.preventDefault();
const trimmed = usernameInput.trim().toLowerCase();
if (!trimmed || trimmed === user?.username) {
setEditingUsername(false);
return;
}

setIsSavingUsername(true);
try {
await changeUsername(trimmed);
toast.success('Username updated.');
setEditingUsername(false);
} catch (err) {
if (err instanceof UsernameChangeCooldownError) {
toast.error(`You can next change your username on ${formatFullDate(err.nextChangeAt)}.`);
} else {
const msg = err instanceof Error ? err.message : 'Could not update your username.';
toast.error(msg);
}
} finally {
setIsSavingUsername(false);
}
};

const handleUpdateEmail = async (e: React.FormEvent) => {
e.preventDefault();
if (!newEmail || !currentPassword) return;
Expand Down Expand Up @@ -205,6 +249,72 @@ export default function PersonalInfoPage() {
</form>
</Card>

{/* Username */}
<Card className="space-y-5">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-white font-syne">Username</h2>
<p className="text-sm text-muted mt-0.5">
Your unique handle across SAMKIEL. Changeable once every 23 days.
</p>
</div>
{!editingUsername && (
<Button variant="outline" size="sm" className="shrink-0" onClick={startEditUsername}>
Change Username
</Button>
)}
</div>
{editingUsername ? (
<form onSubmit={handleChangeUsername} className="space-y-5 max-w-md">
<Input
label="New username"
value={usernameInput}
onChange={(e) => setUsernameInput(e.target.value.toLowerCase())}
placeholder="ezekiel_dev"
required
disabled={isSavingUsername}
hint="3-20 characters. Letters, numbers, and underscores only."
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
/>
<div className="flex items-center gap-3">
<Button
type="submit"
isLoading={isSavingUsername}
disabled={!usernameInput.trim() || usernameInput.trim() === user?.username}
>
Save username
</Button>
<Button
type="button"
variant="ghost"
onClick={() => setEditingUsername(false)}
disabled={isSavingUsername}
>
Cancel
</Button>
</div>
</form>
) : (
<p className="text-xl font-semibold text-white">@{user?.username ?? '—'}</p>
)}
</Card>

{/* SAMKIEL ID */}
<Card className="space-y-3">
<div>
<h2 className="text-lg font-semibold text-white font-syne">SAMKIEL ID</h2>
<p className="text-sm text-muted mt-0.5">Cannot be changed.</p>
</div>
<div className="flex items-center gap-3">
<span className="font-mono text-lg font-semibold text-accent tracking-wider">
{user?.samkielId ?? '—'}
</span>
{user?.samkielId && <CopyButton value={user.samkielId} label="Copy SAMKIEL ID" />}
</div>
</Card>

<Card className="space-y-5">
<div>
<h2 className="text-lg font-semibold text-white font-syne">Email address</h2>
Expand Down
9 changes: 6 additions & 3 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,16 @@ function LoginContent() {
<Card className="p-8 bg-surface/50 backdrop-blur-xl border-border shadow-2xl">
<form onSubmit={handleSubmit} className="space-y-6">
<Input
label="Email Address"
type="email"
placeholder="name@example.com"
label="Email, Username, or SAMKIEL ID"
type="text"
placeholder="e.g. ezekiel, SKL-A4X9K2, or you@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isLoading}
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
className="bg-background/50"
/>
<div className="space-y-1.5">
Expand Down
Loading
Loading