diff --git a/app/(public)/login/page.tsx b/app/(public)/login/page.tsx index e54053f..9e397b0 100644 --- a/app/(public)/login/page.tsx +++ b/app/(public)/login/page.tsx @@ -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 ( -
- +
+
+ } + /> +
) } diff --git a/app/(public)/signup/page.tsx b/app/(public)/signup/page.tsx index f6aca05..d4dd7d6 100644 --- a/app/(public)/signup/page.tsx +++ b/app/(public)/signup/page.tsx @@ -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
Invalid or expired invitation.
+ return ( +
+
+ + + +
Invalid or expired invitation.
+
+
+ +

Reach out to the couple or event planner to get a new invitation link.

+ + + Back to Homepage + +
+
+ +
+
+ ) } return ( -
-

Complete Your Signup

- +
+
+ } + /> +
) } diff --git a/components/EventInfoQuickView.tsx b/components/EventInfoQuickView.tsx index dd2d5ef..83c31d5 100644 --- a/components/EventInfoQuickView.tsx +++ b/components/EventInfoQuickView.tsx @@ -7,7 +7,7 @@ export default function EventInfoQuickView(props: EventProps) {

{props.name}

Date: {props.date ? props.date.toDateString() : 'null'}

-

Location: {props.location ? props.location : 'null'}

+

Location: {props.location ? props.location.name : 'null'}

Created By: {props.creator.username}

diff --git a/components/events/EditEventDialog.tsx b/components/events/EditEventDialog.tsx new file mode 100644 index 0000000..254c33a --- /dev/null +++ b/components/events/EditEventDialog.tsx @@ -0,0 +1,8 @@ +'use client' +import React from 'react' + +export default function EditEventDialog() { + return ( +
EditEventDialog
+ ) +} diff --git a/components/events/EventInfo.tsx b/components/events/EventInfo.tsx index c91acaf..2638767 100644 --- a/components/events/EventInfo.tsx +++ b/components/events/EventInfo.tsx @@ -24,9 +24,9 @@ export default function EventInfo({ event }: EventProps) {

Event Info

-

Nmae: {event.name}

+

Name: {event.name}

Date: {event.date ? event.date.toDateString() : 'Upcoming'}

-

Location: {event.location ? event.location : 'No location yet'}

+

Location: {event.location ? event.location.name : 'No location yet'}

{daysLeft !== null && (

{daysLeft} days until this event! diff --git a/components/forms/FormWrapper.tsx b/components/forms/FormWrapper.tsx new file mode 100644 index 0000000..5f8de63 --- /dev/null +++ b/components/forms/FormWrapper.tsx @@ -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 ( + + + {title} + {description && ( + Enter your email below to login + )} + + + {form} + + + ) +} diff --git a/components/forms/LoginForm.tsx b/components/forms/LoginForm.tsx new file mode 100644 index 0000000..6cf624e --- /dev/null +++ b/components/forms/LoginForm.tsx @@ -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 ( +

+
+
+ + setEmail(e.target.value)} + required + /> +
+
+
+ + + Forgot your password? + +
+ setPassword(e.target.value)} + required + /> +
+
+ + {error &&

{error}

} +
+
+
+ ) +} diff --git a/components/forms/SignUpForm.tsx b/components/forms/SignUpForm.tsx new file mode 100644 index 0000000..76166a4 --- /dev/null +++ b/components/forms/SignUpForm.tsx @@ -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 ( +
+

+ Invited as {invite.email} ({invite.role}) +

+ +
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + {error &&

{error}

} +
+
+
+ ) +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx index a2df8dc..f7caee6 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -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: { diff --git a/lib/queries.ts b/lib/queries.ts index 1069188..5930a13 100644 --- a/lib/queries.ts +++ b/lib/queries.ts @@ -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 diff --git a/prisma/migrations/20250710130603_add_location_model/migration.sql b/prisma/migrations/20250710130603_add_location_model/migration.sql new file mode 100644 index 0000000..0253f52 --- /dev/null +++ b/prisma/migrations/20250710130603_add_location_model/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 000420f..ed23dd7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,16 +28,31 @@ 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[] eventGuests EventGuest[] notes String? - todos EventTodo[] + todos EventTodo[] 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]) diff --git a/types.d.ts b/types.d.ts index 21a0668..c9cb674 100644 --- a/types.d.ts +++ b/types.d.ts @@ -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 @@ -71,4 +71,15 @@ type User = { name?: string 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 } \ No newline at end of file