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'
|
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<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 (
|
return (
|
||||||
<div className="max-w-md mx-auto mt-10">
|
<div className="flex min-h-svh w-full items-center 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} />
|
<FormWrapper
|
||||||
|
title='Complete Your Signup'
|
||||||
|
description='Choose a username to finish signing up'
|
||||||
|
form={<SignUpForm invite={invite} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</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'>
|
<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>
|
||||||
|
|||||||
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'>
|
<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!
|
||||||
|
|||||||
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"
|
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: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,16 +28,31 @@ 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[]
|
||||||
eventGuests EventGuest[]
|
eventGuests EventGuest[]
|
||||||
notes String?
|
notes String?
|
||||||
todos EventTodo[]
|
todos EventTodo[]
|
||||||
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
15
types.d.ts
vendored
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user