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'
export default function LoginPage() {
return (
<div>
<LoginForm />
<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='Login to your account'
description='Enter your email below to login to your account'
form={<LoginForm />}
/>
</div>
</div>
)
}

View File

@@ -1,5 +1,9 @@
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 {
searchParams: {
@@ -11,13 +15,38 @@ export default async function SignupPage({ searchParams }: Props) {
const invite = searchParams.token ? await verifyInvite(searchParams.token) : null
if (!invite || invite.accepted || new Date(invite.expiresAt) < new Date()) {
return <div className="text-center mt-10">Invalid or expired invitation.</div>
}
return (
<div className="max-w-md mx-auto mt-10">
<h1 className="text-2xl font-bold mb-4">Complete Your Signup</h1>
<SignupForm invite={invite} />
<div className="flex min-h-svh w-full justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<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>
)
}

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'>
<h3 className='text-md font-semibold'>{props.name}</h3>
<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>
</div>
</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'>
<CardContent className='p-4'>
<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'>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 && (
<p className='text-sm mt-2 font-medium text-brand-primary-400'>
{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"
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: {
variant: {

View File

@@ -9,8 +9,9 @@ export const queries = {
id: true,
username: true
}
}
}
},
location: true
},
})
return allEvents;
@@ -74,6 +75,7 @@ export const queries = {
todos: {
orderBy: { dueDate: 'asc' },
},
location: true
}
})
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())
name String
date DateTime?
location String?
location Location? @relation(fields: [locationid], references: [id])
locationid String?
creator User @relation("EventCreator", fields: [creatorId], references: [id])
creatorId String
guests Guest[]
@@ -38,6 +39,20 @@ model Event {
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 {
id String @id @default(cuid())
event Event @relation(fields: [eventId], references: [id])

15
types.d.ts vendored
View File

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