diff --git a/app/(app)/me/me-portal-client.tsx b/app/(app)/me/me-portal-client.tsx index 71cccab..6e6fbea 100644 --- a/app/(app)/me/me-portal-client.tsx +++ b/app/(app)/me/me-portal-client.tsx @@ -11,6 +11,7 @@ import { SIGNUP_STATUS_LABEL, SESSION_STATUS_LABEL } from '@/lib/classes-logic' import { RESERVATION_STATUS_LABEL } from '@/lib/equipment-logic' import { presenceStatus } from '@/lib/presence-logic' import { DuesCard } from '@/components/billing/dues-card' +import { ProfileForm, type ProfileInitial } from './profile-form' const STATUS_VARIANT: Record = { active: 'default', @@ -53,6 +54,7 @@ export type HeldPermission = { code: string; label: string } export type Billing = { status: string | null; currentPeriodEnd: string | null; hasCustomer: boolean } | null type Props = { + profile: ProfileInitial grants: Grant[] held: HeldPermission[] billing: Billing @@ -66,6 +68,7 @@ type Props = { const sectionH = 'font-mono text-[10px] uppercase tracking-widest text-muted-foreground mb-3' export function MePortalClient({ + profile, grants, held, billing, @@ -94,6 +97,11 @@ export function MePortalClient({ +
+

Profile

+ +
+

My permissions

{held.length === 0 ? ( diff --git a/app/(app)/me/page.tsx b/app/(app)/me/page.tsx index a43c953..30fde95 100644 --- a/app/(app)/me/page.tsx +++ b/app/(app)/me/page.tsx @@ -24,12 +24,25 @@ export default async function MePage() { const { data: member } = await supabase .from('space_members') - .select('id, space_id, role, display_name') + .select( + 'id, space_id, role, display_name, handle, phone, bio, skills, interests, willing_to, affiliations', + ) .eq('user_id', user.id) .in('status', ['current', 'unverified', 'late']) .maybeSingle() if (!member) redirect('/signup') + const profile = { + display_name: (member.display_name as string | null) ?? '', + handle: (member.handle as string | null) ?? '', + phone: (member.phone as string | null) ?? '', + bio: (member.bio as string | null) ?? '', + skills: (member.skills as string[] | null) ?? [], + interests: (member.interests as string[] | null) ?? [], + willing_to: (member.willing_to as string[] | null) ?? [], + affiliations: (member.affiliations as string[] | null) ?? [], + } + const { data: grantsRaw } = await supabase .from('member_certifications') .select( @@ -91,6 +104,7 @@ export default async function MePage() { + s.split(',').map(x => x.trim()).filter(Boolean) +const sameList = (a: string[], b: string[]) => + a.length === b.length && a.every((x, i) => x === b[i]) + +export function ProfileForm({ initial }: { initial: ProfileInitial }) { + const router = useRouter() + const [busy, setBusy] = useState(false) + const [displayName, setDisplayName] = useState(initial.display_name) + const [handle, setHandle] = useState(initial.handle) + const [phone, setPhone] = useState(initial.phone) + const [bio, setBio] = useState(initial.bio) + const [skills, setSkills] = useState(initial.skills.join(', ')) + const [interests, setInterests] = useState(initial.interests.join(', ')) + const [willingTo, setWillingTo] = useState(initial.willing_to.join(', ')) + const [affiliations, setAffiliations] = useState(initial.affiliations.join(', ')) + + async function save() { + if (!displayName.trim()) return toast.error('Display name is required.') + setBusy(true) + + const res = await updateMyProfile({ + display_name: displayName.trim(), + handle: handle.trim() || null, + phone: phone.trim() || null, + bio: bio.trim() || null, + skills: toList(skills), + interests: toList(interests), + willing_to: toList(willingTo), + }) + if ('error' in res && res.error) { + setBusy(false) + return toast.error(res.error) + } + + // Only re-disclose affiliations when they actually changed, so the + // conflict-of-interest disclosure timestamp is not reset on every save. + const nextAffiliations = toList(affiliations) + if (!sameList(nextAffiliations, initial.affiliations)) { + const aff = await discloseAffiliations({ affiliations: nextAffiliations }) + if ('error' in aff && aff.error) { + setBusy(false) + return toast.error(aff.error) + } + } + + setBusy(false) + toast.success('Profile saved.') + router.refresh() + } + + return ( +
+
+
+ + setDisplayName(e.target.value)} maxLength={100} /> +
+
+ + setHandle(e.target.value)} maxLength={50} placeholder="optional" /> +
+
+ + setPhone(e.target.value)} maxLength={20} placeholder="optional" /> +
+
+ +
+ +