feat(forms): country→timezone autoset, "Other" channel hint, polish

Client form: when nationality is picked and timezone empty, primary
IANA zone for the country is pre-filled (skips when user has chosen
a zone explicitly). When a contact's preferred channel is `'other'`,
the inline `Label` field flips to "Specify" / "e.g. Telegram, Signal"
so the rep records what the channel actually is.

Yacht form: replace the free-text 2-letter flag input with the shared
`CountryCombobox` so flags stay valid ISO codes.

User settings: timezone pre-populates from
`Intl.DateTimeFormat().resolvedOptions().timeZone` on first load
(was empty before); country change auto-fills timezone with the same
helper as the client form. Phone field upgraded to the shared
`<PhoneInput>` (country-flag dropdown + AsYouType formatter) seeded
from the page's country state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 04:10:47 +02:00
parent 3c47f6b7f9
commit 82fd75081a
3 changed files with 45 additions and 9 deletions

View File

@@ -11,6 +11,7 @@ import { Switch } from '@/components/ui/switch';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { PageHeader } from '@/components/shared/page-header';
import { CountryCombobox } from '@/components/shared/country-combobox';
import { PhoneInput, type PhoneInputValue } from '@/components/shared/phone-input';
import { TimezoneCombobox } from '@/components/shared/timezone-combobox';
import { ImageCropperDialog } from '@/components/shared/image-cropper-dialog';
import { apiFetch } from '@/lib/api/client';
@@ -74,12 +75,24 @@ export function UserSettings() {
setEmail(res.data.user?.email ?? '');
setOriginalEmail(res.data.user?.email ?? '');
setCountry(res.data.preferences?.country ?? null);
setTimezone(res.data.preferences?.timezone ?? null);
// Fall back to the browser-detected zone when no value has been saved
// yet — first-time users land on a sensible default rather than an
// empty picker. Doesn't overwrite an explicit choice.
setTimezone(res.data.preferences?.timezone ?? detectedTz ?? null);
const fid = res.data.profile?.avatarFileId ?? null;
setAvatarFileId(fid);
setAvatarUrl(fid ? `/api/v1/files/${fid}/preview` : null);
}
// When the user picks a country and no timezone is set, suggest the
// primary zone for that country. Doesn't fight an explicit timezone
// selection — only fires while the timezone slot is empty.
useEffect(() => {
if (!country || timezone) return;
const primary = primaryTimezoneFor(country as CountryCode);
if (primary) setTimezone(primary);
}, [country, timezone]);
function handleAvatarPicked(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0] ?? null;
if (!file) return;
@@ -261,12 +274,11 @@ export function UserSettings() {
</div>
<div className="space-y-2">
<Label htmlFor="settings-phone">Phone</Label>
<Input
<PhoneInput
id="settings-phone"
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+1 555-0123"
value={phone ? ({ e164: phone, country: (country as never) ?? 'US' } as PhoneInputValue) : null}
onChange={(next) => setPhone(next.e164 ?? '')}
placeholder="555 0123"
/>
</div>
<div className="space-y-2">