wacrm/src/components/settings/password-form.tsx
Arnas Donauskas 32022c13fe
feat(theme): sweep remaining violet-* → primary across all surfaces
PR #132 (color themes) only tokenized the high-visibility chrome.
Inbox / contacts / pipelines / broadcasts / automations / auth /
settings sub-forms still had ~150 hard-coded `violet-*` classes
across ~32 files, so picking a non-violet theme felt half-broken —
sidebar and header changed colour, but message bubbles, deal cards,
and the broadcast wizard all stayed violet.

This is the mechanical follow-up: replace every `violet-N(/N)?`
with `primary(/N)?` so the theme picker themes the whole app.

Two non-trivial swaps inside the pass:
- `text-white` on a `bg-primary` button → `text-primary-foreground`.
  Emerald and Amber define a *dark* `--primary-foreground` because
  their primaries are bright; `text-white` over those would tank
  contrast.
- `bg-primary hover:bg-primary` (hover identical to base — original
  code had this latent bug from `hover:bg-violet-500`) → adds `/90`
  on hover so the lift is visible.

Test update:
- `broadcast-status.test.ts` asserted shade numbers (`bg-X-N/10`).
  Relaxed the regex to also accept token form (`bg-X/10`) since
  brand-accent status badges now ride the active theme.

Files touched: 49. Lint clean (existing warnings only), 173/173
tests, typecheck + build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:00:32 +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 className="bg-slate-900/40 border-slate-800">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-white">
<KeyRound className="size-4 text-primary" />
Password
</CardTitle>
<CardDescription className="text-slate-400">
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-slate-200">
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-slate-200">
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-slate-200">
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-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-300">
{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>
);
}