Files
tower/apps/web/app/my/settings/ProfileForm.tsx
T
maaz519 f7922454ca feat: member portal Sprint 3 — profile edit form and privacy center
- 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>
2026-06-17 16:06:20 +05:30

178 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}