2026-04-04 23:11:51 -05:00
|
|
|
import { useEffect, useState } from 'react';
|
2026-04-09 23:10:23 -05:00
|
|
|
import { Loader2, Check, AlertTriangle } from 'lucide-react';
|
2026-04-04 23:11:51 -05:00
|
|
|
import { Button } from './ui/button';
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
|
|
|
|
import { Input } from './ui/input';
|
|
|
|
|
import { Label } from './ui/label';
|
2026-04-10 01:38:25 -05:00
|
|
|
import { useSession, signOut, updateUser } from '../lib/auth-client';
|
2026-04-04 23:11:51 -05:00
|
|
|
import { validatePhoneOrEmail } from '../lib/validators';
|
|
|
|
|
|
|
|
|
|
export function ProfileSettings() {
|
2026-04-10 21:22:31 -05:00
|
|
|
const { data: session, refetch: refetchSession } = useSession();
|
2026-04-10 01:38:25 -05:00
|
|
|
const [userName, setUserName] = useState('');
|
|
|
|
|
const [savingName, setSavingName] = useState(false);
|
|
|
|
|
const [nameSaved, setNameSaved] = useState(false);
|
|
|
|
|
const [nameError, setNameError] = useState('');
|
2026-04-04 23:11:51 -05:00
|
|
|
const [piFirstName, setPiFirstName] = useState('');
|
|
|
|
|
const [bldgCode, setBldgCode] = useState('');
|
|
|
|
|
const [lab, setLab] = useState('');
|
|
|
|
|
const [contact, setContact] = useState('');
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
const [saved, setSaved] = useState(false);
|
2026-04-09 23:10:23 -05:00
|
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
|
|
|
const [deleting, setDeleting] = useState(false);
|
|
|
|
|
const [deleteError, setDeleteError] = useState('');
|
2026-04-04 23:11:51 -05:00
|
|
|
|
2026-04-10 01:38:25 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (session?.user.name) setUserName(session.user.name);
|
|
|
|
|
}, [session?.user.name]);
|
|
|
|
|
|
2026-04-04 23:11:51 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
fetch('/api/profile', { credentials: 'include' })
|
|
|
|
|
.then(r => (r.ok ? r.json() : null))
|
|
|
|
|
.then(data => {
|
|
|
|
|
if (data) {
|
|
|
|
|
setPiFirstName(data.pi_first_name || '');
|
|
|
|
|
setBldgCode(data.bldg_code || '');
|
|
|
|
|
setLab(data.lab || '');
|
|
|
|
|
setContact(data.contact || '');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.finally(() => setLoading(false));
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-04-10 01:38:25 -05:00
|
|
|
async function handleSaveName() {
|
|
|
|
|
setNameError('');
|
|
|
|
|
setNameSaved(false);
|
|
|
|
|
const trimmed = userName.trim();
|
|
|
|
|
setSavingName(true);
|
2026-04-10 21:22:31 -05:00
|
|
|
const res = await fetch('/api/account/name', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
body: JSON.stringify({ name: trimmed || null }),
|
|
|
|
|
});
|
2026-04-10 01:38:25 -05:00
|
|
|
setSavingName(false);
|
2026-04-10 21:22:31 -05:00
|
|
|
if (!res.ok) {
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
setNameError(data.error || 'Failed to save name.');
|
2026-04-10 01:38:25 -05:00
|
|
|
} else {
|
2026-04-10 21:22:31 -05:00
|
|
|
await refetchSession();
|
2026-04-10 01:38:25 -05:00
|
|
|
setNameSaved(true);
|
|
|
|
|
setTimeout(() => setNameSaved(false), 3000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 23:11:51 -05:00
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setError('');
|
|
|
|
|
setSaved(false);
|
|
|
|
|
|
|
|
|
|
if (contact.trim() && !validatePhoneOrEmail(contact.trim())) {
|
|
|
|
|
setError('Contact must be a valid phone number or email address.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSaving(true);
|
|
|
|
|
const res = await fetch('/api/profile', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
pi_first_name: piFirstName.trim(),
|
|
|
|
|
bldg_code: bldgCode.trim(),
|
|
|
|
|
lab: lab.trim(),
|
|
|
|
|
contact: contact.trim() || undefined,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
setSaving(false);
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
setSaved(true);
|
|
|
|
|
setTimeout(() => setSaved(false), 3000);
|
|
|
|
|
} else {
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
setError(data.error || 'Failed to save profile.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full items-center justify-center">
|
|
|
|
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-8 max-w-2xl mx-auto">
|
|
|
|
|
<div className="mb-6">
|
|
|
|
|
<h1 className="text-2xl font-semibold">Profile settings</h1>
|
|
|
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
|
|
|
Update your account details and lab defaults.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base">Account</CardTitle>
|
|
|
|
|
</CardHeader>
|
2026-04-10 01:38:25 -05:00
|
|
|
<CardContent className="space-y-4 pb-6">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="name">Name</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="name"
|
|
|
|
|
value={userName}
|
|
|
|
|
onChange={e => setUserName(e.target.value)}
|
|
|
|
|
placeholder="Your name"
|
|
|
|
|
/>
|
2026-04-04 23:11:51 -05:00
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="text-xs text-muted-foreground">Email</Label>
|
|
|
|
|
<p className="text-sm">{session?.user.email || '—'}</p>
|
|
|
|
|
</div>
|
2026-04-10 01:38:25 -05:00
|
|
|
{nameError && <p className="text-sm text-red-600">{nameError}</p>}
|
|
|
|
|
{nameSaved && (
|
|
|
|
|
<p className="text-sm text-green-600 flex items-center gap-1">
|
|
|
|
|
<Check className="w-4 h-4" /> Name saved
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="pt-1">
|
|
|
|
|
<Button type="button" onClick={handleSaveName} disabled={savingName}>
|
|
|
|
|
{savingName ? 'Saving…' : 'Save name'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-04-04 23:11:51 -05:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
|
|
|
<Card className="mt-4">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base">Lab defaults</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4 pb-6">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="pi">
|
2026-04-10 20:30:08 -05:00
|
|
|
PI first name <span className="text-muted-foreground font-normal">(optional)</span>
|
2026-04-04 23:11:51 -05:00
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="pi"
|
|
|
|
|
value={piFirstName}
|
|
|
|
|
onChange={e => setPiFirstName(e.target.value)}
|
|
|
|
|
placeholder="e.g. Smith"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="bldg">
|
2026-04-10 20:30:08 -05:00
|
|
|
Building code <span className="text-muted-foreground font-normal">(optional)</span>
|
2026-04-04 23:11:51 -05:00
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="bldg"
|
|
|
|
|
value={bldgCode}
|
|
|
|
|
onChange={e => setBldgCode(e.target.value)}
|
|
|
|
|
placeholder="e.g. EER"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="lab">
|
2026-04-10 20:30:08 -05:00
|
|
|
Lab <span className="text-muted-foreground font-normal">(optional)</span>
|
2026-04-04 23:11:51 -05:00
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="lab"
|
|
|
|
|
value={lab}
|
|
|
|
|
onChange={e => setLab(e.target.value)}
|
|
|
|
|
placeholder="e.g. 3.822"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="contact">
|
|
|
|
|
Contact{' '}
|
|
|
|
|
<span className="text-muted-foreground font-normal">(optional)</span>
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="contact"
|
|
|
|
|
type="text"
|
|
|
|
|
value={contact}
|
|
|
|
|
onChange={e => setContact(e.target.value)}
|
|
|
|
|
placeholder="Phone (e.g. 555-123-4567) or email"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
|
|
|
{saved && (
|
|
|
|
|
<p className="text-sm text-green-600 flex items-center gap-1">
|
|
|
|
|
<Check className="w-4 h-4" /> Saved
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="pt-1">
|
|
|
|
|
<Button type="submit" disabled={saving}>
|
|
|
|
|
{saving ? 'Saving…' : 'Save changes'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</form>
|
2026-04-09 14:20:18 -05:00
|
|
|
|
2026-04-09 23:10:23 -05:00
|
|
|
<Card className="mt-4 border-red-200">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base text-red-600 flex items-center gap-2">
|
|
|
|
|
<AlertTriangle className="w-4 h-4" /> Delete account
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="pb-6">
|
|
|
|
|
{!showDeleteConfirm ? (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-sm text-muted-foreground mb-3">
|
|
|
|
|
Permanently delete your account and all associated data including
|
|
|
|
|
chemicals and protocols. This action cannot be undone.
|
|
|
|
|
</p>
|
|
|
|
|
<Button
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={() => setShowDeleteConfirm(true)}
|
|
|
|
|
>
|
|
|
|
|
Delete account
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-sm text-red-600 font-medium mb-3">
|
|
|
|
|
Are you sure? All your data will be permanently deleted.
|
|
|
|
|
</p>
|
|
|
|
|
{deleteError && (
|
|
|
|
|
<p className="text-sm text-red-600 mb-3">{deleteError}</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="destructive"
|
|
|
|
|
disabled={deleting}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
setDeleting(true);
|
|
|
|
|
setDeleteError('');
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/account/delete', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
setDeleteError(data.error || 'Failed to delete account.');
|
|
|
|
|
setDeleting(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await signOut();
|
|
|
|
|
window.location.href = '/';
|
|
|
|
|
} catch {
|
|
|
|
|
setDeleteError('Failed to delete account.');
|
|
|
|
|
setDeleting(false);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{deleting ? 'Deleting...' : 'Yes, delete my account'}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
disabled={deleting}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowDeleteConfirm(false);
|
|
|
|
|
setDeleteError('');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-04-09 14:20:18 -05:00
|
|
|
<p className="text-center mt-6">
|
|
|
|
|
<a href="/privacy" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
|
|
|
|
Privacy Policy
|
|
|
|
|
</a>
|
|
|
|
|
</p>
|
2026-04-04 23:11:51 -05:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|