wacrm/src/components/settings/password-form.tsx
Arnas Donauskas a45cbe6822
feat(settings): redesign Settings into a grouped rail + Overview landing
Replace the flat 8-tab Settings page with a cleaner information
architecture: a grouped left rail (Account / Workspace) plus a new
Overview landing with live status tiles. Related concerns are merged —
"Login & security" (password + sessions) and "Fields & tags" (tags +
custom fields).

- New shell: settings-sections (section model + legacy ?tab= mapping,
  tags/custom-fields -> fields), settings-rail, settings-overview
  (live counts + identity), security-panel, fields-and-tags-panel,
  settings-panel-head, settings-chip, role-meta (shared role chip
  metadata, de-duped from members-tab).
- Restyle the light panels to the mockup; reframe the heavy panels
  (WhatsApp, Templates) with the shared panel header, internals intact.
- Tokens: add --card-2 (light+dark) and @theme mappings for
  primary-hover/soft/soft-2 so the redesign stays fully tokenized.
- ?tab= remains the URL source of truth, keeping existing sidebar/header
  deep-links working.

Overview decouples the slow WhatsApp Meta health-ping from the cheap
count queries so a flaky ping never blanks the whole landing; template
counts use head-only count queries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:08:33 +03:00

172 lines
5.1 KiB
TypeScript

'use client';
import { useState } from 'react';
import { toast } from 'sonner';
import { Loader2, KeyRound } from 'lucide-react';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from '@/components/ui/card';
const MIN_PASSWORD = 8;
export function PasswordForm() {
const { profile } = useAuth();
const supabase = createClient();
const [current, setCurrent] = useState('');
const [next, setNext] = useState('');
const [confirm, setConfirm] = useState('');
const [saving, setSaving] = useState(false);
const [confirmError, setConfirmError] = useState<string | null>(null);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!profile?.email) {
toast.error('Cannot change password without a current email');
return;
}
if (next.length < MIN_PASSWORD) {
setConfirmError(`Password must be at least ${MIN_PASSWORD} characters`);
return;
}
if (next !== confirm) {
setConfirmError('New password and confirmation do not match');
return;
}
setConfirmError(null);
setSaving(true);
try {
// Supabase doesn't expose a "verify password without issuing a
// session" API, so we re-authenticate with the provided current
// password. If it matches, the session refreshes silently; if it
// doesn't, we abort before calling updateUser.
const { error: signInError } = await supabase.auth.signInWithPassword({
email: profile.email,
password: current,
});
if (signInError) {
toast.error('Current password is incorrect');
return;
}
const { error: updateError } = await supabase.auth.updateUser({
password: next,
});
if (updateError) {
toast.error(`Password update failed: ${updateError.message}`);
return;
}
setCurrent('');
setNext('');
setConfirm('');
toast.success('Password updated');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Unknown error';
toast.error(msg);
} finally {
setSaving(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-foreground">
<KeyRound className="size-4 text-primary" />
Password
</CardTitle>
<CardDescription className="text-muted-foreground">
Use at least {MIN_PASSWORD} characters. You will stay signed in on
this device after changing it.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={onSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="current-password" className="text-foreground">
Current password
</Label>
<Input
id="current-password"
type="password"
value={current}
onChange={(e) => setCurrent(e.target.value)}
autoComplete="current-password"
disabled={saving}
required
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="new-password" className="text-foreground">
New password
</Label>
<Input
id="new-password"
type="password"
value={next}
onChange={(e) => setNext(e.target.value)}
autoComplete="new-password"
minLength={MIN_PASSWORD}
disabled={saving}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password" className="text-foreground">
Confirm new password
</Label>
<Input
id="confirm-password"
type="password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
autoComplete="new-password"
minLength={MIN_PASSWORD}
disabled={saving}
required
/>
</div>
</div>
{confirmError && (
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
{confirmError}
</p>
)}
<div className="flex justify-end">
<Button
type="submit"
disabled={saving || !current || !next || !confirm}
>
{saving ? (
<>
<Loader2 className="size-4 animate-spin" />
Updating…
</>
) : (
'Update password'
)}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}