From 295d4e08d91cdf059b041820e01e8970f80b72fb Mon Sep 17 00:00:00 2001 From: Kaustav Ghosh Date: Sun, 8 Dec 2024 00:25:54 +0530 Subject: [PATCH 01/16] fix in subsccription schema --- .../subscriptions/[subscriptionId]/route.ts | 131 ++++++- app/api/events/subscriptions/book/route.ts | 66 +++- app/api/events/subscriptions/route.ts | 55 ++- app/api/payments/razorpay/route.ts | 317 ++++++++++++++- app/api/payments/stripe/route.ts | 309 ++++++++++++++- .../subscriptions/[subscriptionId]/route.ts | 95 ++++- .../appointments/[appointmentId]/route.ts | 367 +++++++++++++++++- app/api/slots/appointments/route.ts | 24 +- .../consultant/[consultantId]/utils.ts | 75 ++-- prisma/schema.prisma | 4 +- prisma/seedFiles/createAppointments.ts | 16 +- prisma/seedFiles/createConsultantReviews.ts | 54 ++- prisma/seedFiles/createPayments.ts | 73 +++- types/appointment.ts | 4 +- 14 files changed, 1441 insertions(+), 149 deletions(-) diff --git a/app/api/events/subscriptions/[subscriptionId]/route.ts b/app/api/events/subscriptions/[subscriptionId]/route.ts index b37c136..6bd9137 100644 --- a/app/api/events/subscriptions/[subscriptionId]/route.ts +++ b/app/api/events/subscriptions/[subscriptionId]/route.ts @@ -11,13 +11,50 @@ export async function GET( const subscriptionData = await prisma.subscription.findUniqueOrThrow({ where: { id: subscriptionId }, include: { - plan: true, - requestedBy: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, appointments: { include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, }, @@ -64,20 +101,57 @@ export async function PUT( feedbackFromConsultee: body.feedbackFromConsultee, feedbackFromConsultant: body.feedbackFromConsultant, rating: body.rating, - plan: body.planId + subscriptionPlan: body.planId ? { connect: { id: body.planId }, } : undefined, }, include: { - plan: true, - requestedBy: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, appointments: { include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, }, @@ -105,13 +179,50 @@ export async function DELETE( const subscriptionData = await prisma.subscription.delete({ where: { id: subscriptionId }, include: { - plan: true, - requestedBy: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, appointments: { include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, }, diff --git a/app/api/events/subscriptions/book/route.ts b/app/api/events/subscriptions/book/route.ts index 78fa8c8..3225add 100644 --- a/app/api/events/subscriptions/book/route.ts +++ b/app/api/events/subscriptions/book/route.ts @@ -3,6 +3,13 @@ import prisma from "@/lib/prisma"; import { getServerSession } from "next-auth/next"; import authOptions from "@/app/api/auth/[...nextauth]/options"; +interface BookSubscriptionRequest { + subscriptionPlanId: string; + tentativeStartDate: string; + tentativeSchedule?: string; + requestNotes?: string; +} + export async function POST(request: Request) { try { const session = await getServerSession(authOptions); @@ -15,10 +22,20 @@ export async function POST(request: Request) { tentativeStartDate, tentativeSchedule, requestNotes, - } = await request.json(); + }: BookSubscriptionRequest = await request.json(); const consultee = await prisma.consulteeProfile.findUnique({ where: { userId: session.user.id }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, }); if (!consultee) { @@ -30,7 +47,20 @@ export async function POST(request: Request) { const subscriptionPlan = await prisma.subscriptionPlan.findUnique({ where: { id: subscriptionPlanId }, - include: { consultantProfile: true }, + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, }); if (!subscriptionPlan) { @@ -45,7 +75,7 @@ export async function POST(request: Request) { const subscription = await prisma.subscription.create({ data: { - plan: { connect: { id: subscriptionPlanId } }, + subscriptionPlan: { connect: { id: subscriptionPlanId } }, requestedBy: { connect: { id: consultee.id } }, startDate: new Date(tentativeStartDate), endDate: endDate, @@ -55,8 +85,34 @@ export async function POST(request: Request) { requestStatus: "PENDING", }, include: { - plan: true, - requestedBy: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }); diff --git a/app/api/events/subscriptions/route.ts b/app/api/events/subscriptions/route.ts index ed74250..82b19cd 100644 --- a/app/api/events/subscriptions/route.ts +++ b/app/api/events/subscriptions/route.ts @@ -1,7 +1,12 @@ import prisma from "@/lib/prisma"; -import { RequestStatus } from "@prisma/client"; +import { Prisma, RequestStatus } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; +interface UpdateSubscriptionRequest { + id: string; + status: RequestStatus; +} + export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const consultantProfileId = searchParams.get("consultantProfileId"); @@ -17,8 +22,8 @@ export async function GET(request: NextRequest) { } try { - const whereClause: any = { - plan: { + const whereClause: Prisma.SubscriptionWhereInput = { + subscriptionPlan: { consultantProfileId, }, }; @@ -31,18 +36,32 @@ export async function GET(request: NextRequest) { prisma.subscription.findMany({ where: whereClause, include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { - user: true, + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, }, }, requestedBy: { include: { - user: true, + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, }, @@ -75,7 +94,7 @@ export async function GET(request: NextRequest) { export async function PATCH(request: NextRequest) { try { - const body = await request.json(); + const body: UpdateSubscriptionRequest = await request.json(); const { id, status } = body; if (!id || !status) { @@ -85,7 +104,7 @@ export async function PATCH(request: NextRequest) { ); } - if (!Object.values(RequestStatus).includes(status as RequestStatus)) { + if (!Object.values(RequestStatus).includes(status)) { return NextResponse.json({ error: "Invalid status" }, { status: 400 }); } @@ -93,18 +112,32 @@ export async function PATCH(request: NextRequest) { where: { id }, data: { requestStatus: status }, include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { - user: true, + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, }, }, requestedBy: { include: { - user: true, + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, }, diff --git a/app/api/payments/razorpay/route.ts b/app/api/payments/razorpay/route.ts index c205705..ce0df28 100644 --- a/app/api/payments/razorpay/route.ts +++ b/app/api/payments/razorpay/route.ts @@ -3,6 +3,39 @@ import prisma from "@/lib/prisma"; import Razorpay from "razorpay"; import crypto from "crypto"; +interface RazorpayMock { + orders: { + create: () => Promise<{ + id: string; + amount: number; + currency: string; + receipt: string; + status: string; + }>; + }; + payments: { + fetch: () => Promise<{ + id: string; + status: string; + acquirer_data: { + rrn: string; + }; + }>; + }; +} + +interface CreatePaymentRequest { + appointmentId: string; + userId: string; +} + +interface UpdatePaymentRequest { + razorpay_order_id: string; + razorpay_payment_id: string; + razorpay_signature: string; + paymentId: string; +} + const razorpay = process.env.RAZORPAY_KEY_ID && process.env.RAZORPAY_KEY_SECRET ? new Razorpay({ @@ -28,21 +61,97 @@ const razorpay = }, }), }, - } as unknown as Razorpay); + } as RazorpayMock); export async function POST(req: NextRequest) { try { - const body = await req.json(); + const body: CreatePaymentRequest = await req.json(); const { appointmentId, userId } = body; // Fetch the appointment and related data const appointment = await prisma.appointment.findUnique({ where: { id: appointmentId }, include: { - consultation: { include: { consultationPlan: true } }, - subscription: { include: { plan: true } }, - webinar: { include: { webinarPlan: true } }, - class: { include: { classPlan: true } }, + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, }, }); @@ -60,8 +169,8 @@ export async function POST(req: NextRequest) { amount = appointment.consultation.consultationPlan.price; description = `Payment for Consultation: ${appointment.consultation.consultationPlan.title}`; } else if (appointment.subscription) { - amount = appointment.subscription.plan.price; - description = `Payment for Subscription: ${appointment.subscription.plan.title}`; + amount = appointment.subscription.subscriptionPlan.price; + description = `Payment for Subscription: ${appointment.subscription.subscriptionPlan.title}`; } else if (appointment.webinar) { amount = appointment.webinar.webinarPlan.price; description = `Payment for Webinar: ${appointment.webinar.webinarPlan.title}`; @@ -95,6 +204,100 @@ export async function POST(req: NextRequest) { user: { connect: { id: userId } }, appointment: { connect: { id: appointmentId } }, }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + appointment: { + include: { + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }); return NextResponse.json({ @@ -112,7 +315,7 @@ export async function POST(req: NextRequest) { export async function PUT(req: NextRequest) { try { - const body = await req.json(); + const body: UpdatePaymentRequest = await req.json(); const { razorpay_order_id, razorpay_payment_id, @@ -144,7 +347,101 @@ export async function PUT(req: NextRequest) { data: { paymentStatus: paymentDetails.status === "captured" ? "SUCCEEDED" : "FAILED", - receiptUrl: paymentDetails.acquirer_data?.rrn || null, // Using rrn as receipt URL, adjust if needed + receiptUrl: paymentDetails.acquirer_data?.rrn || null, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + appointment: { + include: { + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, }); diff --git a/app/api/payments/stripe/route.ts b/app/api/payments/stripe/route.ts index efabc9f..e5fdce2 100644 --- a/app/api/payments/stripe/route.ts +++ b/app/api/payments/stripe/route.ts @@ -2,6 +2,31 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; import Stripe from "stripe"; +interface StripeMock { + paymentIntents: { + create: () => Promise<{ + id: string; + client_secret: string; + status: string; + }>; + retrieve: () => Promise<{ + id: string; + status: string; + receipt_email: string; + }>; + }; +} + +interface CreatePaymentRequest { + appointmentId: string; + userId: string; +} + +interface UpdatePaymentRequest { + paymentIntentId: string; + paymentId: string; +} + const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2024-10-28.acacia", @@ -19,21 +44,97 @@ const stripe = process.env.STRIPE_SECRET_KEY receipt_email: "mock_receipt@example.com", }), }, - } as unknown as Stripe); + } as StripeMock); export async function POST(req: NextRequest) { try { - const body = await req.json(); + const body: CreatePaymentRequest = await req.json(); const { appointmentId, userId } = body; // Fetch the appointment and related data const appointment = await prisma.appointment.findUnique({ where: { id: appointmentId }, include: { - consultation: { include: { consultationPlan: true } }, - subscription: { include: { plan: true } }, - webinar: { include: { webinarPlan: true } }, - class: { include: { classPlan: true } }, + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, }, }); @@ -51,8 +152,8 @@ export async function POST(req: NextRequest) { amount = appointment.consultation.consultationPlan.price; description = `Payment for Consultation: ${appointment.consultation.consultationPlan.title}`; } else if (appointment.subscription) { - amount = appointment.subscription.plan.price; - description = `Payment for Subscription: ${appointment.subscription.plan.title}`; + amount = appointment.subscription.subscriptionPlan.price; + description = `Payment for Subscription: ${appointment.subscription.subscriptionPlan.title}`; } else if (appointment.webinar) { amount = appointment.webinar.webinarPlan.price; description = `Payment for Webinar: ${appointment.webinar.webinarPlan.title}`; @@ -82,6 +183,100 @@ export async function POST(req: NextRequest) { user: { connect: { id: userId } }, appointment: { connect: { id: appointmentId } }, }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + appointment: { + include: { + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }); return NextResponse.json({ @@ -99,7 +294,7 @@ export async function POST(req: NextRequest) { export async function PUT(req: NextRequest) { try { - const body = await req.json(); + const body: UpdatePaymentRequest = await req.json(); const { paymentIntentId, paymentId } = body; // Retrieve the PaymentIntent from Stripe @@ -111,7 +306,101 @@ export async function PUT(req: NextRequest) { data: { paymentStatus: paymentIntent.status === "succeeded" ? "SUCCEEDED" : "FAILED", - receiptUrl: paymentIntent.receipt_email, // TODO: fix this + receiptUrl: paymentIntent.receipt_email, + }, + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + appointment: { + include: { + consultation: { + include: { + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + subscription: { + include: { + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + webinar: { + include: { + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + class: { + include: { + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, }); diff --git a/app/api/plans/subscriptions/[subscriptionId]/route.ts b/app/api/plans/subscriptions/[subscriptionId]/route.ts index 05f3ae6..3ba0fed 100644 --- a/app/api/plans/subscriptions/[subscriptionId]/route.ts +++ b/app/api/plans/subscriptions/[subscriptionId]/route.ts @@ -2,6 +2,22 @@ import prisma from "@/lib/prisma"; import { Prisma, PlanEmailSupport } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; +interface UpdateSubscriptionPlanRequest { + title?: string; + description?: string; + durationInMonths?: number; + price?: number; + callsPerWeek?: number; + videoMeetings?: number; + emailSupport?: PlanEmailSupport; + language?: string; + level?: string; + prerequisites?: string; + materialProvided?: string; + learningOutcomes?: string[]; + consultantProfileId?: string; +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ subscriptionId: string }> }, @@ -11,8 +27,34 @@ export async function GET( const subscriptionPlan = await prisma.subscriptionPlan.findUniqueOrThrow({ where: { id: subscriptionId }, include: { - consultantProfile: true, - subscriptions: true, + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + subscriptions: { + include: { + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }); @@ -41,7 +83,7 @@ export async function PUT( ) { try { const { subscriptionId } = await params; - const body = await request.json(); + const body: UpdateSubscriptionPlanRequest = await request.json(); // Input validation if (body.durationInMonths && body.durationInMonths <= 0) { @@ -91,7 +133,7 @@ export async function PUT( price: body.price ? Math.round(body.price) : undefined, // Ensure price is an integer callsPerWeek: body.callsPerWeek, videoMeetings: body.videoMeetings, - emailSupport: body.emailSupport as PlanEmailSupport, + emailSupport: body.emailSupport, language: body.language, level: body.level, prerequisites: body.prerequisites, @@ -104,8 +146,34 @@ export async function PUT( : undefined, }, include: { - consultantProfile: true, - subscriptions: true, + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + subscriptions: { + include: { + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }); @@ -137,7 +205,7 @@ export async function DELETE( // Check if there are any associated subscriptions const associatedSubscriptions = await prisma.subscription.findMany({ - where: { planId: subscriptionId }, + where: { subscriptionPlanId: subscriptionId }, }); if (associatedSubscriptions.length > 0) { @@ -153,7 +221,18 @@ export async function DELETE( const subscriptionPlan = await prisma.subscriptionPlan.delete({ where: { id: subscriptionId }, include: { - consultantProfile: true, + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }); diff --git a/app/api/slots/appointments/[appointmentId]/route.ts b/app/api/slots/appointments/[appointmentId]/route.ts index 6944ee3..0bcd8a2 100644 --- a/app/api/slots/appointments/[appointmentId]/route.ts +++ b/app/api/slots/appointments/[appointmentId]/route.ts @@ -2,6 +2,15 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; import { Prisma, AppointmentsType } from "@prisma/client"; +interface UpdateAppointmentRequest { + appointmentType?: AppointmentsType; + slotOfAppointmentId?: string; + consultationId?: string; + subscriptionId?: string; + webinarId?: string; + classId?: string; +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ appointmentId: string }> }, @@ -13,30 +22,136 @@ export async function GET( include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, consultation: { include: { - consultationPlan: true, + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, subscription: { include: { - plan: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, webinar: { include: { - webinarPlan: true, + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }, class: { include: { - classPlan: true, + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + payment: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, - payment: true, }, }); @@ -63,7 +178,7 @@ export async function PUT( ) { try { const { appointmentId } = await params; - const body = await request.json(); + const body: UpdateAppointmentRequest = await request.json(); if ( body.appointmentType && @@ -96,30 +211,136 @@ export async function PUT( include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, consultation: { include: { - consultationPlan: true, + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, subscription: { include: { - plan: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, webinar: { include: { - webinarPlan: true, + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }, class: { include: { - classPlan: true, + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + }, + }, + payment: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, }, }, - payment: true, }, }); @@ -150,7 +371,20 @@ export async function DELETE( // Check if there's an associated payment const appointment = await prisma.appointment.findUnique({ where: { id: appointmentId }, - include: { payment: true }, + include: { + payment: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, }); if (appointment?.payment) { @@ -165,27 +399,122 @@ export async function DELETE( include: { slotOfAppointment: { include: { - consulteeProfile: true, + consulteeProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, consultation: { include: { - consultationPlan: true, + consultationPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, subscription: { include: { - plan: true, + subscriptionPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, + requestedBy: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, }, }, webinar: { include: { - webinarPlan: true, + webinarPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }, class: { include: { - classPlan: true, + classPlan: { + include: { + consultantProfile: { + include: { + user: { + select: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }, }, }, }, diff --git a/app/api/slots/appointments/route.ts b/app/api/slots/appointments/route.ts index 94481ef..3a402fd 100644 --- a/app/api/slots/appointments/route.ts +++ b/app/api/slots/appointments/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -import { AppointmentsType } from "@prisma/client"; +import { AppointmentsType, Prisma } from "@prisma/client"; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); @@ -51,7 +51,7 @@ export async function GET(request: NextRequest) { a.subscription?.requestedBy?.user?.name, consultant: a.consultation?.consultationPlan?.consultantProfile?.user?.name || - a.subscription?.plan?.consultantProfile?.user?.name || + a.subscription?.subscriptionPlan?.consultantProfile?.user?.name || a.webinar?.webinarPlan?.consultantProfile?.user?.name || a.class?.classPlan?.consultantProfile?.user?.name, }, @@ -93,11 +93,11 @@ async function getAppointments( // Get current time in UTC const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000); - const whereClause: any = { + const whereClause: Prisma.AppointmentWhereInput = { slotOfAppointment: { some: { slotStartTimeInUTC: { - gte: thirtyMinutesAgo, // Include appointments from 30 minutes ago + gte: thirtyMinutesAgo, }, }, }, @@ -114,7 +114,7 @@ async function getAppointments( }, { subscription: { - plan: { + subscriptionPlan: { consultantProfileId, }, }, @@ -142,9 +142,8 @@ async function getAppointments( if (consulteeProfileId) { whereClause.slotOfAppointment = { - ...whereClause.slotOfAppointment, some: { - ...whereClause.slotOfAppointment.some, + ...whereClause.slotOfAppointment?.some, consulteeProfileId, }, }; @@ -204,7 +203,7 @@ async function getAppointments( }, subscription: { include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { @@ -276,11 +275,6 @@ async function getAppointments( }, }, orderBy: [ - { - slotOfAppointment: { - _count: "desc", - }, - }, { createdAt: "desc", }, @@ -317,7 +311,7 @@ async function getAppointments( a.subscription?.requestedBy?.user?.name, consultant: a.consultation?.consultationPlan?.consultantProfile?.user?.name || - a.subscription?.plan?.consultantProfile?.user?.name || + a.subscription?.subscriptionPlan?.consultantProfile?.user?.name || a.webinar?.webinarPlan?.consultantProfile?.user?.name || a.class?.classPlan?.consultantProfile?.user?.name, })), @@ -401,7 +395,7 @@ export async function POST(request: NextRequest) { }, subscription: { include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { diff --git a/app/dashboard/consultant/[consultantId]/utils.ts b/app/dashboard/consultant/[consultantId]/utils.ts index b0b24b0..0d1f99b 100644 --- a/app/dashboard/consultant/[consultantId]/utils.ts +++ b/app/dashboard/consultant/[consultantId]/utils.ts @@ -18,7 +18,9 @@ export async function fetchConsultantData( try { const response = await fetch(`/api/user/consultants/${consultantId}`); if (!response.ok) { - throw new Error("Failed to fetch consultant data"); + throw new Error( + `Failed to fetch consultant data: ${response.statusText}`, + ); } const data: ApiResponse = await response.json(); return data.data; @@ -36,7 +38,7 @@ export async function fetchAppointments( `/api/slots/appointments?consultantProfileId=${consultantId}`, ); if (!response.ok) { - throw new Error("Failed to fetch appointments"); + throw new Error(`Failed to fetch appointments: ${response.statusText}`); } const data: ApiResponse = await response.json(); @@ -44,14 +46,14 @@ export async function fetchAppointments( return data.data.map((appointment) => ({ id: appointment.id, name: - appointment.slotOfAppointment?.[0]?.consulteeProfile?.user?.name || + appointment.slotOfAppointment?.[0]?.consulteeProfile?.user?.name ?? "Unknown", description: getAppointmentDescription(appointment), time: formatAppointmentTime( - appointment.slotOfAppointment?.[0]?.slotStartTimeInUTC?.toString(), + appointment.slotOfAppointment?.[0]?.slotStartTimeInUTC, ), badge: getAppointmentBadge( - appointment.slotOfAppointment?.[0]?.slotStartTimeInUTC?.toString(), + appointment.slotOfAppointment?.[0]?.slotStartTimeInUTC, ), })); } catch (error) { @@ -74,21 +76,30 @@ export async function fetchApprovals( ), ]); - if (!consultationsRes.ok || !subscriptionsRes.ok) { - throw new Error("Failed to fetch approvals"); + if (!consultationsRes.ok) { + throw new Error( + `Failed to fetch consultations: ${consultationsRes.statusText}`, + ); + } + if (!subscriptionsRes.ok) { + throw new Error( + `Failed to fetch subscriptions: ${subscriptionsRes.statusText}`, + ); } - const consultationsData = await consultationsRes.json(); - const subscriptionsData = await subscriptionsRes.json(); + const consultationsData: ApiResponse = + await consultationsRes.json(); + const subscriptionsData: ApiResponse = + await subscriptionsRes.json(); // Transform consultations into approvals const consultationApprovals = consultationsData.data.map( (consultation: TConsultation) => ({ id: consultation.id, type: "Consultation", - name: consultation.requestedBy?.user?.name || "Unknown", - date: formatDate(new Date(consultation.requestedAt)), - time: formatTime(new Date(consultation.requestedAt)), + name: consultation.requestedBy?.user?.name ?? "Unknown", + date: formatDate(consultation.requestedAt), + time: formatTime(consultation.requestedAt), }), ); @@ -97,17 +108,17 @@ export async function fetchApprovals( (subscription: TSubscription) => ({ id: subscription.id, type: "Subscription", - name: subscription.requestedBy?.user?.name || "Unknown", - date: formatDate(new Date(subscription.requestedAt)), - time: formatTime(new Date(subscription.requestedAt)), + name: subscription.requestedBy?.user?.name ?? "Unknown", + date: formatDate(subscription.requestedAt), + time: formatTime(subscription.requestedAt), }), ); // Combine and sort by requestedAt return [...consultationApprovals, ...subscriptionApprovals].sort( (a, b) => - new Date(b.date + " " + b.time).getTime() - - new Date(a.date + " " + a.time).getTime(), + new Date(`${b.date} ${b.time}`).getTime() - + new Date(`${a.date} ${a.time}`).getTime(), ); } catch (error) { console.error("Error fetching approvals:", error); @@ -134,21 +145,23 @@ function getAppointmentDescription(appointment: TAppointment): string { const type = appointment.appointmentType?.toLowerCase(); switch (type) { case "consultation": - return `Consultation - ${appointment.consultation?.consultationPlan?.title || "No title"}`; + return `Consultation - ${appointment.consultation?.consultationPlan?.title ?? "No title"}`; case "subscription": - return `Subscription - ${appointment.subscription?.plan?.title || "No title"}`; + return `Subscription - ${appointment.subscription?.subscriptionPlan?.title ?? "No title"}`; case "webinar": - return `Webinar - ${appointment.webinar?.webinarPlan?.title || "No title"}`; + return `Webinar - ${appointment.webinar?.webinarPlan?.title ?? "No title"}`; case "class": - return `Class - ${appointment.class?.classPlan?.title || "No title"}`; + return `Class - ${appointment.class?.classPlan?.title ?? "No title"}`; default: return "Appointment"; } } -function formatAppointmentTime(dateString: string): string { +function formatAppointmentTime(dateString?: string | Date | null): string { if (!dateString) return "Time not set"; const date = new Date(dateString); + if (isNaN(date.getTime())) return "Invalid date"; + return date.toLocaleString("en-US", { weekday: "short", month: "short", @@ -159,10 +172,12 @@ function formatAppointmentTime(dateString: string): string { }); } -function getAppointmentBadge(dateString: string): string { +function getAppointmentBadge(dateString?: string | Date | null): string { if (!dateString) return "Schedule unavailable"; const appointmentTime = new Date(dateString); + if (isNaN(appointmentTime.getTime())) return "Invalid date"; + const now = new Date(); const diffInMinutes = Math.floor( (appointmentTime.getTime() - now.getTime()) / (1000 * 60), @@ -189,8 +204,11 @@ function getAppointmentBadge(dateString: string): string { return `In ${Math.floor(diffInDays / 365)} years`; } -function formatDate(date: Date): string { - if (!date) return "Date not set"; +function formatDate(dateString?: string | Date | null): string { + if (!dateString) return "Date not set"; + const date = new Date(dateString); + if (isNaN(date.getTime())) return "Invalid date"; + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", @@ -198,8 +216,11 @@ function formatDate(date: Date): string { }); } -function formatTime(date: Date): string { - if (!date) return "Time not set"; +function formatTime(dateString?: string | Date | null): string { + if (!dateString) return "Time not set"; + const date = new Date(dateString); + if (isNaN(date.getTime())) return "Invalid time"; + return date.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e443b82..d087988 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -379,8 +379,8 @@ model Subscription { feedbackFromConsultant String? rating Float? - plan SubscriptionPlan @relation(fields: [planId], references: [id], onUpdate: Cascade, onDelete: Cascade) - planId String + subscriptionPlan SubscriptionPlan @relation(fields: [subscriptionPlanId], references: [id], onUpdate: Cascade, onDelete: Cascade) + subscriptionPlanId String appointments Appointment[] diff --git a/prisma/seedFiles/createAppointments.ts b/prisma/seedFiles/createAppointments.ts index 3191be0..fb633ab 100644 --- a/prisma/seedFiles/createAppointments.ts +++ b/prisma/seedFiles/createAppointments.ts @@ -5,6 +5,8 @@ import { Platform, WebinarStatus, ClassStatus, + SlotOfAvailabilityWeekly, + SlotOfAvailabilityCustom, } from "@prisma/client"; import prisma from "../../lib/prisma"; import { UserWithProfiles } from "./createUsers"; @@ -22,6 +24,16 @@ type SubscriptionCreate = NonNullable< type WebinarCreate = NonNullable["create"]; type ClassCreate = NonNullable["create"]; +type SlotData = + | { + type: "weekly"; + slot: SlotOfAvailabilityWeekly; + } + | { + type: "custom"; + slot: SlotOfAvailabilityCustom; + }; + // Helper function to generate tentative schedule function generateTentativeSchedule( startDate: Date, @@ -60,7 +72,7 @@ export async function createAppointments(consultees: UserWithProfiles[]) { const webinarPlans = await prisma.webinarPlan.findMany(); const classPlans = await prisma.classPlan.findMany(); - const allSlots = [ + const allSlots: SlotData[] = [ ...weeklySlots.map((slot) => ({ type: "weekly" as const, slot })), ...customSlots.map((slot) => ({ type: "custom" as const, slot })), ]; @@ -154,7 +166,7 @@ export async function createAppointments(consultees: UserWithProfiles[]) { case AppointmentsType.SUBSCRIPTION: const subscriptionData: SubscriptionCreate = { - plan: { + subscriptionPlan: { connect: { id: faker.helpers.arrayElement(subscriptionPlans).id, }, diff --git a/prisma/seedFiles/createConsultantReviews.ts b/prisma/seedFiles/createConsultantReviews.ts index 071fd3c..b46652f 100644 --- a/prisma/seedFiles/createConsultantReviews.ts +++ b/prisma/seedFiles/createConsultantReviews.ts @@ -1,7 +1,18 @@ import { faker } from "@faker-js/faker"; +import { Prisma } from "@prisma/client"; import prisma from "../../lib/prisma"; import { UserWithProfiles } from "./createUsers"; +type CompletedAppointment = Prisma.AppointmentGetPayload<{ + include: { + slotOfAppointment: { + include: { + consulteeProfile: true; + }; + }; + }; +}>; + export async function createConsultantReviews( consultants: UserWithProfiles[], consultees: UserWithProfiles[], @@ -30,7 +41,7 @@ export async function createConsultantReviews( }, { subscription: { - plan: { + subscriptionPlan: { consultantProfile: { id: consultant.consultantProfile.id }, }, requestStatus: "APPROVED", @@ -68,15 +79,21 @@ export async function createConsultantReviews( min: 1, max: Math.min(5, completedAppointments.length), }); - const appointmentsToReview = faker.helpers.arrayElements( - completedAppointments, - numReviews, - ); + const appointmentsToReview = + faker.helpers.arrayElements( + completedAppointments, + numReviews, + ); for (const appointment of appointmentsToReview) { const consulteeProfile = appointment.slotOfAppointment[0]?.consulteeProfile; - if (!consulteeProfile) continue; + if (!consulteeProfile) { + console.warn( + `Skipping review - no consultee profile found for appointment ${appointment.id}`, + ); + continue; + } const rating = faker.number.int({ min: 1, max: 5 }); @@ -97,21 +114,30 @@ export async function createConsultantReviews( select: { rating: true }, }); - const averageRating = - allReviews.reduce((acc, review) => acc + review.rating, 0) / - allReviews.length; + if (allReviews.length > 0) { + const totalRating = allReviews.reduce( + (acc, review) => acc + review.rating, + 0, + ); + const averageRating = Number( + (totalRating / allReviews.length).toFixed(2), + ); - await prisma.consultantProfile.update({ - where: { id: consultant.consultantProfile.id }, - data: { rating: averageRating }, - }); + await prisma.consultantProfile.update({ + where: { id: consultant.consultantProfile.id }, + data: { rating: averageRating }, + }); + } totalReviews++; + if (totalReviews % 10 === 0) { + console.log(`Created ${totalReviews} reviews so far...`); + } } } catch (error) { console.error( `Failed to create reviews for consultant ${consultant.id}:`, - error, + error instanceof Error ? error.message : String(error), ); } } diff --git a/prisma/seedFiles/createPayments.ts b/prisma/seedFiles/createPayments.ts index 98d4714..9305d91 100644 --- a/prisma/seedFiles/createPayments.ts +++ b/prisma/seedFiles/createPayments.ts @@ -1,10 +1,40 @@ import { faker } from "@faker-js/faker"; -import { PaymentGateway, PaymentStatus, Prisma } from "@prisma/client"; +import { + PaymentGateway, + PaymentStatus, + Prisma, + DiscountType, +} from "@prisma/client"; import prisma from "../../lib/prisma"; import { UserWithProfiles } from "./createUsers"; const NUM_PAYMENTS = 100; +type AppointmentWithPlans = Prisma.AppointmentGetPayload<{ + include: { + consultation: { + include: { + consultationPlan: true; + }; + }; + subscription: { + include: { + subscriptionPlan: true; + }; + }; + webinar: { + include: { + webinarPlan: true; + }; + }; + class: { + include: { + classPlan: true; + }; + }; + }; +}>; + export async function createPayments(users: UserWithProfiles[]) { console.log(`Creating payments...`); const discountCodes = await prisma.discountCode.findMany(); @@ -26,7 +56,7 @@ export async function createPayments(users: UserWithProfiles[]) { }, subscription: { include: { - plan: true, + subscriptionPlan: true, }, }, webinar: { @@ -45,19 +75,25 @@ export async function createPayments(users: UserWithProfiles[]) { for (let i = 0; i < appointments.length; i++) { const user = faker.helpers.arrayElement(users); - const appointment = appointments[i]; + const appointment = appointments[i] as AppointmentWithPlans; try { // Determine the amount based on the appointment type and plan let amount = 0; + let description = ""; + if (appointment.consultation?.consultationPlan) { amount = appointment.consultation.consultationPlan.price; - } else if (appointment.subscription?.plan) { - amount = appointment.subscription.plan.price; + description = `Payment for Consultation: ${appointment.consultation.consultationPlan.title}`; + } else if (appointment.subscription?.subscriptionPlan) { + amount = appointment.subscription.subscriptionPlan.price; + description = `Payment for Subscription: ${appointment.subscription.subscriptionPlan.title}`; } else if (appointment.webinar?.webinarPlan) { amount = appointment.webinar.webinarPlan.price; + description = `Payment for Webinar: ${appointment.webinar.webinarPlan.title}`; } else if (appointment.class?.classPlan) { amount = appointment.class.classPlan.price; + description = `Payment for Class: ${appointment.class.classPlan.title}`; } // Apply discount if available @@ -65,15 +101,17 @@ export async function createPayments(users: UserWithProfiles[]) { const discountCode = useDiscount ? faker.helpers.arrayElement(discountCodes) : null; + + let finalAmount = amount; if (discountCode) { switch (discountCode.discountType) { - case "PERCENTAGE": - amount = Math.round( + case DiscountType.PERCENTAGE: + finalAmount = Math.round( amount * (1 - discountCode.discountValue / 100), ); break; - case "FIXED_AMOUNT": - amount = Math.max(0, amount - discountCode.discountValue); + case DiscountType.FIXED_AMOUNT: + finalAmount = Math.max(0, amount - discountCode.discountValue); break; // FREE_SHIPPING doesn't affect the amount in this context } @@ -81,9 +119,9 @@ export async function createPayments(users: UserWithProfiles[]) { const paymentData: Prisma.PaymentCreateInput = { user: { connect: { id: user.id } }, - amount: amount, + amount: finalAmount, currency: faker.helpers.arrayElement(["USD", "EUR", "GBP"]), - description: faker.lorem.sentence(), + description, receiptUrl: faker.internet.url(), paymentMethod: faker.helpers.arrayElement([ "credit_card", @@ -92,10 +130,12 @@ export async function createPayments(users: UserWithProfiles[]) { "wallet", ]), paymentIntent: faker.string.uuid(), - paymentGateway: faker.helpers.arrayElement( + paymentGateway: faker.helpers.arrayElement( Object.values(PaymentGateway), ), - paymentStatus: faker.helpers.arrayElement(Object.values(PaymentStatus)), + paymentStatus: faker.helpers.arrayElement( + Object.values(PaymentStatus), + ), appointment: { connect: { id: appointment.id } }, ...(discountCode ? { discountCode: { connect: { id: discountCode.id } } } @@ -106,11 +146,16 @@ export async function createPayments(users: UserWithProfiles[]) { data: paymentData, }); } catch (error) { - console.error(`Failed to create payment for user ${user.id}:`, error); + console.error( + `Failed to create payment for user ${user.id}:`, + error instanceof Error ? error.message : String(error), + ); } if ((i + 1) % 20 === 0 || i === appointments.length - 1) { console.log(`Created ${i + 1} payments`); } } + + console.log(`Finished creating payments`); } diff --git a/types/appointment.ts b/types/appointment.ts index b0f2ee6..7c5149e 100644 --- a/types/appointment.ts +++ b/types/appointment.ts @@ -23,7 +23,7 @@ export type TConsultation = Prisma.ConsultationGetPayload<{ // Custom type for Subscription with specific nesting depth export type TSubscription = Prisma.SubscriptionGetPayload<{ include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { @@ -58,7 +58,7 @@ export type TAppointment = Prisma.AppointmentGetPayload<{ }; subscription: { include: { - plan: { + subscriptionPlan: { include: { consultantProfile: { include: { From 2c26643d7febff813dddd4497b84ccd97e4de040 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 02:28:40 +0530 Subject: [PATCH 02/16] upadted packages --- app/api/payments/stripe/route.ts | 2 +- app/api/user/consultants/[id]/route.ts | 8 +- .../experts/[consultantId]/PricingToggle.tsx | 2 +- app/explore/experts/page.tsx | 6 +- package-lock.json | 2233 ++++++----------- package.json | 68 +- 6 files changed, 802 insertions(+), 1517 deletions(-) diff --git a/app/api/payments/stripe/route.ts b/app/api/payments/stripe/route.ts index e5fdce2..22b3916 100644 --- a/app/api/payments/stripe/route.ts +++ b/app/api/payments/stripe/route.ts @@ -29,7 +29,7 @@ interface UpdatePaymentRequest { const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, { - apiVersion: "2024-10-28.acacia", + apiVersion: "2024-11-20.acacia", }) : ({ paymentIntents: { diff --git a/app/api/user/consultants/[id]/route.ts b/app/api/user/consultants/[id]/route.ts index 662b514..898edb7 100644 --- a/app/api/user/consultants/[id]/route.ts +++ b/app/api/user/consultants/[id]/route.ts @@ -85,10 +85,10 @@ export async function GET( { params }: { params: Promise<{ id: string }> }, ) { try { - const session = await getServerSession(authOptions); - if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + // const session = await getServerSession(authOptions); + // if (!session) { + // return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + // } const { id } = await params; if (!id) { diff --git a/app/explore/experts/[consultantId]/PricingToggle.tsx b/app/explore/experts/[consultantId]/PricingToggle.tsx index ab70654..39b71c4 100644 --- a/app/explore/experts/[consultantId]/PricingToggle.tsx +++ b/app/explore/experts/[consultantId]/PricingToggle.tsx @@ -157,7 +157,7 @@ export default function PricingToggle({ ); } // Restrict access to consultees only using session - if (!session?.user?.role || session.user.role.toLowerCase() !== "consultee") { + if (session?.user?.role && ["consultant", "staff"].includes(session.user.role.toLowerCase())) { return (
diff --git a/app/explore/experts/page.tsx b/app/explore/experts/page.tsx index b8ef939..d18e3ba 100644 --- a/app/explore/experts/page.tsx +++ b/app/explore/experts/page.tsx @@ -119,7 +119,11 @@ function FindExperts() { ) || []; if (isLoading) { - return
Loading experts...
; + return ( +
+
+
+ ); } return ( diff --git a/package-lock.json b/package-lock.json index 32a07c6..48ea953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,17 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { - "@faker-js/faker": "^9.2.0", + "@faker-js/faker": "^9.3.0", "@hookform/resolvers": "^3.9.1", "@next-auth/prisma-adapter": "^1.0.7", - "@next/eslint-plugin-next": "^15.0.2", - "@prisma/client": "^5.21.1", + "@next/eslint-plugin-next": "^15.1.0", + "@prisma/client": "^6.0.1", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-icons": "^1.3.1", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-radio-group": "^1.2.1", @@ -29,62 +29,62 @@ "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", - "@reduxjs/toolkit": "^2.3.0", - "@supabase/supabase-js": "^2.46.1", + "@reduxjs/toolkit": "^2.5.0", + "@supabase/supabase-js": "^2.47.5", "@types/micromatch": "^4.0.9", - "axios": "^1.7.7", + "axios": "^1.7.9", "chance": "^1.1.12", - "class-variance-authority": "^0.7.0", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cmdk": "^1.0.0", + "cmdk": "^1.0.4", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", - "dotenv": "^16.4.5", + "dotenv": "^16.4.7", "enquirer": "^2.4.1", - "framer-motion": "^11.11.11", + "framer-motion": "^11.13.5", "install": "^0.13.0", - "lucide-react": "^0.454.0", + "lucide-react": "^0.468.0", "micromatch": "^4.0.8", - "next": "15.0.2", - "next-auth": "^4.24.10", - "npm": "^10.9.0", - "prettier": "^3.3.3", + "next": "15.1.0", + "next-auth": "^4.24.11", + "npm": "^10.9.2", + "prettier": "^3.4.2", "prompt-sync": "^4.2.0", "razorpay": "^2.9.5", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.53.1", - "react-icons": "^5.3.0", - "react-redux": "^9.1.2", + "react-hook-form": "^7.54.0", + "react-icons": "^5.4.0", + "react-redux": "^9.2.0", "react-slick": "^0.30.2", - "shadcn": "^2.1.5", + "shadcn": "^2.1.7", "slick-carousel": "^1.8.1", - "stripe": "^17.3.1", + "stripe": "^17.4.0", "swr": "^2.2.5", - "tailwind-merge": "^2.5.4", + "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", - "uuid": "^11.0.2", - "zod": "^3.23.8" + "uuid": "^11.0.3", + "zod": "^3.24.1" }, "devDependencies": { - "@eslint/js": "^9.14.0", + "@eslint/js": "^9.16.0", "@types/chance": "^1.1.6", - "@types/node": "^22.9.0", + "@types/node": "^22.10.2", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/react-slick": "^0.23.13", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.20", - "eslint": "^9.14.0", - "eslint-config-next": "^15.0.2", + "eslint": "^9.16.0", + "eslint-config-next": "^15.1.0", "eslint-plugin-react": "^7.37.2", - "globals": "^15.12.0", - "postcss": "^8.4.47", - "prisma": "^5.21.1", - "tailwindcss": "^3.4.14", + "globals": "^15.13.0", + "postcss": "^8.4.49", + "prisma": "^6.0.1", + "tailwindcss": "^3.4.16", "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "typescript-eslint": "^8.13.0" + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" } }, "node_modules/@alloc/quick-lru": { @@ -604,13 +604,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -619,19 +619,22 @@ } }, "node_modules/@eslint/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", - "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, "license": "MIT", "dependencies": { @@ -666,9 +669,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", - "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, "license": "MIT", "engines": { @@ -676,9 +679,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -686,9 +689,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -699,9 +702,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.2.0.tgz", - "integrity": "sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.3.0.tgz", + "integrity": "sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==", "funding": [ { "type": "opencollective", @@ -715,26 +718,26 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.3.tgz", - "integrity": "sha512-1ZpCvYf788/ZXOhRQGFxnYQOVgeU+pi0i+d0Ow34La7qjIXETi6RNswGVKkA6KcDO8/+Ysu2E/CeUmmeEBDvTg==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", "dependencies": { - "@floating-ui/utils": "^0.2.3" + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.6.tgz", - "integrity": "sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw==", + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.3" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", - "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "dependencies": { "@floating-ui/dom": "^1.0.0" }, @@ -744,9 +747,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.3.tgz", - "integrity": "sha512-XGndio0l5/Gvd6CLIABvsav9HHezgDFFhDfHk1bvLfr9ni8dojqLSvBbotJEjmIwNHL7vK4QzBJTdBRoB+c1ww==" + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, "node_modules/@hookform/resolvers": { "version": "3.9.1", @@ -1263,15 +1266,15 @@ } }, "node_modules/@next/env": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.2.tgz", - "integrity": "sha512-c0Zr0ModK5OX7D4ZV8Jt/wqoXtitLNPwUfG9zElCZztdaZyNVnN40rDXVZ/+FGuR4CcNV5AEfM6N8f+Ener7Dg==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.0.tgz", + "integrity": "sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.0.2.tgz", - "integrity": "sha512-R9Jc7T6Ge0txjmqpPwqD8vx6onQjynO9JT73ArCYiYPvSrwYXepH/UY/WdKDY8JPWJl72sAE4iGMHPeQ5xdEWg==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.0.tgz", + "integrity": "sha512-+jPT0h+nelBT6HC9ZCHGc7DgGVy04cv4shYdAe6tKlEbjQUtwU3LzQhzbDHQyY2m6g39m6B0kOFVuLGBrxxbGg==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" @@ -1306,9 +1309,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.2.tgz", - "integrity": "sha512-GK+8w88z+AFlmt+ondytZo2xpwlfAR8U6CRwXancHImh6EdGfHMIrTSCcx5sOSBei00GyLVL0ioo1JLKTfprgg==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.0.tgz", + "integrity": "sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==", "cpu": [ "arm64" ], @@ -1322,9 +1325,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.2.tgz", - "integrity": "sha512-KUpBVxIbjzFiUZhiLIpJiBoelqzQtVZbdNNsehhUn36e2YzKHphnK8eTUW1s/4aPy5kH/UTid8IuVbaOpedhpw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.0.tgz", + "integrity": "sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==", "cpu": [ "x64" ], @@ -1338,9 +1341,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.2.tgz", - "integrity": "sha512-9J7TPEcHNAZvwxXRzOtiUvwtTD+fmuY0l7RErf8Yyc7kMpE47MIQakl+3jecmkhOoIyi/Rp+ddq7j4wG6JDskQ==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.0.tgz", + "integrity": "sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==", "cpu": [ "arm64" ], @@ -1354,9 +1357,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.2.tgz", - "integrity": "sha512-BjH4ZSzJIoTTZRh6rG+a/Ry4SW0HlizcPorqNBixBWc3wtQtj4Sn9FnRZe22QqrPnzoaW0ctvSz4FaH4eGKMww==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.0.tgz", + "integrity": "sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==", "cpu": [ "arm64" ], @@ -1370,9 +1373,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.2.tgz", - "integrity": "sha512-i3U2TcHgo26sIhcwX/Rshz6avM6nizrZPvrDVDY1bXcLH1ndjbO8zuC7RoHp0NSK7wjJMPYzm7NYL1ksSKFreA==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.0.tgz", + "integrity": "sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==", "cpu": [ "x64" ], @@ -1386,9 +1389,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.2.tgz", - "integrity": "sha512-AMfZfSVOIR8fa+TXlAooByEF4OB00wqnms1sJ1v+iu8ivwvtPvnkwdzzFMpsK5jA2S9oNeeQ04egIWVb4QWmtQ==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.0.tgz", + "integrity": "sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==", "cpu": [ "x64" ], @@ -1402,9 +1405,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.2.tgz", - "integrity": "sha512-JkXysDT0/hEY47O+Hvs8PbZAeiCQVxKfGtr4GUpNAhlG2E0Mkjibuo8ryGD29Qb5a3IOnKYNoZlh/MyKd2Nbww==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.0.tgz", + "integrity": "sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==", "cpu": [ "arm64" ], @@ -1418,9 +1421,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.2.tgz", - "integrity": "sha512-foaUL0NqJY/dX0Pi/UcZm5zsmSk5MtP/gxx3xOPyREkMFN+CTjctPfu3QaqrQHinaKdPnMWPJDKt4VjDfTBe/Q==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.0.tgz", + "integrity": "sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==", "cpu": [ "x64" ], @@ -1493,13 +1496,13 @@ } }, "node_modules/@prisma/client": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.21.1.tgz", - "integrity": "sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.0.1.tgz", + "integrity": "sha512-60w7kL6bUxz7M6Gs/V+OWMhwy94FshpngVmOY05TmGD0Lhk+Ac0ZgtjlL6Wll9TD4G03t4Sq1wZekNVy+Xdlbg==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "peerDependencies": { "prisma": "*" @@ -1511,53 +1514,53 @@ } }, "node_modules/@prisma/debug": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.21.1.tgz", - "integrity": "sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.0.1.tgz", + "integrity": "sha512-jQylgSOf7ibTVxqBacnAlVGvek6fQxJIYCQOeX2KexsfypNzXjJQSS2o5s+Mjj2Np93iSOQUaw6TvPj8syhG4w==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.21.1.tgz", - "integrity": "sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.0.1.tgz", + "integrity": "sha512-4hxzI+YQIR2uuDyVsDooFZGu5AtixbvM2psp+iayDZ4hRrAHo/YwgA17N23UWq7G6gRu18NvuNMb48qjP3DPQw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.21.1", - "@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", - "@prisma/fetch-engine": "5.21.1", - "@prisma/get-platform": "5.21.1" + "@prisma/debug": "6.0.1", + "@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "@prisma/fetch-engine": "6.0.1", + "@prisma/get-platform": "6.0.1" } }, "node_modules/@prisma/engines-version": { - "version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz", - "integrity": "sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q==", + "version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e.tgz", + "integrity": "sha512-JmIds0Q2/vsOmnuTJYxY4LE+sajqjYKhLtdOT6y4imojqv5d/aeVEfbBGC74t8Be1uSp0OP8lxIj2OqoKbLsfQ==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.21.1.tgz", - "integrity": "sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.0.1.tgz", + "integrity": "sha512-T36bWFVGeGYYSyYOj9d+O9G3sBC+pAyMC+jc45iSL63/Haq1GrYjQPgPMxrEj9m739taXrupoysRedQ+VyvM/Q==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.21.1", - "@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36", - "@prisma/get-platform": "5.21.1" + "@prisma/debug": "6.0.1", + "@prisma/engines-version": "5.23.0-27.5dbef10bdbfb579e07d35cc85fb1518d357cb99e", + "@prisma/get-platform": "6.0.1" } }, "node_modules/@prisma/get-platform": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.21.1.tgz", - "integrity": "sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.0.1.tgz", + "integrity": "sha512-zspC9vlxAqx4E6epMPMLLBMED2VD8axDe8sPnquZ8GOsn6tiacWK0oxrGK4UAHYzYUVuMVUApJbdXB2dFpLhvg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.21.1" + "@prisma/debug": "6.0.1" } }, "node_modules/@radix-ui/number": { @@ -1728,7 +1731,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz", "integrity": "sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==", - "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -1758,7 +1760,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -1826,7 +1827,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", - "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -1862,7 +1862,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -1891,7 +1890,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", - "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -1998,12 +1996,12 @@ } }, "node_modules/@radix-ui/react-icons": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.1.tgz", - "integrity": "sha512-QvYompk0X+8Yjlo/Fv4McrzxohDdM5GgLHyQcPpcsPvlOSXCGFjdbuyGL5dzRbg0GpknAjQJJZzdiRK7iWVuFQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", "license": "MIT", "peerDependencies": { - "react": "^16.x || ^17.x || ^18.x || ^19.x" + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, "node_modules/@radix-ui/react-id": { @@ -2050,7 +2048,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz", "integrity": "sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==", - "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -2090,7 +2087,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2188,7 +2184,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", - "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -2212,7 +2207,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", - "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -2603,7 +2597,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -2707,9 +2700,9 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" }, "node_modules/@reduxjs/toolkit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.3.0.tgz", - "integrity": "sha512-WC7Yd6cNGfHx8zf+iu+Q1UPTfEcXhQ+ATi7CV1hlrSAaQBdlPzg7Ww/wJHNQem7qG9rxmWoFCDCPubSvFObGzA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", + "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", "license": "MIT", "dependencies": { "immer": "^10.0.3", @@ -2718,7 +2711,7 @@ "reselect": "^5.1.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18", + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "peerDependenciesMeta": { @@ -2744,9 +2737,9 @@ "dev": true }, "node_modules/@supabase/auth-js": { - "version": "2.65.1", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.1.tgz", - "integrity": "sha512-IA7i2Xq2SWNCNMKxwmPlHafBQda0qtnFr8QnyyBr+KaSxoXXqEzFCnQ1dGTy6bsZjVBgXu++o3qrDypTspaAPw==", + "version": "2.66.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.66.1.tgz", + "integrity": "sha512-kOW+04SuDXmP2jRX9JL1Rgzduj8BcOG1qC3RaWdZsxnv89svNCdLRv8PfXW3QPKJdw0k1jF30OlQDPkzbDEL9w==", "license": "MIT", "dependencies": { "@supabase/node-fetch": "^2.6.14" @@ -2783,15 +2776,15 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.10.7", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.7.tgz", - "integrity": "sha512-OLI0hiSAqQSqRpGMTUwoIWo51eUivSYlaNBgxsXZE7PSoWh12wPRdVt0psUMaUzEonSB85K21wGc7W5jHnT6uA==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", "license": "MIT", "dependencies": { "@supabase/node-fetch": "^2.6.14", "@types/phoenix": "^1.5.4", "@types/ws": "^8.5.10", - "ws": "^8.14.2" + "ws": "^8.18.0" } }, "node_modules/@supabase/storage-js": { @@ -2804,16 +2797,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.46.1.tgz", - "integrity": "sha512-HiBpd8stf7M6+tlr+/82L8b2QmCjAD8ex9YdSAKU+whB/SHXXJdus1dGlqiH9Umy9ePUuxaYmVkGd9BcvBnNvg==", + "version": "2.47.5", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.47.5.tgz", + "integrity": "sha512-Xd4L2HR0pdZ+rBnoIi33CJ0K0q9BU9b42NaR13jDkiSYGbxJlZdYtXkTEhyVMQBXnYVvUHXM53q54iw/lgFrHA==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.65.1", + "@supabase/auth-js": "2.66.1", "@supabase/functions-js": "2.4.3", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.16.3", - "@supabase/realtime-js": "2.10.7", + "@supabase/realtime-js": "2.11.2", "@supabase/storage-js": "2.7.1" } }, @@ -2824,12 +2817,12 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", - "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@ts-morph/common": { @@ -2933,45 +2926,43 @@ } }, "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/phoenix": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz", - "integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "devOptional": true }, "node_modules/@types/react": { - "version": "18.3.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", - "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "version": "18.3.16", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz", + "integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==", "devOptional": true, - "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", + "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^18.0.0" } }, "node_modules/@types/react-slick": { @@ -2984,9 +2975,10 @@ } }, "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" }, "node_modules/@types/uuid": { "version": "10.0.0", @@ -2995,26 +2987,26 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", - "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", + "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/type-utils": "8.13.0", - "@typescript-eslint/utils": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/type-utils": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3029,25 +3021,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", - "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz", + "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MITClause", "dependencies": { - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4" }, "engines": { @@ -3058,23 +3046,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz", - "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0" + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3085,14 +3069,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", - "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", + "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/utils": "8.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3103,16 +3087,15 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz", - "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", "dev": true, "license": "MIT", "engines": { @@ -3124,14 +3107,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz", - "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3146,10 +3129,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -3179,16 +3160,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.13.0.tgz", - "integrity": "sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0" + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3198,18 +3179,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz", - "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.18.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3219,6 +3201,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3603,9 +3598,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -3869,23 +3864,15 @@ } }, "node_modules/class-variance-authority": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", - "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "license": "Apache-2.0", "dependencies": { - "clsx": "2.0.0" + "clsx": "^2.1.1" }, "funding": { - "url": "https://joebell.co.uk" - } - }, - "node_modules/class-variance-authority/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" + "url": "https://polar.sh/cva" } }, "node_modules/classnames": { @@ -3944,510 +3931,149 @@ } }, "node_modules/cmdk": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", - "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.4.tgz", + "integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==", "license": "MIT", "dependencies": { - "@radix-ui/react-dialog": "1.0.5", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.0", + "use-sync-external-store": "^1.2.2" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/cmdk/node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", + "optional": true, "dependencies": { - "@babel/runtime": "^7.13.10" + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" } }, - "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "license": "MIT", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "color-name": "~1.1.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=7.0.0" } }, - "node_modules/cmdk/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", + "optional": true, "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" } }, - "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", - "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">= 0.8" } }, - "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">= 0.6" } }, - "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "typescript": ">=4.9.5" }, "peerDependenciesMeta": { - "@types/react": { + "typescript": { "optional": true } } }, - "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/cmdk/node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/code-block-writer": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", - "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", - "license": "MIT" - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "license": "MIT", - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { "node": ">= 8" @@ -4457,6 +4083,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -4653,8 +4280,7 @@ "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, "node_modules/didyoumean": { "version": "1.2.2", @@ -4689,9 +4315,10 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -4934,27 +4561,27 @@ } }, "node_modules/eslint": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", - "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.14.0", - "@eslint/plugin-kit": "^0.2.0", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.16.0", + "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.0", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.5", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -4973,8 +4600,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -4995,13 +4621,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.0.2.tgz", - "integrity": "sha512-N8o6cyUXzlMmQbdc2Kc83g1qomFi3ITqrAZfubipVKET2uR2mCStyGRcx/r8WiAIVMul2KfwRiCHBkTpBvGBmA==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.0.tgz", + "integrity": "sha512-gADO+nKVseGso3DtOrYX9H7TxB/MuX7AUYhMlvQMqLYvUWu4HrOQuU7cC1HW74tHIqkAvXdwgAz3TCbczzSEXw==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.0.2", + "@next/eslint-plugin-next": "15.1.0", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -5009,7 +4635,7 @@ "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { @@ -5293,9 +4919,9 @@ } }, "node_modules/eslint/node_modules/@humanwhocodes/retry": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz", - "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5655,17 +5281,19 @@ } }, "node_modules/framer-motion": { - "version": "11.11.11", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.11.tgz", - "integrity": "sha512-tuDH23ptJAKUHGydJQII9PhABNJBpB+z0P1bmgKK9QFIssHGlfPd6kxMq00LSKwE27WFsb2z0ovY0bpUyMvfRw==", + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.13.5.tgz", + "integrity": "sha512-rArI0zPU9VkpS3Wt0J7dmRxAFUWtzPWoSofNQAP0UO276CmJ+Xlf5xN19GMw3w2QsdrS2sU+0+Q2vtuz4IEZaw==", "license": "MIT", "dependencies": { + "motion-dom": "^11.13.0", + "motion-utils": "^11.13.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -5772,7 +5400,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", "engines": { "node": ">=6" } @@ -5886,9 +5513,9 @@ } }, "node_modules/globals": { - "version": "15.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", - "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "version": "15.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", + "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", "dev": true, "license": "MIT", "engines": { @@ -6797,11 +6424,15 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -6911,9 +6542,9 @@ } }, "node_modules/lucide-react": { - "version": "0.454.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz", - "integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==", + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" @@ -7027,6 +6658,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion-dom": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.13.0.tgz", + "integrity": "sha512-Oc1MLGJQ6nrvXccXA89lXtOqFyBmvHtaDcTRGT66o8Czl7nuA8BeHAd9MQV1pQKX0d2RHFBFaw5g3k23hQJt0w==" + }, + "node_modules/motion-utils": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.13.0.tgz", + "integrity": "sha512-lq6TzXkH5c/ysJQBxgLXgM01qwBH1b4goTPh57VvZWJbVJZF/0SB31UWEn4EIqbVPf3au88n2rvK17SpDTja1A==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7066,14 +6707,14 @@ "dev": true }, "node_modules/next": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/next/-/next-15.0.2.tgz", - "integrity": "sha512-rxIWHcAu4gGSDmwsELXacqAPUk+j8dV/A9cDF5fsiCMpkBDYkO2AEaL1dfD+nNmDiU6QMCFN8Q30VEKapT9UHQ==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.0.tgz", + "integrity": "sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==", "license": "MIT", "dependencies": { - "@next/env": "15.0.2", + "@next/env": "15.1.0", "@swc/counter": "0.1.3", - "@swc/helpers": "0.5.13", + "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -7083,25 +6724,25 @@ "next": "dist/bin/next" }, "engines": { - "node": ">=18.18.0" + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.0.2", - "@next/swc-darwin-x64": "15.0.2", - "@next/swc-linux-arm64-gnu": "15.0.2", - "@next/swc-linux-arm64-musl": "15.0.2", - "@next/swc-linux-x64-gnu": "15.0.2", - "@next/swc-linux-x64-musl": "15.0.2", - "@next/swc-win32-arm64-msvc": "15.0.2", - "@next/swc-win32-x64-msvc": "15.0.2", + "@next/swc-darwin-arm64": "15.1.0", + "@next/swc-darwin-x64": "15.1.0", + "@next/swc-linux-arm64-gnu": "15.1.0", + "@next/swc-linux-arm64-musl": "15.1.0", + "@next/swc-linux-x64-gnu": "15.1.0", + "@next/swc-linux-x64-musl": "15.1.0", + "@next/swc-win32-arm64-msvc": "15.1.0", + "@next/swc-win32-x64-msvc": "15.1.0", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-02c0e824-20241028", - "react-dom": "^18.2.0 || 19.0.0-rc-02c0e824-20241028", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "peerDependenciesMeta": { @@ -7120,9 +6761,9 @@ } }, "node_modules/next-auth": { - "version": "4.24.10", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.10.tgz", - "integrity": "sha512-8NGqiRO1GXBcVfV8tbbGcUgQkAGsX4GRzzXXea4lDikAsJtD5KiEY34bfhUOjHLvr6rT6afpcxw2H8EZqOV6aQ==", + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", "license": "ISC", "dependencies": { "@babel/runtime": "^7.20.13", @@ -7139,8 +6780,8 @@ "@auth/core": "0.34.2", "next": "^12.2.5 || ^13 || ^14 || ^15", "nodemailer": "^6.6.5", - "react": "^17.0.2 || ^18", - "react-dom": "^17.0.2 || ^18" + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" }, "peerDependenciesMeta": { "@auth/core": { @@ -7247,9 +6888,9 @@ } }, "node_modules/npm": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.0.tgz", - "integrity": "sha512-ZanDioFylI9helNhl2LNd+ErmVD+H5I53ry41ixlLyCBgkuYb+58CvbAp99hW+zr5L9W4X7CchSoeqKdngOLSw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.2.tgz", + "integrity": "sha512-iriPEPIkoMYUy3F6f3wwSZAU93E0Eg6cHwIR6jzzOXWSy+SD/rOODEs74cVONHKSx2obXtuUoyidVEhISrisgQ==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -7333,25 +6974,25 @@ "@npmcli/arborist": "^8.0.0", "@npmcli/config": "^9.0.0", "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", - "@npmcli/promise-spawn": "^8.0.1", + "@npmcli/map-workspaces": "^4.0.2", + "@npmcli/package-json": "^6.1.0", + "@npmcli/promise-spawn": "^8.0.2", "@npmcli/redact": "^3.0.0", "@npmcli/run-script": "^9.0.1", - "@sigstore/tuf": "^2.3.4", + "@sigstore/tuf": "^3.0.0", "abbrev": "^3.0.0", "archy": "~1.0.0", "cacache": "^19.0.1", "chalk": "^5.3.0", - "ci-info": "^4.0.0", + "ci-info": "^4.1.0", "cli-columns": "^4.0.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", "glob": "^10.4.5", "graceful-fs": "^4.2.11", - "hosted-git-info": "^8.0.0", + "hosted-git-info": "^8.0.2", "ini": "^5.0.0", - "init-package-json": "^7.0.1", + "init-package-json": "^7.0.2", "is-cidr": "^5.1.0", "json-parse-even-better-errors": "^4.0.0", "libnpmaccess": "^9.0.0", @@ -7361,27 +7002,27 @@ "libnpmhook": "^11.0.0", "libnpmorg": "^7.0.0", "libnpmpack": "^8.0.0", - "libnpmpublish": "^10.0.0", + "libnpmpublish": "^10.0.1", "libnpmsearch": "^8.0.0", "libnpmteam": "^7.0.0", "libnpmversion": "^7.0.0", - "make-fetch-happen": "^14.0.1", + "make-fetch-happen": "^14.0.3", "minimatch": "^9.0.5", "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^10.2.0", + "node-gyp": "^11.0.0", "nopt": "^8.0.0", "normalize-package-data": "^7.0.0", "npm-audit-report": "^6.0.0", - "npm-install-checks": "^7.1.0", + "npm-install-checks": "^7.1.1", "npm-package-arg": "^12.0.0", "npm-pick-manifest": "^10.0.0", "npm-profile": "^11.0.1", - "npm-registry-fetch": "^18.0.1", + "npm-registry-fetch": "^18.0.2", "npm-user-validate": "^3.0.0", "p-map": "^4.0.0", - "pacote": "^19.0.0", + "pacote": "^19.0.1", "parse-conflict-json": "^4.0.0", "proc-log": "^5.0.0", "qrcode-terminal": "^0.12.0", @@ -7450,7 +7091,7 @@ } }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", + "version": "6.1.0", "inBundle": true, "license": "MIT", "engines": { @@ -7638,7 +7279,7 @@ } }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "4.0.1", + "version": "4.0.2", "inBundle": true, "license": "ISC", "dependencies": { @@ -7652,13 +7293,13 @@ } }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "8.0.0", + "version": "8.0.1", "inBundle": true, "license": "ISC", "dependencies": { "cacache": "^19.0.0", "json-parse-even-better-errors": "^4.0.0", - "pacote": "^19.0.0", + "pacote": "^20.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5" }, @@ -7666,287 +7307,142 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "6.0.1", + "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": { + "version": "20.0.0", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "normalize-package-data": "^7.0.0", - "proc-log": "^5.0.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/redact": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^4.0.0", + "@npmcli/installed-package-contents": "^3.0.0", "@npmcli/package-json": "^6.0.0", "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^10.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", "proc-log": "^5.0.0", - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "2.3.2", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/core": { - "version": "1.1.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.3.2", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign": { - "version": "2.3.2", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^13.0.1", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1" + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/@npmcli/agent": { - "version": "2.2.2", + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", "inBundle": true, "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/@npmcli/fs": { - "version": "3.1.1", + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", "inBundle": true, "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/cacache": { - "version": "18.0.4", + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "6.1.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", + "@npmcli/git": "^6.0.0", "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "normalize-package-data": "^7.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/make-fetch-happen": { - "version": "13.0.1", + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" + "which": "^5.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/minipass-fetch": { - "version": "3.0.5", + "node_modules/npm/node_modules/@npmcli/query": { + "version": "4.0.0", "inBundle": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "postcss-selector-parser": "^6.1.2" }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "4.2.0", - "inBundle": true, - "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/ssri": { - "version": "10.0.6", + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "3.0.0", "inBundle": true, "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/unique-filename": { - "version": "3.0.0", + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "9.0.2", "inBundle": true, "license": "ISC", "dependencies": { - "unique-slug": "^4.0.0" + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/unique-slug": { - "version": "4.0.0", + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, + "license": "MIT", + "optional": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=14" } }, - "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "2.3.4", + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.3.2", "inBundle": true, "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^2.2.1" - }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@sigstore/verify": { - "version": "1.2.1", + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.2" + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^3.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/@tufjs/canonical-json": { @@ -7957,18 +7453,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@tufjs/models": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/npm/node_modules/abbrev": { "version": "3.0.0", "inBundle": true, @@ -8179,7 +7663,7 @@ } }, "node_modules/npm/node_modules/ci-info": { - "version": "4.0.0", + "version": "4.1.0", "funding": [ { "type": "github", @@ -8253,7 +7737,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", "inBundle": true, "license": "MIT", "dependencies": { @@ -8291,11 +7775,11 @@ } }, "node_modules/npm/node_modules/debug": { - "version": "4.3.6", + "version": "4.3.7", "inBundle": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -8306,11 +7790,6 @@ } } }, - "node_modules/npm/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/diff": { "version": "5.2.0", "inBundle": true, @@ -8415,7 +7894,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/hosted-git-info": { - "version": "8.0.0", + "version": "8.0.2", "inBundle": true, "license": "ISC", "dependencies": { @@ -8502,7 +7981,7 @@ } }, "node_modules/npm/node_modules/init-package-json": { - "version": "7.0.1", + "version": "7.0.2", "inBundle": true, "license": "ISC", "dependencies": { @@ -8560,11 +8039,6 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/is-lambda": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/isexe": { "version": "2.0.0", "inBundle": true, @@ -8723,7 +8197,7 @@ } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "10.0.0", + "version": "10.0.1", "inBundle": true, "license": "ISC", "dependencies": { @@ -8733,7 +8207,7 @@ "npm-registry-fetch": "^18.0.1", "proc-log": "^5.0.0", "semver": "^7.3.7", - "sigstore": "^2.2.0", + "sigstore": "^3.0.0", "ssri": "^12.0.0" }, "engines": { @@ -8784,7 +8258,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/make-fetch-happen": { - "version": "14.0.1", + "version": "14.0.3", "inBundle": true, "license": "ISC", "dependencies": { @@ -8795,7 +8269,7 @@ "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", + "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" @@ -8804,6 +8278,14 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/npm/node_modules/minimatch": { "version": "9.0.5", "inBundle": true, @@ -8978,16 +8460,8 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/negotiator": { - "version": "0.6.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/npm/node_modules/node-gyp": { - "version": "10.2.0", + "version": "11.0.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -8995,189 +8469,76 @@ "exponential-backoff": "^3.1.1", "glob": "^10.3.10", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^4.1.0", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", "semver": "^7.3.5", - "tar": "^6.2.1", - "which": "^4.0.0" + "tar": "^7.4.3", + "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": { - "version": "2.2.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/fs": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { - "version": "18.0.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { - "version": "13.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minipass-fetch": { - "version": "3.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { - "version": "7.2.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/proc-log": { - "version": "4.2.0", + "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/ssri": { - "version": "10.0.6", + "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { + "version": "3.0.1", "inBundle": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "minipass": "^7.0.3" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 18" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-filename": { - "version": "3.0.0", + "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-slug": { - "version": "4.0.0", + "node_modules/npm/node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", "inBundle": true, "license": "ISC", "dependencies": { - "imurmurhash": "^0.1.4" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", + "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">=18" } }, "node_modules/npm/node_modules/nopt": { @@ -9235,7 +8596,7 @@ } }, "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.0", + "version": "7.1.1", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -9305,7 +8666,7 @@ } }, "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "18.0.1", + "version": "18.0.2", "inBundle": true, "license": "ISC", "dependencies": { @@ -9357,12 +8718,12 @@ } }, "node_modules/npm/node_modules/package-json-from-dist": { - "version": "1.0.0", + "version": "1.0.1", "inBundle": true, "license": "BlueOak-1.0.0" }, "node_modules/npm/node_modules/pacote": { - "version": "19.0.0", + "version": "19.0.1", "inBundle": true, "license": "ISC", "dependencies": { @@ -9380,7 +8741,7 @@ "npm-registry-fetch": "^18.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "sigstore": "^2.2.0", + "sigstore": "^3.0.0", "ssri": "^12.0.0", "tar": "^6.1.11" }, @@ -9464,7 +8825,7 @@ } }, "node_modules/npm/node_modules/promise-call-limit": { - "version": "3.0.1", + "version": "3.0.2", "inBundle": true, "license": "ISC", "funding": { @@ -9607,19 +8968,67 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "2.3.1", + "version": "3.0.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^2.3.2", - "@sigstore/tuf": "^2.3.4", - "@sigstore/verify": "^1.2.1" + "@sigstore/sign": "^3.0.0", + "@sigstore/tuf": "^3.0.0", + "@sigstore/verify": "^2.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/bundle": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/core": { + "version": "2.0.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/sign": { + "version": "3.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^14.0.1", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/verify": { + "version": "2.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/smart-buffer": { @@ -9690,7 +9099,7 @@ } }, "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.18", + "version": "3.0.20", "inBundle": true, "license": "CC0-1.0" }, @@ -9836,143 +9245,28 @@ } }, "node_modules/npm/node_modules/tuf-js": { - "version": "2.2.1", + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { - "@tufjs/models": "2.0.1", - "debug": "^4.3.4", - "make-fetch-happen": "^13.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/@npmcli/agent": { - "version": "2.2.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/@npmcli/fs": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/cacache": { - "version": "18.0.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/make-fetch-happen": { - "version": "13.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/tuf-js/node_modules/minipass-fetch": { - "version": "3.0.5", + "node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": { + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/proc-log": { - "version": "4.2.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/ssri": { - "version": "10.0.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/unique-filename": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/unique-slug": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/unique-filename": { @@ -10103,7 +9397,7 @@ } }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", + "version": "6.1.0", "inBundle": true, "license": "MIT", "engines": { @@ -10568,9 +9862,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -10588,7 +9882,7 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -10663,39 +9957,36 @@ } } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10739,9 +10030,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -10759,20 +10050,20 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, "node_modules/prisma": { - "version": "5.21.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.21.1.tgz", - "integrity": "sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.0.1.tgz", + "integrity": "sha512-CaMNFHkf+DDq8zq3X/JJsQ4Koy7dyWwwtOKibkT/Am9j/tDxcfbg7+lB1Dzhx18G/+RQCMgjPYB61bhRqteNBQ==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.21.1" + "@prisma/engines": "6.0.1" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "optionalDependencies": { "fsevents": "2.3.3" @@ -10901,7 +10192,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -10922,9 +10212,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.53.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", - "integrity": "sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==", + "version": "7.54.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz", + "integrity": "sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -10938,9 +10228,9 @@ } }, "node_modules/react-icons": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", - "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", "license": "MIT", "peerDependencies": { "react": "*" @@ -10954,16 +10244,17 @@ "license": "MIT" }, "node_modules/react-redux": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", - "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", "dependencies": { - "@types/use-sync-external-store": "^0.0.3", - "use-sync-external-store": "^1.0.0" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "@types/react": "^18.2.25", - "react": "^18.0", + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", "redux": "^5.0.0" }, "peerDependenciesMeta": { @@ -10979,7 +10270,6 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", - "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.6", "react-style-singleton": "^2.2.1", @@ -11004,7 +10294,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", - "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -11042,7 +10331,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", "invariant": "^2.2.4", @@ -11398,9 +10686,9 @@ } }, "node_modules/shadcn": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-2.1.5.tgz", - "integrity": "sha512-Up2Kx6qwX7o3iCcyj6vuENjDhXVqwiLtDYkdUJWWEziccNpLjnDIFyBcBAd6SCYylZlgU+HAy4IJ4T093O4D9A==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-2.1.7.tgz", + "integrity": "sha512-zSmHJEtVApNGLVau+smmynf9TL1Sl+UPDizAh2VQ3G/1vdZcaBW/czcZwzpwd0xf2kJTwCHNKmZBmP77Tdr2EQ==", "license": "MIT", "dependencies": { "@antfu/ni": "^0.21.4", @@ -11885,9 +11173,9 @@ } }, "node_modules/stripe": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-17.3.1.tgz", - "integrity": "sha512-E9/u+GFBPkYnTmfFCoKX3+gP4R3SkZoGunHe4cw9J+sqkj5uxpLFf1LscuI9BuEyIQ0PFAgPTHavgQwRtOvnag==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-17.4.0.tgz", + "integrity": "sha512-sQQGZguPxe7/QYXJKtDpfzT2OAH9F8nyE2SOsVdTU793iiU33/dpaKgWaJEGJm8396Yy/6NvTLblgdHlueGLhA==", "license": "MIT", "dependencies": { "@types/node": ">=8.1.0", @@ -11977,9 +11265,9 @@ } }, "node_modules/tailwind-merge": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", - "integrity": "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", + "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==", "license": "MIT", "funding": { "type": "github", @@ -11987,33 +11275,33 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", - "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", + "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -12050,12 +11338,6 @@ "node": ">=6" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -12188,9 +11470,10 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -12278,9 +11561,9 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -12292,15 +11575,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.13.0.tgz", - "integrity": "sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.0.tgz", + "integrity": "sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.13.0", - "@typescript-eslint/parser": "8.13.0", - "@typescript-eslint/utils": "8.13.0" + "@typescript-eslint/eslint-plugin": "8.18.0", + "@typescript-eslint/parser": "8.18.0", + "@typescript-eslint/utils": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12309,10 +11592,9 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/unbox-primitive": { @@ -12331,9 +11613,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, "node_modules/universalify": { @@ -12389,7 +11671,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", - "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -12410,7 +11691,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -12429,11 +11709,12 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/util-deprecate": { @@ -12442,9 +11723,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", - "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -12744,9 +12025,9 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 128656a..a31b758 100644 --- a/package.json +++ b/package.json @@ -20,17 +20,17 @@ "studio": "prisma studio" }, "dependencies": { - "@faker-js/faker": "^9.2.0", + "@faker-js/faker": "^9.3.0", "@hookform/resolvers": "^3.9.1", "@next-auth/prisma-adapter": "^1.0.7", - "@next/eslint-plugin-next": "^15.0.2", - "@prisma/client": "^5.21.1", + "@next/eslint-plugin-next": "^15.1.0", + "@prisma/client": "^6.0.1", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-icons": "^1.3.1", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-radio-group": "^1.2.1", @@ -40,61 +40,61 @@ "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", - "@reduxjs/toolkit": "^2.3.0", - "@supabase/supabase-js": "^2.46.1", + "@reduxjs/toolkit": "^2.5.0", + "@supabase/supabase-js": "^2.47.5", "@types/micromatch": "^4.0.9", - "axios": "^1.7.7", + "axios": "^1.7.9", "chance": "^1.1.12", - "class-variance-authority": "^0.7.0", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cmdk": "^1.0.0", + "cmdk": "^1.0.4", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", - "dotenv": "^16.4.5", + "dotenv": "^16.4.7", "enquirer": "^2.4.1", - "framer-motion": "^11.11.11", + "framer-motion": "^11.13.5", "install": "^0.13.0", - "lucide-react": "^0.454.0", + "lucide-react": "^0.468.0", "micromatch": "^4.0.8", - "next": "15.0.2", - "next-auth": "^4.24.10", - "npm": "^10.9.0", - "prettier": "^3.3.3", + "next": "15.1.0", + "next-auth": "^4.24.11", + "npm": "^10.9.2", + "prettier": "^3.4.2", "prompt-sync": "^4.2.0", "razorpay": "^2.9.5", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.53.1", - "react-icons": "^5.3.0", - "react-redux": "^9.1.2", + "react-hook-form": "^7.54.0", + "react-icons": "^5.4.0", + "react-redux": "^9.2.0", "react-slick": "^0.30.2", - "shadcn": "^2.1.5", + "shadcn": "^2.1.7", "slick-carousel": "^1.8.1", - "stripe": "^17.3.1", + "stripe": "^17.4.0", "swr": "^2.2.5", - "tailwind-merge": "^2.5.4", + "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", - "uuid": "^11.0.2", - "zod": "^3.23.8" + "uuid": "^11.0.3", + "zod": "^3.24.1" }, "devDependencies": { - "@eslint/js": "^9.14.0", + "@eslint/js": "^9.16.0", "@types/chance": "^1.1.6", - "@types/node": "^22.9.0", + "@types/node": "^22.10.2", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/react-slick": "^0.23.13", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.20", - "eslint": "^9.14.0", - "eslint-config-next": "^15.0.2", + "eslint": "^9.16.0", + "eslint-config-next": "^15.1.0", "eslint-plugin-react": "^7.37.2", - "globals": "^15.12.0", - "postcss": "^8.4.47", - "prisma": "^5.21.1", - "tailwindcss": "^3.4.14", + "globals": "^15.13.0", + "postcss": "^8.4.49", + "prisma": "^6.0.1", + "tailwindcss": "^3.4.16", "ts-node": "^10.9.2", - "typescript": "^5.6.3", - "typescript-eslint": "^8.13.0" + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" } } From 1abe0e229d2b5f62e4133ec3298423f46f868a87 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 03:21:55 +0530 Subject: [PATCH 03/16] Refactor consultant pricing and subscription options This commit refactors the code in the `defaults.ts` file in the `app/explore/experts/[consultantId]` directory. It introduces two interfaces, `PricingOption` and `defaultConsultationOptions`, and adds default pricing and subscription options. The pricing options include different durations and prices, while the subscription options provide access to a full suite of services for different durations, with additional features and discounts. --- .../experts/[consultantId]/PricingToggle.tsx | 224 +++++++----------- .../experts/[consultantId]/defaults.ts | 66 ++++++ 2 files changed, 158 insertions(+), 132 deletions(-) create mode 100644 app/explore/experts/[consultantId]/defaults.ts diff --git a/app/explore/experts/[consultantId]/PricingToggle.tsx b/app/explore/experts/[consultantId]/PricingToggle.tsx index 39b71c4..32f323e 100644 --- a/app/explore/experts/[consultantId]/PricingToggle.tsx +++ b/app/explore/experts/[consultantId]/PricingToggle.tsx @@ -18,17 +18,10 @@ import { } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { motion } from "framer-motion"; -import { ClockIcon } from "lucide-react"; +import { ClockIcon, X } from "lucide-react"; import { useSession } from "next-auth/react"; -import { useState } from "react"; - -interface PricingOption { - title: string; - description: string; - price: number; - duration?: string; - features?: string[]; -} +import { useState, useMemo } from "react"; +import { PricingOption, defaultConsultationOptions, defaultSubscriptionOptions } from "./defaults"; interface PricingToggleProps { consultationOptions?: PricingOption[]; @@ -52,76 +45,23 @@ const formatTime = (isoString: string): string => { if (isNaN(date.getTime())) { throw new Error("Invalid date"); } - return date.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - hour12: true, - }); + + let hours = date.getHours(); + const minutes = date.getMinutes(); + const ampm = hours >= 12 ? 'pm' : 'am'; + + hours = hours % 12; + hours = hours ? hours : 12; + + const minutesStr = minutes < 10 ? '0' + minutes : minutes; + + return `${hours}:${minutesStr} ${ampm}`; } catch (error) { console.error("Error formatting time:", error); return "Invalid Time"; } }; -const defaultConsultationOptions: PricingOption[] = [ - { - title: "One Hour", - description: "Get a quick consultation", - price: 99, - duration: "1 hour", - }, - { - title: "Two Hour", - description: "Dive deeper into your needs", - price: 199, - duration: "2 hours", - }, - { - title: "Three Hour", - description: "Comprehensive consultation", - price: 299, - duration: "3 hours", - }, -]; - -const defaultSubscriptionOptions: PricingOption[] = [ - { - title: "One Month Subscription", - description: "Get access to our full suite of services for one month.", - price: 49, - duration: "1 month", - features: [ - "Unlimited consultations", - "Priority support", - "Access to all tools and resources", - ], - }, - { - title: "Three Month Subscription", - description: "Get access to our full suite of services for three months.", - price: 129, - duration: "3 months", - features: [ - "Unlimited consultations", - "Priority support", - "Access to all tools and resources", - "10% discount on all services", - ], - }, - { - title: "Six Month Subscription", - description: "Get access to our full suite of services for six months.", - price: 249, - duration: "6 months", - features: [ - "Unlimited consultations", - "Priority support", - "Access to all tools and resources", - "15% discount on all services", - ], - }, -]; - export default function PricingToggle({ consultationOptions = defaultConsultationOptions, subscriptionOptions = defaultSubscriptionOptions, @@ -147,6 +87,24 @@ export default function PricingToggle({ : defaultSubscriptionOptions[0].title.toLowerCase().replace(" ", "-"), ); + // Sort slot timings by start time + const sortedSlotTimings = useMemo(() => { + if (!selectedDate || !slotTimings.length) return []; + + const selectedDay = selectedDate.getDay(); + + return slotTimings + .filter(slot => { + const slotDate = new Date(slot.slotStartTimeInUTC); + return slotDate.getDay() === selectedDay; + }) + .sort((a, b) => { + const dateA = new Date(a.slotStartTimeInUTC); + const dateB = new Date(b.slotStartTimeInUTC); + return dateA.getTime() - dateB.getTime(); + }); + }, [slotTimings, selectedDate]); + if (consultationOptions.length === 0 && subscriptionOptions.length === 0) { return (
@@ -156,7 +114,7 @@ export default function PricingToggle({
); } - // Restrict access to consultees only using session + if (session?.user?.role && ["consultant", "staff"].includes(session.user.role.toLowerCase())) { return (
@@ -184,7 +142,7 @@ export default function PricingToggle({ {consultationOptions.length > 0 && ( Consultation @@ -192,7 +150,7 @@ export default function PricingToggle({ {subscriptionOptions.length > 0 && ( Subscription @@ -211,7 +169,7 @@ export default function PricingToggle({ {option.title} @@ -240,35 +198,37 @@ export default function PricingToggle({ > - + {option.title} - + {option.description} -
+
${option.price}
- + + + Book Consultation +
{/* Calendar Section */}
-

- Select a - Date +

+ Select a Date

-
+
@@ -302,7 +262,7 @@ export default function PricingToggle({ ), ) } - className="text-gray-300 hover:text-gray-100" + className="text-white hover:text-gray-300" > > @@ -318,7 +278,7 @@ export default function PricingToggle({ "Sa", ].map((day) => (
{day} @@ -331,14 +291,13 @@ export default function PricingToggle({ {/* Time Slots Section */}
-

- Available Time - Slots +

+ Available Time Slots

-
- {slotTimings.length > 0 ? ( +
+ {sortedSlotTimings.length > 0 ? (
- {slotTimings.map((slot) => { + {sortedSlotTimings.map((slot) => { const startTime = formatTime( slot.slotStartTimeInUTC, ); @@ -348,7 +307,7 @@ export default function PricingToggle({ return (
- - +
+ + + - +

@@ -426,7 +384,7 @@ export default function PricingToggle({ {option.title.split(" ")[0]} {option.title.split(" ")[1]} @@ -455,30 +413,30 @@ export default function PricingToggle({ > - + {option.title} - + {option.description} -
+
${option.price} - + /{option.duration}
-

Includes:

+

Includes:

    {option.features?.map((feature, index) => (
  • - + Confirm Subscription - + Are you sure you want to subscribe to this plan? - - +
    + + + - +
    diff --git a/app/explore/experts/[consultantId]/defaults.ts b/app/explore/experts/[consultantId]/defaults.ts new file mode 100644 index 0000000..a56df3c --- /dev/null +++ b/app/explore/experts/[consultantId]/defaults.ts @@ -0,0 +1,66 @@ +export interface PricingOption { + title: string; + description: string; + price: number; + duration?: string; + features?: string[]; +} + +export const defaultConsultationOptions: PricingOption[] = [ + { + title: "One Hour", + description: "Get a quick consultation", + price: 99, + duration: "1 hour", + }, + { + title: "Two Hour", + description: "Dive deeper into your needs", + price: 199, + duration: "2 hours", + }, + { + title: "Three Hour", + description: "Comprehensive consultation", + price: 299, + duration: "3 hours", + }, +]; + +export const defaultSubscriptionOptions: PricingOption[] = [ + { + title: "One Month Subscription", + description: "Get access to our full suite of services for one month.", + price: 49, + duration: "1 month", + features: [ + "Unlimited consultations", + "Priority support", + "Access to all tools and resources", + ], + }, + { + title: "Three Month Subscription", + description: "Get access to our full suite of services for three months.", + price: 129, + duration: "3 months", + features: [ + "Unlimited consultations", + "Priority support", + "Access to all tools and resources", + "10% discount on all services", + ], + }, + { + title: "Six Month Subscription", + description: "Get access to our full suite of services for six months.", + price: 249, + duration: "6 months", + features: [ + "Unlimited consultations", + "Priority support", + "Access to all tools and resources", + "15% discount on all services", + ], + }, +]; From 02f2398c40af734f3ae8f5ecf105a02cbb0330cb Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 05:03:14 +0530 Subject: [PATCH 04/16] tallied both render calendar and pricing toggle --- .../{ => components}/ClassesAndWebinars.tsx | 7 +- .../ConsultantSkeletonLoader.tsx | 2 +- .../{ => components}/CustomAvailability.tsx | 0 .../{ => components}/PricingToggle.tsx | 9 +- .../{ => components}/Review.tsx | 0 .../{ => components}/WeeklyAvailability.tsx | 16 ++- app/explore/experts/[consultantId]/page.tsx | 134 +++++++++++------- types/slots.ts | 3 + 8 files changed, 104 insertions(+), 67 deletions(-) rename app/explore/experts/[consultantId]/{ => components}/ClassesAndWebinars.tsx (94%) rename app/explore/experts/[consultantId]/{ => components}/ConsultantSkeletonLoader.tsx (97%) rename app/explore/experts/[consultantId]/{ => components}/CustomAvailability.tsx (100%) rename app/explore/experts/[consultantId]/{ => components}/PricingToggle.tsx (99%) rename app/explore/experts/[consultantId]/{ => components}/Review.tsx (100%) rename app/explore/experts/[consultantId]/{ => components}/WeeklyAvailability.tsx (89%) diff --git a/app/explore/experts/[consultantId]/ClassesAndWebinars.tsx b/app/explore/experts/[consultantId]/components/ClassesAndWebinars.tsx similarity index 94% rename from app/explore/experts/[consultantId]/ClassesAndWebinars.tsx rename to app/explore/experts/[consultantId]/components/ClassesAndWebinars.tsx index 3ebd4fc..c9358db 100644 --- a/app/explore/experts/[consultantId]/ClassesAndWebinars.tsx +++ b/app/explore/experts/[consultantId]/components/ClassesAndWebinars.tsx @@ -1,9 +1,8 @@ -import React, { useEffect, useState } from "react"; -import { useToast } from "@/components/ui/use-toast"; -import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { ClassPlan, WebinarPlan } from "@prisma/client"; import { CalendarIcon, ClockIcon } from "lucide-react"; -import { Class, Webinar, ClassPlan, WebinarPlan } from "@prisma/client"; +import React from "react"; interface ClassesAndWebinarsProps { classPlans: ClassPlan[]; diff --git a/app/explore/experts/[consultantId]/ConsultantSkeletonLoader.tsx b/app/explore/experts/[consultantId]/components/ConsultantSkeletonLoader.tsx similarity index 97% rename from app/explore/experts/[consultantId]/ConsultantSkeletonLoader.tsx rename to app/explore/experts/[consultantId]/components/ConsultantSkeletonLoader.tsx index cd4ab0c..0e13087 100644 --- a/app/explore/experts/[consultantId]/ConsultantSkeletonLoader.tsx +++ b/app/explore/experts/[consultantId]/components/ConsultantSkeletonLoader.tsx @@ -11,7 +11,7 @@ export const ConsultantSkeletonLoader: React.FC = () => { let dotIndex = 0; const interval = setInterval(() => { - setLoadingText((prevText) => { + setLoadingText(() => { const baseText = "Please wait while we are fetching consultant details"; return `${baseText}${dots[dotIndex]}`; }); diff --git a/app/explore/experts/[consultantId]/CustomAvailability.tsx b/app/explore/experts/[consultantId]/components/CustomAvailability.tsx similarity index 100% rename from app/explore/experts/[consultantId]/CustomAvailability.tsx rename to app/explore/experts/[consultantId]/components/CustomAvailability.tsx diff --git a/app/explore/experts/[consultantId]/PricingToggle.tsx b/app/explore/experts/[consultantId]/components/PricingToggle.tsx similarity index 99% rename from app/explore/experts/[consultantId]/PricingToggle.tsx rename to app/explore/experts/[consultantId]/components/PricingToggle.tsx index 32f323e..c76751d 100644 --- a/app/explore/experts/[consultantId]/PricingToggle.tsx +++ b/app/explore/experts/[consultantId]/components/PricingToggle.tsx @@ -11,17 +11,16 @@ import { Dialog, DialogContent, DialogDescription, - DialogFooter, DialogHeader, DialogTitle, - DialogTrigger, + DialogTrigger } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { motion } from "framer-motion"; -import { ClockIcon, X } from "lucide-react"; +import { ClockIcon } from "lucide-react"; import { useSession } from "next-auth/react"; -import { useState, useMemo } from "react"; -import { PricingOption, defaultConsultationOptions, defaultSubscriptionOptions } from "./defaults"; +import { useMemo, useState } from "react"; +import { PricingOption, defaultConsultationOptions, defaultSubscriptionOptions } from "../defaults"; interface PricingToggleProps { consultationOptions?: PricingOption[]; diff --git a/app/explore/experts/[consultantId]/Review.tsx b/app/explore/experts/[consultantId]/components/Review.tsx similarity index 100% rename from app/explore/experts/[consultantId]/Review.tsx rename to app/explore/experts/[consultantId]/components/Review.tsx diff --git a/app/explore/experts/[consultantId]/WeeklyAvailability.tsx b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx similarity index 89% rename from app/explore/experts/[consultantId]/WeeklyAvailability.tsx rename to app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx index 8742611..8e431a1 100644 --- a/app/explore/experts/[consultantId]/WeeklyAvailability.tsx +++ b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx @@ -15,14 +15,18 @@ interface WeeklyAvailabilityProps { selectedSlotId?: string; } +function convertUTCToLocal(date: Date): Date { + const offset = date.getTimezoneOffset(); + return new Date(date.getTime() - (offset * 60 * 1000)); +} + const formatTime = (isoString: string): string => { try { - // Extract hours and minutes from the ISO string - const date = new Date(isoString); - if (isNaN(date.getTime())) { - throw new Error("Invalid date"); - } - return date.toLocaleTimeString([], { + // Convert UTC time to local time + const utcDate = new Date(isoString); + const localDate = convertUTCToLocal(utcDate); + + return localDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: true, diff --git a/app/explore/experts/[consultantId]/page.tsx b/app/explore/experts/[consultantId]/page.tsx index 23b7a54..5481e63 100644 --- a/app/explore/experts/[consultantId]/page.tsx +++ b/app/explore/experts/[consultantId]/page.tsx @@ -14,6 +14,7 @@ import { TSlotTiming } from "@/types/slots"; import { ConsultantReview, ConsultationPlan, + DayOfWeek, SubscriptionPlan, User, } from "@prisma/client"; @@ -21,12 +22,12 @@ import { StarIcon } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { use, useCallback, useEffect, useMemo, useState } from "react"; -import { ClassesAndWebinars } from "./ClassesAndWebinars"; -import { ConsultantSkeletonLoader } from "./ConsultantSkeletonLoader"; -import { CustomAvailability } from "./CustomAvailability"; -import PricingToggle from "./PricingToggle"; -import Review from "./Review"; -import { WeeklyAvailability } from "./WeeklyAvailability"; +import { ClassesAndWebinars } from "./components/ClassesAndWebinars"; +import { ConsultantSkeletonLoader } from "./components/ConsultantSkeletonLoader"; +import { CustomAvailability } from "./components/CustomAvailability"; +import PricingToggle from "./components/PricingToggle"; +import Review from "./components/Review"; +import { WeeklyAvailability } from "./components/WeeklyAvailability"; interface PricingOption { title: string; @@ -39,6 +40,21 @@ interface PricingOption { type Params = Promise<{ consultantId: string }>; type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>; +const dayMap: Record = { + 0: DayOfWeek.SUNDAY, + 1: DayOfWeek.MONDAY, + 2: DayOfWeek.TUESDAY, + 3: DayOfWeek.WEDNESDAY, + 4: DayOfWeek.THURSDAY, + 5: DayOfWeek.FRIDAY, + 6: DayOfWeek.SATURDAY +}; + +function convertUTCToLocal(date: Date): Date { + const offset = date.getTimezoneOffset(); + return new Date(date.getTime() - (offset * 60 * 1000)); +} + export default function ExpertProfile( props: Readonly<{ params: Params; @@ -69,7 +85,6 @@ export default function ExpertProfile( params.consultantId, ); setConsultantDetails(consultantData); - console.log(consultantData); if (consultantData.userId) { const userData = await fetchUserDetails(consultantData.userId); setUserDetails(userData); @@ -103,28 +118,51 @@ export default function ExpertProfile( // For weekly schedule, use the slots directly from consultantDetails const weeklySlots = consultantDetails.slotsOfAvailabilityWeekly.map( (slot) => { - // Extract time from the ISO string and ensure it's a string - const startTimeStr = - typeof slot.slotStartTimeInUTC === "string" - ? slot.slotStartTimeInUTC - : new Date(slot.slotStartTimeInUTC).toISOString(); - const endTimeStr = - typeof slot.slotEndTimeInUTC === "string" - ? slot.slotEndTimeInUTC - : new Date(slot.slotEndTimeInUTC).toISOString(); + // Get the selected date's day + const selectedDay = dayMap[selectedDate.getDay()]; + + // Only map slots for the selected day + if (slot.dayOfWeekforStartTimeInUTC !== selectedDay) { + return null; + } + + // Create a new date object for the selected date + const slotDate = new Date(selectedDate); + + // Parse hours and minutes from the UTC time + const startTime = new Date(slot.slotStartTimeInUTC); + const endTime = new Date(slot.slotEndTimeInUTC); + + // Convert UTC times to local times + const localStartTime = convertUTCToLocal(startTime); + const localEndTime = convertUTCToLocal(endTime); + + // Set the hours and minutes on the selected date + const startDateTime = new Date(slotDate); + startDateTime.setHours(localStartTime.getHours(), localStartTime.getMinutes(), 0, 0); + + const endDateTime = new Date(slotDate); + endDateTime.setHours(localEndTime.getHours(), localEndTime.getMinutes(), 0, 0); + + // If the slot crosses midnight, adjust the end date + if (endDateTime < startDateTime) { + endDateTime.setDate(endDateTime.getDate() + 1); + } return { slotId: slot.id, dateInISO: selectedDate.toISOString(), - slotStartTimeInUTC: startTimeStr, - slotEndTimeInUTC: endTimeStr, + dayOfWeek: slot.dayOfWeekforStartTimeInUTC, + slotStartTimeInUTC: startDateTime.toISOString(), + slotEndTimeInUTC: endDateTime.toISOString(), slotOfAvailabilityId: slot.id, slotOfAppointmentId: "", - localStartTime: startTimeStr, - localEndTime: endTimeStr, + localStartTime: startDateTime.toLocaleTimeString(), + localEndTime: endDateTime.toLocaleTimeString(), }; }, - ); + ).filter((slot): slot is TSlotTiming => slot !== null); + setSlotTimings(weeklySlots); } else if (consultantDetails.scheduleType === "CUSTOM") { // For custom schedule, use the custom slots from consultantDetails @@ -137,28 +175,23 @@ export default function ExpertProfile( ); return slotDate.toDateString() === selectedDate.toDateString(); }) - .map((slot) => ({ - slotId: slot.id, - dateInISO: selectedDate.toISOString(), - slotStartTimeInUTC: - typeof slot.slotStartTimeInUTC === "string" - ? slot.slotStartTimeInUTC - : new Date(slot.slotStartTimeInUTC).toISOString(), - slotEndTimeInUTC: - typeof slot.slotEndTimeInUTC === "string" - ? slot.slotEndTimeInUTC - : new Date(slot.slotEndTimeInUTC).toISOString(), - slotOfAvailabilityId: slot.id, - slotOfAppointmentId: "", - localStartTime: - typeof slot.slotStartTimeInUTC === "string" - ? slot.slotStartTimeInUTC - : new Date(slot.slotStartTimeInUTC).toISOString(), - localEndTime: - typeof slot.slotEndTimeInUTC === "string" - ? slot.slotEndTimeInUTC - : new Date(slot.slotEndTimeInUTC).toISOString(), - })); + .map((slot) => { + const startDateTime = convertUTCToLocal(new Date(slot.slotStartTimeInUTC)); + const endDateTime = convertUTCToLocal(new Date(slot.slotEndTimeInUTC)); + + return { + slotId: slot.id, + dateInISO: selectedDate.toISOString(), + dayOfWeek: dayMap[startDateTime.getDay()], + slotStartTimeInUTC: startDateTime.toISOString(), + slotEndTimeInUTC: endDateTime.toISOString(), + slotOfAvailabilityId: slot.id, + slotOfAppointmentId: "", + localStartTime: startDateTime.toLocaleTimeString(), + localEndTime: endDateTime.toLocaleTimeString(), + }; + }); + setSlotTimings(customSlots); } } @@ -264,16 +297,13 @@ export default function ExpertProfile( setSelectedSlot({ slotId: slot.id, dateInISO: new Date().toISOString(), + dayOfWeek: slot.dayOfWeekforStartTimeInUTC, slotStartTimeInUTC: slot.slotStartTimeInUTC, slotEndTimeInUTC: slot.slotEndTimeInUTC, slotOfAvailabilityId: slot.id, slotOfAppointmentId: "", - localStartTime: new Date( - `1970-01-01T${slot.slotStartTimeInUTC}`, - ).toISOString(), - localEndTime: new Date( - `1970-01-01T${slot.slotEndTimeInUTC}`, - ).toISOString(), + localStartTime: new Date(slot.slotStartTimeInUTC).toLocaleTimeString(), + localEndTime: new Date(slot.slotEndTimeInUTC).toLocaleTimeString(), }) } selectedSlotId={selectedSlot?.slotId} @@ -302,12 +332,13 @@ export default function ExpertProfile( setSelectedSlot({ slotId: slot.id, dateInISO: new Date(slot.slotStartTimeInUTC).toISOString(), + dayOfWeek: dayMap[new Date(slot.slotStartTimeInUTC).getDay()], slotStartTimeInUTC: slot.slotStartTimeInUTC, slotEndTimeInUTC: slot.slotEndTimeInUTC, slotOfAvailabilityId: slot.id, slotOfAppointmentId: "", - localStartTime: new Date(slot.slotStartTimeInUTC).toISOString(), - localEndTime: new Date(slot.slotEndTimeInUTC).toISOString(), + localStartTime: new Date(slot.slotStartTimeInUTC).toLocaleTimeString(), + localEndTime: new Date(slot.slotEndTimeInUTC).toLocaleTimeString(), }) } selectedSlotId={selectedSlot?.slotId} @@ -316,6 +347,7 @@ export default function ExpertProfile( } return null; }, [consultantDetails, selectedSlot]); + const isConsultationPlan = ( plan: ConsultationPlan | SubscriptionPlan, ): plan is ConsultationPlan => { diff --git a/types/slots.ts b/types/slots.ts index 42f05c0..22dc5b9 100644 --- a/types/slots.ts +++ b/types/slots.ts @@ -1,6 +1,9 @@ +import { DayOfWeek } from "@prisma/client"; + export type TSlotTiming = { slotId: string; dateInISO: string; + dayOfWeek: DayOfWeek; slotStartTimeInUTC: string; slotEndTimeInUTC: string; slotOfAvailabilityId: string; From e48fcd1e0ccb5a4dece61e5248584210b7308557 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 06:01:13 +0530 Subject: [PATCH 05/16] Refactor slot types and weekly availability component --- .../components/WeeklyAvailability.tsx | 146 +++++++++++-- app/explore/experts/[consultantId]/page.tsx | 197 ++++++++++++------ types/slots.ts | 14 ++ 3 files changed, 278 insertions(+), 79 deletions(-) diff --git a/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx index 8e431a1..089a178 100644 --- a/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx +++ b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx @@ -20,11 +20,44 @@ function convertUTCToLocal(date: Date): Date { return new Date(date.getTime() - (offset * 60 * 1000)); } +function getDayBefore(day: DayOfWeek): DayOfWeek { + const days = [ + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY + ]; + const index = days.indexOf(day); + return days[(index - 1 + 7) % 7]; +} + +function getDayAfter(day: DayOfWeek): DayOfWeek { + const days = [ + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY + ]; + const index = days.indexOf(day); + return days[(index + 1) % 7]; +} + const formatTime = (isoString: string): string => { try { - // Convert UTC time to local time + // Create a base date for today + const baseDate = new Date(); + // Parse the time from the ISO string const utcDate = new Date(isoString); - const localDate = convertUTCToLocal(utcDate); + // Set the hours and minutes on the base date + baseDate.setHours(utcDate.getUTCHours(), utcDate.getUTCMinutes(), 0, 0); + // Convert to local time + const localDate = convertUTCToLocal(baseDate); return localDate.toLocaleTimeString([], { hour: "2-digit", @@ -43,27 +76,100 @@ export const WeeklyAvailability: React.FC = ({ selectedSlotId, }) => { const daysOfWeek = [ - "MONDAY", - "TUESDAY", - "WEDNESDAY", - "THURSDAY", - "FRIDAY", - "SATURDAY", - "SUNDAY", + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, ]; const dayLabels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; // Group slots by day and sort by time - const slotsByDay = daysOfWeek.map((day) => ({ - day, - slots: slots - .filter((slot) => slot.dayOfWeekforStartTimeInUTC === day) - .sort((a, b) => { - const timeA = new Date(a.slotStartTimeInUTC).getTime(); - const timeB = new Date(b.slotStartTimeInUTC).getTime(); - return timeA - timeB; - }), - })); + const slotsByDay = daysOfWeek.map((day) => { + // Get slots that: + // 1. Start on this day + // 2. End on this day (started previous day) + // 3. Start on this day and end next day + const daySlots = slots.filter((slot) => { + const previousDay = getDayBefore(day); + const nextDay = getDayAfter(day); + + // Include slots that: + // 1. Start on this day + // 2. End on this day (started previous day) + // 3. Start on this day and end next day + // 4. Start on previous day and end on next day (crosses entire day) + return ( + slot.dayOfWeekforStartTimeInUTC === day || + slot.dayOfWeekforEndTimeInUTC === day || + (slot.dayOfWeekforStartTimeInUTC === previousDay && + slot.dayOfWeekforEndTimeInUTC === nextDay) + ); + }); + + // For each slot, determine if it needs to be split at midnight + const processedSlots = daySlots.flatMap((slot) => { + // Create a base date for today + const baseDate = new Date(); + // Parse the times from the ISO strings + const startTime = new Date(slot.slotStartTimeInUTC); + const endTime = new Date(slot.slotEndTimeInUTC); + + // Set the hours and minutes on the base date + const localStartTime = new Date(baseDate); + localStartTime.setHours(startTime.getUTCHours(), startTime.getUTCMinutes(), 0, 0); + + const localEndTime = new Date(baseDate); + localEndTime.setHours(endTime.getUTCHours(), endTime.getUTCMinutes(), 0, 0); + + // Convert to local time + const convertedStartTime = convertUTCToLocal(localStartTime); + const convertedEndTime = convertUTCToLocal(localEndTime); + + // If the slot crosses midnight + if (convertedEndTime < convertedStartTime) { + // Create two slots: one ending at midnight, one starting at midnight + const midnightEnd = new Date(convertedStartTime); + midnightEnd.setHours(23, 59, 59, 999); + + const midnightStart = new Date(convertedEndTime); + midnightStart.setHours(0, 0, 0, 0); + + return [ + { + ...slot, + slotStartTimeInUTC: convertedStartTime.toISOString(), + slotEndTimeInUTC: midnightEnd.toISOString(), + }, + { + ...slot, + slotStartTimeInUTC: midnightStart.toISOString(), + slotEndTimeInUTC: convertedEndTime.toISOString(), + }, + ]; + } + + return [{ + ...slot, + slotStartTimeInUTC: convertedStartTime.toISOString(), + slotEndTimeInUTC: convertedEndTime.toISOString(), + }]; + }); + + // Sort slots by start time + const sortedSlots = processedSlots.sort((a, b) => { + const timeA = new Date(a.slotStartTimeInUTC).getTime(); + const timeB = new Date(b.slotStartTimeInUTC).getTime(); + return timeA - timeB; + }); + + return { + day, + slots: sortedSlots, + }; + }); return (
    @@ -89,7 +195,7 @@ export const WeeklyAvailability: React.FC = ({ return (
    { if (selectedDate && consultantDetails) { if (consultantDetails.scheduleType === "WEEKLY") { - // For weekly schedule, use the slots directly from consultantDetails - const weeklySlots = consultantDetails.slotsOfAvailabilityWeekly.map( - (slot) => { - // Get the selected date's day - const selectedDay = dayMap[selectedDate.getDay()]; - - // Only map slots for the selected day - if (slot.dayOfWeekforStartTimeInUTC !== selectedDay) { - return null; - } + const selectedDay = dayMap[selectedDate.getDay()]; + const previousDay = getDayBefore(selectedDay); + const nextDay = getDayAfter(selectedDay); + + // Get all slots that could be relevant for this day + const relevantSlots = consultantDetails.slotsOfAvailabilityWeekly.filter(slot => { + // Include slots that: + // 1. Start on this day + // 2. End on this day (started previous day) + // 3. Start on this day and end next day + return ( + slot.dayOfWeekforStartTimeInUTC === selectedDay || + slot.dayOfWeekforEndTimeInUTC === selectedDay || + (slot.dayOfWeekforStartTimeInUTC === previousDay && + slot.dayOfWeekforEndTimeInUTC === nextDay) + ); + }); - // Create a new date object for the selected date - const slotDate = new Date(selectedDate); - - // Parse hours and minutes from the UTC time - const startTime = new Date(slot.slotStartTimeInUTC); - const endTime = new Date(slot.slotEndTimeInUTC); - - // Convert UTC times to local times - const localStartTime = convertUTCToLocal(startTime); - const localEndTime = convertUTCToLocal(endTime); - - // Set the hours and minutes on the selected date - const startDateTime = new Date(slotDate); - startDateTime.setHours(localStartTime.getHours(), localStartTime.getMinutes(), 0, 0); - - const endDateTime = new Date(slotDate); - endDateTime.setHours(localEndTime.getHours(), localEndTime.getMinutes(), 0, 0); + const weeklySlots = relevantSlots.flatMap(slot => { + // Create a new date object for the selected date + const slotDate = new Date(selectedDate); + + // Parse hours and minutes from the UTC time + const startTime = new Date(slot.slotStartTimeInUTC); + const endTime = new Date(slot.slotEndTimeInUTC); + + // Convert UTC times to local times + const localStartTime = convertUTCToLocal(startTime); + const localEndTime = convertUTCToLocal(endTime); + + // Set the hours and minutes on the selected date + const startDateTime = new Date(slotDate); + startDateTime.setHours(localStartTime.getHours(), localStartTime.getMinutes(), 0, 0); + + const endDateTime = new Date(slotDate); + endDateTime.setHours(localEndTime.getHours(), localEndTime.getMinutes(), 0, 0); + + // Handle slots that cross midnight + if (endDateTime < startDateTime) { + // Split the slot into two parts + const midnightEnd = new Date(startDateTime); + midnightEnd.setHours(23, 59, 59, 999); + + const midnightStart = new Date(endDateTime); + midnightStart.setHours(0, 0, 0, 0); + + // Return both parts of the slot + return [ + createWeeklySlot(slot, selectedDate, startDateTime, midnightEnd), + createWeeklySlot(slot, selectedDate, midnightStart, endDateTime) + ]; + } - // If the slot crosses midnight, adjust the end date - if (endDateTime < startDateTime) { - endDateTime.setDate(endDateTime.getDate() + 1); - } + // For regular slots that don't cross midnight + return [createWeeklySlot(slot, selectedDate, startDateTime, endDateTime)]; + }); - return { - slotId: slot.id, - dateInISO: selectedDate.toISOString(), - dayOfWeek: slot.dayOfWeekforStartTimeInUTC, - slotStartTimeInUTC: startDateTime.toISOString(), - slotEndTimeInUTC: endDateTime.toISOString(), - slotOfAvailabilityId: slot.id, - slotOfAppointmentId: "", - localStartTime: startDateTime.toLocaleTimeString(), - localEndTime: endDateTime.toLocaleTimeString(), - }; - }, - ).filter((slot): slot is TSlotTiming => slot !== null); + // Sort slots by start time + const sortedSlots = weeklySlots.sort((a, b) => { + return new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime(); + }); - setSlotTimings(weeklySlots); + setSlotTimings(sortedSlots); } else if (consultantDetails.scheduleType === "CUSTOM") { - // For custom schedule, use the custom slots from consultantDetails const customSlots = consultantDetails.slotsOfAvailabilityCustom .filter((slot) => { const slotDate = new Date( @@ -179,20 +258,20 @@ export default function ExpertProfile( const startDateTime = convertUTCToLocal(new Date(slot.slotStartTimeInUTC)); const endDateTime = convertUTCToLocal(new Date(slot.slotEndTimeInUTC)); - return { - slotId: slot.id, - dateInISO: selectedDate.toISOString(), - dayOfWeek: dayMap[startDateTime.getDay()], - slotStartTimeInUTC: startDateTime.toISOString(), - slotEndTimeInUTC: endDateTime.toISOString(), - slotOfAvailabilityId: slot.id, - slotOfAppointmentId: "", - localStartTime: startDateTime.toLocaleTimeString(), - localEndTime: endDateTime.toLocaleTimeString(), - }; + // If end time is before start time, it means it ends next day + const adjustedEndDateTime = endDateTime < startDateTime + ? new Date(endDateTime.setDate(endDateTime.getDate() + 1)) + : endDateTime; + + return createCustomSlot(slot, selectedDate, startDateTime, adjustedEndDateTime); }); - setSlotTimings(customSlots); + // Sort slots by start time + const sortedSlots = customSlots.sort((a, b) => { + return new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime(); + }); + + setSlotTimings(sortedSlots); } } }, [selectedDate, consultantDetails]); diff --git a/types/slots.ts b/types/slots.ts index 22dc5b9..12cda20 100644 --- a/types/slots.ts +++ b/types/slots.ts @@ -11,3 +11,17 @@ export type TSlotTiming = { localStartTime: string; localEndTime: string; }; + +export type TWeeklySlot = { + id: string; + dayOfWeekforStartTimeInUTC: DayOfWeek; + slotStartTimeInUTC: string | Date; + dayOfWeekforEndTimeInUTC: DayOfWeek; + slotEndTimeInUTC: string | Date; +}; + +export type TCustomSlot = { + id: string; + slotStartTimeInUTC: string | Date; + slotEndTimeInUTC: string | Date; +}; From bac34ff7ade712655039a2e7083d7e64c1b33b80 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 16:24:17 +0530 Subject: [PATCH 06/16] Refactor timezone detection and formatting functions --- .../[consultantId]/hooks/useTimezone.ts | 21 ++ app/explore/experts/[consultantId]/page.tsx | 290 ++++++++---------- app/explore/experts/[consultantId]/utils.ts | 204 ++++++++++++ 3 files changed, 354 insertions(+), 161 deletions(-) create mode 100644 app/explore/experts/[consultantId]/hooks/useTimezone.ts create mode 100644 app/explore/experts/[consultantId]/utils.ts diff --git a/app/explore/experts/[consultantId]/hooks/useTimezone.ts b/app/explore/experts/[consultantId]/hooks/useTimezone.ts new file mode 100644 index 0000000..638a6f1 --- /dev/null +++ b/app/explore/experts/[consultantId]/hooks/useTimezone.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from 'react'; + +export function useTimezone() { + const [timezone, setTimezone] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + try { + // Get timezone on client side + const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + console.log('Browser timezone detected:', browserTimezone); + setTimezone(browserTimezone); + } catch (error) { + console.error('Error detecting timezone:', error); + } finally { + setIsLoading(false); + } + }, []); + + return { timezone, isLoading }; +} diff --git a/app/explore/experts/[consultantId]/page.tsx b/app/explore/experts/[consultantId]/page.tsx index 12d325c..8a2aa2c 100644 --- a/app/explore/experts/[consultantId]/page.tsx +++ b/app/explore/experts/[consultantId]/page.tsx @@ -10,11 +10,10 @@ import { } from "@/hooks/useUserData"; import { TConsultantProfile } from "@/types/consultant"; -import { TSlotTiming, TWeeklySlot, TCustomSlot } from "@/types/slots"; +import { TSlotTiming } from "@/types/slots"; import { ConsultantReview, ConsultationPlan, - DayOfWeek, SubscriptionPlan, User, } from "@prisma/client"; @@ -28,6 +27,18 @@ import { CustomAvailability } from "./components/CustomAvailability"; import PricingToggle from "./components/PricingToggle"; import Review from "./components/Review"; import { WeeklyAvailability } from "./components/WeeklyAvailability"; +import { + dayMap, + convertUTCToLocalDate, + formatTime, + getDayAfter, + createWeeklySlot, + createCustomSlot, + mergeOverlappingSlots, + getLocalDay, + isSameLocalDay, +} from "./utils"; +import { useTimezone } from "./hooks/useTimezone"; interface PricingOption { title: string; @@ -40,87 +51,6 @@ interface PricingOption { type Params = Promise<{ consultantId: string }>; type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>; -const dayMap: Record = { - 0: DayOfWeek.SUNDAY, - 1: DayOfWeek.MONDAY, - 2: DayOfWeek.TUESDAY, - 3: DayOfWeek.WEDNESDAY, - 4: DayOfWeek.THURSDAY, - 5: DayOfWeek.FRIDAY, - 6: DayOfWeek.SATURDAY -}; - -function convertUTCToLocal(date: Date): Date { - const offset = date.getTimezoneOffset(); - return new Date(date.getTime() - (offset * 60 * 1000)); -} - -function getDayBefore(day: DayOfWeek): DayOfWeek { - const days = [ - DayOfWeek.SUNDAY, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY - ]; - const index = days.indexOf(day); - return days[(index - 1 + 7) % 7]; -} - -function getDayAfter(day: DayOfWeek): DayOfWeek { - const days = [ - DayOfWeek.SUNDAY, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY - ]; - const index = days.indexOf(day); - return days[(index + 1) % 7]; -} - -function createWeeklySlot( - slot: TWeeklySlot, - selectedDate: Date, - startDateTime: Date, - endDateTime: Date -): TSlotTiming { - return { - slotId: slot.id, - dateInISO: selectedDate.toISOString(), - dayOfWeek: slot.dayOfWeekforStartTimeInUTC, - slotStartTimeInUTC: startDateTime.toISOString(), - slotEndTimeInUTC: endDateTime.toISOString(), - slotOfAvailabilityId: slot.id, - slotOfAppointmentId: "", - localStartTime: startDateTime.toLocaleTimeString(), - localEndTime: endDateTime.toLocaleTimeString(), - }; -} - -function createCustomSlot( - slot: TCustomSlot, - selectedDate: Date, - startDateTime: Date, - endDateTime: Date -): TSlotTiming { - return { - slotId: slot.id, - dateInISO: selectedDate.toISOString(), - dayOfWeek: dayMap[startDateTime.getDay()], - slotStartTimeInUTC: startDateTime.toISOString(), - slotEndTimeInUTC: endDateTime.toISOString(), - slotOfAvailabilityId: slot.id, - slotOfAppointmentId: "", - localStartTime: startDateTime.toLocaleTimeString(), - localEndTime: endDateTime.toLocaleTimeString(), - }; -} - export default function ExpertProfile( props: Readonly<{ params: Params; @@ -129,6 +59,7 @@ export default function ExpertProfile( ) { const params = use(props.params); const searchParams = use(props.searchParams); + const { timezone: browserTimezone, isLoading: isTimezoneLoading } = useTimezone(); const [userDetails, setUserDetails] = useState(null); const [consultantDetails, setConsultantDetails] = @@ -142,6 +73,9 @@ export default function ExpertProfile( const [selectedSlot, setSelectedSlot] = useState(null); const { toast } = useToast(); + // Prioritize browser timezone over user timezone + const timezone = browserTimezone || userDetails?.currentTimezone; + useEffect(() => { const fetchData = async () => { setIsLoading(true); @@ -179,102 +113,136 @@ export default function ExpertProfile( }, [params.consultantId, toast]); useEffect(() => { - if (selectedDate && consultantDetails) { + if (selectedDate && consultantDetails && timezone && !isTimezoneLoading) { + console.log('Using timezone:', timezone); + console.log('Selected date:', selectedDate.toISOString()); + if (consultantDetails.scheduleType === "WEEKLY") { - const selectedDay = dayMap[selectedDate.getDay()]; - const previousDay = getDayBefore(selectedDay); - const nextDay = getDayAfter(selectedDay); + const selectedDay = dayMap[getLocalDay(selectedDate, timezone)]; + console.log('Selected day:', selectedDay); - // Get all slots that could be relevant for this day + // Get slots for the selected day const relevantSlots = consultantDetails.slotsOfAvailabilityWeekly.filter(slot => { - // Include slots that: - // 1. Start on this day - // 2. End on this day (started previous day) - // 3. Start on this day and end next day - return ( - slot.dayOfWeekforStartTimeInUTC === selectedDay || - slot.dayOfWeekforEndTimeInUTC === selectedDay || - (slot.dayOfWeekforStartTimeInUTC === previousDay && - slot.dayOfWeekforEndTimeInUTC === nextDay) - ); - }); - - const weeklySlots = relevantSlots.flatMap(slot => { - // Create a new date object for the selected date - const slotDate = new Date(selectedDate); + const startDay = slot.dayOfWeekforStartTimeInUTC; + const endDay = slot.dayOfWeekforEndTimeInUTC; - // Parse hours and minutes from the UTC time - const startTime = new Date(slot.slotStartTimeInUTC); - const endTime = new Date(slot.slotEndTimeInUTC); + const isRelevant = startDay === selectedDay || + (startDay !== endDay && endDay === selectedDay) || + (startDay !== endDay && getDayAfter(startDay) === selectedDay); - // Convert UTC times to local times - const localStartTime = convertUTCToLocal(startTime); - const localEndTime = convertUTCToLocal(endTime); - - // Set the hours and minutes on the selected date - const startDateTime = new Date(slotDate); - startDateTime.setHours(localStartTime.getHours(), localStartTime.getMinutes(), 0, 0); + if (isRelevant) { + console.log('Found relevant slot:', { + startDay, + endDay, + startTime: slot.slotStartTimeInUTC, + endTime: slot.slotEndTimeInUTC + }); + } - const endDateTime = new Date(slotDate); - endDateTime.setHours(localEndTime.getHours(), localEndTime.getMinutes(), 0, 0); - - // Handle slots that cross midnight - if (endDateTime < startDateTime) { - // Split the slot into two parts - const midnightEnd = new Date(startDateTime); - midnightEnd.setHours(23, 59, 59, 999); - - const midnightStart = new Date(endDateTime); - midnightStart.setHours(0, 0, 0, 0); - - // Return both parts of the slot - return [ - createWeeklySlot(slot, selectedDate, startDateTime, midnightEnd), - createWeeklySlot(slot, selectedDate, midnightStart, endDateTime) - ]; + return isRelevant; + }); + + const weeklySlots = relevantSlots.flatMap(slot => { + // Convert UTC times to local date objects + const startDateTime = convertUTCToLocalDate(slot.slotStartTimeInUTC, selectedDate, timezone); + let endDateTime = convertUTCToLocalDate(slot.slotEndTimeInUTC, selectedDate, timezone); + + console.log('Processing slot:', { + utcStart: slot.slotStartTimeInUTC, + utcEnd: slot.slotEndTimeInUTC, + localStart: startDateTime.toISOString(), + localEnd: endDateTime.toISOString(), + timezone + }); + + // If this slot ends on the next day + if (slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC) { + if (slot.dayOfWeekforStartTimeInUTC === selectedDay) { + // For slots starting on selected day and ending next day, + // show only the portion until midnight + const nextDay = new Date(startDateTime); + nextDay.setDate(nextDay.getDate() + 1); + nextDay.setHours(0, 0, 0, 0); + console.log('Slot crosses to next day, ending at midnight:', nextDay.toISOString()); + return [createWeeklySlot(slot, selectedDate, startDateTime, nextDay, timezone)]; + } else if (slot.dayOfWeekforEndTimeInUTC === selectedDay) { + // For slots ending on selected day (started previous day), + // show only the portion from midnight + const thisDay = new Date(endDateTime); + thisDay.setHours(0, 0, 0, 0); + console.log('Slot started previous day, starting at midnight:', thisDay.toISOString()); + return [createWeeklySlot(slot, selectedDate, thisDay, endDateTime, timezone)]; + } else if (getDayAfter(slot.dayOfWeekforStartTimeInUTC) === selectedDay) { + // For slots spanning multiple days, show full day + const thisDay = new Date(selectedDate); + thisDay.setHours(0, 0, 0, 0); + const nextDay = new Date(selectedDate); + nextDay.setDate(nextDay.getDate() + 1); + nextDay.setHours(0, 0, 0, 0); + console.log('Slot spans multiple days:', { + start: thisDay.toISOString(), + end: nextDay.toISOString() + }); + return [createWeeklySlot(slot, selectedDate, thisDay, nextDay, timezone)]; + } } - // For regular slots that don't cross midnight - return [createWeeklySlot(slot, selectedDate, startDateTime, endDateTime)]; - }); + // For slots within the same day + if (endDateTime <= startDateTime) { + endDateTime = new Date(endDateTime.getTime() + 24 * 60 * 60 * 1000); + console.log('Adjusted end time for same day slot:', endDateTime.toISOString()); + } + + // Only include slots that overlap with the selected date in local time + if (isSameLocalDay(startDateTime, selectedDate, timezone) || + isSameLocalDay(endDateTime, selectedDate, timezone)) { + console.log('Slot overlaps with selected date'); + return [createWeeklySlot(slot, selectedDate, startDateTime, endDateTime, timezone)]; + } + + console.log('Slot does not overlap with selected date'); + return []; + }).filter(Boolean) as TSlotTiming[]; // Sort slots by start time - const sortedSlots = weeklySlots.sort((a, b) => { - return new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime(); - }); + const sortedSlots = weeklySlots.sort((a, b) => + new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime() + ); - setSlotTimings(sortedSlots); + // Merge overlapping slots + const mergedSlots = mergeOverlappingSlots(sortedSlots, timezone); + console.log('Final slots:', mergedSlots.map(slot => ({ + start: slot.localStartTime, + end: slot.localEndTime + }))); + setSlotTimings(mergedSlots); } else if (consultantDetails.scheduleType === "CUSTOM") { const customSlots = consultantDetails.slotsOfAvailabilityCustom - .filter((slot) => { - const slotDate = new Date( - typeof slot.slotStartTimeInUTC === "string" - ? slot.slotStartTimeInUTC - : slot.slotStartTimeInUTC, - ); - return slotDate.toDateString() === selectedDate.toDateString(); + .filter(slot => { + const startDateTime = new Date(slot.slotStartTimeInUTC); + return isSameLocalDay(startDateTime, selectedDate, timezone); }) - .map((slot) => { - const startDateTime = convertUTCToLocal(new Date(slot.slotStartTimeInUTC)); - const endDateTime = convertUTCToLocal(new Date(slot.slotEndTimeInUTC)); + .map(slot => { + const startDateTime = new Date(slot.slotStartTimeInUTC); + const endDateTime = new Date(slot.slotEndTimeInUTC); // If end time is before start time, it means it ends next day - const adjustedEndDateTime = endDateTime < startDateTime - ? new Date(endDateTime.setDate(endDateTime.getDate() + 1)) - : endDateTime; + if (endDateTime <= startDateTime) { + endDateTime.setDate(endDateTime.getDate() + 1); + } - return createCustomSlot(slot, selectedDate, startDateTime, adjustedEndDateTime); + return createCustomSlot(slot, selectedDate, startDateTime, endDateTime, timezone); }); // Sort slots by start time - const sortedSlots = customSlots.sort((a, b) => { - return new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime(); - }); + const sortedSlots = customSlots.sort((a, b) => + new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime() + ); setSlotTimings(sortedSlots); } } - }, [selectedDate, consultantDetails]); + }, [selectedDate, consultantDetails, timezone, isTimezoneLoading]); const handleBooking = useCallback(async () => { if (!selectedSlot) { @@ -349,7 +317,7 @@ export default function ExpertProfile( }, [currentDate, selectedDate]); const renderAvailability = useMemo(() => { - if (!consultantDetails) return null; + if (!consultantDetails || !timezone) return null; if (consultantDetails.scheduleType === "WEEKLY") { // Convert Date objects to strings for weekly slots @@ -381,8 +349,8 @@ export default function ExpertProfile( slotEndTimeInUTC: slot.slotEndTimeInUTC, slotOfAvailabilityId: slot.id, slotOfAppointmentId: "", - localStartTime: new Date(slot.slotStartTimeInUTC).toLocaleTimeString(), - localEndTime: new Date(slot.slotEndTimeInUTC).toLocaleTimeString(), + localStartTime: formatTime(slot.slotStartTimeInUTC, timezone), + localEndTime: formatTime(slot.slotEndTimeInUTC, timezone), }) } selectedSlotId={selectedSlot?.slotId} @@ -416,8 +384,8 @@ export default function ExpertProfile( slotEndTimeInUTC: slot.slotEndTimeInUTC, slotOfAvailabilityId: slot.id, slotOfAppointmentId: "", - localStartTime: new Date(slot.slotStartTimeInUTC).toLocaleTimeString(), - localEndTime: new Date(slot.slotEndTimeInUTC).toLocaleTimeString(), + localStartTime: formatTime(slot.slotStartTimeInUTC, timezone), + localEndTime: formatTime(slot.slotEndTimeInUTC, timezone), }) } selectedSlotId={selectedSlot?.slotId} @@ -425,7 +393,7 @@ export default function ExpertProfile( ); } return null; - }, [consultantDetails, selectedSlot]); + }, [consultantDetails, selectedSlot, timezone]); const isConsultationPlan = ( plan: ConsultationPlan | SubscriptionPlan, diff --git a/app/explore/experts/[consultantId]/utils.ts b/app/explore/experts/[consultantId]/utils.ts new file mode 100644 index 0000000..fc8cbb7 --- /dev/null +++ b/app/explore/experts/[consultantId]/utils.ts @@ -0,0 +1,204 @@ +import { DayOfWeek } from "@prisma/client"; +import { TWeeklySlot, TCustomSlot, TSlotTiming } from "@/types/slots"; + +export const dayMap: Record = { + 0: DayOfWeek.SUNDAY, + 1: DayOfWeek.MONDAY, + 2: DayOfWeek.TUESDAY, + 3: DayOfWeek.WEDNESDAY, + 4: DayOfWeek.THURSDAY, + 5: DayOfWeek.FRIDAY, + 6: DayOfWeek.SATURDAY +}; + +export function convertUTCToLocalDate(utcTime: string | Date, selectedDate: Date, timezone?: string | null): Date { + // Parse the UTC time from 1970-01-01 format + const utcDate = typeof utcTime === 'string' ? new Date(utcTime) : utcTime; + const utcHours = utcDate.getUTCHours(); + const utcMinutes = utcDate.getUTCMinutes(); + + // Create a new date in UTC + const utcDateTime = new Date(Date.UTC( + selectedDate.getFullYear(), + selectedDate.getMonth(), + selectedDate.getDate(), + utcHours, + utcMinutes, + 0, + 0 + )); + + if (timezone) { + try { + // Convert UTC to local time string in the user's timezone + const localTimeStr = utcDateTime.toLocaleString('en-US', { + timeZone: timezone, + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false + }); + + // Parse the local time string back to a Date object + const [datePart, timePart] = localTimeStr.split(', '); + const [month, day, year] = datePart.split('/').map(Number); + const [hours, minutes, seconds] = timePart.split(':').map(Number); + + return new Date(year, month - 1, day, hours, minutes, seconds); + } catch (e) { + console.warn('Invalid timezone, using UTC'); + return utcDateTime; + } + } + + return utcDateTime; +} + +export function formatTime(date: string | Date, timezone?: string | null): string { + const dateObj = typeof date === 'string' ? new Date(date) : date; + + try { + return new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: 'numeric', + hour12: true, + ...(timezone ? { timeZone: timezone } : {}) + }).format(dateObj); + } catch (e) { + return dateObj.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: 'numeric', + hour12: true + }); + } +} + +export function getDayBefore(day: DayOfWeek): DayOfWeek { + const days = [ + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY + ]; + const index = days.indexOf(day); + return days[(index - 1 + 7) % 7]; +} + +export function getDayAfter(day: DayOfWeek): DayOfWeek { + const days = [ + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY + ]; + const index = days.indexOf(day); + return days[(index + 1) % 7]; +} + +export function getLocalDay(date: Date, timezone?: string | null): number { + if (timezone) { + try { + const localDate = new Date(date.toLocaleString('en-US', { timeZone: timezone })); + return localDate.getDay(); + } catch (e) { + console.warn('Invalid timezone, using UTC'); + } + } + return date.getDay(); +} + +export function isSameLocalDay(date1: Date, date2: Date, timezone?: string | null): boolean { + if (!timezone) return false; + + try { + const d1 = new Date(date1.toLocaleString('en-US', { timeZone: timezone })); + const d2 = new Date(date2.toLocaleString('en-US', { timeZone: timezone })); + + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); + } catch (e) { + console.warn('Invalid timezone'); + return false; + } +} + +export function createWeeklySlot( + slot: TWeeklySlot, + selectedDate: Date, + startDateTime: Date, + endDateTime: Date, + timezone?: string | null +): TSlotTiming { + return { + slotId: slot.id, + dateInISO: selectedDate.toISOString(), + dayOfWeek: slot.dayOfWeekforStartTimeInUTC, + slotStartTimeInUTC: startDateTime.toISOString(), + slotEndTimeInUTC: endDateTime.toISOString(), + slotOfAvailabilityId: slot.id, + slotOfAppointmentId: "", + localStartTime: formatTime(startDateTime, timezone), + localEndTime: formatTime(endDateTime, timezone), + }; +} + +export function createCustomSlot( + slot: TCustomSlot, + selectedDate: Date, + startDateTime: Date, + endDateTime: Date, + timezone?: string | null +): TSlotTiming { + return { + slotId: slot.id, + dateInISO: selectedDate.toISOString(), + dayOfWeek: dayMap[startDateTime.getDay()], + slotStartTimeInUTC: startDateTime.toISOString(), + slotEndTimeInUTC: endDateTime.toISOString(), + slotOfAvailabilityId: slot.id, + slotOfAppointmentId: "", + localStartTime: formatTime(startDateTime, timezone), + localEndTime: formatTime(endDateTime, timezone), + }; +} + +export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | null): TSlotTiming[] { + return slots.reduce((acc: TSlotTiming[], curr) => { + if (acc.length === 0) return [curr]; + + const last = acc[acc.length - 1]; + const lastEnd = new Date(last.slotEndTimeInUTC); + const currStart = new Date(curr.slotStartTimeInUTC); + const currEnd = new Date(curr.slotEndTimeInUTC); + + // If current slot starts before or at the same time as the last slot ends + if (currStart <= lastEnd) { + // If current slot ends after the last slot + if (currEnd > lastEnd) { + last.slotEndTimeInUTC = curr.slotEndTimeInUTC; + last.localEndTime = formatTime(currEnd, timezone); + } + return acc; + } + + // If slots are less than 1 minute apart, merge them + const diffInMinutes = (currStart.getTime() - lastEnd.getTime()) / (1000 * 60); + if (diffInMinutes <= 1) { + last.slotEndTimeInUTC = curr.slotEndTimeInUTC; + last.localEndTime = formatTime(currEnd, timezone); + return acc; + } + + return [...acc, curr]; + }, []); +} From f7d5d963f1d57175665fb419ddcc8fb9cd452ed8 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 16:51:16 +0530 Subject: [PATCH 07/16] updated seed files --- prisma/seedFiles/createSlotsOfAvailability.ts | 184 ++++++++++++------ 1 file changed, 122 insertions(+), 62 deletions(-) diff --git a/prisma/seedFiles/createSlotsOfAvailability.ts b/prisma/seedFiles/createSlotsOfAvailability.ts index 7569d2e..a1e7f8f 100644 --- a/prisma/seedFiles/createSlotsOfAvailability.ts +++ b/prisma/seedFiles/createSlotsOfAvailability.ts @@ -3,7 +3,58 @@ import { DayOfWeek, ScheduleType } from "@prisma/client"; import prisma from "../../lib/prisma"; import { UserWithProfiles } from "./createUsers"; -const NUM_SLOTS_PER_CONSULTANT = 20; +const MAX_SLOT_DURATION = 6; // 6 hours +const MIN_SLOT_DURATION = 0.5; // 30 minutes +const MIN_BREAK_DURATION = 0.5; // 30 minutes +const MAX_SLOTS_PER_DAY = 4; + +function generateSlotTime(existingSlots: Array<{ start: number; end: number }>) { + // Keep trying until we find a valid slot + let attempts = 0; + while (attempts < 50) { // Prevent infinite loops + // Generate random start hour (0-23) + const startHour = faker.number.int({ min: 0, max: 23 }); + // Randomly decide if we want to start at half hour + const startMinute = faker.helpers.arrayElement([0, 0.5]); + const start = startHour + startMinute; + + // Generate random duration between 30 mins and 6 hours + const possibleDurations = Array.from( + { length: MAX_SLOT_DURATION * 2 }, // *2 because we're counting in half hours + (_, i) => (i + 1) * 0.5 // Generate durations from 0.5 to 6 in 0.5 increments + ); + const duration = faker.helpers.arrayElement(possibleDurations); + const end = start + duration; + + // Verify this slot doesn't overlap with existing slots + const hasOverlap = existingSlots.some(slot => { + // Add MIN_BREAK_DURATION to ensure minimum break between slots + return !(end + MIN_BREAK_DURATION <= slot.start || start >= slot.end + MIN_BREAK_DURATION); + }); + + if (!hasOverlap) { + return { start, end }; + } + + attempts++; + } + return null; // Couldn't find a valid slot +} + +function generateDaySlots() { + const slots: Array<{ start: number; end: number }> = []; + const numSlots = faker.number.int({ min: 1, max: MAX_SLOTS_PER_DAY }); + + for (let i = 0; i < numSlots; i++) { + const slot = generateSlotTime(slots); + if (slot) { + slots.push(slot); + } + } + + // Sort slots by start time + return slots.sort((a, b) => a.start - b.start); +} export async function createSlotsOfAvailability( consultants: UserWithProfiles[], @@ -11,81 +62,90 @@ export async function createSlotsOfAvailability( console.log( `Creating slots of availability for ${consultants.length} consultants...`, ); + for (let i = 0; i < consultants.length; i++) { const consultant = consultants[i]; if (!consultant.consultantProfile) { console.warn(`Skipping consultant ${consultant.id} - no profile found`); continue; } + try { const slotType = consultant.consultantProfile.scheduleType; if (slotType === ScheduleType.WEEKLY) { - // Create weekly slots - for (let j = 0; j < NUM_SLOTS_PER_CONSULTANT; j++) { - const dayOfWeek = faker.helpers.arrayElement( - Object.values(DayOfWeek), - ); - const startHour = faker.helpers.arrayElement([ - 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, - ]); - const durationHours = faker.helpers.arrayElement([ - 0.5, 1, 1.5, 2, 2.5, 3, - ]); - - const startTime = new Date(); - startTime.setUTCHours(startHour, startHour % 1 === 0 ? 0 : 30, 0, 0); - - const endTime = new Date(startTime); - endTime.setTime(startTime.getTime() + durationHours * 60 * 60 * 1000); - - let endDayOfWeek = dayOfWeek; - if (endTime.getUTCHours() < startTime.getUTCHours()) { - // If end time is on the next day, adjust the day of week - const daysOfWeek = Object.values(DayOfWeek); - const currentIndex = daysOfWeek.indexOf(dayOfWeek); - endDayOfWeek = daysOfWeek[(currentIndex + 1) % 7]; - } + // Create weekly slots for each day + const daysOfWeek = Object.values(DayOfWeek); + + for (const dayOfWeek of daysOfWeek) { + const daySlots = generateDaySlots(); + + for (const slot of daySlots) { + const startHour = Math.floor(slot.start); + const startMinute = (slot.start % 1) * 60; + + const endHour = Math.floor(slot.end); + const endMinute = (slot.end % 1) * 60; - await prisma.slotOfAvailabilityWeekly.create({ - data: { - consultantProfileId: consultant.consultantProfile.id, - dayOfWeekforStartTimeInUTC: dayOfWeek, - slotStartTimeInUTC: startTime, - dayOfWeekforEndTimeInUTC: endDayOfWeek, - slotEndTimeInUTC: endTime, - }, - }); + const startTime = new Date(); + startTime.setUTCHours(startHour, startMinute, 0, 0); + + const endTime = new Date(); + endTime.setUTCHours(endHour, endMinute, 0, 0); + + // If end time is before start time, it means the slot crosses midnight + if (endTime <= startTime) { + endTime.setDate(endTime.getDate() + 1); + } + + await prisma.slotOfAvailabilityWeekly.create({ + data: { + consultantProfileId: consultant.consultantProfile.id, + dayOfWeekforStartTimeInUTC: dayOfWeek, + slotStartTimeInUTC: startTime, + // If slot crosses midnight, end day is next day + dayOfWeekforEndTimeInUTC: endTime <= startTime ? + daysOfWeek[(daysOfWeek.indexOf(dayOfWeek) + 1) % 7] : dayOfWeek, + slotEndTimeInUTC: endTime, + }, + }); + } } } else { - // Create custom slots - for (let j = 0; j < NUM_SLOTS_PER_CONSULTANT; j++) { - const startDate = faker.date.future(); - const startHour = faker.helpers.arrayElement([ - 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, - ]); - const durationHours = faker.helpers.arrayElement([ - 0.5, 1, 1.5, 2, 2.5, 3, - ]); - - const startTime = new Date(startDate); - startTime.setUTCHours(startHour, startHour % 1 === 0 ? 0 : 30, 0, 0); - - const endTime = new Date(startTime); - endTime.setTime(startTime.getTime() + durationHours * 60 * 60 * 1000); - - if (endTime < startTime) { - // If end time is on the next day, add one day to the end time - endTime.setDate(endTime.getDate() + 1); - } + // Create custom slots for next 7 days + const startDate = new Date(); + for (let day = 0; day < 7; day++) { + const date = new Date(startDate); + date.setDate(date.getDate() + day); + + const daySlots = generateDaySlots(); + + for (const slot of daySlots) { + const startHour = Math.floor(slot.start); + const startMinute = (slot.start % 1) * 60; + + const endHour = Math.floor(slot.end); + const endMinute = (slot.end % 1) * 60; - await prisma.slotOfAvailabilityCustom.create({ - data: { - consultantProfileId: consultant.consultantProfile.id, - slotStartTimeInUTC: startTime, - slotEndTimeInUTC: endTime, - }, - }); + const startTime = new Date(date); + startTime.setUTCHours(startHour, startMinute, 0, 0); + + const endTime = new Date(date); + endTime.setUTCHours(endHour, endMinute, 0, 0); + + // If end time is before start time, it means the slot crosses midnight + if (endTime <= startTime) { + endTime.setDate(endTime.getDate() + 1); + } + + await prisma.slotOfAvailabilityCustom.create({ + data: { + consultantProfileId: consultant.consultantProfile.id, + slotStartTimeInUTC: startTime, + slotEndTimeInUTC: endTime, + }, + }); + } } } } catch (error) { From 684eb4abc853fe56cddee5ebc958021b4bf5edee Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 17:02:18 +0530 Subject: [PATCH 08/16] Refactor timezone detection and formatting functions --- app/explore/experts/[consultantId]/utils.ts | 170 ++++++++++++++------ 1 file changed, 122 insertions(+), 48 deletions(-) diff --git a/app/explore/experts/[consultantId]/utils.ts b/app/explore/experts/[consultantId]/utils.ts index fc8cbb7..0ed3bd7 100644 --- a/app/explore/experts/[consultantId]/utils.ts +++ b/app/explore/experts/[consultantId]/utils.ts @@ -11,6 +11,25 @@ export const dayMap: Record = { 6: DayOfWeek.SATURDAY }; + +export function getLocalDay(date: Date, timezone?: string | null): number { + if (!timezone) return date.getDay(); + + try { + const localDate = new Date(date.toLocaleString('en-US', { timeZone: timezone })); + console.log('Local day calculation:', { + originalDate: date.toISOString(), + timezone, + localDate: localDate.toISOString(), + localDay: localDate.getDay() + }); + return localDate.getDay(); + } catch (e) { + console.warn('Invalid timezone, using UTC day'); + return date.getUTCDay(); + } +} + export function convertUTCToLocalDate(utcTime: string | Date, selectedDate: Date, timezone?: string | null): Date { // Parse the UTC time from 1970-01-01 format const utcDate = typeof utcTime === 'string' ? new Date(utcTime) : utcTime; @@ -28,33 +47,39 @@ export function convertUTCToLocalDate(utcTime: string | Date, selectedDate: Date 0 )); - if (timezone) { - try { - // Convert UTC to local time string in the user's timezone - const localTimeStr = utcDateTime.toLocaleString('en-US', { - timeZone: timezone, - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - hour12: false - }); - - // Parse the local time string back to a Date object - const [datePart, timePart] = localTimeStr.split(', '); - const [month, day, year] = datePart.split('/').map(Number); - const [hours, minutes, seconds] = timePart.split(':').map(Number); - - return new Date(year, month - 1, day, hours, minutes, seconds); - } catch (e) { - console.warn('Invalid timezone, using UTC'); - return utcDateTime; - } - } + if (!timezone) return utcDateTime; - return utcDateTime; + try { + // Convert UTC to local time string in the target timezone + const localTimeStr = utcDateTime.toLocaleString('en-US', { + timeZone: timezone, + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false + }); + + // Parse the local time string back to a Date object + const [datePart, timePart] = localTimeStr.split(', '); + const [month, day, year] = datePart.split('/').map(Number); + const [hours, minutes, seconds] = timePart.split(':').map(Number); + + const localDate = new Date(year, month - 1, day, hours, minutes, seconds); + + console.log('UTC to Local conversion:', { + utcTime: utcDateTime.toISOString(), + timezone, + localTime: localDate.toISOString() + }); + + return localDate; + } catch (e) { + console.warn('Invalid timezone, using UTC'); + return utcDateTime; + } } export function formatTime(date: string | Date, timezone?: string | null): string { @@ -65,9 +90,10 @@ export function formatTime(date: string | Date, timezone?: string | null): strin hour: 'numeric', minute: 'numeric', hour12: true, - ...(timezone ? { timeZone: timezone } : {}) + timeZone: timezone || undefined }).format(dateObj); } catch (e) { + console.warn('Error formatting time:', e); return dateObj.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', @@ -104,28 +130,27 @@ export function getDayAfter(day: DayOfWeek): DayOfWeek { return days[(index + 1) % 7]; } -export function getLocalDay(date: Date, timezone?: string | null): number { - if (timezone) { - try { - const localDate = new Date(date.toLocaleString('en-US', { timeZone: timezone })); - return localDate.getDay(); - } catch (e) { - console.warn('Invalid timezone, using UTC'); - } - } - return date.getDay(); -} - export function isSameLocalDay(date1: Date, date2: Date, timezone?: string | null): boolean { if (!timezone) return false; - + try { const d1 = new Date(date1.toLocaleString('en-US', { timeZone: timezone })); const d2 = new Date(date2.toLocaleString('en-US', { timeZone: timezone })); - return d1.getFullYear() === d2.getFullYear() && + const result = d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); + + console.log('Same local day check:', { + date1: date1.toISOString(), + date2: date2.toISOString(), + timezone, + localDate1: d1.toISOString(), + localDate2: d2.toISOString(), + isSameDay: result + }); + + return result; } catch (e) { console.warn('Invalid timezone'); return false; @@ -139,17 +164,46 @@ export function createWeeklySlot( endDateTime: Date, timezone?: string | null ): TSlotTiming { - return { + let adjustedEndDateTime = new Date(endDateTime); + + // Handle slots that cross midnight + if (slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC) { + // If end time is 00:00, it means it ends at midnight of the next day + if (adjustedEndDateTime.getHours() === 0 && adjustedEndDateTime.getMinutes() === 0) { + adjustedEndDateTime = new Date(endDateTime); + adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); + } + // If end time is before start time, it means it ends next day + else if (adjustedEndDateTime <= startDateTime) { + adjustedEndDateTime = new Date(endDateTime); + adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); + } + } + + const slotTiming = { slotId: slot.id, dateInISO: selectedDate.toISOString(), dayOfWeek: slot.dayOfWeekforStartTimeInUTC, slotStartTimeInUTC: startDateTime.toISOString(), - slotEndTimeInUTC: endDateTime.toISOString(), + slotEndTimeInUTC: adjustedEndDateTime.toISOString(), slotOfAvailabilityId: slot.id, slotOfAppointmentId: "", localStartTime: formatTime(startDateTime, timezone), - localEndTime: formatTime(endDateTime, timezone), + localEndTime: formatTime(adjustedEndDateTime, timezone), }; + + console.log('Created weekly slot:', { + startDay: slot.dayOfWeekforStartTimeInUTC, + endDay: slot.dayOfWeekforEndTimeInUTC, + utcStart: slot.slotStartTimeInUTC, + utcEnd: slot.slotEndTimeInUTC, + localStart: slotTiming.localStartTime, + localEnd: slotTiming.localEndTime, + timezone, + crossesMidnight: slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC + }); + + return slotTiming; } export function createCustomSlot( @@ -159,17 +213,36 @@ export function createCustomSlot( endDateTime: Date, timezone?: string | null ): TSlotTiming { - return { + let adjustedEndDateTime = new Date(endDateTime); + + // If end time is before start time, it means the slot crosses midnight + if (adjustedEndDateTime <= startDateTime) { + adjustedEndDateTime = new Date(endDateTime); + adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); + } + + const slotTiming = { slotId: slot.id, dateInISO: selectedDate.toISOString(), - dayOfWeek: dayMap[startDateTime.getDay()], + dayOfWeek: dayMap[getLocalDay(startDateTime, timezone)], slotStartTimeInUTC: startDateTime.toISOString(), - slotEndTimeInUTC: endDateTime.toISOString(), + slotEndTimeInUTC: adjustedEndDateTime.toISOString(), slotOfAvailabilityId: slot.id, slotOfAppointmentId: "", localStartTime: formatTime(startDateTime, timezone), - localEndTime: formatTime(endDateTime, timezone), + localEndTime: formatTime(adjustedEndDateTime, timezone), }; + + console.log('Created custom slot:', { + utcStart: slot.slotStartTimeInUTC, + utcEnd: slot.slotEndTimeInUTC, + localStart: slotTiming.localStartTime, + localEnd: slotTiming.localEndTime, + timezone, + crossesMidnight: adjustedEndDateTime.getDate() > startDateTime.getDate() + }); + + return slotTiming; } export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | null): TSlotTiming[] { @@ -202,3 +275,4 @@ export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | return [...acc, curr]; }, []); } + From 3e4182fc713aaa974928fe524b2f75407acd98b4 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 18:08:19 +0530 Subject: [PATCH 09/16] Refactor timezone detection and formatting functions, and normalize slot times - Refactored the timezone detection and formatting functions in the utils.ts file to improve code readability and maintainability. - Added new utility functions `normalizeUTCTime`, `normalizeWeeklySlot`, and `normalizeCustomSlot` to ensure all times are in UTC format. - Updated the `convertUTCToLocalDate` function to always parse the UTC time as a string. - Adjusted the `convertUTCToLocalDate` function to handle cases where the local time is on the previous day. - Added the `isSlotRelevantForDay` function to determine if a slot is relevant for a specific day. - Created the `createWeeklySlot` and `createCustomSlot` functions to generate slot timings based on selected dates and times. - Updated the `mergeOverlappingSlots` function to handle overlapping slots correctly. --- app/explore/experts/[consultantId]/page.tsx | 167 +++++--------------- app/explore/experts/[consultantId]/utils.ts | 133 ++++++++++++---- 2 files changed, 144 insertions(+), 156 deletions(-) diff --git a/app/explore/experts/[consultantId]/page.tsx b/app/explore/experts/[consultantId]/page.tsx index 8a2aa2c..fa99907 100644 --- a/app/explore/experts/[consultantId]/page.tsx +++ b/app/explore/experts/[consultantId]/page.tsx @@ -10,7 +10,7 @@ import { } from "@/hooks/useUserData"; import { TConsultantProfile } from "@/types/consultant"; -import { TSlotTiming } from "@/types/slots"; +import { TSlotTiming, TWeeklySlot, TCustomSlot } from "@/types/slots"; import { ConsultantReview, ConsultationPlan, @@ -31,12 +31,14 @@ import { dayMap, convertUTCToLocalDate, formatTime, - getDayAfter, createWeeklySlot, createCustomSlot, mergeOverlappingSlots, getLocalDay, isSameLocalDay, + normalizeWeeklySlot, + normalizeCustomSlot, + isSlotRelevantForDay, } from "./utils"; import { useTimezone } from "./hooks/useTimezone"; @@ -58,7 +60,6 @@ export default function ExpertProfile( }>, ) { const params = use(props.params); - const searchParams = use(props.searchParams); const { timezone: browserTimezone, isLoading: isTimezoneLoading } = useTimezone(); const [userDetails, setUserDetails] = useState(null); @@ -122,27 +123,11 @@ export default function ExpertProfile( console.log('Selected day:', selectedDay); // Get slots for the selected day - const relevantSlots = consultantDetails.slotsOfAvailabilityWeekly.filter(slot => { - const startDay = slot.dayOfWeekforStartTimeInUTC; - const endDay = slot.dayOfWeekforEndTimeInUTC; - - const isRelevant = startDay === selectedDay || - (startDay !== endDay && endDay === selectedDay) || - (startDay !== endDay && getDayAfter(startDay) === selectedDay); - - if (isRelevant) { - console.log('Found relevant slot:', { - startDay, - endDay, - startTime: slot.slotStartTimeInUTC, - endTime: slot.slotEndTimeInUTC - }); - } - - return isRelevant; - }); + const relevantSlots = consultantDetails.slotsOfAvailabilityWeekly + .map(normalizeWeeklySlot) + .filter(slot => isSlotRelevantForDay(slot, selectedDay, timezone)); - const weeklySlots = relevantSlots.flatMap(slot => { + const weeklySlots = relevantSlots.map(slot => { // Convert UTC times to local date objects const startDateTime = convertUTCToLocalDate(slot.slotStartTimeInUTC, selectedDate, timezone); let endDateTime = convertUTCToLocalDate(slot.slotEndTimeInUTC, selectedDate, timezone); @@ -155,54 +140,9 @@ export default function ExpertProfile( timezone }); - // If this slot ends on the next day - if (slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC) { - if (slot.dayOfWeekforStartTimeInUTC === selectedDay) { - // For slots starting on selected day and ending next day, - // show only the portion until midnight - const nextDay = new Date(startDateTime); - nextDay.setDate(nextDay.getDate() + 1); - nextDay.setHours(0, 0, 0, 0); - console.log('Slot crosses to next day, ending at midnight:', nextDay.toISOString()); - return [createWeeklySlot(slot, selectedDate, startDateTime, nextDay, timezone)]; - } else if (slot.dayOfWeekforEndTimeInUTC === selectedDay) { - // For slots ending on selected day (started previous day), - // show only the portion from midnight - const thisDay = new Date(endDateTime); - thisDay.setHours(0, 0, 0, 0); - console.log('Slot started previous day, starting at midnight:', thisDay.toISOString()); - return [createWeeklySlot(slot, selectedDate, thisDay, endDateTime, timezone)]; - } else if (getDayAfter(slot.dayOfWeekforStartTimeInUTC) === selectedDay) { - // For slots spanning multiple days, show full day - const thisDay = new Date(selectedDate); - thisDay.setHours(0, 0, 0, 0); - const nextDay = new Date(selectedDate); - nextDay.setDate(nextDay.getDate() + 1); - nextDay.setHours(0, 0, 0, 0); - console.log('Slot spans multiple days:', { - start: thisDay.toISOString(), - end: nextDay.toISOString() - }); - return [createWeeklySlot(slot, selectedDate, thisDay, nextDay, timezone)]; - } - } - - // For slots within the same day - if (endDateTime <= startDateTime) { - endDateTime = new Date(endDateTime.getTime() + 24 * 60 * 60 * 1000); - console.log('Adjusted end time for same day slot:', endDateTime.toISOString()); - } - - // Only include slots that overlap with the selected date in local time - if (isSameLocalDay(startDateTime, selectedDate, timezone) || - isSameLocalDay(endDateTime, selectedDate, timezone)) { - console.log('Slot overlaps with selected date'); - return [createWeeklySlot(slot, selectedDate, startDateTime, endDateTime, timezone)]; - } - - console.log('Slot does not overlap with selected date'); - return []; - }).filter(Boolean) as TSlotTiming[]; + // Create the slot timing + return createWeeklySlot(slot, selectedDate, startDateTime, endDateTime, timezone); + }); // Sort slots by start time const sortedSlots = weeklySlots.sort((a, b) => @@ -218,6 +158,7 @@ export default function ExpertProfile( setSlotTimings(mergedSlots); } else if (consultantDetails.scheduleType === "CUSTOM") { const customSlots = consultantDetails.slotsOfAvailabilityCustom + .map(normalizeCustomSlot) .filter(slot => { const startDateTime = new Date(slot.slotStartTimeInUTC); return isSameLocalDay(startDateTime, selectedDate, timezone); @@ -225,12 +166,6 @@ export default function ExpertProfile( .map(slot => { const startDateTime = new Date(slot.slotStartTimeInUTC); const endDateTime = new Date(slot.slotEndTimeInUTC); - - // If end time is before start time, it means it ends next day - if (endDateTime <= startDateTime) { - endDateTime.setDate(endDateTime.getDate() + 1); - } - return createCustomSlot(slot, selectedDate, startDateTime, endDateTime, timezone); }); @@ -321,73 +256,49 @@ export default function ExpertProfile( if (consultantDetails.scheduleType === "WEEKLY") { // Convert Date objects to strings for weekly slots - const weeklySlots = consultantDetails.slotsOfAvailabilityWeekly.map( - (slot) => ({ - id: slot.id, - dayOfWeekforStartTimeInUTC: slot.dayOfWeekforStartTimeInUTC, - slotStartTimeInUTC: - typeof slot.slotStartTimeInUTC === "string" - ? slot.slotStartTimeInUTC - : slot.slotStartTimeInUTC.toISOString(), - dayOfWeekforEndTimeInUTC: slot.dayOfWeekforEndTimeInUTC, - slotEndTimeInUTC: - typeof slot.slotEndTimeInUTC === "string" - ? slot.slotEndTimeInUTC - : slot.slotEndTimeInUTC.toISOString(), - }), - ); + const weeklySlots = consultantDetails.slotsOfAvailabilityWeekly.map(normalizeWeeklySlot); return ( + onSlotSelect={slot => { + const normalizedSlot = normalizeWeeklySlot(slot); setSelectedSlot({ - slotId: slot.id, + slotId: normalizedSlot.id, dateInISO: new Date().toISOString(), - dayOfWeek: slot.dayOfWeekforStartTimeInUTC, - slotStartTimeInUTC: slot.slotStartTimeInUTC, - slotEndTimeInUTC: slot.slotEndTimeInUTC, - slotOfAvailabilityId: slot.id, + dayOfWeek: normalizedSlot.dayOfWeekforStartTimeInUTC, + slotStartTimeInUTC: normalizedSlot.slotStartTimeInUTC, + slotEndTimeInUTC: normalizedSlot.slotEndTimeInUTC, + slotOfAvailabilityId: normalizedSlot.id, slotOfAppointmentId: "", - localStartTime: formatTime(slot.slotStartTimeInUTC, timezone), - localEndTime: formatTime(slot.slotEndTimeInUTC, timezone), - }) - } + localStartTime: formatTime(normalizedSlot.slotStartTimeInUTC, timezone), + localEndTime: formatTime(normalizedSlot.slotEndTimeInUTC, timezone), + }); + }} selectedSlotId={selectedSlot?.slotId} /> ); } else if (consultantDetails.scheduleType === "CUSTOM") { // Convert Date objects to strings for custom slots - const customSlots = consultantDetails.slotsOfAvailabilityCustom.map( - (slot) => ({ - id: slot.id, - slotStartTimeInUTC: - typeof slot.slotStartTimeInUTC === "string" - ? slot.slotStartTimeInUTC - : slot.slotStartTimeInUTC.toISOString(), - slotEndTimeInUTC: - typeof slot.slotEndTimeInUTC === "string" - ? slot.slotEndTimeInUTC - : slot.slotEndTimeInUTC.toISOString(), - }), - ); + const customSlots = consultantDetails.slotsOfAvailabilityCustom.map(normalizeCustomSlot); return ( + onSlotSelect={slot => { + const normalizedSlot = normalizeCustomSlot(slot); setSelectedSlot({ - slotId: slot.id, - dateInISO: new Date(slot.slotStartTimeInUTC).toISOString(), - dayOfWeek: dayMap[new Date(slot.slotStartTimeInUTC).getDay()], - slotStartTimeInUTC: slot.slotStartTimeInUTC, - slotEndTimeInUTC: slot.slotEndTimeInUTC, - slotOfAvailabilityId: slot.id, + slotId: normalizedSlot.id, + dateInISO: new Date(normalizedSlot.slotStartTimeInUTC).toISOString(), + dayOfWeek: dayMap[new Date(normalizedSlot.slotStartTimeInUTC).getDay()], + slotStartTimeInUTC: normalizedSlot.slotStartTimeInUTC, + slotEndTimeInUTC: normalizedSlot.slotEndTimeInUTC, + slotOfAvailabilityId: normalizedSlot.id, slotOfAppointmentId: "", - localStartTime: formatTime(slot.slotStartTimeInUTC, timezone), - localEndTime: formatTime(slot.slotEndTimeInUTC, timezone), - }) - } + localStartTime: formatTime(normalizedSlot.slotStartTimeInUTC, timezone), + localEndTime: formatTime(normalizedSlot.slotEndTimeInUTC, timezone), + }); + }} selectedSlotId={selectedSlot?.slotId} /> ); @@ -514,7 +425,7 @@ export default function ExpertProfile( {userDetails.name} has experience across multiple industries, with a particular focus on{" "} {consultantDetails?.subDomains - ?.map((domain) => domain.name) + ?.map((domain: { name: string }) => domain.name) .join(", ")} .

    @@ -526,7 +437,7 @@ export default function ExpertProfile(

    {userDetails.name} focuses on{" "} - {consultantDetails.tags?.map((tag) => tag.name).join(", ")}. + {consultantDetails.tags?.map((tag: { name: string }) => tag.name).join(", ")}.

    diff --git a/app/explore/experts/[consultantId]/utils.ts b/app/explore/experts/[consultantId]/utils.ts index 0ed3bd7..8837553 100644 --- a/app/explore/experts/[consultantId]/utils.ts +++ b/app/explore/experts/[consultantId]/utils.ts @@ -11,6 +11,44 @@ export const dayMap: Record = { 6: DayOfWeek.SATURDAY }; +export const dayToNumber: Record = { + [DayOfWeek.SUNDAY]: 0, + [DayOfWeek.MONDAY]: 1, + [DayOfWeek.TUESDAY]: 2, + [DayOfWeek.WEDNESDAY]: 3, + [DayOfWeek.THURSDAY]: 4, + [DayOfWeek.FRIDAY]: 5, + [DayOfWeek.SATURDAY]: 6 +}; + +// Ensure UTC time is always a string +export function normalizeUTCTime(time: string | Date): string { + return typeof time === 'string' ? time : time.toISOString(); +} + +// Normalize weekly slot to ensure all times are strings +export function normalizeWeeklySlot(slot: TWeeklySlot): TWeeklySlot & { + slotStartTimeInUTC: string; + slotEndTimeInUTC: string; +} { + return { + ...slot, + slotStartTimeInUTC: normalizeUTCTime(slot.slotStartTimeInUTC), + slotEndTimeInUTC: normalizeUTCTime(slot.slotEndTimeInUTC) + }; +} + +// Normalize custom slot to ensure all times are strings +export function normalizeCustomSlot(slot: TCustomSlot): TCustomSlot & { + slotStartTimeInUTC: string; + slotEndTimeInUTC: string; +} { + return { + ...slot, + slotStartTimeInUTC: normalizeUTCTime(slot.slotStartTimeInUTC), + slotEndTimeInUTC: normalizeUTCTime(slot.slotEndTimeInUTC) + }; +} export function getLocalDay(date: Date, timezone?: string | null): number { if (!timezone) return date.getDay(); @@ -30,9 +68,9 @@ export function getLocalDay(date: Date, timezone?: string | null): number { } } -export function convertUTCToLocalDate(utcTime: string | Date, selectedDate: Date, timezone?: string | null): Date { +export function convertUTCToLocalDate(utcTime: string, selectedDate: Date, timezone?: string | null): Date { // Parse the UTC time from 1970-01-01 format - const utcDate = typeof utcTime === 'string' ? new Date(utcTime) : utcTime; + const utcDate = new Date(utcTime); const utcHours = utcDate.getUTCHours(); const utcMinutes = utcDate.getUTCMinutes(); @@ -69,6 +107,17 @@ export function convertUTCToLocalDate(utcTime: string | Date, selectedDate: Date const localDate = new Date(year, month - 1, day, hours, minutes, seconds); + // Adjust date if the local time is on the previous day + const localDay = getLocalDay(localDate, timezone); + const selectedDay = getLocalDay(selectedDate, timezone); + if (localDay !== selectedDay) { + if ((selectedDay === 0 && localDay === 6) || localDay === selectedDay - 1) { + localDate.setDate(localDate.getDate() + 1); + } else if ((selectedDay === 6 && localDay === 0) || localDay === selectedDay + 1) { + localDate.setDate(localDate.getDate() - 1); + } + } + console.log('UTC to Local conversion:', { utcTime: utcDateTime.toISOString(), timezone, @@ -157,6 +206,37 @@ export function isSameLocalDay(date1: Date, date2: Date, timezone?: string | nul } } +export function isSlotRelevantForDay( + slot: TWeeklySlot, + selectedDay: DayOfWeek, + timezone?: string | null +): boolean { + const startDay = slot.dayOfWeekforStartTimeInUTC; + const endDay = slot.dayOfWeekforEndTimeInUTC; + + // Direct match + if (startDay === selectedDay || endDay === selectedDay) { + return true; + } + + // Handle overnight slots + if (startDay !== endDay) { + const selectedDayNum = dayToNumber[selectedDay]; + const startDayNum = dayToNumber[startDay]; + const endDayNum = dayToNumber[endDay]; + + // Handle week wrap-around (e.g., Saturday to Sunday) + if (startDayNum > endDayNum) { + return selectedDayNum >= startDayNum || selectedDayNum <= endDayNum; + } + + // Normal case + return selectedDayNum >= startDayNum && selectedDayNum <= endDayNum; + } + + return false; +} + export function createWeeklySlot( slot: TWeeklySlot, selectedDate: Date, @@ -165,42 +245,38 @@ export function createWeeklySlot( timezone?: string | null ): TSlotTiming { let adjustedEndDateTime = new Date(endDateTime); + const normalizedSlot = normalizeWeeklySlot(slot); // Handle slots that cross midnight - if (slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC) { - // If end time is 00:00, it means it ends at midnight of the next day - if (adjustedEndDateTime.getHours() === 0 && adjustedEndDateTime.getMinutes() === 0) { - adjustedEndDateTime = new Date(endDateTime); - adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); - } - // If end time is before start time, it means it ends next day - else if (adjustedEndDateTime <= startDateTime) { - adjustedEndDateTime = new Date(endDateTime); - adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); - } + if (slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC || + endDateTime <= startDateTime || + (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0)) { + + adjustedEndDateTime = new Date(endDateTime); + adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); } const slotTiming = { - slotId: slot.id, + slotId: normalizedSlot.id, dateInISO: selectedDate.toISOString(), - dayOfWeek: slot.dayOfWeekforStartTimeInUTC, + dayOfWeek: normalizedSlot.dayOfWeekforStartTimeInUTC, slotStartTimeInUTC: startDateTime.toISOString(), slotEndTimeInUTC: adjustedEndDateTime.toISOString(), - slotOfAvailabilityId: slot.id, + slotOfAvailabilityId: normalizedSlot.id, slotOfAppointmentId: "", localStartTime: formatTime(startDateTime, timezone), localEndTime: formatTime(adjustedEndDateTime, timezone), }; console.log('Created weekly slot:', { - startDay: slot.dayOfWeekforStartTimeInUTC, - endDay: slot.dayOfWeekforEndTimeInUTC, - utcStart: slot.slotStartTimeInUTC, - utcEnd: slot.slotEndTimeInUTC, + startDay: normalizedSlot.dayOfWeekforStartTimeInUTC, + endDay: normalizedSlot.dayOfWeekforEndTimeInUTC, + utcStart: normalizedSlot.slotStartTimeInUTC, + utcEnd: normalizedSlot.slotEndTimeInUTC, localStart: slotTiming.localStartTime, localEnd: slotTiming.localEndTime, timezone, - crossesMidnight: slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC + crossesMidnight: normalizedSlot.dayOfWeekforStartTimeInUTC !== normalizedSlot.dayOfWeekforEndTimeInUTC }); return slotTiming; @@ -214,28 +290,30 @@ export function createCustomSlot( timezone?: string | null ): TSlotTiming { let adjustedEndDateTime = new Date(endDateTime); + const normalizedSlot = normalizeCustomSlot(slot); - // If end time is before start time, it means the slot crosses midnight - if (adjustedEndDateTime <= startDateTime) { + // Handle slots that cross midnight + if (endDateTime <= startDateTime || + (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0)) { adjustedEndDateTime = new Date(endDateTime); adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); } const slotTiming = { - slotId: slot.id, + slotId: normalizedSlot.id, dateInISO: selectedDate.toISOString(), dayOfWeek: dayMap[getLocalDay(startDateTime, timezone)], slotStartTimeInUTC: startDateTime.toISOString(), slotEndTimeInUTC: adjustedEndDateTime.toISOString(), - slotOfAvailabilityId: slot.id, + slotOfAvailabilityId: normalizedSlot.id, slotOfAppointmentId: "", localStartTime: formatTime(startDateTime, timezone), localEndTime: formatTime(adjustedEndDateTime, timezone), }; console.log('Created custom slot:', { - utcStart: slot.slotStartTimeInUTC, - utcEnd: slot.slotEndTimeInUTC, + utcStart: normalizedSlot.slotStartTimeInUTC, + utcEnd: normalizedSlot.slotEndTimeInUTC, localStart: slotTiming.localStartTime, localEnd: slotTiming.localEndTime, timezone, @@ -275,4 +353,3 @@ export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | return [...acc, curr]; }, []); } - From 5dea7b1e955dd95e9001dd53405db204c9f7b67c Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 18:11:00 +0530 Subject: [PATCH 10/16] Refactor timezone adjustment logic and slot merging algorithm --- app/explore/experts/[consultantId]/utils.ts | 45 ++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/app/explore/experts/[consultantId]/utils.ts b/app/explore/experts/[consultantId]/utils.ts index 8837553..7cacbc8 100644 --- a/app/explore/experts/[consultantId]/utils.ts +++ b/app/explore/experts/[consultantId]/utils.ts @@ -107,13 +107,23 @@ export function convertUTCToLocalDate(utcTime: string, selectedDate: Date, timez const localDate = new Date(year, month - 1, day, hours, minutes, seconds); - // Adjust date if the local time is on the previous day + // Adjust date if the local time is on a different day const localDay = getLocalDay(localDate, timezone); const selectedDay = getLocalDay(selectedDate, timezone); + if (localDay !== selectedDay) { - if ((selectedDay === 0 && localDay === 6) || localDay === selectedDay - 1) { + // Handle week wrap-around + if (selectedDay === 0 && localDay === 6) { + // If selected day is Sunday and local day is Saturday, add a day + localDate.setDate(localDate.getDate() + 1); + } else if (selectedDay === 6 && localDay === 0) { + // If selected day is Saturday and local day is Sunday, subtract a day + localDate.setDate(localDate.getDate() - 1); + } else if (localDay === selectedDay - 1) { + // If local day is the previous day, add a day localDate.setDate(localDate.getDate() + 1); - } else if ((selectedDay === 6 && localDay === 0) || localDay === selectedDay + 1) { + } else if (localDay === selectedDay + 1) { + // If local day is the next day, subtract a day localDate.setDate(localDate.getDate() - 1); } } @@ -215,9 +225,7 @@ export function isSlotRelevantForDay( const endDay = slot.dayOfWeekforEndTimeInUTC; // Direct match - if (startDay === selectedDay || endDay === selectedDay) { - return true; - } + if (startDay === selectedDay) return true; // Handle overnight slots if (startDay !== endDay) { @@ -227,10 +235,17 @@ export function isSlotRelevantForDay( // Handle week wrap-around (e.g., Saturday to Sunday) if (startDayNum > endDayNum) { - return selectedDayNum >= startDayNum || selectedDayNum <= endDayNum; + // For slots that wrap around the week (e.g., Sat 9pm - Sun 3am) + // The slot is relevant for both the start day and the end day + if (selectedDay === endDay) return true; + + // For days in between (in case of multi-day slots) + const dayAfterStart = (startDayNum + 1) % 7; + return selectedDayNum >= startDayNum || selectedDayNum <= endDayNum || + (selectedDayNum >= dayAfterStart && selectedDayNum <= 6); } - // Normal case + // Normal case (e.g., Mon 10pm - Tue 2am) return selectedDayNum >= startDayNum && selectedDayNum <= endDayNum; } @@ -324,6 +339,7 @@ export function createCustomSlot( } export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | null): TSlotTiming[] { + // Don't merge slots that are more than 1 minute apart return slots.reduce((acc: TSlotTiming[], curr) => { if (acc.length === 0) return [curr]; @@ -332,8 +348,9 @@ export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | const currStart = new Date(curr.slotStartTimeInUTC); const currEnd = new Date(curr.slotEndTimeInUTC); - // If current slot starts before or at the same time as the last slot ends - if (currStart <= lastEnd) { + // Only merge if the slots are exactly adjacent (less than 1 minute apart) + const diffInMinutes = (currStart.getTime() - lastEnd.getTime()) / (1000 * 60); + if (diffInMinutes <= 0) { // If current slot ends after the last slot if (currEnd > lastEnd) { last.slotEndTimeInUTC = curr.slotEndTimeInUTC; @@ -342,14 +359,6 @@ export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | return acc; } - // If slots are less than 1 minute apart, merge them - const diffInMinutes = (currStart.getTime() - lastEnd.getTime()) / (1000 * 60); - if (diffInMinutes <= 1) { - last.slotEndTimeInUTC = curr.slotEndTimeInUTC; - last.localEndTime = formatTime(currEnd, timezone); - return acc; - } - return [...acc, curr]; }, []); } From 2e5bd85676bf2f183e59f0d32dc3bda219d7304c Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 19:26:58 +0530 Subject: [PATCH 11/16] near perfect --- .../components/WeeklyAvailability.tsx | 191 +++++------------- 1 file changed, 50 insertions(+), 141 deletions(-) diff --git a/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx index 089a178..8332bd4 100644 --- a/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx +++ b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx @@ -1,5 +1,11 @@ import React from "react"; import { DayOfWeek } from "@prisma/client"; +import { + convertUTCToLocalDate, + formatTime as formatTimeUtil, + isSlotRelevantForDay, + dayToNumber +} from "../utils"; interface WeeklySlot { id: string; @@ -15,61 +21,6 @@ interface WeeklyAvailabilityProps { selectedSlotId?: string; } -function convertUTCToLocal(date: Date): Date { - const offset = date.getTimezoneOffset(); - return new Date(date.getTime() - (offset * 60 * 1000)); -} - -function getDayBefore(day: DayOfWeek): DayOfWeek { - const days = [ - DayOfWeek.SUNDAY, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY - ]; - const index = days.indexOf(day); - return days[(index - 1 + 7) % 7]; -} - -function getDayAfter(day: DayOfWeek): DayOfWeek { - const days = [ - DayOfWeek.SUNDAY, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY - ]; - const index = days.indexOf(day); - return days[(index + 1) % 7]; -} - -const formatTime = (isoString: string): string => { - try { - // Create a base date for today - const baseDate = new Date(); - // Parse the time from the ISO string - const utcDate = new Date(isoString); - // Set the hours and minutes on the base date - baseDate.setHours(utcDate.getUTCHours(), utcDate.getUTCMinutes(), 0, 0); - // Convert to local time - const localDate = convertUTCToLocal(baseDate); - - return localDate.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - hour12: true, - }); - } catch (error) { - console.error("Error formatting time:", error); - return "Invalid Time"; - } -}; - export const WeeklyAvailability: React.FC = ({ slots, onSlotSelect, @@ -86,82 +37,45 @@ export const WeeklyAvailability: React.FC = ({ ]; const dayLabels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + // Get browser's timezone + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + console.log('Using timezone:', timezone); + // Group slots by day and sort by time const slotsByDay = daysOfWeek.map((day) => { - // Get slots that: - // 1. Start on this day - // 2. End on this day (started previous day) - // 3. Start on this day and end next day - const daySlots = slots.filter((slot) => { - const previousDay = getDayBefore(day); - const nextDay = getDayAfter(day); - - // Include slots that: - // 1. Start on this day - // 2. End on this day (started previous day) - // 3. Start on this day and end next day - // 4. Start on previous day and end on next day (crosses entire day) - return ( - slot.dayOfWeekforStartTimeInUTC === day || - slot.dayOfWeekforEndTimeInUTC === day || - (slot.dayOfWeekforStartTimeInUTC === previousDay && - slot.dayOfWeekforEndTimeInUTC === nextDay) - ); - }); - - // For each slot, determine if it needs to be split at midnight - const processedSlots = daySlots.flatMap((slot) => { - // Create a base date for today - const baseDate = new Date(); - // Parse the times from the ISO strings - const startTime = new Date(slot.slotStartTimeInUTC); - const endTime = new Date(slot.slotEndTimeInUTC); - - // Set the hours and minutes on the base date - const localStartTime = new Date(baseDate); - localStartTime.setHours(startTime.getUTCHours(), startTime.getUTCMinutes(), 0, 0); - - const localEndTime = new Date(baseDate); - localEndTime.setHours(endTime.getUTCHours(), endTime.getUTCMinutes(), 0, 0); - - // Convert to local time - const convertedStartTime = convertUTCToLocal(localStartTime); - const convertedEndTime = convertUTCToLocal(localEndTime); - - // If the slot crosses midnight - if (convertedEndTime < convertedStartTime) { - // Create two slots: one ending at midnight, one starting at midnight - const midnightEnd = new Date(convertedStartTime); - midnightEnd.setHours(23, 59, 59, 999); - - const midnightStart = new Date(convertedEndTime); - midnightStart.setHours(0, 0, 0, 0); - - return [ - { - ...slot, - slotStartTimeInUTC: convertedStartTime.toISOString(), - slotEndTimeInUTC: midnightEnd.toISOString(), - }, - { - ...slot, - slotStartTimeInUTC: midnightStart.toISOString(), - slotEndTimeInUTC: convertedEndTime.toISOString(), - }, - ]; + // Get slots that are relevant for this day + const daySlots = slots.filter(slot => isSlotRelevantForDay(slot, day, timezone)); + + // Process each slot to handle timezone and overnight slots + const processedSlots = daySlots.map(slot => { + // Use a reference date for consistent conversion + const referenceDate = new Date(); + referenceDate.setHours(0, 0, 0, 0); + + // Convert UTC times to local + const startDateTime = convertUTCToLocalDate(slot.slotStartTimeInUTC, referenceDate, timezone); + let endDateTime = convertUTCToLocalDate(slot.slotEndTimeInUTC, referenceDate, timezone); + + // If this slot crosses midnight + if (slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC || + endDateTime <= startDateTime || + (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0)) { + + endDateTime = new Date(endDateTime); + endDateTime.setDate(endDateTime.getDate() + 1); } - return [{ + return { ...slot, - slotStartTimeInUTC: convertedStartTime.toISOString(), - slotEndTimeInUTC: convertedEndTime.toISOString(), - }]; + localStartTime: formatTimeUtil(startDateTime, timezone), + localEndTime: formatTimeUtil(endDateTime, timezone) + }; }); // Sort slots by start time const sortedSlots = processedSlots.sort((a, b) => { - const timeA = new Date(a.slotStartTimeInUTC).getTime(); - const timeB = new Date(b.slotStartTimeInUTC).getTime(); + const timeA = new Date(`1970-01-01 ${a.localStartTime}`).getTime(); + const timeB = new Date(`1970-01-01 ${b.localStartTime}`).getTime(); return timeA - timeB; }); @@ -189,26 +103,21 @@ export const WeeklyAvailability: React.FC = ({
    {slotsByDay.map(({ day, slots: daySlots }) => (
    - {daySlots.map((slot) => { - const startTime = formatTime(slot.slotStartTimeInUTC); - const endTime = formatTime(slot.slotEndTimeInUTC); - - return ( -
    onSlotSelect(slot)} - > -
    - {startTime} - {endTime} -
    + {daySlots.map((slot) => ( +
    onSlotSelect(slot)} + > +
    + {slot.localStartTime} - {slot.localEndTime}
    - ); - })} +
    + ))}
    ))}
    From e1b7b55e4e47ff8eea08c7481046cb2cd5a9b500 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 20:46:32 +0530 Subject: [PATCH 12/16] Refactor consultant profile components and add availability and pricing sections --- .../components/AboutSection.tsx | 42 +++ .../components/ConsultantAvailability.tsx | 75 +++++ .../components/ConsultationPricing.tsx | 118 +++++++ .../components/ProfileHeader.tsx | 36 ++ .../components/ReviewsSection.tsx | 23 ++ app/explore/experts/[consultantId]/page.tsx | 318 +++--------------- 6 files changed, 347 insertions(+), 265 deletions(-) create mode 100644 app/explore/experts/[consultantId]/components/AboutSection.tsx create mode 100644 app/explore/experts/[consultantId]/components/ConsultantAvailability.tsx create mode 100644 app/explore/experts/[consultantId]/components/ConsultationPricing.tsx create mode 100644 app/explore/experts/[consultantId]/components/ProfileHeader.tsx create mode 100644 app/explore/experts/[consultantId]/components/ReviewsSection.tsx diff --git a/app/explore/experts/[consultantId]/components/AboutSection.tsx b/app/explore/experts/[consultantId]/components/AboutSection.tsx new file mode 100644 index 0000000..1c362bc --- /dev/null +++ b/app/explore/experts/[consultantId]/components/AboutSection.tsx @@ -0,0 +1,42 @@ +import { User } from "@prisma/client"; +import { TConsultantProfile } from "@/types/consultant"; + +interface AboutSectionProps { + userDetails: User; + consultantDetails: TConsultantProfile; +} + +export function AboutSection({ userDetails, consultantDetails }: AboutSectionProps) { + return ( +
    +
    +

    About

    +

    + {userDetails.name} is a seasoned {consultantDetails.specialization} with{" "} + {consultantDetails.experience} of experience in the{" "} + {consultantDetails.domain.name} sector. +

    +
    + +
    +

    Education & Background

    +

    + {userDetails.name} has experience across multiple industries, + with a particular focus on{" "} + {consultantDetails?.subDomains + ?.map((domain: { name: string }) => domain.name) + .join(", ")} + . +

    +
    + +
    +

    Skills & Specialties

    +

    + {userDetails.name} focuses on{" "} + {consultantDetails.tags?.map((tag: { name: string }) => tag.name).join(", ")}. +

    +
    +
    + ); +} diff --git a/app/explore/experts/[consultantId]/components/ConsultantAvailability.tsx b/app/explore/experts/[consultantId]/components/ConsultantAvailability.tsx new file mode 100644 index 0000000..ae5eefe --- /dev/null +++ b/app/explore/experts/[consultantId]/components/ConsultantAvailability.tsx @@ -0,0 +1,75 @@ +import { TConsultantProfile } from "@/types/consultant"; +import { TSlotTiming } from "@/types/slots"; +import { WeeklyAvailability } from "./WeeklyAvailability"; +import { CustomAvailability } from "./CustomAvailability"; +import { + normalizeWeeklySlot, + normalizeCustomSlot, + formatTime, + dayMap +} from "../utils"; + +interface ConsultantAvailabilityProps { + consultantDetails: TConsultantProfile; + selectedSlot: TSlotTiming | null; + setSelectedSlot: (slot: TSlotTiming | null) => void; + timezone: string; +} + +export function ConsultantAvailability({ + consultantDetails, + selectedSlot, + setSelectedSlot, + timezone +}: ConsultantAvailabilityProps) { + return ( +
    +

    Consultant Availability

    +

    + {consultantDetails.scheduleType === "WEEKLY" + ? "Weekly schedule. Select a time slot to schedule a meeting." + : "Custom schedule for the next 7 days. Select a time slot to schedule a meeting."} +

    + + {consultantDetails.scheduleType === "WEEKLY" ? ( + { + const normalizedSlot = normalizeWeeklySlot(slot); + setSelectedSlot({ + slotId: normalizedSlot.id, + dateInISO: new Date().toISOString(), + dayOfWeek: normalizedSlot.dayOfWeekforStartTimeInUTC, + slotStartTimeInUTC: normalizedSlot.slotStartTimeInUTC, + slotEndTimeInUTC: normalizedSlot.slotEndTimeInUTC, + slotOfAvailabilityId: normalizedSlot.id, + slotOfAppointmentId: "", + localStartTime: formatTime(normalizedSlot.slotStartTimeInUTC, timezone), + localEndTime: formatTime(normalizedSlot.slotEndTimeInUTC, timezone), + }); + }} + selectedSlotId={selectedSlot?.slotId} + /> + ) : ( + { + const normalizedSlot = normalizeCustomSlot(slot); + setSelectedSlot({ + slotId: normalizedSlot.id, + dateInISO: new Date(normalizedSlot.slotStartTimeInUTC).toISOString(), + dayOfWeek: dayMap[new Date(normalizedSlot.slotStartTimeInUTC).getDay()], + slotStartTimeInUTC: normalizedSlot.slotStartTimeInUTC, + slotEndTimeInUTC: normalizedSlot.slotEndTimeInUTC, + slotOfAvailabilityId: normalizedSlot.id, + slotOfAppointmentId: "", + localStartTime: formatTime(normalizedSlot.slotStartTimeInUTC, timezone), + localEndTime: formatTime(normalizedSlot.slotEndTimeInUTC, timezone), + }); + }} + selectedSlotId={selectedSlot?.slotId} + /> + )} +
    + ); +} diff --git a/app/explore/experts/[consultantId]/components/ConsultationPricing.tsx b/app/explore/experts/[consultantId]/components/ConsultationPricing.tsx new file mode 100644 index 0000000..56bc6f1 --- /dev/null +++ b/app/explore/experts/[consultantId]/components/ConsultationPricing.tsx @@ -0,0 +1,118 @@ +import Image from "next/image"; +import { User, ConsultationPlan, SubscriptionPlan } from "@prisma/client"; +import { TConsultantProfile } from "@/types/consultant"; +import { TSlotTiming } from "@/types/slots"; +import PricingToggle from "./PricingToggle"; + +interface PricingOption { + title: string; + description: string; + price: number; + duration: string; + features?: string[]; +} + +interface ConsultationPricingProps { + userDetails: User; + consultantDetails: TConsultantProfile; + handleBooking: () => Promise; + selectedDate: Date | null; + setSelectedDate: (date: Date | null) => void; + currentDate: Date; + setCurrentDate: (date: Date) => void; + renderCalendar: () => JSX.Element[]; + slotTimings: TSlotTiming[]; + selectedSlot: TSlotTiming | null; + setSelectedSlot: (slot: TSlotTiming | null) => void; +} + +export function ConsultationPricing({ + userDetails, + consultantDetails, + handleBooking, + selectedDate, + setSelectedDate, + currentDate, + setCurrentDate, + renderCalendar, + slotTimings, + selectedSlot, + setSelectedSlot +}: ConsultationPricingProps) { + const formatPricingOptions = ( + plans: (ConsultationPlan | SubscriptionPlan)[], + type: "consultation" | "subscription" + ): PricingOption[] => { + return plans.map((plan) => { + if (type === "consultation" && "durationInHours" in plan) { + return { + title: `${plan.durationInHours} Hour${plan.durationInHours > 1 ? "s" : ""}`, + description: `${plan.durationInHours} hour consultation`, + price: plan.price, + duration: `${plan.durationInHours} hour${plan.durationInHours > 1 ? "s" : ""}`, + }; + } else if (type === "subscription" && "durationInMonths" in plan) { + return { + title: `${plan.durationInMonths} Month${plan.durationInMonths > 1 ? "s" : ""}`, + description: `${plan.durationInMonths} month subscription`, + price: plan.price, + duration: `${plan.durationInMonths} month${plan.durationInMonths > 1 ? "s" : ""}`, + features: [ + `${plan.callsPerWeek} call${plan.callsPerWeek > 1 ? "s" : ""} per week`, + `${plan.videoMeetings} video meeting${plan.videoMeetings > 1 ? "s" : ""}`, + `${plan.emailSupport} email support`, + ], + }; + } + return { + title: "", + description: "", + price: 0, + duration: "", + }; + }); + }; + + const consultationOptions = formatPricingOptions( + consultantDetails.consultationPlans, + "consultation" + ); + const subscriptionOptions = formatPricingOptions( + consultantDetails.subscriptionPlans, + "subscription" + ); + + return ( +
    + Profile +
    +

    Consultation Pricing

    + +
    +
    + ); +} diff --git a/app/explore/experts/[consultantId]/components/ProfileHeader.tsx b/app/explore/experts/[consultantId]/components/ProfileHeader.tsx new file mode 100644 index 0000000..4fcf544 --- /dev/null +++ b/app/explore/experts/[consultantId]/components/ProfileHeader.tsx @@ -0,0 +1,36 @@ +import { StarIcon } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { User } from "@prisma/client"; +import { TConsultantProfile } from "@/types/consultant"; + +interface ProfileHeaderProps { + userDetails: User; + consultantDetails: TConsultantProfile; +} + +export function ProfileHeader({ userDetails, consultantDetails }: ProfileHeaderProps) { + return ( +
    +
    +
    +

    {userDetails.name}

    +
    + {[...Array(5)].map((_, i) => ( + + ))} + + ({consultantDetails.rating}) + +
    +
    +
    + +
    + {consultantDetails.specialization} +
    +
    + ); +} diff --git a/app/explore/experts/[consultantId]/components/ReviewsSection.tsx b/app/explore/experts/[consultantId]/components/ReviewsSection.tsx new file mode 100644 index 0000000..21f1cc5 --- /dev/null +++ b/app/explore/experts/[consultantId]/components/ReviewsSection.tsx @@ -0,0 +1,23 @@ +import { ConsultantReview } from "@prisma/client"; +import Review from "./Review"; + +interface ReviewsSectionProps { + reviews: ConsultantReview[]; +} + +export function ReviewsSection({ reviews }: ReviewsSectionProps) { + return ( +
    +

    + All Reviews ({reviews?.length || 0}) +

    +
    + {reviews && reviews.length > 0 ? ( + reviews.map((review) => ) + ) : ( +

    No reviews available.

    + )} +
    +
    + ); +} diff --git a/app/explore/experts/[consultantId]/page.tsx b/app/explore/experts/[consultantId]/page.tsx index fa99907..e98a3a4 100644 --- a/app/explore/experts/[consultantId]/page.tsx +++ b/app/explore/experts/[consultantId]/page.tsx @@ -1,54 +1,36 @@ "use client"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; import { fetchConsultantDetails, fetchReviews, fetchUserDetails, } from "@/hooks/useUserData"; - -import { TConsultantProfile } from "@/types/consultant"; -import { TSlotTiming, TWeeklySlot, TCustomSlot } from "@/types/slots"; -import { - ConsultantReview, - ConsultationPlan, - SubscriptionPlan, - User, -} from "@prisma/client"; -import { StarIcon } from "lucide-react"; -import Image from "next/image"; +import { ConsultantReview, User } from "@prisma/client"; +import { use, useCallback, useEffect, useState } from "react"; import Link from "next/link"; -import { use, useCallback, useEffect, useMemo, useState } from "react"; -import { ClassesAndWebinars } from "./components/ClassesAndWebinars"; +import { Button } from "@/components/ui/button"; +import { TConsultantProfile } from "@/types/consultant"; +import { TSlotTiming } from "@/types/slots"; +import { useTimezone } from "./hooks/useTimezone"; import { ConsultantSkeletonLoader } from "./components/ConsultantSkeletonLoader"; -import { CustomAvailability } from "./components/CustomAvailability"; -import PricingToggle from "./components/PricingToggle"; -import Review from "./components/Review"; -import { WeeklyAvailability } from "./components/WeeklyAvailability"; +import { ClassesAndWebinars } from "./components/ClassesAndWebinars"; +import { ProfileHeader } from "./components/ProfileHeader"; +import { AboutSection } from "./components/AboutSection"; +import { ConsultantAvailability } from "./components/ConsultantAvailability"; +import { ReviewsSection } from "./components/ReviewsSection"; +import { ConsultationPricing } from "./components/ConsultationPricing"; import { - dayMap, - convertUTCToLocalDate, - formatTime, + normalizeWeeklySlot, + normalizeCustomSlot, createWeeklySlot, createCustomSlot, mergeOverlappingSlots, getLocalDay, isSameLocalDay, - normalizeWeeklySlot, - normalizeCustomSlot, - isSlotRelevantForDay, + dayMap, + convertUTCToLocalDate, } from "./utils"; -import { useTimezone } from "./hooks/useTimezone"; - -interface PricingOption { - title: string; - description: string; - price: number; - duration: string; - features?: string[]; -} type Params = Promise<{ consultantId: string }>; type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>; @@ -63,8 +45,7 @@ export default function ExpertProfile( const { timezone: browserTimezone, isLoading: isTimezoneLoading } = useTimezone(); const [userDetails, setUserDetails] = useState(null); - const [consultantDetails, setConsultantDetails] = - useState(null); + const [consultantDetails, setConsultantDetails] = useState(null); const [reviews, setReviews] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -82,9 +63,7 @@ export default function ExpertProfile( setIsLoading(true); setError(null); try { - const consultantData = await fetchConsultantDetails( - params.consultantId, - ); + const consultantData = await fetchConsultantDetails(params.consultantId); setConsultantDetails(consultantData); if (consultantData.userId) { const userData = await fetchUserDetails(consultantData.userId); @@ -96,13 +75,10 @@ export default function ExpertProfile( } } catch (err) { console.error("Error fetching data:", err); - setError( - err instanceof Error ? err : new Error("An unknown error occurred"), - ); + setError(err instanceof Error ? err : new Error("An unknown error occurred")); toast({ title: "Error fetching data", - description: - err instanceof Error ? err.message : "An unknown error occurred", + description: err instanceof Error ? err.message : "An unknown error occurred", variant: "destructive", }); } finally { @@ -125,7 +101,7 @@ export default function ExpertProfile( // Get slots for the selected day const relevantSlots = consultantDetails.slotsOfAvailabilityWeekly .map(normalizeWeeklySlot) - .filter(slot => isSlotRelevantForDay(slot, selectedDay, timezone)); + .filter(slot => slot.dayOfWeekforStartTimeInUTC === selectedDay); const weeklySlots = relevantSlots.map(slot => { // Convert UTC times to local date objects @@ -251,110 +227,6 @@ export default function ExpertProfile( return days; }, [currentDate, selectedDate]); - const renderAvailability = useMemo(() => { - if (!consultantDetails || !timezone) return null; - - if (consultantDetails.scheduleType === "WEEKLY") { - // Convert Date objects to strings for weekly slots - const weeklySlots = consultantDetails.slotsOfAvailabilityWeekly.map(normalizeWeeklySlot); - - return ( - { - const normalizedSlot = normalizeWeeklySlot(slot); - setSelectedSlot({ - slotId: normalizedSlot.id, - dateInISO: new Date().toISOString(), - dayOfWeek: normalizedSlot.dayOfWeekforStartTimeInUTC, - slotStartTimeInUTC: normalizedSlot.slotStartTimeInUTC, - slotEndTimeInUTC: normalizedSlot.slotEndTimeInUTC, - slotOfAvailabilityId: normalizedSlot.id, - slotOfAppointmentId: "", - localStartTime: formatTime(normalizedSlot.slotStartTimeInUTC, timezone), - localEndTime: formatTime(normalizedSlot.slotEndTimeInUTC, timezone), - }); - }} - selectedSlotId={selectedSlot?.slotId} - /> - ); - } else if (consultantDetails.scheduleType === "CUSTOM") { - // Convert Date objects to strings for custom slots - const customSlots = consultantDetails.slotsOfAvailabilityCustom.map(normalizeCustomSlot); - - return ( - { - const normalizedSlot = normalizeCustomSlot(slot); - setSelectedSlot({ - slotId: normalizedSlot.id, - dateInISO: new Date(normalizedSlot.slotStartTimeInUTC).toISOString(), - dayOfWeek: dayMap[new Date(normalizedSlot.slotStartTimeInUTC).getDay()], - slotStartTimeInUTC: normalizedSlot.slotStartTimeInUTC, - slotEndTimeInUTC: normalizedSlot.slotEndTimeInUTC, - slotOfAvailabilityId: normalizedSlot.id, - slotOfAppointmentId: "", - localStartTime: formatTime(normalizedSlot.slotStartTimeInUTC, timezone), - localEndTime: formatTime(normalizedSlot.slotEndTimeInUTC, timezone), - }); - }} - selectedSlotId={selectedSlot?.slotId} - /> - ); - } - return null; - }, [consultantDetails, selectedSlot, timezone]); - - const isConsultationPlan = ( - plan: ConsultationPlan | SubscriptionPlan, - ): plan is ConsultationPlan => { - return "durationInHours" in plan; - }; - - const isSubscriptionPlan = ( - plan: ConsultationPlan | SubscriptionPlan, - ): plan is SubscriptionPlan => { - return "durationInMonths" in plan; - }; - - const formatPricingOptions = useCallback( - ( - plans: (ConsultationPlan | SubscriptionPlan)[], - type: "consultation" | "subscription", - ): PricingOption[] => { - return plans.map((plan) => { - if (type === "consultation" && isConsultationPlan(plan)) { - return { - title: `${plan.durationInHours} Hour${plan.durationInHours > 1 ? "s" : ""}`, - description: `${plan.durationInHours} hour consultation`, - price: plan.price, - duration: `${plan.durationInHours} hour${plan.durationInHours > 1 ? "s" : ""}`, - }; - } else if (type === "subscription" && isSubscriptionPlan(plan)) { - return { - title: `${plan.durationInMonths} Month${plan.durationInMonths > 1 ? "s" : ""}`, - description: `${plan.durationInMonths} month subscription`, - price: plan.price, - duration: `${plan.durationInMonths} month${plan.durationInMonths > 1 ? "s" : ""}`, - features: [ - `${plan.callsPerWeek} call${plan.callsPerWeek > 1 ? "s" : ""} per week`, - `${plan.videoMeetings} video meeting${plan.videoMeetings > 1 ? "s" : ""}`, - `${plan.emailSupport} email support`, - ], - }; - } - return { - title: "", - description: "", - price: 0, - duration: "", - }; - }); - }, - [], - ); - if (isLoading) { return ; } @@ -375,133 +247,49 @@ export default function ExpertProfile( ); } - const consultationOptions = formatPricingOptions( - consultantDetails.consultationPlans, - "consultation", - ); - const subscriptionOptions = formatPricingOptions( - consultantDetails.subscriptionPlans, - "subscription", - ); - return (
    -
    -
    -

    {userDetails.name}

    -
    - {[...Array(5)].map((_, i) => ( - - ))} - - ({consultantDetails.rating}) - -
    -
    -
    - -
    - {consultantDetails.specialization} -
    -

    About

    -

    - {userDetails.name} is a seasoned{" "} - {consultantDetails.specialization} with{" "} - {consultantDetails.experience} of experience in the{" "} - {consultantDetails.domain.name} sector. -

    -
    - -
    -

    - Education & Background -

    -

    - {userDetails.name} has experience across multiple industries, - with a particular focus on{" "} - {consultantDetails?.subDomains - ?.map((domain: { name: string }) => domain.name) - .join(", ")} - . -

    -
    - -
    -

    - Skills & Specialties -

    -

    - {userDetails.name} focuses on{" "} - {consultantDetails.tags?.map((tag: { name: string }) => tag.name).join(", ")}. -

    -
    -
    + + + -
    -

    - Consultant Availability -

    -

    - {consultantDetails.scheduleType === "WEEKLY" - ? "Weekly schedule. Select a time slot to schedule a meeting." - : "Custom schedule for the next 7 days. Select a time slot to schedule a meeting."} -

    - {renderAvailability} -
    -
    - -
    -

    - All Reviews ({reviews?.length || 0}) -

    -
    - {reviews && reviews.length > 0 ? ( - reviews.map((review) => ) - ) : ( -

    No reviews available.

    - )} -
    -
    -
    -
    - Profile -
    -

    Consultation Pricing

    -
    + + + +
    + +
    ); } From 55e0a628e5d48e5d7187c659811556b435592186 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 21:18:23 +0530 Subject: [PATCH 13/16] finally the pricing toggle shows correct slots --- .../availability/[consultantId]/route.ts | 6 +- .../components/PricingToggle.tsx | 147 ++++++++---------- .../components/WeeklyAvailability.tsx | 3 +- app/explore/experts/[consultantId]/utils.ts | 73 +++++---- 4 files changed, 108 insertions(+), 121 deletions(-) diff --git a/app/api/slots/availability/[consultantId]/route.ts b/app/api/slots/availability/[consultantId]/route.ts index 0c45066..cffc5eb 100644 --- a/app/api/slots/availability/[consultantId]/route.ts +++ b/app/api/slots/availability/[consultantId]/route.ts @@ -224,7 +224,6 @@ function setToUserDate(date: Date, userDate: Date): Date { ); return result; } - function mapWeeklySlotToTiming( slot: { id: string; @@ -252,6 +251,7 @@ function mapWeeklySlotToTiming( userTimeZone, "yyyy-MM-dd'T'HH:mm:ssXXX", ), + dayOfWeek: slot.dayOfWeekforStartTimeInUTC, slotStartTimeInUTC: formatInTimeZone( fromZonedTime(adjustedStart, userTimeZone), "UTC", @@ -279,6 +279,9 @@ function mapCustomSlotToTiming( ): TSlotTiming { const slotStart = toZonedTime(slot.slotStartTimeInUTC, userTimeZone); const slotEnd = toZonedTime(slot.slotEndTimeInUTC, userTimeZone); + + // Get the day of week for the slot's start time + const dayOfWeek = getDayOfWeek(slotStart); return { slotId: slot.id, @@ -287,6 +290,7 @@ function mapCustomSlotToTiming( userTimeZone, "yyyy-MM-dd'T'HH:mm:ssXXX", ), + dayOfWeek, slotStartTimeInUTC: formatInTimeZone( slot.slotStartTimeInUTC, "UTC", diff --git a/app/explore/experts/[consultantId]/components/PricingToggle.tsx b/app/explore/experts/[consultantId]/components/PricingToggle.tsx index c76751d..f2c3b40 100644 --- a/app/explore/experts/[consultantId]/components/PricingToggle.tsx +++ b/app/explore/experts/[consultantId]/components/PricingToggle.tsx @@ -21,6 +21,9 @@ import { ClockIcon } from "lucide-react"; import { useSession } from "next-auth/react"; import { useMemo, useState } from "react"; import { PricingOption, defaultConsultationOptions, defaultSubscriptionOptions } from "../defaults"; +import { breakDownSlotsByDuration, formatTime } from "../utils"; +import { TSlotTiming } from "@/types/slots"; + interface PricingToggleProps { consultationOptions?: PricingOption[]; @@ -33,34 +36,11 @@ interface PricingToggleProps { currentDate: Date; setCurrentDate: (date: Date) => void; renderCalendar: () => JSX.Element[]; - slotTimings: any[]; - selectedSlot: any; - setSelectedSlot: (slot: any) => void; + slotTimings: TSlotTiming[]; + selectedSlot: TSlotTiming | null; + setSelectedSlot: (slot: TSlotTiming | null) => void; } -const formatTime = (isoString: string): string => { - try { - const date = new Date(isoString); - if (isNaN(date.getTime())) { - throw new Error("Invalid date"); - } - - let hours = date.getHours(); - const minutes = date.getMinutes(); - const ampm = hours >= 12 ? 'pm' : 'am'; - - hours = hours % 12; - hours = hours ? hours : 12; - - const minutesStr = minutes < 10 ? '0' + minutes : minutes; - - return `${hours}:${minutesStr} ${ampm}`; - } catch (error) { - console.error("Error formatting time:", error); - return "Invalid Time"; - } -}; - export default function PricingToggle({ consultationOptions = defaultConsultationOptions, subscriptionOptions = defaultSubscriptionOptions, @@ -86,27 +66,33 @@ export default function PricingToggle({ : defaultSubscriptionOptions[0].title.toLowerCase().replace(" ", "-"), ); - // Sort slot timings by start time - const sortedSlotTimings = useMemo(() => { + // Get the duration of the selected consultation option + const selectedDuration = useMemo(() => { + const option = consultationOptions.find( + opt => opt.title.toLowerCase().replace(" ", "-") === activeConsultationOption + ); + return option && option.duration ? parseInt(option.duration.split(" ")[0]) : 1; + }, [activeConsultationOption, consultationOptions]); + + // Sort and break down slot timings by duration + const availableSlots = useMemo(() => { if (!selectedDate || !slotTimings.length) return []; const selectedDay = selectedDate.getDay(); - return slotTimings - .filter(slot => { - const slotDate = new Date(slot.slotStartTimeInUTC); - return slotDate.getDay() === selectedDay; - }) - .sort((a, b) => { - const dateA = new Date(a.slotStartTimeInUTC); - const dateB = new Date(b.slotStartTimeInUTC); - return dateA.getTime() - dateB.getTime(); - }); - }, [slotTimings, selectedDate]); + // First filter slots for the selected day + const daySlots = slotTimings.filter(slot => { + const slotDate = new Date(slot.slotStartTimeInUTC); + return slotDate.getDay() === selectedDay; + }); + + // Then break down the slots based on the selected duration + return breakDownSlotsByDuration(daySlots, selectedDuration); + }, [slotTimings, selectedDate, selectedDuration]); if (consultationOptions.length === 0 && subscriptionOptions.length === 0) { return ( -
    +

    No pricing options available at the moment.

    @@ -116,7 +102,7 @@ export default function PricingToggle({ if (session?.user?.role && ["consultant", "staff"].includes(session.user.role.toLowerCase())) { return ( -
    +

    Consultee Access Required @@ -131,17 +117,17 @@ export default function PricingToggle({ } return ( -
    +
    - + {consultationOptions.length > 0 && ( Consultation @@ -149,7 +135,7 @@ export default function PricingToggle({ {subscriptionOptions.length > 0 && ( Subscription @@ -163,12 +149,12 @@ export default function PricingToggle({ onValueChange={setActiveConsultationOption} className="space-y-8" > - + {consultationOptions.map((option) => ( {option.title} @@ -219,7 +205,10 @@ export default function PricingToggle({ - Book Consultation + Book {option.title} Consultation + + Select a date and time for your {option.duration} consultation +
    {/* Calendar Section */} @@ -291,48 +280,37 @@ export default function PricingToggle({ {/* Time Slots Section */}

    - Available Time Slots + Available {option.duration} Slots

    - {sortedSlotTimings.length > 0 ? ( + {availableSlots.length > 0 ? (
    - {sortedSlotTimings.map((slot) => { - const startTime = formatTime( - slot.slotStartTimeInUTC, - ); - const endTime = formatTime( - slot.slotEndTimeInUTC, - ); - - return ( - - ); - })} + {availableSlots.map((slot) => ( + + ))}
    ) : (

    - No available slots for this date. + No available {option.duration} slots for this date.
    Please select a different date.

    @@ -378,12 +356,12 @@ export default function PricingToggle({ onValueChange={setActiveSubscriptionOption} className="space-y-8" > - + {subscriptionOptions.map((option) => ( {option.title.split(" ")[0]} {option.title.split(" ")[1]} @@ -499,3 +477,4 @@ export default function PricingToggle({
    ); } + diff --git a/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx index 8332bd4..608b41e 100644 --- a/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx +++ b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx @@ -3,8 +3,7 @@ import { DayOfWeek } from "@prisma/client"; import { convertUTCToLocalDate, formatTime as formatTimeUtil, - isSlotRelevantForDay, - dayToNumber + isSlotRelevantForDay } from "../utils"; interface WeeklySlot { diff --git a/app/explore/experts/[consultantId]/utils.ts b/app/explore/experts/[consultantId]/utils.ts index 7cacbc8..44a4da9 100644 --- a/app/explore/experts/[consultantId]/utils.ts +++ b/app/explore/experts/[consultantId]/utils.ts @@ -62,7 +62,7 @@ export function getLocalDay(date: Date, timezone?: string | null): number { localDay: localDate.getDay() }); return localDate.getDay(); - } catch (e) { + } catch (_) { console.warn('Invalid timezone, using UTC day'); return date.getUTCDay(); } @@ -135,7 +135,7 @@ export function convertUTCToLocalDate(utcTime: string, selectedDate: Date, timez }); return localDate; - } catch (e) { + } catch (_) { console.warn('Invalid timezone, using UTC'); return utcDateTime; } @@ -151,8 +151,8 @@ export function formatTime(date: string | Date, timezone?: string | null): strin hour12: true, timeZone: timezone || undefined }).format(dateObj); - } catch (e) { - console.warn('Error formatting time:', e); + } catch (_) { + console.warn('Error formatting time'); return dateObj.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', @@ -161,34 +161,6 @@ export function formatTime(date: string | Date, timezone?: string | null): strin } } -export function getDayBefore(day: DayOfWeek): DayOfWeek { - const days = [ - DayOfWeek.SUNDAY, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY - ]; - const index = days.indexOf(day); - return days[(index - 1 + 7) % 7]; -} - -export function getDayAfter(day: DayOfWeek): DayOfWeek { - const days = [ - DayOfWeek.SUNDAY, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY - ]; - const index = days.indexOf(day); - return days[(index + 1) % 7]; -} - export function isSameLocalDay(date1: Date, date2: Date, timezone?: string | null): boolean { if (!timezone) return false; @@ -210,7 +182,7 @@ export function isSameLocalDay(date1: Date, date2: Date, timezone?: string | nul }); return result; - } catch (e) { + } catch (_) { console.warn('Invalid timezone'); return false; } @@ -219,7 +191,7 @@ export function isSameLocalDay(date1: Date, date2: Date, timezone?: string | nul export function isSlotRelevantForDay( slot: TWeeklySlot, selectedDay: DayOfWeek, - timezone?: string | null + _timezone?: string | null ): boolean { const startDay = slot.dayOfWeekforStartTimeInUTC; const endDay = slot.dayOfWeekforEndTimeInUTC; @@ -362,3 +334,36 @@ export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | return [...acc, curr]; }, []); } + +export function breakDownSlotsByDuration(slots: TSlotTiming[], durationInHours: number, timezone?: string | null): TSlotTiming[] { + const brokenDownSlots: TSlotTiming[] = []; + + slots.forEach(slot => { + const startTime = new Date(slot.slotStartTimeInUTC); + const endTime = new Date(slot.slotEndTimeInUTC); + const durationMs = durationInHours * 60 * 60 * 1000; + + // Calculate how many slots of the given duration can fit in this time period + let currentStart = new Date(startTime); + + while (currentStart.getTime() + durationMs <= endTime.getTime()) { + const currentEnd = new Date(currentStart.getTime() + durationMs); + + brokenDownSlots.push({ + ...slot, + slotId: `${slot.slotId}-${currentStart.getTime()}`, + slotStartTimeInUTC: currentStart.toISOString(), + slotEndTimeInUTC: currentEnd.toISOString(), + localStartTime: formatTime(currentStart, timezone), + localEndTime: formatTime(currentEnd, timezone) + }); + + // Move to next potential slot + currentStart = new Date(currentStart.getTime() + durationMs); + } + }); + + return brokenDownSlots.sort((a, b) => + new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime() + ); +} From acaa3e4c8f463d5f14cad33cabc238e216080ea3 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 21:26:12 +0530 Subject: [PATCH 14/16] made pricing toggle more responsive --- .../components/PricingToggle.tsx | 83 +++++++++++++------ 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/app/explore/experts/[consultantId]/components/PricingToggle.tsx b/app/explore/experts/[consultantId]/components/PricingToggle.tsx index f2c3b40..2cde2e1 100644 --- a/app/explore/experts/[consultantId]/components/PricingToggle.tsx +++ b/app/explore/experts/[consultantId]/components/PricingToggle.tsx @@ -13,18 +13,21 @@ import { DialogDescription, DialogHeader, DialogTitle, - DialogTrigger + DialogTrigger, } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { motion } from "framer-motion"; import { ClockIcon } from "lucide-react"; import { useSession } from "next-auth/react"; import { useMemo, useState } from "react"; -import { PricingOption, defaultConsultationOptions, defaultSubscriptionOptions } from "../defaults"; +import { + PricingOption, + defaultConsultationOptions, + defaultSubscriptionOptions, +} from "../defaults"; import { breakDownSlotsByDuration, formatTime } from "../utils"; import { TSlotTiming } from "@/types/slots"; - interface PricingToggleProps { consultationOptions?: PricingOption[]; subscriptionOptions?: PricingOption[]; @@ -69,9 +72,12 @@ export default function PricingToggle({ // Get the duration of the selected consultation option const selectedDuration = useMemo(() => { const option = consultationOptions.find( - opt => opt.title.toLowerCase().replace(" ", "-") === activeConsultationOption + (opt) => + opt.title.toLowerCase().replace(" ", "-") === activeConsultationOption, ); - return option && option.duration ? parseInt(option.duration.split(" ")[0]) : 1; + return option && option.duration + ? parseInt(option.duration.split(" ")[0]) + : 1; }, [activeConsultationOption, consultationOptions]); // Sort and break down slot timings by duration @@ -79,9 +85,9 @@ export default function PricingToggle({ if (!selectedDate || !slotTimings.length) return []; const selectedDay = selectedDate.getDay(); - + // First filter slots for the selected day - const daySlots = slotTimings.filter(slot => { + const daySlots = slotTimings.filter((slot) => { const slotDate = new Date(slot.slotStartTimeInUTC); return slotDate.getDay() === selectedDay; }); @@ -100,7 +106,10 @@ export default function PricingToggle({ ); } - if (session?.user?.role && ["consultant", "staff"].includes(session.user.role.toLowerCase())) { + if ( + session?.user?.role && + ["consultant", "staff"].includes(session.user.role.toLowerCase()) + ) { return (
    @@ -123,11 +132,15 @@ export default function PricingToggle({ onValueChange={setActiveTab} className="space-y-8" > - + {consultationOptions.length > 0 && ( Consultation @@ -135,7 +148,11 @@ export default function PricingToggle({ {subscriptionOptions.length > 0 && ( Subscription @@ -149,12 +166,17 @@ export default function PricingToggle({ onValueChange={setActiveConsultationOption} className="space-y-8" > - + {consultationOptions.map((option) => ( {option.title} @@ -205,16 +227,20 @@ export default function PricingToggle({ - Book {option.title} Consultation + + Book {option.title} Consultation + - Select a date and time for your {option.duration} consultation + Select a date and time for your{" "} + {option.duration} consultation
    {/* Calendar Section */}

    - Select a Date + {" "} + Select a Date

    @@ -280,7 +306,8 @@ export default function PricingToggle({ {/* Time Slots Section */}

    - Available {option.duration} Slots + {" "} + Available {option.duration} Slots

    {availableSlots.length > 0 ? ( @@ -289,20 +316,19 @@ export default function PricingToggle({ ))}
    @@ -310,7 +336,8 @@ export default function PricingToggle({

    - No available {option.duration} slots for this date. + No available {option.duration} slots for + this date.
    Please select a different date.

    @@ -356,12 +383,17 @@ export default function PricingToggle({ onValueChange={setActiveSubscriptionOption} className="space-y-8" > - + {subscriptionOptions.map((option) => ( {option.title.split(" ")[0]} {option.title.split(" ")[1]} @@ -477,4 +509,3 @@ export default function PricingToggle({
    ); } - From 0df98a536de18886e7fa0106b410f90f10657ec4 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 21:26:22 +0530 Subject: [PATCH 15/16] ran prettier on all files --- .../availability/[consultantId]/route.ts | 2 +- .../components/AboutSection.tsx | 18 +- .../components/ConsultantAvailability.tsx | 51 ++-- .../components/ConsultationPricing.tsx | 8 +- .../components/ProfileHeader.tsx | 5 +- .../components/WeeklyAvailability.tsx | 33 ++- .../[consultantId]/hooks/useTimezone.ts | 6 +- app/explore/experts/[consultantId]/page.tsx | 104 ++++++--- app/explore/experts/[consultantId]/utils.ts | 218 ++++++++++-------- prisma/seedFiles/createSlotsOfAvailability.ts | 24 +- 10 files changed, 292 insertions(+), 177 deletions(-) diff --git a/app/api/slots/availability/[consultantId]/route.ts b/app/api/slots/availability/[consultantId]/route.ts index cffc5eb..005a874 100644 --- a/app/api/slots/availability/[consultantId]/route.ts +++ b/app/api/slots/availability/[consultantId]/route.ts @@ -279,7 +279,7 @@ function mapCustomSlotToTiming( ): TSlotTiming { const slotStart = toZonedTime(slot.slotStartTimeInUTC, userTimeZone); const slotEnd = toZonedTime(slot.slotEndTimeInUTC, userTimeZone); - + // Get the day of week for the slot's start time const dayOfWeek = getDayOfWeek(slotStart); diff --git a/app/explore/experts/[consultantId]/components/AboutSection.tsx b/app/explore/experts/[consultantId]/components/AboutSection.tsx index 1c362bc..ac77a91 100644 --- a/app/explore/experts/[consultantId]/components/AboutSection.tsx +++ b/app/explore/experts/[consultantId]/components/AboutSection.tsx @@ -6,14 +6,17 @@ interface AboutSectionProps { consultantDetails: TConsultantProfile; } -export function AboutSection({ userDetails, consultantDetails }: AboutSectionProps) { +export function AboutSection({ + userDetails, + consultantDetails, +}: AboutSectionProps) { return (

    About

    - {userDetails.name} is a seasoned {consultantDetails.specialization} with{" "} - {consultantDetails.experience} of experience in the{" "} + {userDetails.name} is a seasoned {consultantDetails.specialization}{" "} + with {consultantDetails.experience} of experience in the{" "} {consultantDetails.domain.name} sector.

    @@ -21,8 +24,8 @@ export function AboutSection({ userDetails, consultantDetails }: AboutSectionPro

    Education & Background

    - {userDetails.name} has experience across multiple industries, - with a particular focus on{" "} + {userDetails.name} has experience across multiple industries, with a + particular focus on{" "} {consultantDetails?.subDomains ?.map((domain: { name: string }) => domain.name) .join(", ")} @@ -34,7 +37,10 @@ export function AboutSection({ userDetails, consultantDetails }: AboutSectionPro

    Skills & Specialties

    {userDetails.name} focuses on{" "} - {consultantDetails.tags?.map((tag: { name: string }) => tag.name).join(", ")}. + {consultantDetails.tags + ?.map((tag: { name: string }) => tag.name) + .join(", ")} + .

    diff --git a/app/explore/experts/[consultantId]/components/ConsultantAvailability.tsx b/app/explore/experts/[consultantId]/components/ConsultantAvailability.tsx index ae5eefe..33481ef 100644 --- a/app/explore/experts/[consultantId]/components/ConsultantAvailability.tsx +++ b/app/explore/experts/[consultantId]/components/ConsultantAvailability.tsx @@ -2,11 +2,11 @@ import { TConsultantProfile } from "@/types/consultant"; import { TSlotTiming } from "@/types/slots"; import { WeeklyAvailability } from "./WeeklyAvailability"; import { CustomAvailability } from "./CustomAvailability"; -import { - normalizeWeeklySlot, - normalizeCustomSlot, +import { + normalizeWeeklySlot, + normalizeCustomSlot, formatTime, - dayMap + dayMap, } from "../utils"; interface ConsultantAvailabilityProps { @@ -20,7 +20,7 @@ export function ConsultantAvailability({ consultantDetails, selectedSlot, setSelectedSlot, - timezone + timezone, }: ConsultantAvailabilityProps) { return (
    @@ -30,11 +30,13 @@ export function ConsultantAvailability({ ? "Weekly schedule. Select a time slot to schedule a meeting." : "Custom schedule for the next 7 days. Select a time slot to schedule a meeting."}

    - + {consultantDetails.scheduleType === "WEEKLY" ? ( { + slots={consultantDetails.slotsOfAvailabilityWeekly.map( + normalizeWeeklySlot, + )} + onSlotSelect={(slot) => { const normalizedSlot = normalizeWeeklySlot(slot); setSelectedSlot({ slotId: normalizedSlot.id, @@ -44,27 +46,44 @@ export function ConsultantAvailability({ slotEndTimeInUTC: normalizedSlot.slotEndTimeInUTC, slotOfAvailabilityId: normalizedSlot.id, slotOfAppointmentId: "", - localStartTime: formatTime(normalizedSlot.slotStartTimeInUTC, timezone), - localEndTime: formatTime(normalizedSlot.slotEndTimeInUTC, timezone), + localStartTime: formatTime( + normalizedSlot.slotStartTimeInUTC, + timezone, + ), + localEndTime: formatTime( + normalizedSlot.slotEndTimeInUTC, + timezone, + ), }); }} selectedSlotId={selectedSlot?.slotId} /> ) : ( { + slots={consultantDetails.slotsOfAvailabilityCustom.map( + normalizeCustomSlot, + )} + onSlotSelect={(slot) => { const normalizedSlot = normalizeCustomSlot(slot); setSelectedSlot({ slotId: normalizedSlot.id, - dateInISO: new Date(normalizedSlot.slotStartTimeInUTC).toISOString(), - dayOfWeek: dayMap[new Date(normalizedSlot.slotStartTimeInUTC).getDay()], + dateInISO: new Date( + normalizedSlot.slotStartTimeInUTC, + ).toISOString(), + dayOfWeek: + dayMap[new Date(normalizedSlot.slotStartTimeInUTC).getDay()], slotStartTimeInUTC: normalizedSlot.slotStartTimeInUTC, slotEndTimeInUTC: normalizedSlot.slotEndTimeInUTC, slotOfAvailabilityId: normalizedSlot.id, slotOfAppointmentId: "", - localStartTime: formatTime(normalizedSlot.slotStartTimeInUTC, timezone), - localEndTime: formatTime(normalizedSlot.slotEndTimeInUTC, timezone), + localStartTime: formatTime( + normalizedSlot.slotStartTimeInUTC, + timezone, + ), + localEndTime: formatTime( + normalizedSlot.slotEndTimeInUTC, + timezone, + ), }); }} selectedSlotId={selectedSlot?.slotId} diff --git a/app/explore/experts/[consultantId]/components/ConsultationPricing.tsx b/app/explore/experts/[consultantId]/components/ConsultationPricing.tsx index 56bc6f1..14979e5 100644 --- a/app/explore/experts/[consultantId]/components/ConsultationPricing.tsx +++ b/app/explore/experts/[consultantId]/components/ConsultationPricing.tsx @@ -37,11 +37,11 @@ export function ConsultationPricing({ renderCalendar, slotTimings, selectedSlot, - setSelectedSlot + setSelectedSlot, }: ConsultationPricingProps) { const formatPricingOptions = ( plans: (ConsultationPlan | SubscriptionPlan)[], - type: "consultation" | "subscription" + type: "consultation" | "subscription", ): PricingOption[] => { return plans.map((plan) => { if (type === "consultation" && "durationInHours" in plan) { @@ -75,11 +75,11 @@ export function ConsultationPricing({ const consultationOptions = formatPricingOptions( consultantDetails.consultationPlans, - "consultation" + "consultation", ); const subscriptionOptions = formatPricingOptions( consultantDetails.subscriptionPlans, - "subscription" + "subscription", ); return ( diff --git a/app/explore/experts/[consultantId]/components/ProfileHeader.tsx b/app/explore/experts/[consultantId]/components/ProfileHeader.tsx index 4fcf544..baeef42 100644 --- a/app/explore/experts/[consultantId]/components/ProfileHeader.tsx +++ b/app/explore/experts/[consultantId]/components/ProfileHeader.tsx @@ -8,7 +8,10 @@ interface ProfileHeaderProps { consultantDetails: TConsultantProfile; } -export function ProfileHeader({ userDetails, consultantDetails }: ProfileHeaderProps) { +export function ProfileHeader({ + userDetails, + consultantDetails, +}: ProfileHeaderProps) { return (
    diff --git a/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx index 608b41e..7a035fd 100644 --- a/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx +++ b/app/explore/experts/[consultantId]/components/WeeklyAvailability.tsx @@ -3,7 +3,7 @@ import { DayOfWeek } from "@prisma/client"; import { convertUTCToLocalDate, formatTime as formatTimeUtil, - isSlotRelevantForDay + isSlotRelevantForDay, } from "../utils"; interface WeeklySlot { @@ -38,28 +38,39 @@ export const WeeklyAvailability: React.FC = ({ // Get browser's timezone const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - console.log('Using timezone:', timezone); + console.log("Using timezone:", timezone); // Group slots by day and sort by time const slotsByDay = daysOfWeek.map((day) => { // Get slots that are relevant for this day - const daySlots = slots.filter(slot => isSlotRelevantForDay(slot, day, timezone)); + const daySlots = slots.filter((slot) => + isSlotRelevantForDay(slot, day, timezone), + ); // Process each slot to handle timezone and overnight slots - const processedSlots = daySlots.map(slot => { + const processedSlots = daySlots.map((slot) => { // Use a reference date for consistent conversion const referenceDate = new Date(); referenceDate.setHours(0, 0, 0, 0); // Convert UTC times to local - const startDateTime = convertUTCToLocalDate(slot.slotStartTimeInUTC, referenceDate, timezone); - let endDateTime = convertUTCToLocalDate(slot.slotEndTimeInUTC, referenceDate, timezone); + const startDateTime = convertUTCToLocalDate( + slot.slotStartTimeInUTC, + referenceDate, + timezone, + ); + let endDateTime = convertUTCToLocalDate( + slot.slotEndTimeInUTC, + referenceDate, + timezone, + ); // If this slot crosses midnight - if (slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC || - endDateTime <= startDateTime || - (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0)) { - + if ( + slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC || + endDateTime <= startDateTime || + (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0) + ) { endDateTime = new Date(endDateTime); endDateTime.setDate(endDateTime.getDate() + 1); } @@ -67,7 +78,7 @@ export const WeeklyAvailability: React.FC = ({ return { ...slot, localStartTime: formatTimeUtil(startDateTime, timezone), - localEndTime: formatTimeUtil(endDateTime, timezone) + localEndTime: formatTimeUtil(endDateTime, timezone), }; }); diff --git a/app/explore/experts/[consultantId]/hooks/useTimezone.ts b/app/explore/experts/[consultantId]/hooks/useTimezone.ts index 638a6f1..148177a 100644 --- a/app/explore/experts/[consultantId]/hooks/useTimezone.ts +++ b/app/explore/experts/[consultantId]/hooks/useTimezone.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect } from "react"; export function useTimezone() { const [timezone, setTimezone] = useState(null); @@ -8,10 +8,10 @@ export function useTimezone() { try { // Get timezone on client side const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - console.log('Browser timezone detected:', browserTimezone); + console.log("Browser timezone detected:", browserTimezone); setTimezone(browserTimezone); } catch (error) { - console.error('Error detecting timezone:', error); + console.error("Error detecting timezone:", error); } finally { setIsLoading(false); } diff --git a/app/explore/experts/[consultantId]/page.tsx b/app/explore/experts/[consultantId]/page.tsx index e98a3a4..b0f29b7 100644 --- a/app/explore/experts/[consultantId]/page.tsx +++ b/app/explore/experts/[consultantId]/page.tsx @@ -42,10 +42,12 @@ export default function ExpertProfile( }>, ) { const params = use(props.params); - const { timezone: browserTimezone, isLoading: isTimezoneLoading } = useTimezone(); + const { timezone: browserTimezone, isLoading: isTimezoneLoading } = + useTimezone(); const [userDetails, setUserDetails] = useState(null); - const [consultantDetails, setConsultantDetails] = useState(null); + const [consultantDetails, setConsultantDetails] = + useState(null); const [reviews, setReviews] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -63,7 +65,9 @@ export default function ExpertProfile( setIsLoading(true); setError(null); try { - const consultantData = await fetchConsultantDetails(params.consultantId); + const consultantData = await fetchConsultantDetails( + params.consultantId, + ); setConsultantDetails(consultantData); if (consultantData.userId) { const userData = await fetchUserDetails(consultantData.userId); @@ -75,10 +79,13 @@ export default function ExpertProfile( } } catch (err) { console.error("Error fetching data:", err); - setError(err instanceof Error ? err : new Error("An unknown error occurred")); + setError( + err instanceof Error ? err : new Error("An unknown error occurred"), + ); toast({ title: "Error fetching data", - description: err instanceof Error ? err.message : "An unknown error occurred", + description: + err instanceof Error ? err.message : "An unknown error occurred", variant: "destructive", }); } finally { @@ -91,63 +98,90 @@ export default function ExpertProfile( useEffect(() => { if (selectedDate && consultantDetails && timezone && !isTimezoneLoading) { - console.log('Using timezone:', timezone); - console.log('Selected date:', selectedDate.toISOString()); - + console.log("Using timezone:", timezone); + console.log("Selected date:", selectedDate.toISOString()); + if (consultantDetails.scheduleType === "WEEKLY") { const selectedDay = dayMap[getLocalDay(selectedDate, timezone)]; - console.log('Selected day:', selectedDay); - + console.log("Selected day:", selectedDay); + // Get slots for the selected day const relevantSlots = consultantDetails.slotsOfAvailabilityWeekly .map(normalizeWeeklySlot) - .filter(slot => slot.dayOfWeekforStartTimeInUTC === selectedDay); + .filter((slot) => slot.dayOfWeekforStartTimeInUTC === selectedDay); - const weeklySlots = relevantSlots.map(slot => { + const weeklySlots = relevantSlots.map((slot) => { // Convert UTC times to local date objects - const startDateTime = convertUTCToLocalDate(slot.slotStartTimeInUTC, selectedDate, timezone); - let endDateTime = convertUTCToLocalDate(slot.slotEndTimeInUTC, selectedDate, timezone); + const startDateTime = convertUTCToLocalDate( + slot.slotStartTimeInUTC, + selectedDate, + timezone, + ); + let endDateTime = convertUTCToLocalDate( + slot.slotEndTimeInUTC, + selectedDate, + timezone, + ); - console.log('Processing slot:', { + console.log("Processing slot:", { utcStart: slot.slotStartTimeInUTC, utcEnd: slot.slotEndTimeInUTC, localStart: startDateTime.toISOString(), localEnd: endDateTime.toISOString(), - timezone + timezone, }); // Create the slot timing - return createWeeklySlot(slot, selectedDate, startDateTime, endDateTime, timezone); + return createWeeklySlot( + slot, + selectedDate, + startDateTime, + endDateTime, + timezone, + ); }); // Sort slots by start time - const sortedSlots = weeklySlots.sort((a, b) => - new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime() + const sortedSlots = weeklySlots.sort( + (a, b) => + new Date(a.slotStartTimeInUTC).getTime() - + new Date(b.slotStartTimeInUTC).getTime(), ); // Merge overlapping slots const mergedSlots = mergeOverlappingSlots(sortedSlots, timezone); - console.log('Final slots:', mergedSlots.map(slot => ({ - start: slot.localStartTime, - end: slot.localEndTime - }))); + console.log( + "Final slots:", + mergedSlots.map((slot) => ({ + start: slot.localStartTime, + end: slot.localEndTime, + })), + ); setSlotTimings(mergedSlots); } else if (consultantDetails.scheduleType === "CUSTOM") { const customSlots = consultantDetails.slotsOfAvailabilityCustom .map(normalizeCustomSlot) - .filter(slot => { + .filter((slot) => { const startDateTime = new Date(slot.slotStartTimeInUTC); return isSameLocalDay(startDateTime, selectedDate, timezone); }) - .map(slot => { + .map((slot) => { const startDateTime = new Date(slot.slotStartTimeInUTC); const endDateTime = new Date(slot.slotEndTimeInUTC); - return createCustomSlot(slot, selectedDate, startDateTime, endDateTime, timezone); + return createCustomSlot( + slot, + selectedDate, + startDateTime, + endDateTime, + timezone, + ); }); // Sort slots by start time - const sortedSlots = customSlots.sort((a, b) => - new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime() + const sortedSlots = customSlots.sort( + (a, b) => + new Date(a.slotStartTimeInUTC).getTime() - + new Date(b.slotStartTimeInUTC).getTime(), ); setSlotTimings(sortedSlots); @@ -251,14 +285,14 @@ export default function ExpertProfile(
    - - - = { 3: DayOfWeek.WEDNESDAY, 4: DayOfWeek.THURSDAY, 5: DayOfWeek.FRIDAY, - 6: DayOfWeek.SATURDAY + 6: DayOfWeek.SATURDAY, }; export const dayToNumber: Record = { @@ -18,23 +18,23 @@ export const dayToNumber: Record = { [DayOfWeek.WEDNESDAY]: 3, [DayOfWeek.THURSDAY]: 4, [DayOfWeek.FRIDAY]: 5, - [DayOfWeek.SATURDAY]: 6 + [DayOfWeek.SATURDAY]: 6, }; // Ensure UTC time is always a string export function normalizeUTCTime(time: string | Date): string { - return typeof time === 'string' ? time : time.toISOString(); + return typeof time === "string" ? time : time.toISOString(); } // Normalize weekly slot to ensure all times are strings -export function normalizeWeeklySlot(slot: TWeeklySlot): TWeeklySlot & { - slotStartTimeInUTC: string; - slotEndTimeInUTC: string; +export function normalizeWeeklySlot(slot: TWeeklySlot): TWeeklySlot & { + slotStartTimeInUTC: string; + slotEndTimeInUTC: string; } { return { ...slot, slotStartTimeInUTC: normalizeUTCTime(slot.slotStartTimeInUTC), - slotEndTimeInUTC: normalizeUTCTime(slot.slotEndTimeInUTC) + slotEndTimeInUTC: normalizeUTCTime(slot.slotEndTimeInUTC), }; } @@ -46,7 +46,7 @@ export function normalizeCustomSlot(slot: TCustomSlot): TCustomSlot & { return { ...slot, slotStartTimeInUTC: normalizeUTCTime(slot.slotStartTimeInUTC), - slotEndTimeInUTC: normalizeUTCTime(slot.slotEndTimeInUTC) + slotEndTimeInUTC: normalizeUTCTime(slot.slotEndTimeInUTC), }; } @@ -54,63 +54,71 @@ export function getLocalDay(date: Date, timezone?: string | null): number { if (!timezone) return date.getDay(); try { - const localDate = new Date(date.toLocaleString('en-US', { timeZone: timezone })); - console.log('Local day calculation:', { + const localDate = new Date( + date.toLocaleString("en-US", { timeZone: timezone }), + ); + console.log("Local day calculation:", { originalDate: date.toISOString(), timezone, localDate: localDate.toISOString(), - localDay: localDate.getDay() + localDay: localDate.getDay(), }); return localDate.getDay(); } catch (_) { - console.warn('Invalid timezone, using UTC day'); + console.warn("Invalid timezone, using UTC day"); return date.getUTCDay(); } } -export function convertUTCToLocalDate(utcTime: string, selectedDate: Date, timezone?: string | null): Date { +export function convertUTCToLocalDate( + utcTime: string, + selectedDate: Date, + timezone?: string | null, +): Date { // Parse the UTC time from 1970-01-01 format const utcDate = new Date(utcTime); const utcHours = utcDate.getUTCHours(); const utcMinutes = utcDate.getUTCMinutes(); // Create a new date in UTC - const utcDateTime = new Date(Date.UTC( - selectedDate.getFullYear(), - selectedDate.getMonth(), - selectedDate.getDate(), - utcHours, - utcMinutes, - 0, - 0 - )); + const utcDateTime = new Date( + Date.UTC( + selectedDate.getFullYear(), + selectedDate.getMonth(), + selectedDate.getDate(), + utcHours, + utcMinutes, + 0, + 0, + ), + ); if (!timezone) return utcDateTime; try { // Convert UTC to local time string in the target timezone - const localTimeStr = utcDateTime.toLocaleString('en-US', { + const localTimeStr = utcDateTime.toLocaleString("en-US", { timeZone: timezone, - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - hour12: false + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: false, }); // Parse the local time string back to a Date object - const [datePart, timePart] = localTimeStr.split(', '); - const [month, day, year] = datePart.split('/').map(Number); - const [hours, minutes, seconds] = timePart.split(':').map(Number); + const [datePart, timePart] = localTimeStr.split(", "); + const [month, day, year] = datePart.split("/").map(Number); + const [hours, minutes, seconds] = timePart.split(":").map(Number); const localDate = new Date(year, month - 1, day, hours, minutes, seconds); - + // Adjust date if the local time is on a different day const localDay = getLocalDay(localDate, timezone); const selectedDay = getLocalDay(selectedDate, timezone); - + if (localDay !== selectedDay) { // Handle week wrap-around if (selectedDay === 0 && localDay === 6) { @@ -128,62 +136,70 @@ export function convertUTCToLocalDate(utcTime: string, selectedDate: Date, timez } } - console.log('UTC to Local conversion:', { + console.log("UTC to Local conversion:", { utcTime: utcDateTime.toISOString(), timezone, - localTime: localDate.toISOString() + localTime: localDate.toISOString(), }); return localDate; } catch (_) { - console.warn('Invalid timezone, using UTC'); + console.warn("Invalid timezone, using UTC"); return utcDateTime; } } -export function formatTime(date: string | Date, timezone?: string | null): string { - const dateObj = typeof date === 'string' ? new Date(date) : date; - +export function formatTime( + date: string | Date, + timezone?: string | null, +): string { + const dateObj = typeof date === "string" ? new Date(date) : date; + try { - return new Intl.DateTimeFormat('en-US', { - hour: 'numeric', - minute: 'numeric', + return new Intl.DateTimeFormat("en-US", { + hour: "numeric", + minute: "numeric", hour12: true, - timeZone: timezone || undefined + timeZone: timezone || undefined, }).format(dateObj); } catch (_) { - console.warn('Error formatting time'); - return dateObj.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: 'numeric', - hour12: true + console.warn("Error formatting time"); + return dateObj.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "numeric", + hour12: true, }); } } -export function isSameLocalDay(date1: Date, date2: Date, timezone?: string | null): boolean { +export function isSameLocalDay( + date1: Date, + date2: Date, + timezone?: string | null, +): boolean { if (!timezone) return false; try { - const d1 = new Date(date1.toLocaleString('en-US', { timeZone: timezone })); - const d2 = new Date(date2.toLocaleString('en-US', { timeZone: timezone })); - - const result = d1.getFullYear() === d2.getFullYear() && - d1.getMonth() === d2.getMonth() && - d1.getDate() === d2.getDate(); - - console.log('Same local day check:', { + const d1 = new Date(date1.toLocaleString("en-US", { timeZone: timezone })); + const d2 = new Date(date2.toLocaleString("en-US", { timeZone: timezone })); + + const result = + d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); + + console.log("Same local day check:", { date1: date1.toISOString(), date2: date2.toISOString(), timezone, localDate1: d1.toISOString(), localDate2: d2.toISOString(), - isSameDay: result + isSameDay: result, }); return result; } catch (_) { - console.warn('Invalid timezone'); + console.warn("Invalid timezone"); return false; } } @@ -191,11 +207,11 @@ export function isSameLocalDay(date1: Date, date2: Date, timezone?: string | nul export function isSlotRelevantForDay( slot: TWeeklySlot, selectedDay: DayOfWeek, - _timezone?: string | null + _timezone?: string | null, ): boolean { const startDay = slot.dayOfWeekforStartTimeInUTC; const endDay = slot.dayOfWeekforEndTimeInUTC; - + // Direct match if (startDay === selectedDay) return true; @@ -210,11 +226,14 @@ export function isSlotRelevantForDay( // For slots that wrap around the week (e.g., Sat 9pm - Sun 3am) // The slot is relevant for both the start day and the end day if (selectedDay === endDay) return true; - + // For days in between (in case of multi-day slots) const dayAfterStart = (startDayNum + 1) % 7; - return selectedDayNum >= startDayNum || selectedDayNum <= endDayNum || - (selectedDayNum >= dayAfterStart && selectedDayNum <= 6); + return ( + selectedDayNum >= startDayNum || + selectedDayNum <= endDayNum || + (selectedDayNum >= dayAfterStart && selectedDayNum <= 6) + ); } // Normal case (e.g., Mon 10pm - Tue 2am) @@ -229,16 +248,17 @@ export function createWeeklySlot( selectedDate: Date, startDateTime: Date, endDateTime: Date, - timezone?: string | null + timezone?: string | null, ): TSlotTiming { let adjustedEndDateTime = new Date(endDateTime); const normalizedSlot = normalizeWeeklySlot(slot); // Handle slots that cross midnight - if (slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC || - endDateTime <= startDateTime || - (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0)) { - + if ( + slot.dayOfWeekforStartTimeInUTC !== slot.dayOfWeekforEndTimeInUTC || + endDateTime <= startDateTime || + (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0) + ) { adjustedEndDateTime = new Date(endDateTime); adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); } @@ -255,7 +275,7 @@ export function createWeeklySlot( localEndTime: formatTime(adjustedEndDateTime, timezone), }; - console.log('Created weekly slot:', { + console.log("Created weekly slot:", { startDay: normalizedSlot.dayOfWeekforStartTimeInUTC, endDay: normalizedSlot.dayOfWeekforEndTimeInUTC, utcStart: normalizedSlot.slotStartTimeInUTC, @@ -263,7 +283,9 @@ export function createWeeklySlot( localStart: slotTiming.localStartTime, localEnd: slotTiming.localEndTime, timezone, - crossesMidnight: normalizedSlot.dayOfWeekforStartTimeInUTC !== normalizedSlot.dayOfWeekforEndTimeInUTC + crossesMidnight: + normalizedSlot.dayOfWeekforStartTimeInUTC !== + normalizedSlot.dayOfWeekforEndTimeInUTC, }); return slotTiming; @@ -274,14 +296,16 @@ export function createCustomSlot( selectedDate: Date, startDateTime: Date, endDateTime: Date, - timezone?: string | null + timezone?: string | null, ): TSlotTiming { let adjustedEndDateTime = new Date(endDateTime); const normalizedSlot = normalizeCustomSlot(slot); // Handle slots that cross midnight - if (endDateTime <= startDateTime || - (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0)) { + if ( + endDateTime <= startDateTime || + (endDateTime.getHours() === 0 && endDateTime.getMinutes() === 0) + ) { adjustedEndDateTime = new Date(endDateTime); adjustedEndDateTime.setDate(adjustedEndDateTime.getDate() + 1); } @@ -298,30 +322,34 @@ export function createCustomSlot( localEndTime: formatTime(adjustedEndDateTime, timezone), }; - console.log('Created custom slot:', { + console.log("Created custom slot:", { utcStart: normalizedSlot.slotStartTimeInUTC, utcEnd: normalizedSlot.slotEndTimeInUTC, localStart: slotTiming.localStartTime, localEnd: slotTiming.localEndTime, timezone, - crossesMidnight: adjustedEndDateTime.getDate() > startDateTime.getDate() + crossesMidnight: adjustedEndDateTime.getDate() > startDateTime.getDate(), }); return slotTiming; } -export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | null): TSlotTiming[] { +export function mergeOverlappingSlots( + slots: TSlotTiming[], + timezone?: string | null, +): TSlotTiming[] { // Don't merge slots that are more than 1 minute apart return slots.reduce((acc: TSlotTiming[], curr) => { if (acc.length === 0) return [curr]; - + const last = acc[acc.length - 1]; const lastEnd = new Date(last.slotEndTimeInUTC); const currStart = new Date(curr.slotStartTimeInUTC); const currEnd = new Date(curr.slotEndTimeInUTC); - + // Only merge if the slots are exactly adjacent (less than 1 minute apart) - const diffInMinutes = (currStart.getTime() - lastEnd.getTime()) / (1000 * 60); + const diffInMinutes = + (currStart.getTime() - lastEnd.getTime()) / (1000 * 60); if (diffInMinutes <= 0) { // If current slot ends after the last slot if (currEnd > lastEnd) { @@ -330,40 +358,46 @@ export function mergeOverlappingSlots(slots: TSlotTiming[], timezone?: string | } return acc; } - + return [...acc, curr]; }, []); } -export function breakDownSlotsByDuration(slots: TSlotTiming[], durationInHours: number, timezone?: string | null): TSlotTiming[] { +export function breakDownSlotsByDuration( + slots: TSlotTiming[], + durationInHours: number, + timezone?: string | null, +): TSlotTiming[] { const brokenDownSlots: TSlotTiming[] = []; - slots.forEach(slot => { + slots.forEach((slot) => { const startTime = new Date(slot.slotStartTimeInUTC); const endTime = new Date(slot.slotEndTimeInUTC); const durationMs = durationInHours * 60 * 60 * 1000; - + // Calculate how many slots of the given duration can fit in this time period let currentStart = new Date(startTime); - + while (currentStart.getTime() + durationMs <= endTime.getTime()) { const currentEnd = new Date(currentStart.getTime() + durationMs); - + brokenDownSlots.push({ ...slot, slotId: `${slot.slotId}-${currentStart.getTime()}`, slotStartTimeInUTC: currentStart.toISOString(), slotEndTimeInUTC: currentEnd.toISOString(), localStartTime: formatTime(currentStart, timezone), - localEndTime: formatTime(currentEnd, timezone) + localEndTime: formatTime(currentEnd, timezone), }); - + // Move to next potential slot currentStart = new Date(currentStart.getTime() + durationMs); } }); - return brokenDownSlots.sort((a, b) => - new Date(a.slotStartTimeInUTC).getTime() - new Date(b.slotStartTimeInUTC).getTime() + return brokenDownSlots.sort( + (a, b) => + new Date(a.slotStartTimeInUTC).getTime() - + new Date(b.slotStartTimeInUTC).getTime(), ); } diff --git a/prisma/seedFiles/createSlotsOfAvailability.ts b/prisma/seedFiles/createSlotsOfAvailability.ts index a1e7f8f..b8edbf8 100644 --- a/prisma/seedFiles/createSlotsOfAvailability.ts +++ b/prisma/seedFiles/createSlotsOfAvailability.ts @@ -3,15 +3,18 @@ import { DayOfWeek, ScheduleType } from "@prisma/client"; import prisma from "../../lib/prisma"; import { UserWithProfiles } from "./createUsers"; -const MAX_SLOT_DURATION = 6; // 6 hours +const MAX_SLOT_DURATION = 6; // 6 hours const MIN_SLOT_DURATION = 0.5; // 30 minutes const MIN_BREAK_DURATION = 0.5; // 30 minutes const MAX_SLOTS_PER_DAY = 4; -function generateSlotTime(existingSlots: Array<{ start: number; end: number }>) { +function generateSlotTime( + existingSlots: Array<{ start: number; end: number }>, +) { // Keep trying until we find a valid slot let attempts = 0; - while (attempts < 50) { // Prevent infinite loops + while (attempts < 50) { + // Prevent infinite loops // Generate random start hour (0-23) const startHour = faker.number.int({ min: 0, max: 23 }); // Randomly decide if we want to start at half hour @@ -21,15 +24,18 @@ function generateSlotTime(existingSlots: Array<{ start: number; end: number }>) // Generate random duration between 30 mins and 6 hours const possibleDurations = Array.from( { length: MAX_SLOT_DURATION * 2 }, // *2 because we're counting in half hours - (_, i) => (i + 1) * 0.5 // Generate durations from 0.5 to 6 in 0.5 increments + (_, i) => (i + 1) * 0.5, // Generate durations from 0.5 to 6 in 0.5 increments ); const duration = faker.helpers.arrayElement(possibleDurations); const end = start + duration; // Verify this slot doesn't overlap with existing slots - const hasOverlap = existingSlots.some(slot => { + const hasOverlap = existingSlots.some((slot) => { // Add MIN_BREAK_DURATION to ensure minimum break between slots - return !(end + MIN_BREAK_DURATION <= slot.start || start >= slot.end + MIN_BREAK_DURATION); + return !( + end + MIN_BREAK_DURATION <= slot.start || + start >= slot.end + MIN_BREAK_DURATION + ); }); if (!hasOverlap) { @@ -104,8 +110,10 @@ export async function createSlotsOfAvailability( dayOfWeekforStartTimeInUTC: dayOfWeek, slotStartTimeInUTC: startTime, // If slot crosses midnight, end day is next day - dayOfWeekforEndTimeInUTC: endTime <= startTime ? - daysOfWeek[(daysOfWeek.indexOf(dayOfWeek) + 1) % 7] : dayOfWeek, + dayOfWeekforEndTimeInUTC: + endTime <= startTime + ? daysOfWeek[(daysOfWeek.indexOf(dayOfWeek) + 1) % 7] + : dayOfWeek, slotEndTimeInUTC: endTime, }, }); From a652985681d691d93bae307ba28d41290b856748 Mon Sep 17 00:00:00 2001 From: teetangh Date: Thu, 12 Dec 2024 22:00:00 +0530 Subject: [PATCH 16/16] added default date on calendar in pricing toggle --- .../experts/[consultantId]/components/PricingToggle.tsx | 8 ++++++++ app/explore/experts/[consultantId]/page.tsx | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/app/explore/experts/[consultantId]/components/PricingToggle.tsx b/app/explore/experts/[consultantId]/components/PricingToggle.tsx index 2cde2e1..89a3ab7 100644 --- a/app/explore/experts/[consultantId]/components/PricingToggle.tsx +++ b/app/explore/experts/[consultantId]/components/PricingToggle.tsx @@ -49,6 +49,7 @@ export default function PricingToggle({ subscriptionOptions = defaultSubscriptionOptions, handleBooking, selectedDate, + setSelectedDate, currentDate, setCurrentDate, renderCalendar, @@ -96,6 +97,12 @@ export default function PricingToggle({ return breakDownSlotsByDuration(daySlots, selectedDuration); }, [slotTimings, selectedDate, selectedDuration]); + const handleBookNowClick = () => { + const today = new Date(); + setSelectedDate(today); + setCurrentDate(new Date(today.getFullYear(), today.getMonth(), 1)); + }; + if (consultationOptions.length === 0 && subscriptionOptions.length === 0) { return (
    @@ -221,6 +228,7 @@ export default function PricingToggle({ diff --git a/app/explore/experts/[consultantId]/page.tsx b/app/explore/experts/[consultantId]/page.tsx index b0f29b7..448fdd7 100644 --- a/app/explore/experts/[consultantId]/page.tsx +++ b/app/explore/experts/[consultantId]/page.tsx @@ -97,6 +97,7 @@ export default function ExpertProfile( }, [params.consultantId, toast]); useEffect(() => { + // Only process slots if timezone is available and not loading if (selectedDate && consultantDetails && timezone && !isTimezoneLoading) { console.log("Using timezone:", timezone); console.log("Selected date:", selectedDate.toISOString()); @@ -186,6 +187,9 @@ export default function ExpertProfile( setSlotTimings(sortedSlots); } + } else { + // Clear slots if timezone is not available + setSlotTimings([]); } }, [selectedDate, consultantDetails, timezone, isTimezoneLoading]);