updated login/signup form to shadcn
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
8
components/events/EditEventDialog.tsx
Normal file
8
components/events/EditEventDialog.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
export default function EditEventDialog() {
|
||||
return (
|
||||
<div>EditEventDialog</div>
|
||||
)
|
||||
}
|
||||
@@ -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!
|
||||
|
||||
26
components/forms/FormWrapper.tsx
Normal file
26
components/forms/FormWrapper.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
77
components/forms/LoginForm.tsx
Normal file
77
components/forms/LoginForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
78
components/forms/SignUpForm.tsx
Normal file
78
components/forms/SignUpForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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
15
types.d.ts
vendored
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user