# User Profile Profile editing form for Next.js 14 — avatar upload to Supabase Storage, field validation with Zod, notification preferences, and soft-delete account removal, all in a single client component. ## What's included **Server functions** - `uploadAvatar(userId, file)` — uploads to the `avatars` Supabase Storage bucket at `{userId}/avatar.{ext}`, upserts on re-upload, updates `avatar_url` in `profiles`, returns the public URL - `updateProfile(userId, data)` — patches the extended profile columns; strips `@` prefix from Twitter handle; clears optional fields with `null` if empty - `deleteAccount(userId)` — soft delete: anonymizes email, name, bio, avatar, and password hash in-place; does not hard-delete the row **UI** - `ProfileForm` — client component with avatar preview + file picker, five text/textarea fields, two notification checkboxes, and a save button; accepts `userId`, `defaultValues`, `currentAvatar`, `onSuccess` **Schema** - `ProfileSchema` (internal) — Zod schema covering `name`, `bio`, `website`, `twitter`, `location`, `notifications_email`, `notifications_marketing` - `ProfileInput` — inferred TypeScript type ## Setup ### 1. Install dependencies ```bash npm install react-hook-form @hookform/resolvers zod @supabase/supabase-js ``` ### 2. Environment variables ``` NEXT_PUBLIC_SUPABASE_URL=your Supabase project URL NEXT_PUBLIC_SUPABASE_ANON_KEY=anon public key ``` ### 3. Database ```sql ALTER TABLE profiles ADD COLUMN IF NOT EXISTS bio TEXT; ALTER TABLE profiles ADD COLUMN IF NOT EXISTS website TEXT; ALTER TABLE profiles ADD COLUMN IF NOT EXISTS twitter TEXT; ALTER TABLE profiles ADD COLUMN IF NOT EXISTS location TEXT; ALTER TABLE profiles ADD COLUMN IF NOT EXISTS notifications_email BOOLEAN DEFAULT true; ALTER TABLE profiles ADD COLUMN IF NOT EXISTS notifications_marketing BOOLEAN DEFAULT false; ``` ### 4. Storage bucket In the Supabase dashboard: **Storage → New Bucket → name it `avatars` → set to Public**. If you want private avatars served via signed URLs, set the bucket to private and replace `getPublicUrl` in `uploadAvatar` with `createSignedUrl`. ## Usage examples ```tsx // app/profile/page.tsx — server component fetches data, passes to form import { getServerSession } from 'next-auth' import { authOptions } from '@/blocks/auth' import { getProfile } from '@/blocks/auth' import { ProfileForm } from '@/blocks/profile' export default async function ProfilePage() { const session = await getServerSession(authOptions) const profile = await getProfile(session.user.id) return ( { // e.g. router.refresh() to re-fetch server component data }} /> ) } ``` ```ts // Calling server functions directly (e.g. from an API route) import { updateProfile, uploadAvatar } from '@/blocks/profile' // Update fields only await updateProfile(userId, { name: 'Jane', notifications_marketing: true }) // Upload from a FormData request const file = formData.get('avatar') as File const url = await uploadAvatar(userId, file) ``` ## Notes - The file has `'use client'` at the top — `uploadAvatar`, `updateProfile`, and `deleteAccount` use the anon Supabase client, so they run in the browser with the user's session; make sure your `profiles` RLS policies allow users to update their own row (`auth.uid()::text = id::text`), otherwise updates will silently fail - `deleteAccount` is a soft delete only — it anonymizes the row but does not revoke active sessions or delete the Supabase Auth user; call `supabase.auth.admin.deleteUser(userId)` via the service role client if you need a hard delete - Avatar uploads overwrite the same path (`{userId}/avatar.{ext}`) on every upload; Supabase CDN may cache the old image for up to the `cacheControl` duration (3600s) — append a cache-busting query param to the returned URL if you need the new image to show immediately - Error handling in `ProfileForm` uses `alert()` — replace with a toast or inline error state before shipping