f7922454ca
- PATCH /my/profile API endpoint (displayName, hometown, currentLocation, interests, language, digestPreference, directoryVisible) - BFF PATCH /api/my/profile route - ProfileForm client component: interest chips, digest preference, language, directory toggle - Settings page rewrite: profile edit + privacy center (consent list + opt-out) + session + account delete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
6.4 KiB
TypeScript
178 lines
6.4 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
|
||
interface ProfileData {
|
||
displayName: string | null;
|
||
hometown: string | null;
|
||
currentLocation: string | null;
|
||
interests: string[];
|
||
language: string;
|
||
digestPreference: string;
|
||
directoryVisible: boolean;
|
||
}
|
||
|
||
interface Props {
|
||
initial: ProfileData;
|
||
}
|
||
|
||
export function ProfileForm({ initial }: Props) {
|
||
const [form, setForm] = useState<ProfileData>(initial);
|
||
const [interestInput, setInterestInput] = useState('');
|
||
const [saving, setSaving] = useState(false);
|
||
const [saved, setSaved] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setSaving(true);
|
||
setError(null);
|
||
setSaved(false);
|
||
try {
|
||
const res = await fetch('/api/my/profile', {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(form),
|
||
});
|
||
if (!res.ok) throw new Error(await res.text());
|
||
setSaved(true);
|
||
setTimeout(() => setSaved(false), 3000);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Failed to save');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const addInterest = () => {
|
||
const val = interestInput.trim();
|
||
if (val && !form.interests.includes(val)) {
|
||
setForm((f) => ({ ...f, interests: [...f.interests, val] }));
|
||
}
|
||
setInterestInput('');
|
||
};
|
||
|
||
const removeInterest = (item: string) => {
|
||
setForm((f) => ({ ...f, interests: f.interests.filter((i) => i !== item) }));
|
||
};
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 mb-1">Display name</label>
|
||
<input
|
||
type="text"
|
||
value={form.displayName ?? ''}
|
||
onChange={(e) => setForm((f) => ({ ...f, displayName: e.target.value }))}
|
||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
placeholder="Your name"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 mb-1">Hometown</label>
|
||
<input
|
||
type="text"
|
||
value={form.hometown ?? ''}
|
||
onChange={(e) => setForm((f) => ({ ...f, hometown: e.target.value }))}
|
||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
placeholder="e.g. Mumbai"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 mb-1">Current location</label>
|
||
<input
|
||
type="text"
|
||
value={form.currentLocation ?? ''}
|
||
onChange={(e) => setForm((f) => ({ ...f, currentLocation: e.target.value }))}
|
||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
placeholder="e.g. Pune"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 mb-1">Interests</label>
|
||
<div className="flex flex-wrap gap-2 mb-2">
|
||
{form.interests.map((interest) => (
|
||
<span
|
||
key={interest}
|
||
className="text-xs bg-indigo-50 text-indigo-700 rounded-full px-3 py-1 flex items-center gap-1"
|
||
>
|
||
{interest}
|
||
<button type="button" onClick={() => removeInterest(interest)} className="text-indigo-400 hover:text-indigo-700 ml-0.5">×</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={interestInput}
|
||
onChange={(e) => setInterestInput(e.target.value)}
|
||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addInterest(); } }}
|
||
className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
placeholder="Add interest and press Enter"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={addInterest}
|
||
className="px-3 py-2 text-sm bg-gray-100 rounded-lg hover:bg-gray-200 text-gray-700"
|
||
>
|
||
Add
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 mb-1">Digest preference</label>
|
||
<select
|
||
value={form.digestPreference}
|
||
onChange={(e) => setForm((f) => ({ ...f, digestPreference: e.target.value }))}
|
||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
>
|
||
<option value="daily">Daily</option>
|
||
<option value="weekly">Weekly</option>
|
||
<option value="never">Never</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-700 mb-1">Language</label>
|
||
<select
|
||
value={form.language}
|
||
onChange={(e) => setForm((f) => ({ ...f, language: e.target.value }))}
|
||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
>
|
||
<option value="en">English</option>
|
||
<option value="hi">हिन्दी</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
id="directoryVisible"
|
||
type="checkbox"
|
||
checked={form.directoryVisible}
|
||
onChange={(e) => setForm((f) => ({ ...f, directoryVisible: e.target.checked }))}
|
||
className="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||
/>
|
||
<label htmlFor="directoryVisible" className="text-sm text-gray-700">
|
||
Show my name in the member directory
|
||
</label>
|
||
</div>
|
||
|
||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={saving}
|
||
className="w-full rounded-lg bg-indigo-600 text-white text-sm font-medium py-2.5 hover:bg-indigo-700 disabled:opacity-50 transition-colors"
|
||
>
|
||
{saving ? 'Saving…' : saved ? 'Saved ✓' : 'Save profile'}
|
||
</button>
|
||
</form>
|
||
);
|
||
}
|