eny.space Landingpage
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(signup): show success and error feedback after form submission

authored by

Sam Sauer and committed by
Tangled
af7161cc 71f4ff99

+120 -92
+9 -10
app/actions/auth.ts
··· 4 4 import { redirect } from "next/navigation"; 5 5 import { createClient } from "@/lib/supabase/server"; 6 6 7 - export async function signUp(formData: FormData) { 7 + export async function signUp( 8 + _prevState: { error?: string; success?: boolean } | null, 9 + formData: FormData 10 + ): Promise<{ error?: string; success?: boolean }> { 8 11 const supabase = await createClient(); 9 12 10 - const data = { 11 - email: formData.get("email") as string, 12 - password: formData.get("password") as string, 13 - }; 13 + const email = formData.get("email") as string; 14 + const password = formData.get("password") as string; 14 15 15 16 const { error } = await supabase.auth.signUp({ 16 - email: data.email, 17 - password: data.password, 17 + email, 18 + password, 18 19 options: { 19 20 emailRedirectTo: `${ 20 21 process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" ··· 26 27 return { error: error.message }; 27 28 } 28 29 29 - const next = (formData.get("next") as string) || "/dashboard"; 30 - revalidatePath("/", "layout"); 31 - redirect(next); 30 + return { success: true }; 32 31 } 33 32 34 33 export async function signIn(formData: FormData) {
+15 -82
app/signup/page.tsx
··· 1 - import Link from "next/link"; 2 - import { signUp } from "@/actions/auth"; 3 - import { Button } from "@/actions/components/ui/button"; 4 - import { 5 - Card, 6 - CardContent, 7 - CardDescription, 8 - CardHeader, 9 - CardTitle, 10 - CardFooter, 11 - } from "@/actions/components/ui/card"; 12 - import { Input } from "@/actions/components/ui/input"; 13 - import { Label } from "@/actions/components/ui/label"; 1 + import { SignUpForm } from "./signup-form"; 14 2 15 3 type SignUpPageProps = { 16 - searchParams?: { 4 + searchParams?: Promise<{ 17 5 auto_checkout?: string; 18 6 pds_plan?: string; 19 7 pds_username?: string; 20 8 pds_hostname?: string; 21 9 pds_disksize_gb?: string; 22 - }; 10 + }>; 23 11 }; 24 12 25 - export default function SignUpPage({ searchParams }: SignUpPageProps) { 13 + export default async function SignUpPage({ searchParams }: SignUpPageProps) { 14 + const params = await searchParams; 26 15 const nextParams = new URLSearchParams(); 27 - if (searchParams?.auto_checkout) { 28 - nextParams.set("auto_checkout", searchParams.auto_checkout); 29 - } 30 - if (searchParams?.pds_plan) { 31 - nextParams.set("pds_plan", searchParams.pds_plan); 32 - } 33 - if (searchParams?.pds_username) { 34 - nextParams.set("pds_username", searchParams.pds_username); 35 - } 36 - if (searchParams?.pds_hostname) { 37 - nextParams.set("pds_hostname", searchParams.pds_hostname); 38 - } 39 - if (searchParams?.pds_disksize_gb) { 40 - nextParams.set("pds_disksize_gb", searchParams.pds_disksize_gb); 41 - } 16 + 17 + if (params?.auto_checkout) nextParams.set("auto_checkout", params.auto_checkout); 18 + if (params?.pds_plan) nextParams.set("pds_plan", params.pds_plan); 19 + if (params?.pds_username) nextParams.set("pds_username", params.pds_username); 20 + if (params?.pds_hostname) nextParams.set("pds_hostname", params.pds_hostname); 21 + if (params?.pds_disksize_gb) nextParams.set("pds_disksize_gb", params.pds_disksize_gb); 42 22 43 - const next = `/dashboard${nextParams.toString() ? `?${nextParams.toString()}` : ""}`; 44 - const loginHref = `/login${nextParams.toString() ? `?${nextParams.toString()}` : ""}`; 23 + const qs = nextParams.toString(); 24 + const next = `/dashboard${qs ? `?${qs}` : ""}`; 25 + const loginHref = `/login${qs ? `?${qs}` : ""}`; 45 26 46 - return ( 47 - <main className="flex min-h-[60vh] items-center justify-center px-4"> 48 - <Card className="w-full max-w-sm"> 49 - <CardHeader> 50 - <CardTitle>Create account</CardTitle> 51 - <CardDescription> 52 - Get started with eny.space in a few seconds. 53 - </CardDescription> 54 - </CardHeader> 55 - <CardContent> 56 - <form action={signUp} className="space-y-4"> 57 - <input type="hidden" name="next" value={next} /> 58 - <div className="space-y-2"> 59 - <Label htmlFor="email">Email</Label> 60 - <Input 61 - id="email" 62 - name="email" 63 - type="email" 64 - autoComplete="email" 65 - required 66 - /> 67 - </div> 68 - <div className="space-y-2"> 69 - <Label htmlFor="password">Password</Label> 70 - <Input 71 - id="password" 72 - name="password" 73 - type="password" 74 - minLength={6} 75 - autoComplete="new-password" 76 - required 77 - /> 78 - </div> 79 - <Button type="submit" className="w-full"> 80 - Sign Up 81 - </Button> 82 - </form> 83 - </CardContent> 84 - <CardFooter className="flex justify-center text-sm text-muted-foreground"> 85 - <span> 86 - Already have an account?{" "} 87 - <Link href={loginHref} className="underline underline-offset-4"> 88 - Login 89 - </Link> 90 - </span> 91 - </CardFooter> 92 - </Card> 93 - </main> 94 - ); 27 + return <SignUpForm next={next} loginHref={loginHref} />; 95 28 }
+96
app/signup/signup-form.tsx
··· 1 + "use client"; 2 + 3 + import { useActionState } from "react"; 4 + import Link from "next/link"; 5 + import { signUp } from "@/actions/auth"; 6 + import { Button } from "@/actions/components/ui/button"; 7 + import { 8 + Card, 9 + CardContent, 10 + CardDescription, 11 + CardHeader, 12 + CardTitle, 13 + CardFooter, 14 + } from "@/actions/components/ui/card"; 15 + import { Input } from "@/actions/components/ui/input"; 16 + import { Label } from "@/actions/components/ui/label"; 17 + 18 + interface SignUpFormProps { 19 + next: string; 20 + loginHref: string; 21 + } 22 + 23 + export function SignUpForm({ next, loginHref }: SignUpFormProps) { 24 + const [state, action, pending] = useActionState(signUp, null); 25 + 26 + if (state?.success) { 27 + return ( 28 + <main className="flex min-h-[60vh] items-center justify-center px-4"> 29 + <Card className="w-full max-w-sm text-center"> 30 + <CardHeader> 31 + <CardTitle>Check your email</CardTitle> 32 + <CardDescription> 33 + We sent you a confirmation link. Click it to activate your account 34 + and you&apos;ll be taken to your dashboard. 35 + </CardDescription> 36 + </CardHeader> 37 + </Card> 38 + </main> 39 + ); 40 + } 41 + 42 + return ( 43 + <main className="flex min-h-[60vh] items-center justify-center px-4"> 44 + <Card className="w-full max-w-sm"> 45 + <CardHeader> 46 + <CardTitle>Create account</CardTitle> 47 + <CardDescription> 48 + Get started with eny.space in a few seconds. 49 + </CardDescription> 50 + </CardHeader> 51 + <CardContent> 52 + <form action={action} className="space-y-4"> 53 + <input type="hidden" name="next" value={next} /> 54 + {state?.error && ( 55 + <p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"> 56 + {state.error} 57 + </p> 58 + )} 59 + <div className="space-y-2"> 60 + <Label htmlFor="email">Email</Label> 61 + <Input 62 + id="email" 63 + name="email" 64 + type="email" 65 + autoComplete="email" 66 + required 67 + /> 68 + </div> 69 + <div className="space-y-2"> 70 + <Label htmlFor="password">Password</Label> 71 + <Input 72 + id="password" 73 + name="password" 74 + type="password" 75 + minLength={6} 76 + autoComplete="new-password" 77 + required 78 + /> 79 + </div> 80 + <Button type="submit" className="w-full" disabled={pending}> 81 + {pending ? "Creating account…" : "Sign Up"} 82 + </Button> 83 + </form> 84 + </CardContent> 85 + <CardFooter className="flex justify-center text-sm text-muted-foreground"> 86 + <span> 87 + Already have an account?{" "} 88 + <Link href={loginHref} className="underline underline-offset-4"> 89 + Login 90 + </Link> 91 + </span> 92 + </CardFooter> 93 + </Card> 94 + </main> 95 + ); 96 + }