mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
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>
172 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|