Overview
Short, copy-paste patterns that match aSaaSin conventions. Use yarn, keep server-only secrets off the client, and reuse shared UI (TransitionButton, ActionMessageToast).
Form submit (action + toast)
Submit a server action that returns ActionResponse
, show success/error with ActionMessageToast
, and keep UX consistent via TransitionButton
.
// One-liner: client form that calls a typed action and shows toasts.
'use client'
import * as React from 'react'
import { TransitionButton } from '@/components/TransitionButton'
import { ActionMessageToast } from '@/components/ActionMessageToast'
import { updateUserProfileAction } from '@/actions/updateUserProfileAction'
export function ProfileForm() {
const [isPending, startTransition] = React.useTransition()
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
startTransition(async () => {
try {
const res = await updateUserProfileAction(formData) // ActionResponse
if (res?.success) ActionMessageToast.success('Profile updated')
else ActionMessageToast.error(res?.error ?? 'Update failed')
} catch {
ActionMessageToast.error('Unexpected error')
}
})
}
return (
<form onSubmit={onSubmit} className="grid gap-4">
<input name="name" placeholder="Your name" />
<input name="existingAvatar" type="hidden" value="" />
<input name="avatarFile" type="file" accept="image/*" />
<TransitionButton type="submit" isPending={isPending}>
Save changes
</TransitionButton>
</form>
)
}
Profile update + avatar
Accept FormData
, upload a new file to Supabase Storage when provided, keep or clear the existing avatar accordingly, and update profiles
.
// One-liner: server action that conditionally uploads avatar and updates Supabase.
import { createAdminClient } from '@/config/supabase/adminClient'
import { DatabaseTable } from '@/constants/enums'
import { getCurrentUser } from '@/lib/auth'
import { uploadAvatar } from '@/lib/storage'
import type { ActionResponse } from '@/types' // { success: boolean; error?: string; data?: T }
export async function updateUserProfileAction(formData: FormData): Promise<ActionResponse> {
const supabase = createAdminClient()
const user = await getCurrentUser()
if (!user) return { success: false, error: 'Not authenticated' }
const name = String(formData.get('name') ?? '')
const file = formData.get('avatarFile') as File | null
const existing = String(formData.get('existingAvatar') ?? '')
const removed = existing === ''
let avatarUrl: string | null = null
if (file) avatarUrl = await uploadAvatar(file, user.id)
else if (!removed) avatarUrl = existing || null
const update: Record<string, string | null> = { full_name: name }
if (file || removed) update.avatar_url = avatarUrl
const { error } = await supabase
.from(DatabaseTable.Profiles)
.update(update)
.eq('id', user.id)
if (error) return { success: false, error: 'Failed to update profile' }
return { success: true }
}
Keep redirect params
Forward redirect_to
and variant_id
so users land in the right place or continue checkout after auth.
// One-liner: keep redirect_to + variant_id when switching auth pages.
'use client'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { AppRoute } from '@/constants/routes'
export function SwitchAuthLink({ to }: { to: 'sign-in' | 'sign-up' }) {
const sp = useSearchParams()
const qp = new URLSearchParams()
;['redirect_to', 'variant_id'].forEach((k) => {
const v = sp.get(k); if (v) qp.set(k, v)
})
const href =
to === 'sign-in' ? `${AppRoute.SignInPage}?${qp}` : `${AppRoute.SignUpPage}?${qp}`
return <Link href={href}>{to === 'sign-in' ? 'Sign in' : 'Create account'}</Link>
}
Post-auth redirect
Read redirect_to
and optional variant_id
on callback; prefer a safe internal path and hand off to checkout when present.
// One-liner: central post-auth redirect handler.
import { redirect } from 'next/navigation'
import { DashboardRoute } from '@/constants/routes'
import { startCheckoutAction } from '@/actions/startCheckoutAction' // returns { success, data: { url } }
export async function handlePostAuth(params: { redirect_to?: string | null; variant_id?: string | null }) {
const { redirect_to, variant_id } = params
if (variant_id) {
const res = await startCheckoutAction({ variantId: variant_id })
if (res?.success && res.data?.url) return redirect(res.data.url)
}
const safe = redirect_to?.startsWith('/') ? redirect_to : DashboardRoute.OverviewPage
return redirect(safe)
}
There will be more recipes.