diff --git a/www/src/app/(auth)/login/actions.ts b/www/src/app/(auth)/login/actions.ts index c38bdda..cfa21c5 100644 --- a/www/src/app/(auth)/login/actions.ts +++ b/www/src/app/(auth)/login/actions.ts @@ -1,30 +1,40 @@ -'use server'; +"use server"; -import {revalidatePath} from 'next/cache'; -import {redirect} from 'next/navigation'; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; -import {createClient} from '@/lib/supabase/server'; +import { createClient } from "@/lib/supabase/server"; -export async function login(formData: FormData) { - const supabase = createClient(); +export type LoginActionState = { + success: boolean | null; + email?: string; + password?: string; +}; + +export async function login( + _state: LoginActionState, + formData: FormData | undefined = undefined, +): Promise { + const supabase = await createClient(); - // type-casting here for convenience - // in practice, you should validate your inputs const data = { - email: formData.get('email') as string, - password: formData.get('password') as string, + email: formData?.get("email") as string, + password: formData?.get("password") as string, }; - console.log(data); - - const {error} = await supabase.auth.signInWithPassword(data); - - console.log(error); + const { error } = await supabase.auth.signInWithPassword(data); if (error) { - redirect('/login?error=invalid'); + return { + success: false, + ...data, + }; } - revalidatePath('/', 'layout'); + revalidatePath("/", "layout"); redirect(`/login/complete`); + + return { + success: true, + }; } diff --git a/www/src/app/(auth)/login/form.tsx b/www/src/app/(auth)/login/form.tsx new file mode 100644 index 0000000..dfbb496 --- /dev/null +++ b/www/src/app/(auth)/login/form.tsx @@ -0,0 +1,82 @@ +'use client'; + +import {useActionState} from 'react'; + +import Link from 'next/link'; + +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import {Input} from '@/components/ui/input'; +import {Label} from '@/components/ui/label'; + +import {FFrLogo} from '@/components/ffr-logo'; +import {SubmitButton} from '@/components/submit-button'; +import {Alert} from '@/components/ui/alert'; + +import {login, type LoginActionState} from './actions'; + +export function LoginForm() { + const [state, formAction] = useActionState(login, { + success: null, + }); + + return ( +
+ + + +
+ + + Login + + Enter your email below to login to your account. + + + +
+ + +
+
+ + +
+ Login + + {state.success === false && ( + + There was an error logging in. Please try again. + + )} +
+ + Need an account? + + Sign Up + + +
+
+
+ ); +} diff --git a/www/src/app/(auth)/login/page.tsx b/www/src/app/(auth)/login/page.tsx index 795ecd9..e244269 100644 --- a/www/src/app/(auth)/login/page.tsx +++ b/www/src/app/(auth)/login/page.tsx @@ -1,20 +1,12 @@ import {redirect} from 'next/navigation'; -import Link from 'next/link'; -import {Button} from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import {Input} from '@/components/ui/input'; -import {Label} from '@/components/ui/label'; import {createClient} from '@/lib/supabase/server'; -import {login} from './actions'; +import {LoginForm} from './form'; + +export const metadata = { + title: 'Login', +}; type Props = { searchParams: Promise<{ @@ -23,53 +15,12 @@ type Props = { }; export default async function Page(props: Props) { - const client = createClient(); + const client = await createClient(); const {data} = await client.auth.getSession(); if (data.session?.user && !(await props.searchParams).error) { redirect('/login/complete'); } - return ( -
-
- - - Login - - Enter your email below to login to your account. - - - -
- - -
-
- - -
-
- - - -
- Need an account?{' '} - - Sign Up - -
-
-
-
-
- ); + return ; } diff --git a/www/src/app/(auth)/logout/page.tsx b/www/src/app/(auth)/logout/page.tsx index 0a6cfa4..bf7a38e 100644 --- a/www/src/app/(auth)/logout/page.tsx +++ b/www/src/app/(auth)/logout/page.tsx @@ -3,7 +3,7 @@ import {createClient} from '@/lib/supabase/server'; import {BrowserLogout} from './browser'; export default async function Page() { - const client = createClient(); + const client = await createClient(); await client.auth.signOut(); diff --git a/www/src/app/(auth)/oauth/cli/complete/page.tsx b/www/src/app/(auth)/oauth/cli/complete/page.tsx index af84430..c033150 100644 --- a/www/src/app/(auth)/oauth/cli/complete/page.tsx +++ b/www/src/app/(auth)/oauth/cli/complete/page.tsx @@ -9,7 +9,7 @@ import {Logout} from './logout'; import Link from 'next/link'; export default async function Page() { - const client = createClient(); + const client = await createClient(); const apiUrl = process.env.API_URL!; const store = await cookies(); const cliSession = store.get('cli-session')?.value; diff --git a/www/src/app/(auth)/signup/actions.ts b/www/src/app/(auth)/signup/actions.ts index 1feb457..9c499df 100644 --- a/www/src/app/(auth)/signup/actions.ts +++ b/www/src/app/(auth)/signup/actions.ts @@ -1,30 +1,53 @@ -'use server'; +"use server"; -import {revalidatePath} from 'next/cache'; -import {redirect} from 'next/navigation'; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; -import {createClient} from '@/lib/supabase/server'; +import { createClient } from "@/lib/supabase/server"; -export async function signup(formData: FormData) { - const supabase = createClient(); +export type SignupActionState = { + success: boolean | null; + email?: string; + password?: string; + first_name?: string; + last_name?: string; +}; - const {error} = await supabase.auth.signUp({ - email: formData.get('email') as string, - password: formData.get('password') as string, +export async function signup( + _state: SignupActionState, + formData: FormData | undefined = undefined, +) { + const supabase = await createClient(); + + const first_name = formData?.get("first_name") as string; + const last_name = formData?.get("last_name") as string; + const email = formData?.get("email") as string; + const password = formData?.get("password") as string; + + const { error } = await supabase.auth.signUp({ + email, + password, options: { data: { - first_name: formData.get('first_name') as string, - last_name: formData.get('last_name') as string, + first_name, + last_name, }, - emailRedirectTo: 'http://localhost:3000/signup/confirm', - } + emailRedirectTo: "http://localhost:3000/signup/confirm", + }, }); if (error) { - console.log(error) - redirect('/signup?error=fail'); + console.log(error); + + return { + success: false, + first_name, + last_name, + email, + password, + }; } - revalidatePath('/', 'layout'); - redirect('/signup/complete'); + revalidatePath("/", "layout"); + redirect("/signup/complete"); } diff --git a/www/src/app/(auth)/signup/complete/route.ts b/www/src/app/(auth)/signup/complete/route.ts index b0a7245..ebe6734 100644 --- a/www/src/app/(auth)/signup/complete/route.ts +++ b/www/src/app/(auth)/signup/complete/route.ts @@ -1,51 +1,50 @@ - - -import {api} from '@/lib/api'; -import {createClient} from '@/lib/supabase/server'; -import {NextRequest, NextResponse} from 'next/server'; +import { api } from "@/lib/api"; +import { createClient } from "@/lib/supabase/server"; +import { NextRequest, NextResponse } from "next/server"; export async function GET(req: NextRequest) { try { - const client = createClient(); - const {data: user} = await client.auth.getUser(); - const cliSession = req.cookies.get('cli-session'); - - if (!user.user?.id) { - throw new Error('no_user') - } + const client = await createClient(); + const { data: user } = await client.auth.getUser(); + const cliSession = req.cookies.get("cli-session"); + + if (!user.user?.id) { + throw new Error("no_user"); + } + + const { data, error } = await api.post< + { has_stripe_subscription: boolean } + >( + "/account", + { + method: "POST", + }, + ); - const {data, error} = await api.post<{has_stripe_subscription: boolean}>( - '/account', - { - method: 'POST', - }, - ); + if (error) { + console.log(error); + throw new Error("bad_error"); + } - if (error) { - console.log(error) - throw new Error('bad_error'); - } + if (!data) { + throw new Error("bad_data"); + } - if (!data) { - throw new Error('bad_data') - } + if (!data.has_stripe_subscription) { + return NextResponse.redirect(new URL("/plan", req.nextUrl.href)); + } - if (!data.has_stripe_subscription) { - return NextResponse.redirect(new URL('/plan', req.nextUrl.href)); - } + if (cliSession) { + return NextResponse.redirect( + new URL("/oauth/cli/complete", req.nextUrl.href), + ); + } - if (cliSession) { + return NextResponse.redirect(new URL("/", req.nextUrl.href)); + } catch (err) { + console.log(err); return NextResponse.redirect( - new URL('/oauth/cli/complete', req.nextUrl.href), - ); - } - - return NextResponse.redirect(new URL('/', req.nextUrl.href)); -} -catch (err) { - console.log(err) - return NextResponse.redirect( new URL(`/signup?error=${(err as Error).message}`, req.nextUrl.href), ); -} + } } diff --git a/www/src/app/(auth)/signup/form.tsx b/www/src/app/(auth)/signup/form.tsx new file mode 100644 index 0000000..565e490 --- /dev/null +++ b/www/src/app/(auth)/signup/form.tsx @@ -0,0 +1,100 @@ +'use client'; + +import {useActionState} from 'react'; +import Link from 'next/link'; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + CardFooter, +} from '@/components/ui/card'; +import {Input} from '@/components/ui/input'; +import {Label} from '@/components/ui/label'; +import {FFrLogo} from '@/components/ffr-logo'; +import {SubmitButton} from '@/components/submit-button'; + +import {signup, type SignupActionState} from './actions'; + +export const metadata = { + title: 'Sign Up', +}; + +export function SignUpForm() { + const [state, formAction] = useActionState(signup, { + success: null, + }); + + return ( +
+ +
+ + + Sign Up + + Enter your information to create an account + + + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + Create an account + +
+
+ + Already have an account? + + Sign in + + +
+
+
+ ); +} diff --git a/www/src/app/(auth)/signup/page.tsx b/www/src/app/(auth)/signup/page.tsx index c6fb267..9049fed 100644 --- a/www/src/app/(auth)/signup/page.tsx +++ b/www/src/app/(auth)/signup/page.tsx @@ -1,78 +1,9 @@ -import Link from 'next/link'; +import {SignUpForm} from './form'; -import {Button} from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import {Input} from '@/components/ui/input'; -import {Label} from '@/components/ui/label'; - -import {signup} from './actions'; +export const metadata = { + title: 'Sign Up', +}; export default function Page() { - return ( -
-
- - - Sign Up - - Enter your information to create an account - - - -
-
-
- - -
-
- - -
-
-
- - -
-
- - -
- -
-
- Already have an account?{' '} - - Sign in - -
-
-
-
-
- ); + return ; } diff --git a/www/src/app/layout.tsx b/www/src/app/layout.tsx index 7a0b3e7..aae5084 100644 --- a/www/src/app/layout.tsx +++ b/www/src/app/layout.tsx @@ -17,7 +17,7 @@ const geistMono = localFont({ }); export const metadata: Metadata = { - title: 'Elwood Run', + title: {default: 'Elwood Run', template: '%s | Elwood Run'}, description: 'Automate your file management. Elwood Run lets you automate your file management tasks by responding to files as they move through your system.', }; diff --git a/www/src/app/plan/api/route.ts b/www/src/app/plan/api/route.ts index 487ef33..0527fe4 100644 --- a/www/src/app/plan/api/route.ts +++ b/www/src/app/plan/api/route.ts @@ -4,7 +4,7 @@ import { createClient } from "@/lib/supabase/server"; import { api } from "@/lib/api"; export async function GET(req: NextRequest) { - const client = createClient(); + const client = await createClient(); const { data: user } = await client.auth.getUser(); if (!user.user?.id) { diff --git a/www/src/app/plan/complete/api/route.ts b/www/src/app/plan/complete/api/route.ts index 81292e3..ce02bc6 100644 --- a/www/src/app/plan/complete/api/route.ts +++ b/www/src/app/plan/complete/api/route.ts @@ -4,32 +4,30 @@ import { createClient } from "@/lib/supabase/server"; import { api } from "@/lib/api"; export async function POST(req: NextRequest) { - const {session_id} = await req.json(); - const client = createClient(); + const { session_id } = await req.json(); + const client = await createClient(); const { data: user } = await client.auth.getUser(); - const cliSession = req.cookies.get('cli-session'); + const cliSession = req.cookies.get("cli-session"); if (!user.user?.id) { return NextResponse.json({ - error: true - }) + error: true, + }); } - const { data } = await api.post<{ complete:boolean }>( + const { data } = await api.post<{ complete: boolean }>( `/stripe/session/${session_id}/verify`, ); - if (!data || data?.complete === false) { return NextResponse.json({ - complete:false - }) + complete: false, + }); } - if (cliSession) { return NextResponse.redirect( - new URL('/oauth/cli/complete', req.nextUrl.href), + new URL("/oauth/cli/complete", req.nextUrl.href), ); } diff --git a/www/src/app/plan/page.tsx b/www/src/app/plan/page.tsx index 75df989..5ef8aeb 100644 --- a/www/src/app/plan/page.tsx +++ b/www/src/app/plan/page.tsx @@ -8,7 +8,7 @@ import {redirect} from 'next/navigation'; import {ContinueButton} from './button'; export default async function Page() { - const client = createClient(); + const client = await createClient(); const {data} = await client.auth.getUser(); if (!data.user?.id) { diff --git a/www/src/components/submit-button.tsx b/www/src/components/submit-button.tsx new file mode 100644 index 0000000..6168785 --- /dev/null +++ b/www/src/components/submit-button.tsx @@ -0,0 +1,11 @@ +'use client'; + +import {useFormStatus} from 'react-dom'; + +import {Button, type ButtonProps} from './ui/button'; + +export function SubmitButton(props: ButtonProps) { + const {pending} = useFormStatus(); + + return