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