Merge branch 'main' into renovate/node-24.x
CI / Build (pull_request) Successful in 7m21s

This commit is contained in:
2026-07-10 23:21:17 +02:00
26 changed files with 352 additions and 392 deletions
+16 -120
View File
@@ -1,138 +1,34 @@
name: CI
on:
#push:
# branches: [main]
#pull_request:
# branches: [main]
workflow_dispatch:
pull_request:
branches: [main]
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/test?schema=public"
NEXTAUTH_SECRET: "test-secret-for-ci"
NEXTAUTH_URL: "http://localhost:3000"
AUTH_SECRET: "test-secret-for-ci"
AUTH_URL: "http://localhost:3000"
RESEND_APIKEY: "any-api-key-for-ci"
jobs:
build:
name: Build
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ env.POSTGRES_DB }}
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
runs-on: easynode-debian
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Cache npm dependencies
uses: actions/cache@v3
with:
node-version: '20'
cache: 'npm'
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
run: npm ci
- name: Generate Prisma Client
run: npx prisma generate
- name: Run database migrations
run: npx prisma db push
run: npm i
- name: Run lint
run: npm run lint
- name: Build application
run: npm run build
lint:
name: Lint
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Biome
uses: biomejs/setup-biome@v2
with:
version: 2.4.4
- name: Run Biome
run: biome ci .
e2e:
name: E2E Tests (Playwright)
runs-on: ubuntu-latest
needs: [lint, build]
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ env.POSTGRES_DB }}
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Generate Prisma Client
run: npx prisma generate
- name: Run database migrations
run: npx prisma db push
- name: Run Playwright tests
run: npm test
- name: Upload Playwright Report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: test-results/
retention-days: 7
+3
View File
@@ -1,8 +1,11 @@
name: Deploy to Vercel
on:
workflow_dispatch:
push:
branches: [main]
paths-ignore:
- '**.md'
jobs:
deploy:
+3 -1
View File
@@ -43,7 +43,9 @@ export async function POST(
// Check weekly limit for target week, excluding the booking being moved
if (booking.eventType.maxBookingsPerWeek !== null) {
const weekStart = new Date(newStartTime);
weekStart.setUTCDate(newStartTime.getUTCDate() - newStartTime.getUTCDay());
weekStart.setUTCDate(
newStartTime.getUTCDate() - newStartTime.getUTCDay(),
);
weekStart.setUTCHours(0, 0, 0, 0);
const weekEnd = new Date(weekStart);
weekEnd.setUTCDate(weekStart.getUTCDate() + 7);
+4 -1
View File
@@ -156,7 +156,10 @@ export async function POST(
});
}
} catch (calendarError) {
console.error("Google Calendar error on proposal approval:", calendarError);
console.error(
"Google Calendar error on proposal approval:",
calendarError,
);
}
try {
+8 -2
View File
@@ -2,7 +2,10 @@ import { type NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { getGoogleCalendarClient } from "@/lib/google-calendar";
import { prisma } from "@/lib/prisma.server";
import { sendBookingCompletedGuestEmail, sendBookingStatusChangedEmail } from "@/lib/resend";
import {
sendBookingCompletedGuestEmail,
sendBookingStatusChangedEmail,
} from "@/lib/resend";
export async function GET(
_request: NextRequest,
@@ -120,7 +123,10 @@ export async function PATCH(
timezone: user.timezone,
};
await sendBookingStatusChangedEmail({ to: session.user.email, ...emailParams });
await sendBookingStatusChangedEmail({
to: session.user.email,
...emailParams,
});
if (status === "completed") {
await sendBookingCompletedGuestEmail({
+1 -1
View File
@@ -92,7 +92,7 @@ export async function POST(request: NextRequest) {
try {
await sendProposalReceivedEmail({
to: booking.user.email!,
to: booking.user.email,
guestName,
guestEmail,
eventTitle: booking.eventType.title,
+1 -1
View File
@@ -66,7 +66,7 @@ export async function GET(request: NextRequest) {
(b) => b.startTime >= weekStart && b.startTime < weekEnd,
).length;
if (count >= eventType.maxBookingsPerWeek!) {
if (count >= eventType.maxBookingsPerWeek) {
const weekEndInclusive = new Date(weekEnd);
weekEndInclusive.setDate(weekEndInclusive.getDate() - 1);
fullWeeks.push({
+3 -1
View File
@@ -46,7 +46,9 @@ export default async function BookingSuccessPage({
<p className="font-medium text-sm">Guest Details</p>
<p className="text-sm text-muted-foreground">{guestName}</p>
{guestEmail && (
<p className="text-sm text-muted-foreground">{guestEmail}</p>
<p className="text-sm text-muted-foreground">
{guestEmail}
</p>
)}
</div>
</div>
+3 -1
View File
@@ -151,7 +151,9 @@ export default function ReschedulePage({ params }: ReschedulePageProps) {
</h3>
{loadingSlots ? (
<p className="text-sm text-muted-foreground">Loading slots...</p>
<p className="text-sm text-muted-foreground">
Loading slots...
</p>
) : availableSlots.length === 0 ? (
<p className="text-sm text-muted-foreground">
No available slots for this date
+4 -1
View File
@@ -64,7 +64,10 @@ export default async function BookingsPage() {
<CardTitle>Upcoming</CardTitle>
</CardHeader>
<CardContent>
<BookingsTable bookings={confirmed} emptyMessage="No upcoming bookings" />
<BookingsTable
bookings={confirmed}
emptyMessage="No upcoming bookings"
/>
</CardContent>
</Card>
+120 -124
View File
@@ -65,139 +65,135 @@ export default async function NewEventTypePage() {
return (
<div className="max-w-2xl mx-auto">
<div className="mb-8">
<Link
href="/dashboard/event-types"
className="text-indigo-600 hover:text-indigo-700 text-sm"
>
Back to Event Types
</Link>
<h1 className="text-3xl font-bold mt-4">Create Event Type</h1>
<p className="text-muted-foreground dark:text-gray-400 mt-2">
Define a new type of meeting you want to offer
</p>
</div>
<div className="mb-8">
<Link
href="/dashboard/event-types"
className="text-indigo-600 hover:text-indigo-700 text-sm"
>
Back to Event Types
</Link>
<h1 className="text-3xl font-bold mt-4">Create Event Type</h1>
<p className="text-muted-foreground dark:text-gray-400 mt-2">
Define a new type of meeting you want to offer
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Event Details</CardTitle>
<CardDescription>
Provide information about this event type
</CardDescription>
</CardHeader>
<CardContent>
<form action={createEventType} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input
id="title"
name="title"
placeholder="30 Minute Meeting"
required
/>
</div>
<CardHeader>
<CardTitle>Event Details</CardTitle>
<CardDescription>
Provide information about this event type
</CardDescription>
</CardHeader>
<CardContent>
<form action={createEventType} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input
id="title"
name="title"
placeholder="30 Minute Meeting"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="slug">URL Slug *</Label>
<Input
id="slug"
name="slug"
placeholder="30min"
required
pattern="[a-z0-9-]+"
title="Only lowercase letters, numbers, and hyphens"
/>
<p className="text-sm text-muted-foreground">
This will be part of your booking URL
</p>
</div>
<div className="space-y-2">
<Label htmlFor="slug">URL Slug *</Label>
<Input
id="slug"
name="slug"
placeholder="30min"
required
pattern="[a-z0-9-]+"
title="Only lowercase letters, numbers, and hyphens"
/>
<p className="text-sm text-muted-foreground">
This will be part of your booking URL
</p>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
name="description"
placeholder="A brief 30-minute meeting to discuss..."
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
name="description"
placeholder="A brief 30-minute meeting to discuss..."
rows={4}
/>
</div>
<div className="space-y-2">
<Label htmlFor="duration">Duration (minutes) *</Label>
<Input
id="duration"
name="duration"
type="number"
placeholder="30"
min="15"
max="240"
step="15"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="duration">Duration (minutes) *</Label>
<Input
id="duration"
name="duration"
type="number"
placeholder="30"
min="15"
max="240"
step="15"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxBookingsPerWeek">
Max Bookings Per Week
</Label>
<Input
id="maxBookingsPerWeek"
name="maxBookingsPerWeek"
type="number"
placeholder="Unlimited"
min="1"
/>
<p className="text-sm text-muted-foreground">
Leave empty for unlimited bookings
</p>
</div>
<div className="space-y-2">
<Label htmlFor="maxBookingsPerWeek">Max Bookings Per Week</Label>
<Input
id="maxBookingsPerWeek"
name="maxBookingsPerWeek"
type="number"
placeholder="Unlimited"
min="1"
/>
<p className="text-sm text-muted-foreground">
Leave empty for unlimited bookings
</p>
</div>
<div className="space-y-2">
<Label htmlFor="minimumNoticeHours">
Minimum Notice (hours) *
</Label>
<Input
id="minimumNoticeHours"
name="minimumNoticeHours"
type="number"
placeholder="24"
defaultValue="24"
min="1"
required
/>
<p className="text-sm text-muted-foreground">
Minimum hours before event can start (e.g., 24 or 48)
</p>
</div>
<div className="space-y-2">
<Label htmlFor="minimumNoticeHours">
Minimum Notice (hours) *
</Label>
<Input
id="minimumNoticeHours"
name="minimumNoticeHours"
type="number"
placeholder="24"
defaultValue="24"
min="1"
required
/>
<p className="text-sm text-muted-foreground">
Minimum hours before event can start (e.g., 24 or 48)
</p>
</div>
<div className="space-y-2">
<Label htmlFor="maximumNoticeDays">
Maximum Notice (days) *
</Label>
<Input
id="maximumNoticeDays"
name="maximumNoticeDays"
type="number"
placeholder="14"
defaultValue="14"
min="1"
max="365"
required
/>
<p className="text-sm text-muted-foreground">
Maximum days in advance for booking (e.g., 7 or 14)
</p>
</div>
<div className="space-y-2">
<Label htmlFor="maximumNoticeDays">Maximum Notice (days) *</Label>
<Input
id="maximumNoticeDays"
name="maximumNoticeDays"
type="number"
placeholder="14"
defaultValue="14"
min="1"
max="365"
required
/>
<p className="text-sm text-muted-foreground">
Maximum days in advance for booking (e.g., 7 or 14)
</p>
</div>
<div className="flex gap-3">
<Button type="submit">Create Event Type</Button>
<Button variant="outline" asChild>
<Link href="/dashboard/event-types">Cancel</Link>
</Button>
</div>
</form>
</CardContent>
</Card>
<div className="flex gap-3">
<Button type="submit">Create Event Type</Button>
<Button variant="outline" asChild>
<Link href="/dashboard/event-types">Cancel</Link>
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
}
+7 -3
View File
@@ -1,15 +1,15 @@
import { Calendar, LogOut, Menu } from "lucide-react";
import { redirect } from "next/navigation";
import { auth, signOut } from "@/auth";
import { Button } from "@/components/ui/button";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetClose,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
SheetClose,
} from "@/components/ui/sheet";
async function handleSignOut() {
@@ -88,7 +88,11 @@ export default async function DashboardLayout({
<nav className="flex flex-col gap-2">
{navItems.map((item) => (
<SheetClose asChild key={item.href}>
<Button variant="ghost" asChild className="justify-start">
<Button
variant="ghost"
asChild
className="justify-start"
>
<a href={item.href}>{item.label}</a>
</Button>
</SheetClose>
+3 -1
View File
@@ -24,7 +24,9 @@ export function BookingLinkCard({ username }: { username: string }) {
return (
<Card className="mb-6 bg-indigo-50 dark:bg-indigo-950 border-indigo-200 dark:border-indigo-800">
<CardHeader>
<CardTitle className="text-indigo-900 dark:text-indigo-100">Your Booking Link</CardTitle>
<CardTitle className="text-indigo-900 dark:text-indigo-100">
Your Booking Link
</CardTitle>
<CardDescription className="text-indigo-700 dark:text-indigo-300">
Share this link with people to allow them to book meetings with you
</CardDescription>
+7 -1
View File
@@ -23,7 +23,13 @@ export default async function SettingsPage() {
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { username: true, name: true, email: true, timezone: true, image: true },
select: {
username: true,
name: true,
email: true,
timezone: true,
image: true,
},
});
if (!user) {
+5 -1
View File
@@ -38,7 +38,11 @@ export default function Home() {
</Button>
</Link>
<Link href="/login" className="w-full sm:w-auto">
<Button size="lg" variant="outline" className="text-lg px-8 py-6 w-full sm:w-auto">
<Button
size="lg"
variant="outline"
className="text-lg px-8 py-6 w-full sm:w-auto"
>
Sign In
</Button>
</Link>
+3 -5
View File
@@ -34,9 +34,7 @@ export default function PrivacyPolicyPage() {
</section>
<section>
<h2 className="text-xl font-semibold mb-3">
2. Data We Store
</h2>
<h2 className="text-xl font-semibold mb-3">2. Data We Store</h2>
<p className="text-muted-foreground mb-2">
When you create an account, we store:
</p>
@@ -138,8 +136,8 @@ export default function PrivacyPolicyPage() {
</code>{" "}
scopes to identify your account and display your name and
profile picture. If you connect your Google Calendar, we
also request OAuth access to read and write calendar
events on your behalf (specifically, the{" "}
also request OAuth access to read and write calendar events
on your behalf (specifically, the{" "}
<code className="text-xs bg-muted px-1 py-0.5 rounded">
calendar.events
</code>{" "}
+9 -4
View File
@@ -34,7 +34,9 @@ export default function TermsOfUsePage() {
</section>
<section>
<h2 className="text-xl font-semibold mb-3">2. What Event.me Is</h2>
<h2 className="text-xl font-semibold mb-3">
2. What Event.me Is
</h2>
<p className="text-muted-foreground">
Event.me is a scheduling platform. It lets you define your
availability, create bookable event types, and share a link so
@@ -55,8 +57,8 @@ export default function TermsOfUsePage() {
account.
</li>
<li>
You must provide accurate information when registering and keep
it up to date.
You must provide accurate information when registering and
keep it up to date.
</li>
<li>You must be at least 13 years old to use Event.me.</li>
<li>
@@ -217,7 +219,10 @@ export default function TermsOfUsePage() {
<h2 className="text-xl font-semibold mb-3">14. Contact</h2>
<p className="text-muted-foreground">
Questions about these terms can be sent to{" "}
<a href="mailto:ricardompcampos@gmail.com" className="underline">
<a
href="mailto:ricardompcampos@gmail.com"
className="underline"
>
ricardompcampos@gmail.com
</a>
.
+102 -95
View File
@@ -153,110 +153,117 @@ export function BookingForm({ eventType }: BookingFormProps) {
/>
<div className="grid md:grid-cols-2 gap-8">
<div>
<h3 className="font-semibold mb-4">Select a date</h3>
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleDateSelect}
disabled={[
(date) =>
date <
startOfDay(
addMinutes(new Date(), eventType.minimumNoticeHours * 60),
),
...fullWeekRanges,
]}
showOutsideDays={false}
className="rounded-md border"
/>
<div className="mt-4 text-center">
<Button
type="button"
variant="link"
className="text-sm text-muted-foreground"
onClick={() => setProposalOpen(true)}
>
Can&apos;t find a time that works?
</Button>
</div>
<h3 className="font-semibold mb-4">Select a date</h3>
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleDateSelect}
disabled={[
(date) =>
date <
startOfDay(
addMinutes(new Date(), eventType.minimumNoticeHours * 60),
),
...fullWeekRanges,
]}
showOutsideDays={false}
className="rounded-md border"
/>
<div className="mt-4 text-center">
<Button
type="button"
variant="link"
className="text-sm text-muted-foreground"
onClick={() => setProposalOpen(true)}
>
Can&apos;t find a time that works?
</Button>
</div>
</div>
<div>
{selectedDate && (
<>
<h3 className="font-semibold mb-4">
Available times - {format(selectedDate, "MMMM d, yyyy")}
</h3>
{selectedDate && (
<>
<h3 className="font-semibold mb-4">
Available times - {format(selectedDate, "MMMM d, yyyy")}
</h3>
{loadingSlots ? (
<p className="text-sm text-muted-foreground">
Loading slots...
</p>
) : availableSlots.length === 0 ? (
<p className="text-sm text-muted-foreground">
No available slots for this date
</p>
) : (
<div className="grid grid-cols-2 sm:grid-cols-1 gap-2 mb-6">
{availableSlots.map((slot) => (
<Button
key={slot.start.toISOString()}
type="button"
variant={
selectedSlot?.start.getTime() === slot.start.getTime()
? "default"
: "outline"
}
className="w-full"
onClick={() => setSelectedSlot(slot)}
>
{format(slot.start, "HH:mm")} -{" "}
{format(slot.end, "HH:mm")}
</Button>
))}
</div>
)}
{selectedSlot && (
<form onSubmit={handleSubmit} className="space-y-4 mt-6">
<div className="space-y-2">
<Label htmlFor="guestName">Name *</Label>
<Input
id="guestName"
name="guestName"
type="text"
required
placeholder="John Doe"
/>
</div>
<div className="space-y-2">
<Label htmlFor="guestEmail">Email *</Label>
<Input
id="guestEmail"
name="guestEmail"
type="email"
required
placeholder="john@example.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="guestNotes">Notes (optional)</Label>
<Textarea
id="guestNotes"
name="guestNotes"
placeholder="Any additional information..."
rows={3}
/>
</div>
{loadingSlots ? (
<p className="text-sm text-muted-foreground">Loading slots...</p>
) : availableSlots.length === 0 ? (
<p className="text-sm text-muted-foreground">
No available slots for this date
</p>
) : (
<div className="grid grid-cols-2 sm:grid-cols-1 gap-2 mb-6">
{availableSlots.map((slot) => (
<Button
key={slot.start.toISOString()}
type="button"
variant={
selectedSlot?.start.getTime() === slot.start.getTime()
? "default"
: "outline"
}
type="submit"
className="w-full"
onClick={() => setSelectedSlot(slot)}
disabled={submitting}
>
{format(slot.start, "HH:mm")} - {format(slot.end, "HH:mm")}
{submitting ? "Booking..." : "Confirm booking"}
</Button>
))}
</div>
)}
{selectedSlot && (
<form onSubmit={handleSubmit} className="space-y-4 mt-6">
<div className="space-y-2">
<Label htmlFor="guestName">Name *</Label>
<Input
id="guestName"
name="guestName"
type="text"
required
placeholder="John Doe"
/>
</div>
<div className="space-y-2">
<Label htmlFor="guestEmail">Email *</Label>
<Input
id="guestEmail"
name="guestEmail"
type="email"
required
placeholder="john@example.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="guestNotes">Notes (optional)</Label>
<Textarea
id="guestNotes"
name="guestNotes"
placeholder="Any additional information..."
rows={3}
/>
</div>
<Button type="submit" className="w-full" disabled={submitting}>
{submitting ? "Booking..." : "Confirm booking"}
</Button>
</form>
)}
</>
)}
</form>
)}
</>
)}
</div>
</div>
</div>
</>
);
}
+8 -4
View File
@@ -123,8 +123,8 @@ export function BookingsTable({
<div className="text-sm">
<p className="text-muted-foreground">
<span className="font-medium text-foreground">Guest:</span> {booking.guestName}{" "}
({booking.guestEmail})
<span className="font-medium text-foreground">Guest:</span>
{booking.guestName} ({booking.guestEmail})
</p>
{booking.guestNotes && (
<p className="text-muted-foreground mt-1">
@@ -145,7 +145,9 @@ export function BookingsTable({
className="flex-1 sm:flex-none"
>
<Check className="w-4 h-4 shrink-0" />
<span className="ml-1">{completing === booking.id ? "Completing..." : "Complete"}</span>
<span className="ml-1">
{completing === booking.id ? "Completing..." : "Complete"}
</span>
</Button>
)}
<Button
@@ -164,7 +166,9 @@ export function BookingsTable({
className="flex-1 sm:flex-none"
>
<X className="w-4 h-4 shrink-0" />
<span className="ml-1">{cancelling === booking.id ? "Cancelling..." : "Cancel"}</span>
<span className="ml-1">
{cancelling === booking.id ? "Cancelling..." : "Cancel"}
</span>
</Button>
</div>
)}
+3 -1
View File
@@ -8,7 +8,9 @@ interface ProposalsSectionProps {
proposals: (Booking & { eventType: EventType })[];
}
export function ProposalsSection({ proposals: initial }: ProposalsSectionProps) {
export function ProposalsSection({
proposals: initial,
}: ProposalsSectionProps) {
const [proposals, setProposals] = useState(initial);
const handleResponded = (id: string) => {
+4 -2
View File
@@ -1,7 +1,9 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes";
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
+8 -2
View File
@@ -54,7 +54,10 @@ function DialogContent({
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
@@ -66,7 +69,10 @@ function DialogTitle({
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
);
+1 -4
View File
@@ -60,10 +60,7 @@ function SheetContent({
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex flex-col space-y-1.5", className)}
{...props}
/>
<div className={cn("flex flex-col space-y-1.5", className)} {...props} />
);
}
+8 -1
View File
@@ -1,5 +1,12 @@
import type { Availability, Booking, EventType } from "@prisma/client";
import { addDays, addMinutes, format, isAfter, isBefore, startOfDay } from "date-fns";
import {
addDays,
addMinutes,
format,
isAfter,
isBefore,
startOfDay,
} from "date-fns";
import { fromZonedTime, toZonedTime } from "date-fns-tz";
export interface TimeSlot {
+17 -14
View File
@@ -44,8 +44,8 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/bcryptjs": "^2.4.6",
"@types/node": "20.19.43",
"@types/bcryptjs": "^3.0.0",
"@types/node": "24.13.3",
"@types/pg": "^8.20.0",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -8754,11 +8754,15 @@
}
},
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-3.0.0.tgz",
"integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==",
"deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.",
"dev": true,
"license": "MIT"
"license": "MIT",
"dependencies": {
"bcryptjs": "*"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
@@ -8786,12 +8790,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~7.18.0"
}
},
"node_modules/@types/pg": {
@@ -11120,7 +11124,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -15916,9 +15919,9 @@
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unicorn-magic": {
+1 -1
View File
@@ -61,7 +61,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/bcryptjs": "^2.4.6",
"@types/bcryptjs": "^3.0.0",
"@types/node": "24.13.3",
"@types/pg": "^8.20.0",
"@types/react": "^19",