updated login/signup form to shadcn

This commit is contained in:
2025-07-10 09:39:49 -04:00
parent 5143be1a67
commit 14cbbccd3a
13 changed files with 298 additions and 18 deletions

View File

@@ -1,10 +1,17 @@
import LoginForm from '@/components/LoginForm'; import FormWrapper from '@/components/forms/FormWrapper'
import LoginForm from '@/components/forms/LoginForm'
import React from 'react' import React from 'react'
export default function LoginPage() { export default function LoginPage() {
return ( return (
<div> <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<LoginForm /> <div className="w-full max-w-sm">
<FormWrapper
title='Login to your account'
description='Enter your email below to login to your account'
form={<LoginForm />}
/>
</div>
</div> </div>
) )
} }

View File

@@ -1,5 +1,9 @@
import { verifyInvite } from '@/lib/invite' import { verifyInvite } from '@/lib/invite'
import SignupForm from '@/components/SignupForm' import FormWrapper from '@/components/forms/FormWrapper'
import SignUpForm from '@/components/forms/SignUpForm'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import Link from 'next/link'
import { IconArrowLeft } from '@tabler/icons-react'
interface Props { interface Props {
searchParams: { searchParams: {
@@ -11,13 +15,38 @@ export default async function SignupPage({ searchParams }: Props) {
const invite = searchParams.token ? await verifyInvite(searchParams.token) : null const invite = searchParams.token ? await verifyInvite(searchParams.token) : null
if (!invite || invite.accepted || new Date(invite.expiresAt) < new Date()) { if (!invite || invite.accepted || new Date(invite.expiresAt) < new Date()) {
return <div className="text-center mt-10">Invalid or expired invitation.</div>
}
return ( return (
<div className="max-w-md mx-auto mt-10"> <div className="flex min-h-svh w-full justify-center p-6 md:p-10">
<h1 className="text-2xl font-bold mb-4">Complete Your Signup</h1> <div className="w-full max-w-sm">
<SignupForm invite={invite} /> <Card>
<CardHeader className='py-2'>
<CardTitle>
<div className="text-center">Invalid or expired invitation.</div>
</CardTitle>
</CardHeader>
<CardContent>
<p>Reach out to the couple or event planner to get a new invitation link.</p>
<Link href={'/'} className='mt-4 text-brand-primary-400 flex items-center hover:underline'>
<IconArrowLeft />
Back to Homepage
</Link>
</CardContent>
</Card>
</div>
</div>
)
}
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<FormWrapper
title='Complete Your Signup'
description='Choose a username to finish signing up'
form={<SignUpForm invite={invite} />}
/>
</div>
</div> </div>
) )
} }

View File

@@ -7,7 +7,7 @@ export default function EventInfoQuickView(props: EventProps) {
<div className='hover:cursor-pointer rounded-lg p-2 bg-brand-primary-900 hover:bg-brand-primary-800 transition-colors duration-200'> <div className='hover:cursor-pointer rounded-lg p-2 bg-brand-primary-900 hover:bg-brand-primary-800 transition-colors duration-200'>
<h3 className='text-md font-semibold'>{props.name}</h3> <h3 className='text-md font-semibold'>{props.name}</h3>
<p>Date: {props.date ? props.date.toDateString() : 'null'}</p> <p>Date: {props.date ? props.date.toDateString() : 'null'}</p>
<p>Location: {props.location ? props.location : 'null'}</p> <p>Location: {props.location ? props.location.name : 'null'}</p>
<p className='text-xs mt-2'>Created By: {props.creator.username}</p> <p className='text-xs mt-2'>Created By: {props.creator.username}</p>
</div> </div>
</Link> </Link>

View File

@@ -0,0 +1,8 @@
'use client'
import React from 'react'
export default function EditEventDialog() {
return (
<div>EditEventDialog</div>
)
}

View File

@@ -24,9 +24,9 @@ export default function EventInfo({ event }: EventProps) {
<Card className='py-0'> <Card className='py-0'>
<CardContent className='p-4'> <CardContent className='p-4'>
<h2 className='text-xl font-semibold'>Event Info</h2> <h2 className='text-xl font-semibold'>Event Info</h2>
<p className='text-sm mt-2'>Nmae: {event.name}</p> <p className='text-sm mt-2'>Name: {event.name}</p>
<p className='text-sm'>Date: {event.date ? event.date.toDateString() : 'Upcoming'}</p> <p className='text-sm'>Date: {event.date ? event.date.toDateString() : 'Upcoming'}</p>
<p className='text-sm'>Location: {event.location ? event.location : 'No location yet'}</p> <p className='text-sm'>Location: {event.location ? event.location.name : 'No location yet'}</p>
{daysLeft !== null && ( {daysLeft !== null && (
<p className='text-sm mt-2 font-medium text-brand-primary-400'> <p className='text-sm mt-2 font-medium text-brand-primary-400'>
{daysLeft} days until this event! {daysLeft} days until this event!

View File

@@ -0,0 +1,26 @@
import React from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
export default function FormWrapper({
title,
description,
form
}: {
title: string,
description?: string,
form: React.ReactNode
}) {
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
{description && (
<CardDescription>Enter your email below to login</CardDescription>
)}
</CardHeader>
<CardContent>
{form}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,77 @@
'use client'
import React, { useState } from 'react'
import { Label } from '../ui/label'
import { Input } from '../ui/input'
import Link from 'next/link'
import { Button } from '../ui/button'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export default function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('')
const [error, setError] = useState('');
const router = useRouter();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const result = await signIn('credentials', {
redirect: false,
email,
password,
})
console.log('[CLIENT] signIn result:', result)
if (result?.error) {
setError(result.error)
} else {
router.push('/dashboard')
}
}
return (
<form onSubmit={handleSubmit}>
<div className='flex flex-col gap-6'>
<div className='grid gap-3'>
<Label htmlFor='email'>Email</Label>
<Input
id='email'
type='email'
placeholder='m@example.com'
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className='grid gap-3'>
<div className='flex items-center'>
<Label htmlFor='password'>Password</Label>
<Link
href={'#'}
className='ml-auto inline-block text-sm underline-offset-4 hover:underline'
>
Forgot your password?
</Link>
</div>
<Input
id='password'
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className='flex flex-col gap-3'>
<Button
type="submit"
className="w-full bg-brand-primary-600 hover:bg-brand-primary-400"
>
Login
</Button>
{error && <p className="text-red-500">{error}</p>}
</div>
</div>
</form>
)
}

View File

@@ -0,0 +1,78 @@
'use client'
import { useRouter } from 'next/navigation'
import React, { useState } from 'react'
import { Label } from '../ui/label'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
interface Props {
invite: {
token: string
email: string
role: 'COUPLE' | 'PLANNER' | 'GUEST'
}
}
export default function SignUpForm({ invite }: Props) {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const router = useRouter()
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
const res = await fetch('/api/signup/from-invite', {
method: 'POST',
body: JSON.stringify({ token: invite.token, username, password }),
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) {
router.push('/login')
} else {
const { message } = await res.json()
setError(message || 'Signup failed')
}
}
return (
<form onSubmit={handleSubmit}>
<p className="text-sm text-gray-600">
Invited as <strong>{invite.email}</strong> ({invite.role})
</p>
<div className='flex flex-col gap-6'>
<div className='grid gap-3'>
<Label htmlFor='username'>Username</Label>
<Input
id='username'
type='text'
placeholder='Choose a username'
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div className='grid gap-3'>
<Label htmlFor='password'>Password</Label>
<Input
id='password'
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className='flex flex-col gap-3'>
<Button
type="submit"
className="w-full bg-brand-primary-600 hover:bg-brand-primary-400"
>
Sign Up
</Button>
{error && <p className="text-red-500">{error}</p>}
</div>
</div>
</form>
)
}

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:cursor-pointer",
{ {
variants: { variants: {
variant: { variant: {

View File

@@ -9,8 +9,9 @@ export const queries = {
id: true, id: true,
username: true username: true
} }
} },
} location: true
},
}) })
return allEvents; return allEvents;
@@ -74,6 +75,7 @@ export const queries = {
todos: { todos: {
orderBy: { dueDate: 'asc' }, orderBy: { dueDate: 'asc' },
}, },
location: true
} }
}) })
return event return event

