feat: add draft date/time selection to league creation and settings

This commit is contained in:
Chris Parsons 2025-10-15 22:02:21 -07:00
parent 501c5b183f
commit dd9d385f16
11 changed files with 1416 additions and 26 deletions

View file

@ -0,0 +1,68 @@
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker, type DayPickerProps } from "react-day-picker"
import { cn } from "~/lib/utils"
import { buttonVariants } from "~/components/ui/button"
export type CalendarProps = DayPickerProps
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
month_caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
button_previous: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute left-1"
),
button_next: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute right-1"
),
month_grid: "w-full border-collapse space-y-1",
weekdays: "flex",
weekday:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
week: "flex w-full mt-2",
day: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day_button: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
range_end: "day-range-end",
selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
today: "bg-accent text-accent-foreground",
outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
disabled: "text-muted-foreground opacity-50",
range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
hidden: "invisible",
...classNames,
}}
components={{
Chevron: ({ orientation }) => {
const Icon = orientation === "left" ? ChevronLeft : ChevronRight
return <Icon className="h-4 w-4" />
},
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View file

@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "~/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View file

@ -1,6 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Form, Link, redirect, useNavigation } from "react-router"; import { Form, Link, redirect, useNavigation } from "react-router";
import { getAuth } from "@clerk/react-router/server"; import { getAuth } from "@clerk/react-router/server";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import { findLeagueById, updateLeague, deleteLeague } from "~/models/league"; import { findLeagueById, updateLeague, deleteLeague } from "~/models/league";
import { isCommissioner } from "~/models/commissioner"; import { isCommissioner } from "~/models/commissioner";
import { findCurrentSeasonWithSports, updateSeason } from "~/models/season"; import { findCurrentSeasonWithSports, updateSeason } from "~/models/season";
@ -12,6 +14,13 @@ import type { Route } from "./+types/$leagueId.settings";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
import { Calendar } from "~/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { cn } from "~/lib/utils";
import { import {
Card, Card,
CardContent, CardContent,
@ -117,31 +126,6 @@ export async function action(args: Route.ActionArgs) {
return { error: "No active season found" }; return { error: "No active season found" };
} }
if (intent === "update-draft-rounds") {
if (season.status !== "pre_draft") {
return { error: "Cannot modify draft rounds after draft has started" };
}
const draftRounds = formData.get("draftRounds");
const draftRoundsNum = typeof draftRounds === "string" ? parseInt(draftRounds, 10) : null;
if (draftRoundsNum === null || isNaN(draftRoundsNum)) {
return { error: "Draft rounds must be a valid number" };
}
const sportsCount = season.seasonSports?.length || 0;
if (draftRoundsNum < sportsCount) {
return { error: `Draft rounds must be at least ${sportsCount} (number of sports selected)` };
}
if (draftRoundsNum < 1 || draftRoundsNum > 50) {
return { error: "Draft rounds must be between 1 and 50" };
}
await updateSeason(season.id, { draftRounds: draftRoundsNum });
return { success: true, message: "Draft rounds updated successfully" };
}
if (intent === "update-sports") { if (intent === "update-sports") {
if (season.status !== "pre_draft") { if (season.status !== "pre_draft") {
return { error: "Cannot modify sports after draft has started" }; return { error: "Cannot modify sports after draft has started" };
@ -226,6 +210,8 @@ export async function action(args: Route.ActionArgs) {
if (intent === "update") { if (intent === "update") {
const name = formData.get("name"); const name = formData.get("name");
const teamCount = formData.get("teamCount"); const teamCount = formData.get("teamCount");
const draftDateTime = formData.get("draftDateTime");
const draftRounds = formData.get("draftRounds");
// Validation // Validation
if (typeof name !== "string" || !name.trim()) { if (typeof name !== "string" || !name.trim()) {
@ -241,6 +227,34 @@ export async function action(args: Route.ActionArgs) {
name: name.trim(), name: name.trim(),
}); });
// Update season settings
const seasonUpdates: any = {};
// Handle draft rounds
if (typeof draftRounds === "string") {
const draftRoundsNum = parseInt(draftRounds, 10);
if (!isNaN(draftRoundsNum)) {
const sportsCount = season.seasonSports?.length || 0;
if (draftRoundsNum < sportsCount) {
return { error: `Draft rounds must be at least ${sportsCount} (number of sports selected)` };
}
if (draftRoundsNum < 1 || draftRoundsNum > 50) {
return { error: "Draft rounds must be between 1 and 50" };
}
seasonUpdates.draftRounds = draftRoundsNum;
}
}
// Handle draft date/time
if (typeof draftDateTime === "string") {
seasonUpdates.draftDateTime = draftDateTime ? new Date(draftDateTime) : null;
}
// Update season if there are changes
if (Object.keys(seasonUpdates).length > 0) {
await updateSeason(season.id, seasonUpdates);
}
// Handle team count changes if provided // Handle team count changes if provided
if (typeof teamCount === "string") { if (typeof teamCount === "string") {
const newTeamCount = parseInt(teamCount, 10); const newTeamCount = parseInt(teamCount, 10);
@ -307,6 +321,14 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
: teams.map((team: any) => team.id) : teams.map((team: any) => team.id)
); );
const [draftRounds, setDraftRounds] = useState<number>(season?.draftRounds || 20); const [draftRounds, setDraftRounds] = useState<number>(season?.draftRounds || 20);
const [draftDate, setDraftDate] = useState<Date | undefined>(
season?.draftDateTime ? new Date(season.draftDateTime) : undefined
);
const [draftTime, setDraftTime] = useState<string>(
season?.draftDateTime
? format(new Date(season.draftDateTime), "HH:mm")
: ""
);
// Update draft order when loader data changes (after randomization) // Update draft order when loader data changes (after randomization)
useEffect(() => { useEffect(() => {
@ -478,6 +500,58 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
)} )}
</div> </div>
</div> </div>
<div className="space-y-2">
<Label htmlFor="draftDate">Draft Date & Time</Label>
<div className="grid gap-2 sm:grid-cols-2">
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"justify-start text-left font-normal",
!draftDate && "text-muted-foreground"
)}
disabled={!canEditDraftRounds}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{draftDate ? format(draftDate, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={draftDate}
onSelect={setDraftDate}
initialFocus
disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
/>
</PopoverContent>
</Popover>
<Input
type="time"
value={draftTime}
onChange={(e) => setDraftTime(e.target.value)}
placeholder="Select time"
disabled={!canEditDraftRounds}
/>
</div>
{draftDate && draftTime && (
<input
type="hidden"
name="draftDateTime"
value={new Date(`${format(draftDate, "yyyy-MM-dd")}T${draftTime}`).toISOString()}
/>
)}
{!draftDate && !draftTime && (
<input type="hidden" name="draftDateTime" value="" />
)}
<p className="text-sm text-muted-foreground">
{canEditDraftRounds
? "You must set a draft date and time before starting the draft"
: "Draft date cannot be changed after draft has started"}
</p>
</div>
</CardContent> </CardContent>
</Card> </Card>

View file

@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { Link, useSearchParams } from "react-router"; import { Link, useSearchParams } from "react-router";
import { getAuth } from "@clerk/react-router/server"; import { getAuth } from "@clerk/react-router/server";
import { toast } from "sonner"; import { toast } from "sonner";
import { format } from "date-fns";
import { import {
findLeagueById, findLeagueById,
findCurrentSeason, findCurrentSeason,
@ -360,6 +361,14 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
{Math.max(0, season.draftRounds - sportsCount)} {Math.max(0, season.draftRounds - sportsCount)}
</p> </p>
</div> </div>
{season.draftDateTime && (
<div>
<p className="text-sm text-muted-foreground">Draft Date</p>
<p className="font-medium">
{format(new Date(season.draftDateTime), "PPP 'at' p")}
</p>
</div>
)}
</> </>
)} )}
</CardContent> </CardContent>

View file

@ -10,6 +10,8 @@ import { createManyTeams } from "~/models/team";
import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template"; import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template";
import { findAllSportsSeasons } from "~/models/sports-season"; import { findAllSportsSeasons } from "~/models/sports-season";
import { generateUniqueTeamNames } from "~/utils/team-names"; import { generateUniqueTeamNames } from "~/utils/team-names";
import { format } from "date-fns";
import { CalendarIcon, Check } from "lucide-react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { import {
Card, Card,
@ -29,7 +31,13 @@ import { Badge } from "~/components/ui/badge";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
import { Checkbox } from "~/components/ui/checkbox"; import { Checkbox } from "~/components/ui/checkbox";
import { Check } from "lucide-react"; import { Calendar } from "~/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { cn } from "~/lib/utils";
export async function loader() { export async function loader() {
const templates = await findActiveSeasonTemplates(); const templates = await findActiveSeasonTemplates();
@ -67,6 +75,7 @@ export async function action(args: Route.ActionArgs) {
const teamCount = formData.get("teamCount"); const teamCount = formData.get("teamCount");
const templateId = formData.get("templateId"); const templateId = formData.get("templateId");
const draftRounds = formData.get("draftRounds"); const draftRounds = formData.get("draftRounds");
const draftDateTime = formData.get("draftDateTime");
// Validation // Validation
if (typeof name !== "string" || !name.trim()) { if (typeof name !== "string" || !name.trim()) {
@ -108,6 +117,7 @@ export async function action(args: Route.ActionArgs) {
status: "pre_draft", status: "pre_draft",
templateId: typeof templateId === "string" && templateId !== "" ? templateId : null, templateId: typeof templateId === "string" && templateId !== "" ? templateId : null,
draftRounds: draftRoundsNum, draftRounds: draftRoundsNum,
draftDateTime: typeof draftDateTime === "string" && draftDateTime ? new Date(draftDateTime) : null,
}); });
// Set this as the current season for the league // Set this as the current season for the league
@ -155,6 +165,8 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
const [selectedTemplate, setSelectedTemplate] = useState<string>(""); const [selectedTemplate, setSelectedTemplate] = useState<string>("");
const [selectedSports, setSelectedSports] = useState<Set<string>>(new Set()); const [selectedSports, setSelectedSports] = useState<Set<string>>(new Set());
const [draftRounds, setDraftRounds] = useState<number>(20); const [draftRounds, setDraftRounds] = useState<number>(20);
const [draftDate, setDraftDate] = useState<Date>();
const [draftTime, setDraftTime] = useState<string>("");
const handleSportToggle = (sportId: string) => { const handleSportToggle = (sportId: string) => {
const newSelected = new Set(selectedSports); const newSelected = new Set(selectedSports);
@ -269,6 +281,51 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
</div> </div>
</div> </div>
<div className="space-y-2">
<Label htmlFor="draftDate">Draft Date & Time</Label>
<div className="grid gap-2 sm:grid-cols-2">
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"justify-start text-left font-normal",
!draftDate && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{draftDate ? format(draftDate, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={draftDate}
onSelect={setDraftDate}
initialFocus
disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))}
/>
</PopoverContent>
</Popover>
<Input
type="time"
value={draftTime}
onChange={(e) => setDraftTime(e.target.value)}
placeholder="Select time"
/>
</div>
{draftDate && draftTime && (
<input
type="hidden"
name="draftDateTime"
value={new Date(`${format(draftDate, "yyyy-MM-dd")}T${draftTime}`).toISOString()}
/>
)}
<p className="text-sm text-muted-foreground">
You must set a draft date and time before starting the draft
</p>
</div>
<div className="space-y-4"> <div className="space-y-4">
<Label>Sports Selection</Label> <Label>Sports Selection</Label>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">

View file

@ -69,6 +69,7 @@ export const seasons = pgTable("seasons", {
.references(() => seasonTemplates.id, { onDelete: "set null" }), .references(() => seasonTemplates.id, { onDelete: "set null" }),
draftRounds: integer("draft_rounds").notNull().default(20), draftRounds: integer("draft_rounds").notNull().default(20),
flexSpots: integer("flex_spots").notNull().default(0), flexSpots: integer("flex_spots").notNull().default(0),
draftDateTime: timestamp("draft_date_time"),
inviteCode: varchar("invite_code", { length: 20 }).notNull().unique(), inviteCode: varchar("invite_code", { length: 20 }).notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(),

View file

@ -0,0 +1 @@
ALTER TABLE "seasons" ADD COLUMN "draft_date_time" timestamp;

File diff suppressed because it is too large Load diff

View file

@ -113,6 +113,13 @@
"when": 1760589195551, "when": 1760589195551,
"tag": "0015_exotic_loners", "tag": "0015_exotic_loners",
"breakpoints": true "breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1760590495868,
"tag": "0016_majestic_roulette",
"breakpoints": true
} }
] ]
} }

83
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
@ -20,6 +21,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"compression": "^1.8.0", "compression": "^1.8.0",
"date-fns": "^4.1.0",
"drizzle-kit": "~0.28.1", "drizzle-kit": "~0.28.1",
"drizzle-orm": "~0.36.3", "drizzle-orm": "~0.36.3",
"express": "^5.1.0", "express": "^5.1.0",
@ -29,6 +31,7 @@
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"postgres": "^3.4.5", "postgres": "^3.4.5",
"react": "^19.1.0", "react": "^19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router": "^7.7.1", "react-router": "^7.7.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
@ -625,6 +628,12 @@
"node": ">=18.17.0" "node": ">=18.17.0"
} }
}, },
"node_modules/@date-fns/tz": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT"
},
"node_modules/@drizzle-team/brocli": { "node_modules/@drizzle-team/brocli": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
@ -2003,6 +2012,43 @@
} }
} }
}, },
"node_modules/@radix-ui/react-popover": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
"integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": { "node_modules/@radix-ui/react-popper": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
@ -3726,6 +3772,22 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-jalali": {
"version": "4.1.0-0",
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -5567,6 +5629,27 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-day-picker": {
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz",
"integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==",
"license": "MIT",
"dependencies": {
"@date-fns/tz": "^1.4.1",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.1.0-0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.0", "version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",

View file

@ -18,6 +18,7 @@
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
@ -26,6 +27,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"compression": "^1.8.0", "compression": "^1.8.0",
"date-fns": "^4.1.0",
"drizzle-kit": "~0.28.1", "drizzle-kit": "~0.28.1",
"drizzle-orm": "~0.36.3", "drizzle-orm": "~0.36.3",
"express": "^5.1.0", "express": "^5.1.0",
@ -35,6 +37,7 @@
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"postgres": "^3.4.5", "postgres": "^3.4.5",
"react": "^19.1.0", "react": "^19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router": "^7.7.1", "react-router": "^7.7.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",