View File

@@ -0,0 +1,27 @@
/*
Warnings:
- You are about to drop the column `location` on the `Event` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Event" DROP COLUMN "location",
ADD COLUMN "locationid" TEXT;
-- CreateTable
CREATE TABLE "Location" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"address" TEXT NOT NULL,
"city" TEXT NOT NULL,
"state" TEXT NOT NULL,
"postalCode" TEXT NOT NULL,
"country" TEXT NOT NULL DEFAULT 'United States',
"phone" TEXT,
"email" TEXT,
CONSTRAINT "Location_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Event" ADD CONSTRAINT "Event_locationid_fkey" FOREIGN KEY ("locationid") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -28,7 +28,8 @@ model Event {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
date DateTime? date DateTime?
location String? location Location? @relation(fields: [locationid], references: [id])
locationid String?
creator User @relation("EventCreator", fields: [creatorId], references: [id]) creator User @relation("EventCreator", fields: [creatorId], references: [id])
creatorId String creatorId String
guests Guest[] guests Guest[]
@@ -38,6 +39,20 @@ model Event {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
} }
model Location {
id String @id @default(cuid())
name String
address String
city String
state String
postalCode String
country String @default("United States")
phone String?
email String?
Event Event[]
}
model Guest { model Guest {
id String @id @default(cuid()) id String @id @default(cuid())
event Event @relation(fields: [eventId], references: [id]) event Event @relation(fields: [eventId], references: [id])

15
types.d.ts vendored
View File

@@ -15,7 +15,7 @@ interface EventProps {
id: string id: string
name: string name: string
date?: Date | null date?: Date | null
location?: string | null location?: EventLocation | null
creator: { id: string; username: string; } creator: { id: string; username: string; }
createdAt: Date; date: Date | null; createdAt: Date; date: Date | null;
creatorId: string; creatorId: string;
@@ -44,7 +44,7 @@ interface EventData {
id: string id: string
name: string name: string
date: Date | null date: Date | null
location: string | null location: EventLocation | null
creatorId: string creatorId: string
createdAt: string createdAt: string
creator: Creator creator: Creator
@@ -72,3 +72,14 @@ type User = {
username: string username: string
role: 'COUPLE' | 'PLANNER' | 'GUEST' role: 'COUPLE' | 'PLANNER' | 'GUEST'
} }
interface EventLocation {
id: string
name: string
address: string
city: string
state: string
country: string
phone: string | null
email: string | null
}