(!!isYearlyDefault);
const { setShowSignInModal } = useContext(ModalContext);
diff --git a/components/pricing/pricing-faq.tsx b/components/pricing/pricing-faq.tsx
index c1a74520..ef9bb48f 100644
--- a/components/pricing/pricing-faq.tsx
+++ b/components/pricing/pricing-faq.tsx
@@ -1,6 +1,7 @@
"use client";
import { motion } from "framer-motion";
+import { HeaderSection } from "../shared/header-section";
import {
Accordion,
AccordionContent,
@@ -8,8 +9,6 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
-import { HeaderSection } from "../shared/header-section";
-
const pricingFaqData = [
{
id: "item-1",
diff --git a/components/pricing/quota-usage/index.ts b/components/pricing/quota-usage/index.ts
index e004e889..449024e0 100644
--- a/components/pricing/quota-usage/index.ts
+++ b/components/pricing/quota-usage/index.ts
@@ -1,2 +1,2 @@
-export { QuotaUsageCard } from "./quota-usage-card";
export { QuotaItem } from "./quota-item";
+export { QuotaUsageCard } from "./quota-usage-card";
diff --git a/components/pricing/quota-usage/quota-item.tsx b/components/pricing/quota-usage/quota-item.tsx
index 1e39ea64..df376ab9 100644
--- a/components/pricing/quota-usage/quota-item.tsx
+++ b/components/pricing/quota-usage/quota-item.tsx
@@ -1,5 +1,4 @@
import { Infinity as InfinityIcon } from "lucide-react";
-
import { Badge } from "@/components/ui/badge";
import { QuotaProgress } from "@/components/ui/quota-progress";
import type { QuotaUsage, TierName } from "@/lib/validations/quota";
diff --git a/components/pricing/quota-usage/quota-usage-card.tsx b/components/pricing/quota-usage/quota-usage-card.tsx
index 4c51f237..4f0a5f72 100644
--- a/components/pricing/quota-usage/quota-usage-card.tsx
+++ b/components/pricing/quota-usage/quota-usage-card.tsx
@@ -1,7 +1,6 @@
-import Link from "next/link";
import { Sparkles, Upload } from "lucide-react";
-
-import { cn, formatDate } from "@/lib/utils";
+import Link from "next/link";
+import { QuotaItem } from "./quota-item";
import { buttonVariants } from "@/components/ui/button";
import {
Card,
@@ -10,10 +9,9 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
+import { cn, formatDate } from "@/lib/utils";
import type { QuotaUsageData } from "@/lib/validations/quota";
-import { QuotaItem } from "./quota-item";
-
interface QuotaUsageCardProps {
quotaData: QuotaUsageData;
}
diff --git a/components/sections/bentogrid.tsx b/components/sections/bentogrid.tsx
index 1be12d57..d282ee46 100644
--- a/components/sections/bentogrid.tsx
+++ b/components/sections/bentogrid.tsx
@@ -1,7 +1,6 @@
"use client";
import { motion } from "framer-motion";
-
import { Icons } from "@/components/shared/icons";
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
@@ -99,8 +98,7 @@ export default function BentoGrid() {
viewport={{ once: true, margin: "-50px" }}
>
- Chaos in.{" "}
- Clarity out.
+ Chaos in. Clarity out.
No more lost commitments. Everything you say, write, or receive
diff --git a/components/sections/features.tsx b/components/sections/features.tsx
index 47b18829..17e94457 100644
--- a/components/sections/features.tsx
+++ b/components/sections/features.tsx
@@ -1,11 +1,10 @@
"use client";
import { motion } from "framer-motion";
-
-import { features } from "@/config/landing";
-import { Icons } from "@/components/shared/icons";
import { HeaderSection } from "@/components/shared/header-section";
+import { Icons } from "@/components/shared/icons";
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
+import { features } from "@/config/landing";
const headerVariants = {
hidden: { opacity: 0, scale: 0.96 },
diff --git a/components/sections/hero-landing.tsx b/components/sections/hero-landing.tsx
index ee308416..c93de2f3 100644
--- a/components/sections/hero-landing.tsx
+++ b/components/sections/hero-landing.tsx
@@ -1,12 +1,11 @@
"use client";
-import Link from "next/link";
import { motion } from "framer-motion";
-
+import Link from "next/link";
+import { Icons } from "@/components/shared/icons";
+import { buttonVariants } from "@/components/ui/button";
import { heroContent } from "@/config/landing";
import { cn } from "@/lib/utils";
-import { buttonVariants } from "@/components/ui/button";
-import { Icons } from "@/components/shared/icons";
const containerVariants = {
hidden: { opacity: 0 },
@@ -48,7 +47,9 @@ export default function HeroLanding() {
variants={itemVariants}
>
{heroContent.title}{" "}
- {heroContent.titleHighlight}
+
+ {heroContent.titleHighlight}
+
(initialPreferences);
- const [calendars, setCalendars] = useState(initialCalendars);
+ const [preferences, setPreferences] =
+ useState(initialPreferences);
+ const [calendars, setCalendars] =
+ useState(initialCalendars);
const [isSaving, setIsSaving] = useState(false);
const [savingCalendars, setSavingCalendars] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
@@ -78,7 +77,7 @@ export function ConflictSettings({
const handleUpdatePreference = async (
key: K,
- value: ConflictPreferences[K]
+ value: ConflictPreferences[K],
) => {
const previousValue = preferences[key];
setPreferences((prev) => ({ ...prev, [key]: value }));
@@ -99,8 +98,8 @@ export function ConflictSettings({
const previousCalendars = [...calendars];
setCalendars((prev) =>
prev.map((cal) =>
- cal.id === calendarId ? { ...cal, enabledForConflicts: enabled } : cal
- )
+ cal.id === calendarId ? { ...cal, enabledForConflicts: enabled } : cal,
+ ),
);
setSavingCalendars(true);
@@ -137,7 +136,11 @@ export function ConflictSettings({
{!canEnableConflictDetection && (
-
-
+
Warn me before scheduling events outside my work schedule.{" "}
-
+
Configure working hours
.
@@ -338,7 +371,11 @@ export function ConflictSettings({
{!canEnableConflictDetection && (
-
+
Upgrade to Enable
@@ -348,7 +385,7 @@ export function ConflictSettings({
id="working-hours-warning"
checked={preferences.workingHoursWarningEnabled}
onCheckedChange={(checked) =>
- handleUpdatePreference("workingHoursWarningEnabled", checked)
+ void handleUpdatePreference("workingHoursWarningEnabled", checked)
}
disabled={isSaving || !canEnableConflictDetection}
/>
diff --git a/components/settings/provider-connection-button.tsx b/components/settings/provider-connection-button.tsx
index ef3314d9..02899730 100644
--- a/components/settings/provider-connection-button.tsx
+++ b/components/settings/provider-connection-button.tsx
@@ -1,8 +1,8 @@
"use client";
+import { ExternalLink, Loader2 } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
-import { ExternalLink, Loader2 } from "lucide-react";
type CalendarProvider = "GOOGLE_CALENDAR" | "MICROSOFT_CALENDAR";
type TaskProvider = "GOOGLE_TASKS" | "MICROSOFT_TODO";
@@ -19,7 +19,7 @@ interface ProviderConnectionButtonProps {
function getOAuthUrl(provider: Provider, redirectUrl?: string): string {
const encodedRedirect = redirectUrl ? encodeURIComponent(redirectUrl) : "";
-
+
switch (provider) {
case "GOOGLE_CALENDAR":
return "/api/calendar/oauth/authorize";
@@ -30,7 +30,7 @@ function getOAuthUrl(provider: Provider, redirectUrl?: string): string {
case "MICROSOFT_TODO":
return `/api/auth/microsoft-todo/authorize${encodedRedirect ? `?redirectUrl=${encodedRedirect}` : ""}`;
default:
- throw new Error(`Unknown provider: ${provider}`);
+ throw new Error(`Unknown provider: ${String(provider)}`);
}
}
@@ -69,4 +69,3 @@ export function ProviderConnectionButton({
}
export type { Provider, CalendarProvider, TaskProvider };
-
diff --git a/components/settings/provider-status-indicator.tsx b/components/settings/provider-status-indicator.tsx
index ac82db94..59c97e6a 100644
--- a/components/settings/provider-status-indicator.tsx
+++ b/components/settings/provider-status-indicator.tsx
@@ -1,6 +1,5 @@
"use client";
-import { cn } from "@/lib/utils";
import { AlertCircle, CheckCircle2, XCircle } from "lucide-react";
import {
Tooltip,
@@ -8,6 +7,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
type ConnectionStatus = "connected" | "disconnected" | "invalid";
@@ -87,4 +87,3 @@ export function getStatusFromCredential(
}
export type { ConnectionStatus };
-
diff --git a/components/settings/settings-nav.tsx b/components/settings/settings-nav.tsx
index d25484d0..f1bfbf81 100644
--- a/components/settings/settings-nav.tsx
+++ b/components/settings/settings-nav.tsx
@@ -1,12 +1,11 @@
"use client";
+import { motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { motion } from "framer-motion";
-
+import { Icons } from "@/components/shared/icons";
import { settingsSections } from "@/config/settings";
import { cn } from "@/lib/utils";
-import { Icons } from "@/components/shared/icons";
export function SettingsNav() {
const pathname = usePathname();
@@ -27,7 +26,9 @@ export function SettingsNav() {
href={item.href}
className={cn(
"relative flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200",
- isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground",
+ isActive
+ ? "text-foreground"
+ : "text-muted-foreground hover:text-foreground",
)}
>
{isActive && (
@@ -59,7 +60,9 @@ export function SettingsNav() {
href={item.href}
className={cn(
"relative flex min-w-max flex-1 items-center justify-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
- isActive ? "text-foreground" : "text-muted-foreground hover:text-foreground",
+ isActive
+ ? "text-foreground"
+ : "text-muted-foreground hover:text-foreground",
)}
>
{isActive && (
@@ -74,7 +77,9 @@ export function SettingsNav() {
) : null}
- {item.title}
+
+ {item.title}
+
);
})}
diff --git a/components/shared/blur-image.tsx b/components/shared/blur-image.tsx
index 62181ed2..5e2523ec 100644
--- a/components/shared/blur-image.tsx
+++ b/components/shared/blur-image.tsx
@@ -1,9 +1,8 @@
"use client";
-import { useState } from "react";
-import type { ComponentProps } from "react";
import Image from "next/image";
-
+import type { ComponentProps } from "react";
+import { useState } from "react";
import { cn } from "@/lib/utils";
export default function BlurImage(props: ComponentProps) {
diff --git a/components/shared/callout.tsx b/components/shared/callout.tsx
index 21ab452a..ce0f5786 100644
--- a/components/shared/callout.tsx
+++ b/components/shared/callout.tsx
@@ -7,7 +7,6 @@ import {
Info,
Lightbulb,
} from "lucide-react";
-
import { cn } from "@/lib/utils";
interface CalloutProps {
diff --git a/components/shared/copy-button.tsx b/components/shared/copy-button.tsx
index 8c05d7b5..b7967198 100644
--- a/components/shared/copy-button.tsx
+++ b/components/shared/copy-button.tsx
@@ -1,12 +1,10 @@
"use client";
import React from "react";
-
+import { Icons } from "./icons";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
-import { Icons } from "./icons";
-
interface CopyButtonProps extends React.HTMLAttributes {
value: string;
}
@@ -18,10 +16,10 @@ export function CopyButton({ value, className, ...props }: CopyButtonProps) {
setTimeout(() => {
setHasCopied(false);
}, 2000);
- }, [hasCopied]);
+ }, []);
const handleCopyValue = (value: string) => {
- navigator.clipboard.writeText(value);
+ void navigator.clipboard.writeText(value);
setHasCopied(true);
};
diff --git a/components/shared/empty-placeholder.tsx b/components/shared/empty-placeholder.tsx
index dd116074..ab5660ec 100644
--- a/components/shared/empty-placeholder.tsx
+++ b/components/shared/empty-placeholder.tsx
@@ -1,9 +1,8 @@
-import * as React from "react";
-
-import { cn } from "@/lib/utils";
+import type * as React from "react";
import { Icons } from "@/components/shared/icons";
+import { cn } from "@/lib/utils";
-interface EmptyPlaceholderProps extends React.HTMLAttributes {}
+type EmptyPlaceholderProps = React.HTMLAttributes;
export function EmptyPlaceholder({
className,
@@ -52,8 +51,7 @@ EmptyPlaceholder.Icon = function EmptyPlaceholderIcon({
);
};
-interface EmptyPlaceholderTitleProps
- extends React.HTMLAttributes {}
+type EmptyPlaceholderTitleProps = React.HTMLAttributes;
EmptyPlaceholder.Title = function EmptyPlaceholderTitle({
className,
@@ -67,8 +65,8 @@ EmptyPlaceholder.Title = function EmptyPlaceholderTitle({
);
};
-interface EmptyPlaceholderDescriptionProps
- extends React.HTMLAttributes {}
+type EmptyPlaceholderDescriptionProps =
+ React.HTMLAttributes;
EmptyPlaceholder.Description = function EmptyPlaceholderDescription({
className,
diff --git a/components/shared/header-section.tsx b/components/shared/header-section.tsx
index f9bb75ea..3df626ca 100644
--- a/components/shared/header-section.tsx
+++ b/components/shared/header-section.tsx
@@ -1,4 +1,4 @@
-import { ReactNode } from "react";
+import type { ReactNode } from "react";
interface HeaderSectionProps {
label?: string;
@@ -10,9 +10,7 @@ export function HeaderSection({ label, title, subtitle }: HeaderSectionProps) {
return (
{label ? (
-
- {label}
-
+
{label}
) : null}
{title}
diff --git a/components/shared/icons.tsx b/components/shared/icons.tsx
index 6cf5b2e4..131c86bb 100644
--- a/components/shared/icons.tsx
+++ b/components/shared/icons.tsx
@@ -6,47 +6,47 @@ import {
ArrowUpRight,
BookOpen,
Calendar,
- Edit,
Check,
CheckSquare,
ChevronDown,
ChevronLeft,
ChevronRight,
Clock,
+ Cloud,
Copy,
CreditCard,
+ Edit,
File,
FileText,
Globe,
HelpCircle,
Home,
Image,
+ Inbox,
Laptop,
LayoutGrid,
LayoutPanelLeft,
- Cloud,
Lightbulb,
LineChart,
ListTodo,
Loader2,
- LucideIcon,
- LucideProps,
+ type LucideIcon,
+ type LucideProps,
MapPin,
MessagesSquare,
- Inbox,
Mic,
Moon,
MoreVertical,
Package,
Plus,
- Puzzle,
RefreshCw,
- ShieldCheck,
- Square,
Search,
Settings,
+ ShieldCheck,
Sparkles,
+ Square,
SunMedium,
+ Tag,
Trash,
User,
Users,
@@ -145,11 +145,12 @@ export const Icons = {
fill="currentColor"
{...props}
>
-
-
+
+
),
sparkles: Sparkles,
+ tag: Tag,
mapPin: MapPin,
media: Image,
messages: MessagesSquare,
diff --git a/components/shared/max-width-wrapper.tsx b/components/shared/max-width-wrapper.tsx
index f35027d1..1b860636 100644
--- a/components/shared/max-width-wrapper.tsx
+++ b/components/shared/max-width-wrapper.tsx
@@ -1,5 +1,4 @@
-import { ReactNode } from "react";
-
+import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export default function MaxWidthWrapper({
diff --git a/components/shared/user-avatar.tsx b/components/shared/user-avatar.tsx
index 71eae764..dcdcf6e4 100644
--- a/components/shared/user-avatar.tsx
+++ b/components/shared/user-avatar.tsx
@@ -1,18 +1,21 @@
-import { User } from "@prisma/client"
-import { AvatarProps } from "@radix-ui/react-avatar"
-
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
-import { Icons } from "@/components/shared/icons"
+import type { User } from "@prisma/client";
+import type { AvatarProps } from "@radix-ui/react-avatar";
+import { Icons } from "@/components/shared/icons";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
interface UserAvatarProps extends AvatarProps {
- user: Pick
+ user: Pick;
}
export function UserAvatar({ user, ...props }: UserAvatarProps) {
return (
{user.image ? (
-
+
) : (
{user.name}
@@ -20,5 +23,5 @@ export function UserAvatar({ user, ...props }: UserAvatarProps) {
)}
- )
+ );
}
diff --git a/components/sync/sync-indicator-icon.tsx b/components/sync/sync-indicator-icon.tsx
index dd4294b8..c9825837 100644
--- a/components/sync/sync-indicator-icon.tsx
+++ b/components/sync/sync-indicator-icon.tsx
@@ -1,19 +1,18 @@
"use client";
import * as React from "react";
-
-import { cn } from "@/lib/utils";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
+ getProviderDisplayName,
getProviderIcon,
getStatusColorClass,
- getProviderDisplayName,
} from "@/lib/sync/sync-icon-utils";
import type { DisplayStatus, SyncReference } from "@/lib/types/sync";
+import { cn } from "@/lib/utils";
// ============
// Types
@@ -159,7 +158,7 @@ function SyncIndicatorIconInner({
"size-3.5 rounded-full",
"bg-muted text-muted-foreground",
"text-[9px] font-medium leading-none",
- "transition-colors hover:bg-muted/80"
+ "transition-colors hover:bg-muted/80",
)}
aria-label={`${additionalCount} more provider${additionalCount > 1 ? "s" : ""}`}
aria-expanded={isPopoverOpen}
@@ -185,7 +184,7 @@ function SyncIndicatorIconInner({
? "synced"
: ref.syncStatus === "FAILED"
? "failed"
- : "pending"
+ : "pending",
);
return (
diff --git a/components/sync/sync-status-indicator.tsx b/components/sync/sync-status-indicator.tsx
index 44f71f1e..f0e746ab 100644
--- a/components/sync/sync-status-indicator.tsx
+++ b/components/sync/sync-status-indicator.tsx
@@ -1,5 +1,6 @@
"use client";
+import { formatInTimeZone } from "date-fns-tz";
import { Icons } from "@/components/shared/icons";
import { Badge } from "@/components/ui/badge";
import {
@@ -10,7 +11,6 @@ import {
} from "@/components/ui/tooltip";
import { useSyncedTransition } from "@/hooks/use-synced-transition";
import type { SyncStatus } from "@/lib/types/sync";
-import { formatInTimeZone } from "date-fns-tz";
interface SyncStatusIndicatorProps {
status: SyncStatus | null;
diff --git a/components/tailwind-indicator.tsx b/components/tailwind-indicator.tsx
index 6aeee645..ada998b5 100644
--- a/components/tailwind-indicator.tsx
+++ b/components/tailwind-indicator.tsx
@@ -1,5 +1,5 @@
export function TailwindIndicator() {
- if (process.env.NODE_ENV === "production") return null
+ if (process.env.NODE_ENV === "production") return null;
return (
@@ -10,5 +10,5 @@ export function TailwindIndicator() {
xl
2xl
- )
+ );
}
diff --git a/components/task/task-side-panel-shell.tsx b/components/task/task-side-panel-shell.tsx
index e12e9e38..f8fb5c01 100644
--- a/components/task/task-side-panel-shell.tsx
+++ b/components/task/task-side-panel-shell.tsx
@@ -1,12 +1,11 @@
"use client";
-import { useRef, type ReactNode } from "react";
-import { motion, AnimatePresence } from "framer-motion";
-import { X, Trash2 } from "lucide-react";
-
-import { cn } from "@/lib/utils";
+import { AnimatePresence, motion } from "framer-motion";
+import { Trash2, X } from "lucide-react";
+import { type ReactNode, useRef } from "react";
import { Button } from "@/components/ui/button";
import { useFocusTrap } from "@/hooks/use-focus-trap";
+import { cn } from "@/lib/utils";
export interface TaskSidePanelShellProps {
isSaving?: boolean;
diff --git a/components/tasks/RetryFailedSyncsButton.tsx b/components/tasks/RetryFailedSyncsButton.tsx
index 3361b27d..38cf92e1 100644
--- a/components/tasks/RetryFailedSyncsButton.tsx
+++ b/components/tasks/RetryFailedSyncsButton.tsx
@@ -2,8 +2,8 @@
import { useState } from "react";
import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
import { Icons } from "@/components/shared/icons";
+import { Button } from "@/components/ui/button";
export function RetryFailedSyncsButton() {
const [isRetrying, setIsRetrying] = useState(false);
@@ -24,7 +24,7 @@ export function RetryFailedSyncsButton() {
toast.success("Retry initiated", {
description: "Failed task syncs are being retried",
});
- } catch (error) {
+ } catch {
toast.error("Failed to retry syncs", {
description: "Please try again",
});
@@ -38,7 +38,7 @@ export function RetryFailedSyncsButton() {
type="button"
variant="secondary"
className="w-full rounded-full sm:w-auto"
- onClick={handleRetry}
+ onClick={() => void handleRetry()}
disabled={isRetrying}
>
{isRetrying ? (
diff --git a/components/tasks/SyncErrorMessage.tsx b/components/tasks/SyncErrorMessage.tsx
index 336e6cd4..ae79cde9 100644
--- a/components/tasks/SyncErrorMessage.tsx
+++ b/components/tasks/SyncErrorMessage.tsx
@@ -1,7 +1,7 @@
"use client";
-import { ReactNode } from "react";
import { AlertCircle, RefreshCcw } from "lucide-react";
+import type { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -30,7 +30,9 @@ export function SyncErrorMessage({
>
-
{title}
+
+ {title}
+
{message ? (
{message}
) : null}
@@ -43,7 +45,11 @@ export function SyncErrorMessage({
);
}
-function getContent(kind: ErrorKind, onRetry?: () => void, onReconnect?: () => void): {
+function getContent(
+ kind: ErrorKind,
+ onRetry?: () => void,
+ onReconnect?: () => void,
+): {
title: string;
help?: ReactNode;
action?: ReactNode;
diff --git a/components/tasks/TaskPageActions.tsx b/components/tasks/TaskPageActions.tsx
index 6b12f346..9be1e8f2 100644
--- a/components/tasks/TaskPageActions.tsx
+++ b/components/tasks/TaskPageActions.tsx
@@ -1,6 +1,6 @@
+import { RetryFailedSyncsButton } from "./RetryFailedSyncsButton";
import { auth } from "@/auth";
import { getFailedSyncCount } from "@/lib/tasks";
-import { RetryFailedSyncsButton } from "./RetryFailedSyncsButton";
export async function TaskPageActions() {
const session = await auth();
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
index 83f11f44..518106f5 100644
--- a/components/ui/accordion.tsx
+++ b/components/ui/accordion.tsx
@@ -1,12 +1,11 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as AccordionPrimitive from "@radix-ui/react-accordion"
-import { ChevronDown } from "lucide-react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDown } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
-
-const Accordion = AccordionPrimitive.Root
+const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef
,
@@ -17,8 +16,8 @@ const AccordionItem = React.forwardRef<
className={cn("border-b", className)}
{...props}
/>
-))
-AccordionItem.displayName = "AccordionItem"
+));
+AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef,
@@ -29,7 +28,7 @@ const AccordionTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-left font-medium transition-all hover:underline max-sm:text-sm [&[data-state=open]>svg]:rotate-180",
- className
+ className,
)}
{...props}
>
@@ -37,8 +36,8 @@ const AccordionTrigger = React.forwardRef<
-))
-AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+));
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef,
@@ -48,13 +47,13 @@ const AccordionContent = React.forwardRef<
ref={ref}
className={cn(
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
- className
+ className,
)}
{...props}
>
{children}
-))
-AccordionContent.displayName = AccordionPrimitive.Content.displayName
+));
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
-export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
index 297b248b..328e5c25 100644
--- a/components/ui/alert-dialog.tsx
+++ b/components/ui/alert-dialog.tsx
@@ -1,31 +1,30 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+import * as React from "react";
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
-import { buttonVariants } from "@/components/ui/button"
+const AlertDialog = AlertDialogPrimitive.Root;
-const AlertDialog = AlertDialogPrimitive.Root
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger
-
-const AlertDialogPortal = AlertDialogPrimitive.Portal
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
+>(({ className, ...props }, ref) => (
-))
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef,
@@ -37,13 +36,13 @@ const AlertDialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
- className
+ className,
)}
{...props}
/>
-))
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
@@ -52,12 +51,12 @@ const AlertDialogHeader = ({
-)
-AlertDialogHeader.displayName = "AlertDialogHeader"
+);
+AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({
className,
@@ -66,12 +65,12 @@ const AlertDialogFooter = ({
-)
-AlertDialogFooter.displayName = "AlertDialogFooter"
+);
+AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef,
@@ -82,8 +81,8 @@ const AlertDialogTitle = React.forwardRef<
className={cn("text-lg font-semibold", className)}
{...props}
/>
-))
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef,
@@ -94,9 +93,9 @@ const AlertDialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
-))
+));
AlertDialogDescription.displayName =
- AlertDialogPrimitive.Description.displayName
+ AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef,
@@ -107,8 +106,8 @@ const AlertDialogAction = React.forwardRef<
className={cn(buttonVariants(), className)}
{...props}
/>
-))
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef,
@@ -119,12 +118,12 @@ const AlertDialogCancel = React.forwardRef<
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
- className
+ className,
)}
{...props}
/>
-))
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
@@ -138,4 +137,4 @@ export {
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
-}
+};
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
index 6cd63fbb..daee45f6 100644
--- a/components/ui/alert.tsx
+++ b/components/ui/alert.tsx
@@ -1,7 +1,6 @@
-import * as React from "react"
-import { VariantProps, cva } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11",
@@ -16,8 +15,8 @@ const alertVariants = cva(
defaultVariants: {
variant: "default",
},
- }
-)
+ },
+);
const Alert = React.forwardRef<
HTMLDivElement,
@@ -29,8 +28,8 @@ const Alert = React.forwardRef<
className={cn(alertVariants({ variant }), className)}
{...props}
/>
-))
-Alert.displayName = "Alert"
+));
+Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
@@ -41,8 +40,8 @@ const AlertTitle = React.forwardRef<
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
-))
-AlertTitle.displayName = "AlertTitle"
+));
+AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
@@ -53,7 +52,7 @@ const AlertDescription = React.forwardRef<
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
-))
-AlertDescription.displayName = "AlertDescription"
+));
+AlertDescription.displayName = "AlertDescription";
-export { Alert, AlertTitle, AlertDescription }
+export { Alert, AlertTitle, AlertDescription };
diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx
index d6a5226f..359bc940 100644
--- a/components/ui/aspect-ratio.tsx
+++ b/components/ui/aspect-ratio.tsx
@@ -1,7 +1,7 @@
-"use client"
+"use client";
-import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
-const AspectRatio = AspectRatioPrimitive.Root
+const AspectRatio = AspectRatioPrimitive.Root;
-export { AspectRatio }
+export { AspectRatio };
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
index a1ab22ee..03a7a8f7 100644
--- a/components/ui/avatar.tsx
+++ b/components/ui/avatar.tsx
@@ -1,9 +1,8 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
-
-import { cn } from "@/lib/utils"
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef,
@@ -13,12 +12,12 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
- className
+ className,
)}
{...props}
/>
-))
-Avatar.displayName = AvatarPrimitive.Root.displayName
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef,
@@ -29,8 +28,8 @@ const AvatarImage = React.forwardRef<
className={cn("aspect-square size-full", className)}
{...props}
/>
-))
-AvatarImage.displayName = AvatarPrimitive.Image.displayName
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef,
@@ -40,11 +39,11 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted",
- className
+ className,
)}
{...props}
/>
-))
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
-export { Avatar, AvatarImage, AvatarFallback }
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
index bb37dc82..34595732 100644
--- a/components/ui/badge.tsx
+++ b/components/ui/badge.tsx
@@ -1,7 +1,6 @@
-import * as React from "react"
-import { VariantProps, cva } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
+import { cva, type VariantProps } from "class-variance-authority";
+import type * as React from "react";
+import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -20,8 +19,8 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
- }
-)
+ },
+);
export interface BadgeProps
extends React.HTMLAttributes,
@@ -30,7 +29,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
- )
+ );
}
-export { Badge, badgeVariants }
+export { Badge, badgeVariants };
diff --git a/components/ui/button-group.tsx b/components/ui/button-group.tsx
index 6c8a6388..d69f31e1 100644
--- a/components/ui/button-group.tsx
+++ b/components/ui/button-group.tsx
@@ -1,8 +1,7 @@
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-import { Separator } from "@/components/ui/separator"
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 [&>input]:flex-1 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md",
@@ -18,8 +17,8 @@ const buttonGroupVariants = cva(
defaultVariants: {
orientation: "horizontal",
},
- }
-)
+ },
+);
function ButtonGroup({
className,
@@ -34,7 +33,7 @@ function ButtonGroup({
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
- )
+ );
}
function ButtonGroupText({
@@ -42,19 +41,19 @@ function ButtonGroupText({
asChild = false,
...props
}: React.ComponentProps<"div"> & {
- asChild?: boolean
+ asChild?: boolean;
}) {
- const Comp = asChild ? Slot : "div"
+ const Comp = asChild ? Slot : "div";
return (
- )
+ );
}
function ButtonGroupSeparator({
@@ -68,11 +67,11 @@ function ButtonGroupSeparator({
orientation={orientation}
className={cn(
"relative !m-0 self-stretch bg-input data-[orientation=vertical]:h-auto",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
export {
@@ -80,4 +79,4 @@ export {
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
-}
+};
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index 36496a28..710eb092 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -1,8 +1,7 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
@@ -30,27 +29,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
- }
-)
+ },
+);
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
- asChild?: boolean
+ asChild?: boolean;
}
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button"
+ const Comp = asChild ? Slot : "button";
return (
- )
- }
-)
-Button.displayName = "Button"
+ );
+ },
+);
+Button.displayName = "Button";
-export { Button, buttonVariants }
+export { Button, buttonVariants };
diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx
index 62a43705..6094a285 100644
--- a/components/ui/calendar.tsx
+++ b/components/ui/calendar.tsx
@@ -1,15 +1,18 @@
-"use client"
+"use client";
-import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
-} from "lucide-react"
-import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
-
-import { cn } from "@/lib/utils"
-import { Button, buttonVariants } from "@/components/ui/button"
+} from "lucide-react";
+import * as React from "react";
+import {
+ type DayButton,
+ DayPicker,
+ getDefaultClassNames,
+} from "react-day-picker";
+import { Button, buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
function Calendar({
className,
@@ -21,9 +24,9 @@ function Calendar({
components,
...props
}: React.ComponentProps & {
- buttonVariant?: React.ComponentProps["variant"]
+ buttonVariant?: React.ComponentProps["variant"];
}) {
- const defaultClassNames = getDefaultClassNames()
+ const defaultClassNames = getDefaultClassNames();
return (
svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
- className
+ className,
)}
captionLayout={captionLayout}
formatters={{
@@ -44,82 +47,82 @@ function Calendar({
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
- defaultClassNames.months
+ defaultClassNames.months,
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
- defaultClassNames.nav
+ defaultClassNames.nav,
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-[--cell-size] select-none p-0 aria-disabled:opacity-50",
- defaultClassNames.button_previous
+ defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-[--cell-size] select-none p-0 aria-disabled:opacity-50",
- defaultClassNames.button_next
+ defaultClassNames.button_next,
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
- defaultClassNames.month_caption
+ defaultClassNames.month_caption,
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
- defaultClassNames.dropdowns
+ defaultClassNames.dropdowns,
),
dropdown_root: cn(
"has-focus:border-ring shadow-xs has-focus:ring-ring/50 has-focus:ring relative rounded-md border border-input",
- defaultClassNames.dropdown_root
+ defaultClassNames.dropdown_root,
),
dropdown: cn(
"absolute inset-0 bg-popover opacity-0",
- defaultClassNames.dropdown
+ defaultClassNames.dropdown,
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
- defaultClassNames.caption_label
+ defaultClassNames.caption_label,
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"flex-1 select-none rounded-md text-[0.8rem] font-normal text-muted-foreground",
- defaultClassNames.weekday
+ defaultClassNames.weekday,
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
- defaultClassNames.week_number_header
+ defaultClassNames.week_number_header,
),
week_number: cn(
"select-none text-[0.8rem] text-muted-foreground",
- defaultClassNames.week_number
+ defaultClassNames.week_number,
),
day: cn(
"group/day relative aspect-square size-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
- defaultClassNames.day
+ defaultClassNames.day,
),
range_start: cn(
"rounded-l-md bg-accent",
- defaultClassNames.range_start
+ defaultClassNames.range_start,
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
- defaultClassNames.today
+ defaultClassNames.today,
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
- defaultClassNames.outside
+ defaultClassNames.outside,
),
disabled: cn(
"text-muted-foreground opacity-50",
- defaultClassNames.disabled
+ defaultClassNames.disabled,
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
@@ -133,13 +136,13 @@ function Calendar({
className={cn(className)}
{...props}
/>
- )
+ );
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
- )
+ );
}
if (orientation === "right") {
@@ -148,12 +151,12 @@ function Calendar({
className={cn("size-4", className)}
{...props}
/>
- )
+ );
}
return (
- )
+ );
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
@@ -163,13 +166,13 @@ function Calendar({
{children}
- )
+ );
},
...components,
}}
{...props}
/>
- )
+ );
}
function CalendarDayButton({
@@ -178,12 +181,12 @@ function CalendarDayButton({
modifiers,
...props
}: React.ComponentProps) {
- const defaultClassNames = getDefaultClassNames()
+ const defaultClassNames = getDefaultClassNames();
- const ref = React.useRef(null)
+ const ref = React.useRef(null);
React.useEffect(() => {
- if (modifiers.focused) ref.current?.focus()
- }, [modifiers.focused])
+ if (modifiers.focused) ref.current?.focus();
+ }, [modifiers.focused]);
return (
span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
- className
+ className,
)}
{...props}
/>
- )
+ );
}
-export { Calendar, CalendarDayButton }
+export { Calendar, CalendarDayButton };
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
index 4d73316f..f03f3a04 100644
--- a/components/ui/card.tsx
+++ b/components/ui/card.tsx
@@ -1,6 +1,5 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
+import * as React from "react";
+import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
@@ -10,12 +9,12 @@ const Card = React.forwardRef<
ref={ref}
className={cn(
"overflow-hidden rounded-lg border bg-card text-card-foreground shadow-sm",
- className
+ className,
)}
{...props}
/>
-))
-Card.displayName = "Card"
+));
+Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -26,8 +25,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
-))
-CardHeader.displayName = "CardHeader"
+));
+CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
@@ -37,12 +36,12 @@ const CardTitle = React.forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
- className
+ className,
)}
{...props}
/>
-))
-CardTitle.displayName = "CardTitle"
+));
+CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
@@ -53,16 +52,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
-))
-CardDescription.displayName = "CardDescription"
+));
+CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
-))
-CardContent.displayName = "CardContent"
+));
+CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -73,7 +72,14 @@ const CardFooter = React.forwardRef<
className={cn(" flex items-center p-6 pt-0", className)}
{...props}
/>
-))
-CardFooter.displayName = "CardFooter"
+));
+CardFooter.displayName = "CardFooter";
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx
index d338a991..ff24b70a 100644
--- a/components/ui/checkbox.tsx
+++ b/components/ui/checkbox.tsx
@@ -1,10 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
-import { Check } from "lucide-react"
-
-import { cn } from "@/lib/utils"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { Check } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef,
@@ -14,7 +13,7 @@ const Checkbox = React.forwardRef<
ref={ref}
className={cn(
"peer size-4 shrink-0 rounded-sm border border-input ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary",
- className
+ className,
)}
{...props}
>
@@ -24,7 +23,7 @@ const Checkbox = React.forwardRef<
-))
-Checkbox.displayName = CheckboxPrimitive.Root.displayName
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
-export { Checkbox }
+export { Checkbox };
diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx
index 9fa48946..cb003d17 100644
--- a/components/ui/collapsible.tsx
+++ b/components/ui/collapsible.tsx
@@ -1,11 +1,11 @@
-"use client"
+"use client";
-import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
-const Collapsible = CollapsiblePrimitive.Root
+const Collapsible = CollapsiblePrimitive.Root;
-const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
-const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
-export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/components/ui/command.tsx b/components/ui/command.tsx
index d73162a7..3427dcd6 100644
--- a/components/ui/command.tsx
+++ b/components/ui/command.tsx
@@ -1,13 +1,12 @@
"use client";
-import * as React from "react";
-import { type DialogProps } from "@radix-ui/react-dialog";
+import type { DialogProps } from "@radix-ui/react-dialog";
+import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
-
-import { cn } from "@/lib/utils";
+import * as React from "react";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
-import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
+import { cn } from "@/lib/utils";
const Command = React.forwardRef<
React.ElementRef,
diff --git a/components/ui/context-menu.tsx b/components/ui/context-menu.tsx
index 371ce39d..23f654cb 100644
--- a/components/ui/context-menu.tsx
+++ b/components/ui/context-menu.tsx
@@ -1,27 +1,26 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
-import { Check, ChevronRight, Circle } from "lucide-react"
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
+const ContextMenu = ContextMenuPrimitive.Root;
-const ContextMenu = ContextMenuPrimitive.Root
+const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
-const ContextMenuTrigger = ContextMenuPrimitive.Trigger
+const ContextMenuGroup = ContextMenuPrimitive.Group;
-const ContextMenuGroup = ContextMenuPrimitive.Group
+const ContextMenuPortal = ContextMenuPrimitive.Portal;
-const ContextMenuPortal = ContextMenuPrimitive.Portal
+const ContextMenuSub = ContextMenuPrimitive.Sub;
-const ContextMenuSub = ContextMenuPrimitive.Sub
-
-const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
+const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
{children}
-))
-ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
+));
+ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef,
@@ -47,12 +46,12 @@ const ContextMenuSubContent = React.forwardRef<
ref={ref}
className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in slide-in-from-left-1",
- className
+ className,
)}
{...props}
/>
-))
-ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
+));
+ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef,
@@ -63,18 +62,18 @@ const ContextMenuContent = React.forwardRef<
ref={ref}
className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80",
- className
+ className,
)}
{...props}
/>
-))
-ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
+));
+ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
+));
+ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef,
@@ -97,7 +96,7 @@ const ContextMenuCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
+ className,
)}
checked={checked}
{...props}
@@ -109,9 +108,9 @@ const ContextMenuCheckboxItem = React.forwardRef<
{children}
-))
+));
ContextMenuCheckboxItem.displayName =
- ContextMenuPrimitive.CheckboxItem.displayName
+ ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef,
@@ -121,7 +120,7 @@ const ContextMenuRadioItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
+ className,
)}
{...props}
>
@@ -132,13 +131,13 @@ const ContextMenuRadioItem = React.forwardRef<
{children}
-))
-ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
+));
+ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
+));
+ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef,
@@ -162,8 +161,8 @@ const ContextMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
-))
-ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
+));
+ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({
className,
@@ -173,13 +172,13 @@ const ContextMenuShortcut = ({
- )
-}
-ContextMenuShortcut.displayName = "ContextMenuShortcut"
+ );
+};
+ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
@@ -197,4 +196,4 @@ export {
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
-}
+};
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
index 63abc9a6..00c00c3c 100644
--- a/components/ui/dialog.tsx
+++ b/components/ui/dialog.tsx
@@ -1,18 +1,17 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { X } from "lucide-react"
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { X } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
+const Dialog = DialogPrimitive.Root;
-const Dialog = DialogPrimitive.Root
+const DialogTrigger = DialogPrimitive.Trigger;
-const DialogTrigger = DialogPrimitive.Trigger
+const DialogPortal = DialogPrimitive.Portal;
-const DialogPortal = DialogPrimitive.Portal
-
-const DialogClose = DialogPrimitive.Close
+const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef,
@@ -22,12 +21,12 @@ const DialogOverlay = React.forwardRef<
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
- className
+ className,
)}
{...props}
/>
-))
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef,
@@ -41,7 +40,7 @@ const DialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
- className
+ className,
)}
{...props}
>
@@ -54,8 +53,8 @@ const DialogContent = React.forwardRef<
)}
-))
-DialogContent.displayName = DialogPrimitive.Content.displayName
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@@ -64,12 +63,12 @@ const DialogHeader = ({
-)
-DialogHeader.displayName = "DialogHeader"
+);
+DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
@@ -78,12 +77,12 @@ const DialogFooter = ({
-)
-DialogFooter.displayName = "DialogFooter"
+);
+DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef,
@@ -93,12 +92,12 @@ const DialogTitle = React.forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
- className
+ className,
)}
{...props}
/>
-))
-DialogTitle.displayName = DialogPrimitive.Title.displayName
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef,
@@ -109,8 +108,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
-))
-DialogDescription.displayName = DialogPrimitive.Description.displayName
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@@ -123,4 +122,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
-}
+};
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
index ff45de2b..58730d4a 100644
--- a/components/ui/dropdown-menu.tsx
+++ b/components/ui/dropdown-menu.tsx
@@ -1,27 +1,26 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
-import { Check, ChevronRight, Circle } from "lucide-react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { Check, ChevronRight, Circle } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
+const DropdownMenu = DropdownMenuPrimitive.Root;
-const DropdownMenu = DropdownMenuPrimitive.Root
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
-const DropdownMenuGroup = DropdownMenuPrimitive.Group
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
-const DropdownMenuSub = DropdownMenuPrimitive.Sub
-
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
{children}
-))
+));
DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName
+ DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
@@ -48,13 +47,13 @@ const DropdownMenuSubContent = React.forwardRef<
ref={ref}
className={cn(
"text-on-popover z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
- className
+ className,
)}
{...props}
/>
-))
+));
DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName
+ DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
@@ -66,18 +65,18 @@ const DropdownMenuContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in 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
+ className,
)}
{...props}
/>
-))
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
@@ -100,7 +99,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
+ className,
)}
checked={checked}
{...props}
@@ -112,9 +111,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
{children}
-))
+));
DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName
+ DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
@@ -124,7 +123,7 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
+ className,
)}
{...props}
>
@@ -135,13 +134,13 @@ const DropdownMenuRadioItem = React.forwardRef<
{children}
-))
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
@@ -165,8 +164,8 @@ const DropdownMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
-))
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
@@ -177,9 +176,9 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
- )
-}
-DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+ );
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
@@ -197,4 +196,4 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
-}
+};
diff --git a/components/ui/form.tsx b/components/ui/form.tsx
index ce264aef..bad6df06 100644
--- a/components/ui/form.tsx
+++ b/components/ui/form.tsx
@@ -1,36 +1,35 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { Slot } from "@radix-ui/react-slot"
+import type * as LabelPrimitive from "@radix-ui/react-label";
+import { Slot } from "@radix-ui/react-slot";
+import * as React from "react";
import {
Controller,
- ControllerProps,
- FieldPath,
- FieldValues,
+ type ControllerProps,
+ type FieldPath,
+ type FieldValues,
FormProvider,
useFormContext,
-} from "react-hook-form"
+} from "react-hook-form";
+import { Label } from "@/components/ui/label";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
-
-const Form = FormProvider
+const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath
+ TName extends FieldPath = FieldPath,
> = {
- name: TName
-}
+ name: TName;
+};
const FormFieldContext = React.createContext(
- {} as FormFieldContextValue
-)
+ {} as FormFieldContextValue,
+);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath
+ TName extends FieldPath = FieldPath,
>({
...props
}: ControllerProps) => {
@@ -38,21 +37,21 @@ const FormField = <
- )
-}
+ );
+};
const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext)
- const itemContext = React.useContext(FormItemContext)
- const { getFieldState, formState } = useFormContext()
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
- const fieldState = getFieldState(fieldContext.name, formState)
+ const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
- throw new Error("useFormField should be used within ")
+ throw new Error("useFormField should be used within ");
}
- const { id } = itemContext
+ const { id } = itemContext;
return {
id,
@@ -61,36 +60,36 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
- }
-}
+ };
+};
type FormItemContextValue = {
- id: string
-}
+ id: string;
+};
const FormItemContext = React.createContext(
- {} as FormItemContextValue
-)
+ {} as FormItemContextValue,
+);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const id = React.useId()
+ const id = React.useId();
return (
- )
-})
-FormItem.displayName = "FormItem"
+ );
+});
+FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
- const { error, formItemId } = useFormField()
+ const { error, formItemId } = useFormField();
return (
- )
-})
-FormLabel.displayName = "FormLabel"
+ );
+});
+FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ ...props }, ref) => {
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
return (
- )
-})
-FormControl.displayName = "FormControl"
+ );
+});
+FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => {
- const { formDescriptionId } = useFormField()
+ const { formDescriptionId } = useFormField();
return (
- )
-})
-FormDescription.displayName = "FormDescription"
+ );
+});
+FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes
>(({ className, children, ...props }, ref) => {
- const { error, formMessageId } = useFormField()
- const body = error ? String(error?.message) : children
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
if (!body) {
- return null
+ return null;
}
return (
@@ -162,9 +162,9 @@ const FormMessage = React.forwardRef<
>
{body}
- )
-})
-FormMessage.displayName = "FormMessage"
+ );
+});
+FormMessage.displayName = "FormMessage";
export {
useFormField,
@@ -175,4 +175,4 @@ export {
FormDescription,
FormMessage,
FormField,
-}
+};
diff --git a/components/ui/hover-card.tsx b/components/ui/hover-card.tsx
index 8abff5ac..0b8b4963 100644
--- a/components/ui/hover-card.tsx
+++ b/components/ui/hover-card.tsx
@@ -1,13 +1,12 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
+const HoverCard = HoverCardPrimitive.Root;
-const HoverCard = HoverCardPrimitive.Root
-
-const HoverCardTrigger = HoverCardPrimitive.Trigger
+const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef,
@@ -19,11 +18,11 @@ const HoverCardContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in zoom-in-90",
- className
+ className,
)}
{...props}
/>
-))
-HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
+));
+HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
-export { HoverCard, HoverCardTrigger, HoverCardContent }
+export { HoverCard, HoverCardTrigger, HoverCardContent };
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
index ccd69a97..aaadd14a 100644
--- a/components/ui/input.tsx
+++ b/components/ui/input.tsx
@@ -1,9 +1,7 @@
import * as React from "react";
-
import { cn } from "@/lib/utils";
-export interface InputProps
- extends React.InputHTMLAttributes {}
+export type InputProps = React.InputHTMLAttributes;
const Input = React.forwardRef(
({ className, type, ...props }, ref) => {
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
index b32881e6..f0ab25b0 100644
--- a/components/ui/label.tsx
+++ b/components/ui/label.tsx
@@ -1,14 +1,13 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { VariantProps, cva } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-)
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
const Label = React.forwardRef<
React.ElementRef,
@@ -20,7 +19,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...props}
/>
-))
-Label.displayName = LabelPrimitive.Root.displayName
+));
+Label.displayName = LabelPrimitive.Root.displayName;
-export { Label }
+export { Label };
diff --git a/components/ui/menubar.tsx b/components/ui/menubar.tsx
index e103d1e8..2790b059 100644
--- a/components/ui/menubar.tsx
+++ b/components/ui/menubar.tsx
@@ -1,20 +1,19 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as MenubarPrimitive from "@radix-ui/react-menubar"
-import { Check, ChevronRight, Circle } from "lucide-react"
+import * as MenubarPrimitive from "@radix-ui/react-menubar";
+import { Check, ChevronRight, Circle } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
+const MenubarMenu = MenubarPrimitive.Menu;
-const MenubarMenu = MenubarPrimitive.Menu
+const MenubarGroup = MenubarPrimitive.Group;
-const MenubarGroup = MenubarPrimitive.Group
+const MenubarPortal = MenubarPrimitive.Portal;
-const MenubarPortal = MenubarPrimitive.Portal
+const MenubarSub = MenubarPrimitive.Sub;
-const MenubarSub = MenubarPrimitive.Sub
-
-const MenubarRadioGroup = MenubarPrimitive.RadioGroup
+const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef,
@@ -24,12 +23,12 @@ const Menubar = React.forwardRef<
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
- className
+ className,
)}
{...props}
/>
-))
-Menubar.displayName = MenubarPrimitive.Root.displayName
+));
+Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef,
@@ -39,17 +38,17 @@ const MenubarTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
- className
+ className,
)}
{...props}
/>
-))
-MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
+));
+MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
{children}
-))
-MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
+));
+MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef,
@@ -75,12 +74,12 @@ const MenubarSubContent = React.forwardRef<
ref={ref}
className={cn(
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
- className
+ className,
)}
{...props}
/>
-))
-MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
+));
+MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef,
@@ -88,7 +87,7 @@ const MenubarContent = React.forwardRef<
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
- ref
+ ref,
) => (
- )
-)
-MenubarContent.displayName = MenubarPrimitive.Content.displayName
+ ),
+);
+MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-MenubarItem.displayName = MenubarPrimitive.Item.displayName
+));
+MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef,
@@ -133,7 +132,7 @@ const MenubarCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
+ className,
)}
checked={checked}
{...props}
@@ -145,8 +144,8 @@ const MenubarCheckboxItem = React.forwardRef<
{children}
-))
-MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
+));
+MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef,
@@ -156,7 +155,7 @@ const MenubarRadioItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className
+ className,
)}
{...props}
>
@@ -167,13 +166,13 @@ const MenubarRadioItem = React.forwardRef<
{children}
-))
-MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
+));
+MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
- inset?: boolean
+ inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
-))
-MenubarLabel.displayName = MenubarPrimitive.Label.displayName
+));
+MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef,
@@ -197,8 +196,8 @@ const MenubarSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
-))
-MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
+));
+MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({
className,
@@ -208,13 +207,13 @@ const MenubarShortcut = ({
- )
-}
-MenubarShortcut.displayname = "MenubarShortcut"
+ );
+};
+MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
@@ -233,4 +232,4 @@ export {
MenubarGroup,
MenubarSub,
MenubarShortcut,
-}
+};
diff --git a/components/ui/modal.tsx b/components/ui/modal.tsx
index 34973325..f030865d 100644
--- a/components/ui/modal.tsx
+++ b/components/ui/modal.tsx
@@ -1,9 +1,8 @@
"use client";
-import { Dispatch, SetStateAction } from "react";
+import type { Dispatch, SetStateAction } from "react";
// import { useRouter } from "next/router";
import { Drawer } from "vaul";
-
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { useMediaQuery } from "@/hooks/use-media-query";
import { cn } from "@/lib/utils";
@@ -34,7 +33,7 @@ export function Modal({
return;
}
// fire onClose event if provided
- onClose && onClose();
+ onClose?.();
// if setShowModal is defined, use it to close modal
if (setShowModal) {
diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx
index 0bb7b6d5..16a96d9a 100644
--- a/components/ui/navigation-menu.tsx
+++ b/components/ui/navigation-menu.tsx
@@ -1,9 +1,8 @@
-import * as React from "react"
-import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
-import { cva } from "class-variance-authority"
-import { ChevronDown } from "lucide-react"
-
-import { cn } from "@/lib/utils"
+import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
+import { cva } from "class-variance-authority";
+import { ChevronDown } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef,
@@ -13,15 +12,15 @@ const NavigationMenu = React.forwardRef<
ref={ref}
className={cn(
"relative z-10 flex flex-1 items-center justify-center",
- className
+ className,
)}
{...props}
>
{children}
-))
-NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
+));
+NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef,
@@ -31,18 +30,18 @@ const NavigationMenuList = React.forwardRef<
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
- className
+ className,
)}
{...props}
/>
-))
-NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
+));
+NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
-const NavigationMenuItem = NavigationMenuPrimitive.Item
+const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
- "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:bg-accent focus:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none bg-background hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent/50 data-[active]:bg-accent/50 h-10 py-2 px-4 group w-max"
-)
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:bg-accent focus:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none bg-background hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent/50 data-[active]:bg-accent/50 h-10 py-2 px-4 group w-max",
+);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef,
@@ -59,8 +58,8 @@ const NavigationMenuTrigger = React.forwardRef<
aria-hidden="true"
/>
-))
-NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
+));
+NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef,
@@ -70,14 +69,14 @@ const NavigationMenuContent = React.forwardRef<
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
- className
+ className,
)}
{...props}
/>
-))
-NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
+));
+NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
-const NavigationMenuLink = NavigationMenuPrimitive.Link
+const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef,
@@ -87,15 +86,15 @@ const NavigationMenuViewport = React.forwardRef<
-))
+));
NavigationMenuViewport.displayName =
- NavigationMenuPrimitive.Viewport.displayName
+ NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef,
@@ -105,15 +104,15 @@ const NavigationMenuIndicator = React.forwardRef<
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
- className
+ className,
)}
{...props}
>
-))
+));
NavigationMenuIndicator.displayName =
- NavigationMenuPrimitive.Indicator.displayName
+ NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
@@ -125,4 +124,4 @@ export {
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
-}
+};
diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx
index ceee78f2..28c1df7d 100644
--- a/components/ui/popover.tsx
+++ b/components/ui/popover.tsx
@@ -1,13 +1,12 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as PopoverPrimitive from "@radix-ui/react-popover"
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
+const Popover = PopoverPrimitive.Root;
-const Popover = PopoverPrimitive.Root
-
-const PopoverTrigger = PopoverPrimitive.Trigger
+const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef,
@@ -20,12 +19,12 @@ const PopoverContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in 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
+ className,
)}
{...props}
/>
-))
-PopoverContent.displayName = PopoverPrimitive.Content.displayName
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
-export { Popover, PopoverTrigger, PopoverContent }
+export { Popover, PopoverTrigger, PopoverContent };
diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx
index 1e8831f2..a01729af 100644
--- a/components/ui/progress.tsx
+++ b/components/ui/progress.tsx
@@ -1,9 +1,8 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as ProgressPrimitive from "@radix-ui/react-progress"
-
-import { cn } from "@/lib/utils"
+import * as ProgressPrimitive from "@radix-ui/react-progress";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef,
@@ -13,7 +12,7 @@ const Progress = React.forwardRef<
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
- className
+ className,
)}
{...props}
>
@@ -22,7 +21,7 @@ const Progress = React.forwardRef<
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
-))
-Progress.displayName = ProgressPrimitive.Root.displayName
+));
+Progress.displayName = ProgressPrimitive.Root.displayName;
-export { Progress }
+export { Progress };
diff --git a/components/ui/quota-progress.tsx b/components/ui/quota-progress.tsx
index 9684285a..c637509c 100644
--- a/components/ui/quota-progress.tsx
+++ b/components/ui/quota-progress.tsx
@@ -1,9 +1,8 @@
"use client";
-import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cva, type VariantProps } from "class-variance-authority";
-
+import * as React from "react";
import { cn } from "@/lib/utils";
const progressIndicatorVariants = cva(
diff --git a/components/ui/radio-group.tsx b/components/ui/radio-group.tsx
index 250bcf0d..4210ed30 100644
--- a/components/ui/radio-group.tsx
+++ b/components/ui/radio-group.tsx
@@ -1,10 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
-import { Circle } from "lucide-react"
-
-import { cn } from "@/lib/utils"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
+import { Circle } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef,
@@ -16,20 +15,20 @@ const RadioGroup = React.forwardRef<
{...props}
ref={ref}
/>
- )
-})
-RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
+ );
+});
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => {
+>(({ className, ...props }, ref) => {
return (
@@ -37,8 +36,8 @@ const RadioGroupItem = React.forwardRef<
- )
-})
-RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
+ );
+});
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
-export { RadioGroup, RadioGroupItem }
+export { RadioGroup, RadioGroupItem };
diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx
index bf0a1a49..1946c8e6 100644
--- a/components/ui/scroll-area.tsx
+++ b/components/ui/scroll-area.tsx
@@ -1,9 +1,8 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
-
-import { cn } from "@/lib/utils"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef,
@@ -20,8 +19,8 @@ const ScrollArea = React.forwardRef<
-))
-ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef,
@@ -36,13 +35,13 @@ const ScrollBar = React.forwardRef<
"h-full w-2.5 border-l border-l-transparent p-px",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-px",
- className
+ className,
)}
{...props}
>
-))
-ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
-export { ScrollArea, ScrollBar }
\ No newline at end of file
+export { ScrollArea, ScrollBar };
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
index 73bf2cc0..4fc293b6 100644
--- a/components/ui/select.tsx
+++ b/components/ui/select.tsx
@@ -1,9 +1,8 @@
"use client";
-import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
-
+import * as React from "react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
index 1b22f483..a6089ddf 100644
--- a/components/ui/separator.tsx
+++ b/components/ui/separator.tsx
@@ -1,9 +1,8 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as SeparatorPrimitive from "@radix-ui/react-separator"
-
-import { cn } from "@/lib/utils"
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef,
@@ -11,7 +10,7 @@ const Separator = React.forwardRef<
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
- ref
+ ref,
) => (
- )
-)
-Separator.displayName = SeparatorPrimitive.Root.displayName
+ ),
+);
+Separator.displayName = SeparatorPrimitive.Root.displayName;
-export { Separator }
+export { Separator };
diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx
index 1faabbfe..cc061076 100644
--- a/components/ui/sheet.tsx
+++ b/components/ui/sheet.tsx
@@ -1,19 +1,18 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as SheetPrimitive from "@radix-ui/react-dialog"
-import { cva, type VariantProps } from "class-variance-authority"
-import { X } from "lucide-react"
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { cva, type VariantProps } from "class-variance-authority";
+import { X } from "lucide-react";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
+const Sheet = SheetPrimitive.Root;
-const Sheet = SheetPrimitive.Root
+const SheetTrigger = SheetPrimitive.Trigger;
-const SheetTrigger = SheetPrimitive.Trigger
+const SheetClose = SheetPrimitive.Close;
-const SheetClose = SheetPrimitive.Close
-
-const SheetPortal = SheetPrimitive.Portal
+const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef,
@@ -22,13 +21,13 @@ const SheetOverlay = React.forwardRef<
-))
-SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+));
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
@@ -46,12 +45,12 @@ const sheetVariants = cva(
defaultVariants: {
side: "right",
},
- }
-)
+ },
+);
interface SheetContentProps
extends React.ComponentPropsWithoutRef,
- VariantProps { }
+ VariantProps {}
const SheetContent = React.forwardRef<
React.ElementRef,
@@ -71,8 +70,8 @@ const SheetContent = React.forwardRef<
-))
-SheetContent.displayName = SheetPrimitive.Content.displayName
+));
+SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
@@ -81,12 +80,12 @@ const SheetHeader = ({
-)
-SheetHeader.displayName = "SheetHeader"
+);
+SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
@@ -95,12 +94,12 @@ const SheetFooter = ({
-)
-SheetFooter.displayName = "SheetFooter"
+);
+SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef,
@@ -111,8 +110,8 @@ const SheetTitle = React.forwardRef<
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
-))
-SheetTitle.displayName = SheetPrimitive.Title.displayName
+));
+SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef,
@@ -123,8 +122,8 @@ const SheetDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
-))
-SheetDescription.displayName = SheetPrimitive.Description.displayName
+));
+SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
@@ -137,4 +136,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
-}
+};
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
index 01b8b6d4..2cdf440d 100644
--- a/components/ui/skeleton.tsx
+++ b/components/ui/skeleton.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Skeleton({
className,
@@ -9,7 +9,7 @@ function Skeleton({
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
- )
+ );
}
-export { Skeleton }
+export { Skeleton };
diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx
index 48fdbbfb..1b567cb2 100644
--- a/components/ui/slider.tsx
+++ b/components/ui/slider.tsx
@@ -1,9 +1,8 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as SliderPrimitive from "@radix-ui/react-slider"
-
-import { cn } from "@/lib/utils"
+import * as SliderPrimitive from "@radix-ui/react-slider";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const Slider = React.forwardRef<
React.ElementRef,
@@ -13,7 +12,7 @@ const Slider = React.forwardRef<
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
- className
+ className,
)}
{...props}
>
@@ -22,7 +21,7 @@ const Slider = React.forwardRef<
-))
-Slider.displayName = SliderPrimitive.Root.displayName
+));
+Slider.displayName = SliderPrimitive.Root.displayName;
-export { Slider }
+export { Slider };
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
index 61dead24..fe6cebdc 100644
--- a/components/ui/sonner.tsx
+++ b/components/ui/sonner.tsx
@@ -1,12 +1,12 @@
-"use client"
+"use client";
-import { useTheme } from "next-themes"
-import { Toaster as Sonner } from "sonner"
+import { useTheme } from "next-themes";
+import { Toaster as Sonner } from "sonner";
-type ToasterProps = React.ComponentProps
+type ToasterProps = React.ComponentProps;
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme()
+ const { theme = "system" } = useTheme();
return (
{
}}
{...props}
/>
- )
-}
+ );
+};
-export { Toaster }
+export { Toaster };
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
index b0f07a83..2908b431 100644
--- a/components/ui/switch.tsx
+++ b/components/ui/switch.tsx
@@ -1,9 +1,8 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as SwitchPrimitives from "@radix-ui/react-switch"
-
-import { cn } from "@/lib/utils"
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef,
@@ -12,18 +11,18 @@ const Switch = React.forwardRef<
-))
-Switch.displayName = SwitchPrimitives.Root.displayName
+));
+Switch.displayName = SwitchPrimitives.Root.displayName;
-export { Switch }
+export { Switch };
diff --git a/components/ui/table.tsx b/components/ui/table.tsx
index 7f3502f8..95b43b8c 100644
--- a/components/ui/table.tsx
+++ b/components/ui/table.tsx
@@ -1,6 +1,5 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
+import * as React from "react";
+import { cn } from "@/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
@@ -13,16 +12,16 @@ const Table = React.forwardRef<
{...props}
/>
-))
-Table.displayName = "Table"
+));
+Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
-))
-TableHeader.displayName = "TableHeader"
+));
+TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
@@ -33,8 +32,8 @@ const TableBody = React.forwardRef<
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
-))
-TableBody.displayName = "TableBody"
+));
+TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
@@ -44,12 +43,12 @@ const TableFooter = React.forwardRef<
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
- className
+ className,
)}
{...props}
/>
-))
-TableFooter.displayName = "TableFooter"
+));
+TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
@@ -59,12 +58,12 @@ const TableRow = React.forwardRef<
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
- className
+ className,
)}
{...props}
/>
-))
-TableRow.displayName = "TableRow"
+));
+TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
@@ -74,12 +73,12 @@ const TableHead = React.forwardRef<
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
- className
+ className,
)}
{...props}
/>
-))
-TableHead.displayName = "TableHead"
+));
+TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
@@ -90,8 +89,8 @@ const TableCell = React.forwardRef<
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
-))
-TableCell.displayName = "TableCell"
+));
+TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
@@ -102,8 +101,8 @@ const TableCaption = React.forwardRef<
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
-))
-TableCaption.displayName = "TableCaption"
+));
+TableCaption.displayName = "TableCaption";
export {
Table,
@@ -114,4 +113,4 @@ export {
TableRow,
TableCell,
TableCaption,
-}
+};
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
index 26eb1091..29d582a5 100644
--- a/components/ui/tabs.tsx
+++ b/components/ui/tabs.tsx
@@ -1,11 +1,10 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as TabsPrimitive from "@radix-ui/react-tabs"
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
-
-const Tabs = TabsPrimitive.Root
+const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef,
@@ -15,12 +14,12 @@ const TabsList = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
- className
+ className,
)}
{...props}
/>
-))
-TabsList.displayName = TabsPrimitive.List.displayName
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef,
@@ -30,12 +29,12 @@ const TabsTrigger = React.forwardRef<
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
- className
+ className,
)}
{...props}
/>
-))
-TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef,
@@ -45,11 +44,11 @@ const TabsContent = React.forwardRef<
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
- className
+ className,
)}
{...props}
/>
-))
-TabsContent.displayName = TabsPrimitive.Content.displayName
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
-export { Tabs, TabsList, TabsTrigger, TabsContent }
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
index e7c13a69..7e69d03c 100644
--- a/components/ui/textarea.tsx
+++ b/components/ui/textarea.tsx
@@ -1,9 +1,7 @@
-import * as React from "react"
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
-
-export interface TextareaProps
- extends React.TextareaHTMLAttributes {}
+export type TextareaProps = React.TextareaHTMLAttributes;
const Textarea = React.forwardRef(
({ className, ...props }, ref) => {
@@ -11,14 +9,14 @@ const Textarea = React.forwardRef(
- )
- }
-)
-Textarea.displayName = "Textarea"
+ );
+ },
+);
+Textarea.displayName = "Textarea";
-export { Textarea }
+export { Textarea };
diff --git a/components/ui/toggle-group.tsx b/components/ui/toggle-group.tsx
index 6cadb522..0ecd5625 100644
--- a/components/ui/toggle-group.tsx
+++ b/components/ui/toggle-group.tsx
@@ -1,18 +1,17 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
-import { VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-import { toggleVariants } from "@/components/ui/toggle"
+import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
+import type { VariantProps } from "class-variance-authority";
+import * as React from "react";
+import { toggleVariants } from "@/components/ui/toggle";
+import { cn } from "@/lib/utils";
const ToggleGroupContext = React.createContext<
VariantProps
>({
size: "default",
variant: "default",
-})
+});
const ToggleGroup = React.forwardRef<
React.ElementRef,
@@ -28,16 +27,16 @@ const ToggleGroup = React.forwardRef<
{children}
-))
+));
-ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
+ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef &
VariantProps
>(({ className, children, variant, size, ...props }, ref) => {
- const context = React.useContext(ToggleGroupContext)
+ const context = React.useContext(ToggleGroupContext);
return (
{children}
- )
-})
+ );
+});
-ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
+ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
-export { ToggleGroup, ToggleGroupItem }
+export { ToggleGroup, ToggleGroupItem };
diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx
index f1a31d38..6ea4a3f1 100644
--- a/components/ui/toggle.tsx
+++ b/components/ui/toggle.tsx
@@ -1,10 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as TogglePrimitive from "@radix-ui/react-toggle"
-import { VariantProps, cva } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
+import * as TogglePrimitive from "@radix-ui/react-toggle";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background hover:bg-muted hover:text-muted-foreground",
@@ -25,8 +24,8 @@ const toggleVariants = cva(
variant: "default",
size: "default",
},
- }
-)
+ },
+);
const Toggle = React.forwardRef<
React.ElementRef,
@@ -38,8 +37,8 @@ const Toggle = React.forwardRef<
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
-))
+));
-Toggle.displayName = TogglePrimitive.Root.displayName
+Toggle.displayName = TogglePrimitive.Root.displayName;
-export { Toggle, toggleVariants }
+export { Toggle, toggleVariants };
diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
index 6a306b0f..96198e99 100644
--- a/components/ui/tooltip.tsx
+++ b/components/ui/tooltip.tsx
@@ -1,17 +1,16 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+import * as React from "react";
+import { cn } from "@/lib/utils";
-import { cn } from "@/lib/utils"
+const TooltipProvider = TooltipPrimitive.Provider;
-const TooltipProvider = TooltipPrimitive.Provider
+const Tooltip = TooltipPrimitive.Root;
-const Tooltip = TooltipPrimitive.Root
+const TooltipTrigger = TooltipPrimitive.Trigger;
-const TooltipTrigger = TooltipPrimitive.Trigger
-
-const TooltipPortal = TooltipPrimitive.Portal
+const TooltipPortal = TooltipPrimitive.Portal;
const TooltipContent = React.forwardRef<
React.ElementRef,
@@ -22,11 +21,17 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-[99999] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
- className
+ className,
)}
{...props}
/>
-))
-TooltipContent.displayName = TooltipPrimitive.Content.displayName
-
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipPortal, TooltipProvider }
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export {
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
+ TooltipPortal,
+ TooltipProvider,
+};
diff --git a/config/dashboard.ts b/config/dashboard.ts
index 5f2e291a..7bce55a0 100644
--- a/config/dashboard.ts
+++ b/config/dashboard.ts
@@ -1,6 +1,4 @@
-import { UserRole } from "@prisma/client";
-
-import { SidebarNavItem } from "types";
+import type { SidebarNavItem } from "types";
export const sidebarLinks: SidebarNavItem[] = [
{
@@ -14,8 +12,6 @@ export const sidebarLinks: SidebarNavItem[] = [
},
{
title: "OPTIONS",
- items: [
- { href: "/settings", icon: "settings", title: "Settings" },
- ],
+ items: [{ href: "/settings", icon: "settings", title: "Settings" }],
},
];
diff --git a/config/docs.ts b/config/docs.ts
index c9d79d29..bd02e0a9 100644
--- a/config/docs.ts
+++ b/config/docs.ts
@@ -1,4 +1,4 @@
-import { DocsConfig } from "types";
+import type { DocsConfig } from "types";
export const docsConfig: DocsConfig = {
mainNav: [],
diff --git a/config/landing.ts b/config/landing.ts
index 0481c1c8..9c796d1f 100644
--- a/config/landing.ts
+++ b/config/landing.ts
@@ -1,4 +1,4 @@
-import { FeatureLdg, InfoLdg, TestimonialType } from "types";
+import type { FeatureLdg, InfoLdg, TestimonialType } from "types";
export const heroContent = {
title: "Stop managing.",
diff --git a/config/marketing.ts b/config/marketing.ts
index e43d0d4a..4ea67345 100644
--- a/config/marketing.ts
+++ b/config/marketing.ts
@@ -1,4 +1,4 @@
-import { MarketingConfig } from "types"
+import type { MarketingConfig } from "types";
export const marketingConfig: MarketingConfig = {
mainNav: [
@@ -7,4 +7,4 @@ export const marketingConfig: MarketingConfig = {
href: "/pricing",
},
],
-}
+};
diff --git a/config/site.ts b/config/site.ts
index 90e07d77..56a26f0d 100644
--- a/config/site.ts
+++ b/config/site.ts
@@ -1,12 +1,11 @@
-import { SidebarNavItem, SiteConfig } from "types";
+import type { SidebarNavItem, SiteConfig } from "types";
import { env } from "@/env.mjs";
const site_url = env.NEXT_PUBLIC_APP_URL;
export const siteConfig: SiteConfig = {
name: "Simplifying",
- description:
- "Simplifying the way you manage your tasks and events.",
+ description: "Simplifying the way you manage your tasks and events.",
url: site_url,
ogImage: `${site_url}/_static/og.jpg`,
links: {
diff --git a/config/subscriptions.ts b/config/subscriptions.ts
index 12b1a6af..e5f8267e 100644
--- a/config/subscriptions.ts
+++ b/config/subscriptions.ts
@@ -1,4 +1,4 @@
-import { PlansRow, SubscriptionPlan } from "types";
+import type { PlansRow, SubscriptionPlan } from "types";
import { env } from "@/env.mjs";
export const pricingData: SubscriptionPlan[] = [
@@ -70,12 +70,7 @@ export const pricingData: SubscriptionPlan[] = [
},
];
-export const plansColumns = [
- "free",
- "plus",
- "pro",
- "enterprise",
-] as const;
+export const plansColumns = ["free", "plus", "pro", "enterprise"] as const;
export const comparePlans: PlansRow[] = [
{
@@ -134,4 +129,3 @@ export const comparePlans: PlansRow[] = [
tooltip: "Get early access to new features.",
},
];
-
diff --git a/docs/monorepo/migration-plan.md b/docs/monorepo/migration-plan.md
deleted file mode 100644
index 66d8cd06..00000000
--- a/docs/monorepo/migration-plan.md
+++ /dev/null
@@ -1,207 +0,0 @@
-# Additive Monorepo Migration Plan (No Legacy Moves)
-
-This repository is being migrated to a pnpm + Turborepo monorepo **without moving existing code**.
-The legacy root Next.js app remains in place (`app/`, `components/`, `lib/`, `actions/`, etc.).
-All **new features** should be developed in `packages/*` and consumed by the root app via `@simplifying/*` imports.
-
----
-
-## Milestone 0 — Enable pnpm workspace + Turborepo (no code moves)
-
-### What changes
-
-- Turn on pnpm workspace globs for `packages/*`.
-- Add Turborepo task graph and root scripts for running tasks across the workspace.
-
-### Where
-
-- `pnpm-workspace.yaml`
-- `turbo.json`
-- `package.json` (scripts)
-
-### Why
-
-- Establishes a cacheable, parallelizable task pipeline before any packaging work.
-
-### How to validate
-
-Commands:
-- `pnpm install`
-- `pnpm lint`
-- `pnpm test`
-- `pnpm build`
-
-Expected signals:
-- Install completes with all workspace projects discovered.
-- Lint/test/build behave the same as pre-monorepo (no new errors introduced).
-
-### Rollback
-
-- Revert changes to `pnpm-workspace.yaml`, `turbo.json`, and root `package.json` scripts.
-
----
-
-## Milestone 1 — Add config packages (tsconfig + eslint-config)
-
-### What changes
-
-- Add shared configuration packages under `packages/`:
- - `@simplifying/tsconfig`
- - `@simplifying/eslint-config`
-- Wire root ESLint to extend `@simplifying/eslint-config`.
-
-### Where
-
-- `packages/tsconfig/*`
-- `packages/eslint-config/*`
-- `.eslintrc.json`
-- `package.json` (devDependencies uses `workspace:*`)
-
-### Why
-
-- Keeps cross-repo standards centralized and reusable as `packages/*` grows.
-
-### How to validate
-
-Commands:
-- `pnpm lint`
-
-Expected signals:
-- ESLint resolves the workspace config package successfully.
-- No new lint errors in legacy root code.
-
-### Rollback
-
-- Delete `packages/tsconfig` and `packages/eslint-config`.
-- Delete `@simplifying/*` workspace devDependencies from root `package.json`.
-- Delete `@simplifying/eslint-config` from `.eslintrc.json` extends.
-
----
-
-## Milestone 2 — Enforce package boundaries (packages-only)
-
-### What changes
-
-- Enforce that `packages/**` does not import legacy root folders (including the `@/` alias).
-- Enforce client/server separation inside feature packages:
- - client code (`components/**`, `hooks/**`) cannot import `@simplifying//server`
- - client code cannot import repositories or `types.server.ts`
-
-### Where
-
-- `packages/eslint-config/index.cjs`
-
-### Why
-
-- Prevents `packages/*` from becoming thin wrappers around legacy root code.
-- Prevents accidental server-only imports into client bundles.
-
-### How to validate
-
-Commands:
-- `pnpm lint`
-
-Expected signals:
-- Lint passes for the repo today.
-- If you intentionally add a forbidden import inside `packages/**`, ESLint fails with a clear message.
-
-### Rollback
-
-- Remove or relax the package-only ESLint overrides in `packages/eslint-config/index.cjs`.
-
----
-
-## Milestone 3 — Add packages/README.md and a feature package template
-
-### What changes
-
-- Add `packages/README.md` documenting:
- - how to add a package
- - dependency rules (including client/server entrypoints)
- - exports/public API patterns
- - testing/build conventions
- - Turbo tasks and outputs
-- Add a "template package" folder example for new features (optional).
-
-### Where
-
-- `packages/README.md`
-- (optional) `packages/_template-feature/`
-
-### Why
-
-- Reduces guesswork and makes the monorepo scalable for new contributors.
-
-### How to validate
-
-Commands:
-- `pnpm -r lint`
-- `pnpm -r type-check` (after packages introduce `type-check`)
-- `pnpm dev` (root app still runs)
-
-Expected signals:
-- New packages can be created and consumed without changing legacy root layout.
-
-### Rollback
-
-- Remove `packages/README.md` and template folder(s).
-
----
-
-## Milestone 4 — Update agent/docs guidance for additive monorepo
-
-### What changes
-
-- Update documentation so agents and contributors default to:
- - new features in `packages/*`
- - root app stays legacy until explicitly migrated
- - enforce client/server entrypoints in packages
-
-### Where
-
-- `AGENTS.md`
-- `docs/agents/architecture-patterns.md`
-- `.agent/rules/agents.md`
-- Any docs that prescribe the old "5-layer repo layout" as *the* structure
-
-### Why
-
-- Avoids reintroducing new logic in legacy layers by default.
-
-### How to validate
-
-Signals:
-- Onboarding docs describe the new package-first workflow in under 10 minutes.
-
-### Rollback
-
-- Revert doc changes only (no runtime impact).
-
----
-
-## Milestone 5 — GitHub Actions CI (Turbo + pnpm cache)
-
-### What changes
-
-- Add GitHub Actions workflow that runs:
- - `turbo run lint type-check test`
- - with pnpm caching
- - with "affected-only" filtering where possible
-
-### Where
-
-- `.github/workflows/ci.yml`
-
-### Why
-
-- Standardizes quality gates and speeds up PR feedback with caching and parallel execution.
-
-### How to validate
-
-Signals:
-- Workflow is green on PRs.
-- Re-running CI shows Turbo cache hits.
-
-### Rollback
-
-- Delete `.github/workflows/ci.yml`.
diff --git a/emails/magic-link-email.tsx b/emails/magic-link-email.tsx
index a453e4bd..fe4a77a6 100644
--- a/emails/magic-link-email.tsx
+++ b/emails/magic-link-email.tsx
@@ -10,7 +10,6 @@ import {
Tailwind,
Text,
} from "@react-email/components";
-
import { Icons } from "../components/shared/icons";
type MagicLinkEmailProps = {
diff --git a/emails/payment-failed-email.tsx b/emails/payment-failed-email.tsx
index bdc6b803..1be8268b 100644
--- a/emails/payment-failed-email.tsx
+++ b/emails/payment-failed-email.tsx
@@ -10,7 +10,6 @@ import {
Tailwind,
Text,
} from "@react-email/components";
-
import { Icons } from "../components/shared/icons";
type PaymentFailedEmailProps = {
@@ -28,7 +27,9 @@ export const PaymentFailedEmail = ({
}: PaymentFailedEmailProps) => (
- Your recent payment failed - please update your payment method
+
+ Your recent payment failed - please update your payment method
+
@@ -36,12 +37,12 @@ export const PaymentFailedEmail = ({
Hi {firstName},
We were unable to process your recent payment for your {siteName}{" "}
- subscription. This could be due to an expired card, insufficient funds,
- or other issues with your payment method.
+ subscription. This could be due to an expired card, insufficient
+ funds, or other issues with your payment method.
- To avoid any interruption to your service, please update your payment
- information.
+ To avoid any interruption to your service, please update your
+ payment information.
- If you have any questions, feel free to reach out to our support team.
+ If you have any questions, feel free to reach out to our support
+ team.
{companyAddress ? (
diff --git a/env.mjs b/env.mjs
index 3e54186b..bdb65385 100644
--- a/env.mjs
+++ b/env.mjs
@@ -60,7 +60,10 @@ export const env = createEnv({
CALENDAR_CREDENTIAL_ENCRYPTION_KEY: z
.string()
.trim()
- .regex(/^[0-9a-fA-F]{64}$/, "CALENDAR_CREDENTIAL_ENCRYPTION_KEY must be 64 hex characters")
+ .regex(
+ /^[0-9a-fA-F]{64}$/,
+ "CALENDAR_CREDENTIAL_ENCRYPTION_KEY must be 64 hex characters",
+ )
.optional(),
// Microsoft Azure AD (for Outlook Calendar + Microsoft To Do)
AZURE_AD_CLIENT_ID: z.string().min(1),
@@ -124,7 +127,8 @@ export const env = createEnv({
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
- CALENDAR_CREDENTIAL_ENCRYPTION_KEY: process.env.CALENDAR_CREDENTIAL_ENCRYPTION_KEY,
+ CALENDAR_CREDENTIAL_ENCRYPTION_KEY:
+ process.env.CALENDAR_CREDENTIAL_ENCRYPTION_KEY,
// Microsoft Azure AD
AZURE_AD_CLIENT_ID: process.env.AZURE_AD_CLIENT_ID,
AZURE_AD_CLIENT_SECRET: process.env.AZURE_AD_CLIENT_SECRET,
@@ -139,13 +143,12 @@ export const env = createEnv({
process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_SENTRY_DSN_CLIENT: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
- NEXT_PUBLIC_SENTRY_ENABLE_LOGS:
- process.env.NEXT_PUBLIC_SENTRY_ENABLE_LOGS,
+ NEXT_PUBLIC_SENTRY_ENABLE_LOGS: process.env.NEXT_PUBLIC_SENTRY_ENABLE_LOGS,
},
});
// Only validate encryption keys on the server
-if (typeof window === 'undefined') {
+if (typeof window === "undefined") {
if (!env.ENCRYPTION_KEY && !env.CALENDAR_CREDENTIAL_ENCRYPTION_KEY) {
throw new Error(
"Encryption key is not configured. Set ENCRYPTION_KEY (base64 32 bytes) or CALENDAR_CREDENTIAL_ENCRYPTION_KEY (64 hex chars).",
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 00bc169b..369a4475 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,17 +1,12 @@
-import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
-import prettier from "eslint-config-prettier";
-import tailwind from "eslint-plugin-tailwindcss";
-import react from "eslint-plugin-react";
-import tseslint from "typescript-eslint";
-import simplifyingConfig from "@simplifying/eslint-config";
+import js from "@eslint/js";
+
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
recommendedConfig: js.configs.recommended,
});
-/** @type {import('typescript-eslint').ConfigArray} */
-export default [
+const config = [
{
ignores: [
".next/**",
@@ -19,66 +14,17 @@ export default [
"build/**",
"dist/**",
"node_modules/**",
- "next-env.d.ts",
".turbo/**",
"coverage/**",
- "eslint.config.mjs",
],
},
-
- js.configs.recommended,
-
- ...tseslint.configs.recommendedTypeChecked,
- {
- files: ["**/*.ts", "**/*.tsx"],
- languageOptions: {
- parserOptions: {
- projectService: true,
- tsconfigRootDir: import.meta.dirname,
- },
- },
- },
- {
- name: "disable-type-checked-for-js",
- files: ["**/*.js", "**/*.mjs", "**/*.cjs"],
- ...tseslint.configs.disableTypeChecked,
- },
-
...compat.extends("next/core-web-vitals"),
-
- ...tailwind.configs["flat/recommended"],
{
- settings: {
- tailwindcss: {
- callees: ["cn"],
- config: "tailwind.config.ts",
- },
- },
- },
-
- {
- plugins: {
- react,
- },
- settings: {
- react: {
- version: "detect",
- },
- },
- },
-
- ...simplifyingConfig,
-
- prettier,
-
- {
- name: "root-overrides",
files: ["**/*.{ts,tsx,js,jsx}"],
rules: {
"@next/next/no-html-link-for-pages": "off",
- "react/jsx-key": "warn",
- "tailwindcss/no-custom-classname": "off",
- "tailwindcss/classnames-order": "error",
},
},
];
+
+export default config;
diff --git a/hooks/use-focus-trap.ts b/hooks/use-focus-trap.ts
index 0367d65a..afe9cbf6 100644
--- a/hooks/use-focus-trap.ts
+++ b/hooks/use-focus-trap.ts
@@ -35,7 +35,10 @@ import { useEffect } from "react";
*
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/} - ARIA Dialog Pattern
*/
-export function useFocusTrap(ref: React.RefObject, enabled: boolean = true) {
+export function useFocusTrap(
+ ref: React.RefObject,
+ enabled: boolean = true,
+) {
useEffect(() => {
if (!enabled) return;
@@ -43,20 +46,21 @@ export function useFocusTrap(ref: React.RefObject, enabled:
if (!root) return;
const selector = [
- 'a[href]',
- 'button:not([disabled])',
- 'textarea:not([disabled])',
- 'input:not([disabled])',
- 'select:not([disabled])',
+ "a[href]",
+ "button:not([disabled])",
+ "textarea:not([disabled])",
+ "input:not([disabled])",
+ "select:not([disabled])",
'[tabindex]:not([tabindex="-1"])',
- ].join(',');
+ ].join(",");
- const getFocusable = () => Array.from(root.querySelectorAll(selector)).filter(
- (el) => !el.hasAttribute('aria-hidden') && el.tabIndex !== -1,
- );
+ const getFocusable = () =>
+ Array.from(root.querySelectorAll(selector)).filter(
+ (el) => !el.hasAttribute("aria-hidden") && el.tabIndex !== -1,
+ );
const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key !== 'Tab') return;
+ if (e.key !== "Tab") return;
const focusable = getFocusable();
if (focusable.length === 0) return;
const first = focusable[0];
@@ -75,9 +79,9 @@ export function useFocusTrap(ref: React.RefObject, enabled:
}
};
- root.addEventListener('keydown', handleKeyDown);
+ root.addEventListener("keydown", handleKeyDown);
return () => {
- root.removeEventListener('keydown', handleKeyDown);
+ root.removeEventListener("keydown", handleKeyDown);
};
}, [ref, enabled]);
}
diff --git a/hooks/use-intersection-observer.ts b/hooks/use-intersection-observer.ts
index 4fd6a345..03a620ff 100644
--- a/hooks/use-intersection-observer.ts
+++ b/hooks/use-intersection-observer.ts
@@ -1,6 +1,6 @@
"use client";
-import { RefObject, useEffect, useState } from "react";
+import { type RefObject, useEffect, useState } from "react";
interface Args extends IntersectionObserverInit {
freezeOnceVisible?: boolean;
@@ -35,9 +35,8 @@ function useIntersectionObserver(
observer.observe(node);
return () => observer.disconnect();
-
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [threshold, root, rootMargin, frozen]);
+ }, [threshold, root, rootMargin, frozen, elementRef?.current, updateEntry]);
return entry;
}
diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts
index ee5f5ed9..74c22212 100644
--- a/hooks/use-local-storage.ts
+++ b/hooks/use-local-storage.ts
@@ -13,9 +13,8 @@ const useLocalStorage = (
const item = window.localStorage.getItem(key);
if (item) {
try {
- setStoredValue(JSON.parse(item));
- } catch (error) {
- // If JSON is malformed, fall back to initial value and clear bad data
+ setStoredValue(JSON.parse(item) as T);
+ } catch {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
}
diff --git a/hooks/use-lock-body.ts b/hooks/use-lock-body.ts
index 5e14b03c..7673467c 100644
--- a/hooks/use-lock-body.ts
+++ b/hooks/use-lock-body.ts
@@ -1,14 +1,14 @@
"use client";
-import * as React from "react"
+import * as React from "react";
// @see https://usehooks.com/useLockBodyScroll.
export function useLockBody() {
React.useLayoutEffect((): (() => void) => {
const originalStyle: string = window.getComputedStyle(
- document.body
- ).overflow
- document.body.style.overflow = "hidden"
- return () => (document.body.style.overflow = originalStyle)
- }, [])
+ document.body,
+ ).overflow;
+ document.body.style.overflow = "hidden";
+ return () => (document.body.style.overflow = originalStyle);
+ }, []);
}
diff --git a/hooks/use-media-query.ts b/hooks/use-media-query.ts
index d65163b2..d250ff1c 100644
--- a/hooks/use-media-query.ts
+++ b/hooks/use-media-query.ts
@@ -3,9 +3,9 @@
import { useEffect, useState } from "react";
export function useMediaQuery() {
- const [device, setDevice] = useState<"mobile" | "sm" | "tablet" | "desktop" | null>(
- null,
- );
+ const [device, setDevice] = useState<
+ "mobile" | "sm" | "tablet" | "desktop" | null
+ >(null);
const [dimensions, setDimensions] = useState<{
width: number;
height: number;
diff --git a/hooks/use-mounted.ts b/hooks/use-mounted.ts
index 22911442..1903b283 100644
--- a/hooks/use-mounted.ts
+++ b/hooks/use-mounted.ts
@@ -1,13 +1,13 @@
"use client";
-import * as React from "react"
+import * as React from "react";
export function useMounted() {
- const [mounted, setMounted] = React.useState(false)
+ const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
- setMounted(true)
- }, [])
+ setMounted(true);
+ }, []);
- return mounted
+ return mounted;
}
diff --git a/hooks/use-sync-references.ts b/hooks/use-sync-references.ts
index 79cbfc9b..33f5f80f 100644
--- a/hooks/use-sync-references.ts
+++ b/hooks/use-sync-references.ts
@@ -1,19 +1,18 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
-
-import { aggregateSyncReferences } from "@/lib/sync/sync-icon-utils";
import {
- SYNC_TASK_BATCH_SIZE,
SYNC_EVENT_BATCH_SIZE,
- SYNC_POLL_INTERVAL_MS,
SYNC_MAX_DURATION_MS,
+ SYNC_POLL_INTERVAL_MS,
+ SYNC_TASK_BATCH_SIZE,
TERMINAL_SYNC_STATUSES,
} from "@/lib/sync/sync-constants";
+import { aggregateSyncReferences } from "@/lib/sync/sync-icon-utils";
import type {
+ SyncDomain,
SyncIndicatorData,
SyncReference,
- SyncDomain,
SyncStatusAPIResponse,
SyncStatusPayload,
} from "@/lib/types/sync";
@@ -105,18 +104,19 @@ function parseReferences(rawRefs: unknown): SyncReference[] {
}
return rawRefs
- .filter((ref): ref is Record =>
- ref !== null && typeof ref === "object"
+ .filter(
+ (ref): ref is Record =>
+ ref !== null && typeof ref === "object",
)
.map((ref) => ({
provider: typeof ref.provider === "string" ? ref.provider : "",
syncStatus: typeof ref.syncStatus === "string" ? ref.syncStatus : "",
- syncStatusReason: typeof ref.syncStatusReason === "string"
- ? ref.syncStatusReason
- : undefined,
- syncedAt: typeof ref.syncedAt === "string"
- ? new Date(ref.syncedAt)
- : undefined,
+ syncStatusReason:
+ typeof ref.syncStatusReason === "string"
+ ? ref.syncStatusReason
+ : undefined,
+ syncedAt:
+ typeof ref.syncedAt === "string" ? new Date(ref.syncedAt) : undefined,
}))
.filter((ref) => ref.provider && ref.syncStatus);
}
@@ -130,9 +130,9 @@ export function useSyncReferences(
id: string,
options: UseSyncReferencesOptions = {},
): UseSyncReferencesResult {
- const {
- poll = true,
- intervalMs = SYNC_POLL_INTERVAL_MS,
+ const {
+ poll = true,
+ intervalMs = SYNC_POLL_INTERVAL_MS,
maxDurationMs = SYNC_MAX_DURATION_MS,
} = options;
@@ -181,13 +181,13 @@ export function useSyncReferences(
const response = await fetch(endpoint, {
signal: abortControllerRef.current.signal,
});
-
+
if (!response.ok) {
throw new Error(`Sync status fetch failed: ${response.status}`);
}
const json: unknown = await response.json();
-
+
// Type-safe response validation
if (
typeof json !== "object" ||
@@ -197,7 +197,7 @@ export function useSyncReferences(
) {
throw new Error("Invalid sync status response format");
}
-
+
const statuses = (json as SyncStatusAPIResponse).statuses;
// Extract references based on response format
@@ -228,11 +228,11 @@ export function useSyncReferences(
if (err instanceof Error && err.name === "AbortError") {
return;
}
-
+
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
setIsLoading(false);
-
+
// Sync status is non-critical, log in development for debugging
if (process.env.NODE_ENV === "development") {
console.warn("[SyncReferences] Single fetch failed:", error);
@@ -255,11 +255,14 @@ export function useSyncReferences(
}
// Initial fetch
- fetchSyncStatus();
+ void fetchSyncStatus();
// Set up polling if enabled
if (poll) {
- intervalRef.current = setInterval(fetchSyncStatus, intervalMs);
+ intervalRef.current = setInterval(
+ () => void fetchSyncStatus(),
+ intervalMs,
+ );
}
// Cleanup on unmount or when dependencies change
@@ -272,7 +275,7 @@ export function useSyncReferences(
abortControllerRef.current?.abort();
abortControllerRef.current = null;
};
- }, [type, id, poll, intervalMs, fetchSyncStatus, isTemporaryId]);
+ }, [poll, intervalMs, fetchSyncStatus, isTemporaryId]);
return { data, isLoading, error };
}
@@ -286,9 +289,9 @@ export function useSyncReferencesBatch(
ids: string[],
options: UseSyncReferencesOptions = {},
): UseSyncReferencesBatchResult {
- const {
- poll = true,
- intervalMs = SYNC_POLL_INTERVAL_MS,
+ const {
+ poll = true,
+ intervalMs = SYNC_POLL_INTERVAL_MS,
maxDurationMs = SYNC_MAX_DURATION_MS,
} = options;
@@ -298,26 +301,26 @@ export function useSyncReferencesBatch(
// Use ref to store IDs to avoid stale closures in the polling callback
const idsRef = useRef([]);
-
+
// Update ref when ids change (filter out empty and temp IDs)
useEffect(() => {
idsRef.current = Array.from(
- new Set(ids.filter((id) => id && !id.startsWith("temp-")))
+ new Set(ids.filter((id) => id && !id.startsWith("temp-"))),
);
}, [ids]);
// Create a stable key for dependency tracking
- const idsKey = ids.join(",");
+ const _idsKey = ids.join(",");
useEffect(() => {
const uniqueIds = idsRef.current;
-
+
// Reset state when IDs change to prevent unbounded state growth.
// This ensures we don't accumulate stale entries for IDs no longer in the list.
setDataMap({});
setIsLoading(true);
setError(null);
-
+
if (uniqueIds.length === 0) {
setIsLoading(false);
return;
@@ -331,7 +334,7 @@ export function useSyncReferencesBatch(
const fetchStatuses = async () => {
// Always read fresh IDs from ref
const currentIds = idsRef.current;
-
+
if (isTerminal || currentIds.length === 0) {
return;
}
@@ -368,9 +371,9 @@ export function useSyncReferencesBatch(
if (!response.ok) {
throw new Error(`Sync status fetch failed: ${response.status}`);
}
-
+
const json: unknown = await response.json();
-
+
// Type-safe response validation
if (
typeof json !== "object" ||
@@ -380,9 +383,9 @@ export function useSyncReferencesBatch(
) {
throw new Error("Invalid sync status response format");
}
-
+
return (json as SyncStatusAPIResponse).statuses;
- })
+ }),
);
// Merge results from all chunks
@@ -417,7 +420,7 @@ export function useSyncReferencesBatch(
...nextMap,
}));
}
-
+
setError(null);
setIsLoading(false);
@@ -433,11 +436,11 @@ export function useSyncReferencesBatch(
if (err instanceof Error && err.name === "AbortError") {
return;
}
-
+
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
setIsLoading(false);
-
+
// Sync status is non-critical, log in development for debugging
if (process.env.NODE_ENV === "development") {
console.warn("[SyncReferences] Batch fetch error:", error);
@@ -445,10 +448,10 @@ export function useSyncReferencesBatch(
}
};
- fetchStatuses();
+ void fetchStatuses();
if (poll) {
- interval = setInterval(fetchStatuses, intervalMs);
+ interval = setInterval(() => void fetchStatuses(), intervalMs);
}
return () => {
@@ -458,7 +461,7 @@ export function useSyncReferencesBatch(
// Cancel any in-flight requests
abortController?.abort();
};
- }, [type, idsKey, poll, intervalMs, maxDurationMs]);
+ }, [type, poll, intervalMs, maxDurationMs]);
return { data: dataMap, isLoading, error };
}
diff --git a/hooks/use-synced-transition.ts b/hooks/use-synced-transition.ts
index 67b2aac4..fc85a508 100644
--- a/hooks/use-synced-transition.ts
+++ b/hooks/use-synced-transition.ts
@@ -1,7 +1,6 @@
"use client";
import { useEffect, useRef, useState } from "react";
-
import type { SyncStatus } from "@/lib/types/sync";
const PENDING_STATES: SyncStatus[] = [
diff --git a/instrumentation-client.ts b/instrumentation-client.ts
index 8f72f766..7eaaae43 100644
--- a/instrumentation-client.ts
+++ b/instrumentation-client.ts
@@ -1,6 +1,9 @@
import * as Sentry from "@sentry/nextjs";
-
-import { isIgnorableError, parseSampleRate, sanitizeEvent } from "@/lib/sentry-utils";
+import {
+ isIgnorableError,
+ parseSampleRate,
+ sanitizeEvent,
+} from "@/lib/sentry-utils";
// Initialize Sentry in production, or when DSN is explicitly provided (for testing)
const dsn =
@@ -51,4 +54,3 @@ if (process.env.NODE_ENV === "production" || dsn) {
}
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
-
diff --git a/lib/__tests__/ai-settings-validation.test.ts b/lib/__tests__/ai-settings-validation.test.ts
index e45ea44f..7d01a3a6 100644
--- a/lib/__tests__/ai-settings-validation.test.ts
+++ b/lib/__tests__/ai-settings-validation.test.ts
@@ -1,69 +1,78 @@
-import { describe, it, expect } from 'vitest';
-
+import { describe, expect, it } from "vitest";
import {
- aiSettingsUpdateSchema,
+ type AISettings,
aiSettingsResponseSchema,
+ aiSettingsUpdateSchema,
transformAISettingsToResponse,
- type AISettings,
-} from '@/lib/validations/ai-settings';
+} from "@/lib/validations/ai-settings";
-describe('AI Settings Validation', () => {
- describe('aiSettingsUpdateSchema', () => {
- it('accepts valid input with autoConfirmEnabled: true', () => {
- const result = aiSettingsUpdateSchema.safeParse({ autoConfirmEnabled: true });
+describe("AI Settings Validation", () => {
+ describe("aiSettingsUpdateSchema", () => {
+ it("accepts valid input with autoConfirmEnabled: true", () => {
+ const result = aiSettingsUpdateSchema.safeParse({
+ autoConfirmEnabled: true,
+ });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.autoConfirmEnabled).toBe(true);
}
});
- it('accepts valid input with autoConfirmEnabled: false', () => {
- const result = aiSettingsUpdateSchema.safeParse({ autoConfirmEnabled: false });
+ it("accepts valid input with autoConfirmEnabled: false", () => {
+ const result = aiSettingsUpdateSchema.safeParse({
+ autoConfirmEnabled: false,
+ });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.autoConfirmEnabled).toBe(false);
}
});
- it('rejects missing autoConfirmEnabled', () => {
+ it("rejects missing autoConfirmEnabled", () => {
const result = aiSettingsUpdateSchema.safeParse({});
expect(result.success).toBe(false);
});
- it('rejects non-boolean autoConfirmEnabled', () => {
- const result = aiSettingsUpdateSchema.safeParse({ autoConfirmEnabled: 'true' });
+ it("rejects non-boolean autoConfirmEnabled", () => {
+ const result = aiSettingsUpdateSchema.safeParse({
+ autoConfirmEnabled: "true",
+ });
expect(result.success).toBe(false);
});
- it('rejects null autoConfirmEnabled', () => {
- const result = aiSettingsUpdateSchema.safeParse({ autoConfirmEnabled: null });
+ it("rejects null autoConfirmEnabled", () => {
+ const result = aiSettingsUpdateSchema.safeParse({
+ autoConfirmEnabled: null,
+ });
expect(result.success).toBe(false);
});
- it('rejects undefined autoConfirmEnabled', () => {
- const result = aiSettingsUpdateSchema.safeParse({ autoConfirmEnabled: undefined });
+ it("rejects undefined autoConfirmEnabled", () => {
+ const result = aiSettingsUpdateSchema.safeParse({
+ autoConfirmEnabled: undefined,
+ });
expect(result.success).toBe(false);
});
- it('ignores extra/unknown fields (passthrough by default)', () => {
+ it("ignores extra/unknown fields (passthrough by default)", () => {
// Note: Zod default is passthrough, so extra fields are stripped but don't fail
const result = aiSettingsUpdateSchema.safeParse({
autoConfirmEnabled: true,
- autoConfirmEnabledAt: '2024-01-01T00:00:00.000Z',
- extraField: 'ignored',
+ autoConfirmEnabledAt: "2024-01-01T00:00:00.000Z",
+ extraField: "ignored",
});
expect(result.success).toBe(true);
if (result.success) {
// Extra fields should not be in the output
expect(result.data).toEqual({ autoConfirmEnabled: true });
- expect('autoConfirmEnabledAt' in result.data).toBe(false);
- expect('extraField' in result.data).toBe(false);
+ expect("autoConfirmEnabledAt" in result.data).toBe(false);
+ expect("extraField" in result.data).toBe(false);
}
});
});
- describe('aiSettingsResponseSchema', () => {
- it('accepts valid response with null timestamp', () => {
+ describe("aiSettingsResponseSchema", () => {
+ it("accepts valid response with null timestamp", () => {
const result = aiSettingsResponseSchema.safeParse({
autoConfirmEnabled: false,
autoConfirmEnabledAt: null,
@@ -71,23 +80,23 @@ describe('AI Settings Validation', () => {
expect(result.success).toBe(true);
});
- it('accepts valid response with ISO datetime timestamp', () => {
+ it("accepts valid response with ISO datetime timestamp", () => {
const result = aiSettingsResponseSchema.safeParse({
autoConfirmEnabled: true,
- autoConfirmEnabledAt: '2024-01-15T10:30:00.000Z',
+ autoConfirmEnabledAt: "2024-01-15T10:30:00.000Z",
});
expect(result.success).toBe(true);
});
- it('rejects invalid datetime format', () => {
+ it("rejects invalid datetime format", () => {
const result = aiSettingsResponseSchema.safeParse({
autoConfirmEnabled: true,
- autoConfirmEnabledAt: 'not-a-date',
+ autoConfirmEnabledAt: "not-a-date",
});
expect(result.success).toBe(false);
});
- it('rejects missing autoConfirmEnabledAt', () => {
+ it("rejects missing autoConfirmEnabledAt", () => {
const result = aiSettingsResponseSchema.safeParse({
autoConfirmEnabled: true,
});
@@ -95,9 +104,9 @@ describe('AI Settings Validation', () => {
});
});
- describe('transformAISettingsToResponse', () => {
- it('converts Date to ISO string', () => {
- const date = new Date('2024-01-15T10:30:00.000Z');
+ describe("transformAISettingsToResponse", () => {
+ it("converts Date to ISO string", () => {
+ const date = new Date("2024-01-15T10:30:00.000Z");
const settings: AISettings = {
autoConfirmEnabled: true,
autoConfirmEnabledAt: date,
@@ -106,10 +115,10 @@ describe('AI Settings Validation', () => {
const result = transformAISettingsToResponse(settings);
expect(result.autoConfirmEnabled).toBe(true);
- expect(result.autoConfirmEnabledAt).toBe('2024-01-15T10:30:00.000Z');
+ expect(result.autoConfirmEnabledAt).toBe("2024-01-15T10:30:00.000Z");
});
- it('keeps null as null', () => {
+ it("keeps null as null", () => {
const settings: AISettings = {
autoConfirmEnabled: false,
autoConfirmEnabledAt: null,
@@ -121,7 +130,7 @@ describe('AI Settings Validation', () => {
expect(result.autoConfirmEnabledAt).toBeNull();
});
- it('preserves autoConfirmEnabled boolean value', () => {
+ it("preserves autoConfirmEnabled boolean value", () => {
const settingsEnabled: AISettings = {
autoConfirmEnabled: true,
autoConfirmEnabledAt: new Date(),
@@ -131,8 +140,12 @@ describe('AI Settings Validation', () => {
autoConfirmEnabledAt: null,
};
- expect(transformAISettingsToResponse(settingsEnabled).autoConfirmEnabled).toBe(true);
- expect(transformAISettingsToResponse(settingsDisabled).autoConfirmEnabled).toBe(false);
+ expect(
+ transformAISettingsToResponse(settingsEnabled).autoConfirmEnabled,
+ ).toBe(true);
+ expect(
+ transformAISettingsToResponse(settingsDisabled).autoConfirmEnabled,
+ ).toBe(false);
});
});
});
diff --git a/lib/__tests__/db-encrypted.test.ts b/lib/__tests__/db-encrypted.test.ts
index 96d797f5..9a492845 100644
--- a/lib/__tests__/db-encrypted.test.ts
+++ b/lib/__tests__/db-encrypted.test.ts
@@ -1,24 +1,23 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-
-import { testEncryptionKey, testArrays } from '@/tests/fixtures';
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { testArrays, testEncryptionKey } from "@/tests/fixtures";
// We need to test the internal helper functions from db-encrypted.ts
// Since they're not exported, we'll test them indirectly through the encryption behavior
// or extract testable logic. For now, we focus on what we can test.
// Import field-encryption to understand the encryption behavior
-let encryptField: typeof import('@/lib/field-encryption').encryptField;
-let decryptField: typeof import('@/lib/field-encryption').decryptField;
-let encryptArrayField: typeof import('@/lib/field-encryption').encryptArrayField;
-let decryptArrayField: typeof import('@/lib/field-encryption').decryptArrayField;
-let isEncryptedValue: typeof import('@/lib/field-encryption').isEncryptedValue;
-let ENCRYPTED_FIELDS: typeof import('@/lib/field-encryption').ENCRYPTED_FIELDS;
-
-describe('db-encrypted helpers', () => {
+let encryptField: typeof import("@/lib/field-encryption").encryptField;
+let decryptField: typeof import("@/lib/field-encryption").decryptField;
+let encryptArrayField: typeof import("@/lib/field-encryption").encryptArrayField;
+let decryptArrayField: typeof import("@/lib/field-encryption").decryptArrayField;
+let isEncryptedValue: typeof import("@/lib/field-encryption").isEncryptedValue;
+let ENCRYPTED_FIELDS: typeof import("@/lib/field-encryption").ENCRYPTED_FIELDS;
+
+describe("db-encrypted helpers", () => {
beforeEach(async () => {
- vi.stubEnv('ENCRYPTION_KEY', testEncryptionKey);
+ vi.stubEnv("ENCRYPTION_KEY", testEncryptionKey);
vi.resetModules();
- const mod = await import('@/lib/field-encryption');
+ const mod = await import("@/lib/field-encryption");
encryptField = mod.encryptField;
decryptField = mod.decryptField;
encryptArrayField = mod.encryptArrayField;
@@ -31,15 +30,15 @@ describe('db-encrypted helpers', () => {
vi.unstubAllEnvs();
});
- describe('encryptModelData behavior', () => {
+ describe("encryptModelData behavior", () => {
// Tests simulate what encryptModelData does internally
- it('encrypts only configured fields for RawInput', () => {
+ it("encrypts only configured fields for RawInput", () => {
const input = {
- id: 'raw-123',
- text: 'User input text',
- userId: 'user-456',
- createdAt: new Date('2025-01-01'),
+ id: "raw-123",
+ text: "User input text",
+ userId: "user-456",
+ createdAt: new Date("2025-01-01"),
};
// Simulate encryption of configured fields
@@ -54,13 +53,13 @@ describe('db-encrypted helpers', () => {
expect(encrypted.createdAt).toBe(input.createdAt);
});
- it('encrypts only configured fields for ParsedEvent', () => {
+ it("encrypts only configured fields for ParsedEvent", () => {
const input = {
- id: 'event-123',
- title: 'Meeting',
- attendees: ['alice@example.com', 'bob@example.com'],
- reasoning: 'This is why we scheduled it',
- start: new Date('2025-06-15T10:00:00Z'),
+ id: "event-123",
+ title: "Meeting",
+ attendees: ["alice@example.com", "bob@example.com"],
+ reasoning: "This is why we scheduled it",
+ start: new Date("2025-06-15T10:00:00Z"),
confidence: 0.95,
};
@@ -78,25 +77,25 @@ describe('db-encrypted helpers', () => {
expect(encrypted.confidence).toBe(input.confidence);
});
- it('encrypts attendees as string for Event model', () => {
+ it("encrypts attendees as string for Event model", () => {
const input = {
- id: 'event-123',
+ id: "event-123",
attendees: testArrays.emails,
};
const encrypted = encryptArrayField(input.attendees);
- expect(typeof encrypted).toBe('string');
+ expect(typeof encrypted).toBe("string");
expect(isEncryptedValue(encrypted)).toBe(true);
});
- it('preserves non-encrypted fields unchanged', () => {
+ it("preserves non-encrypted fields unchanged", () => {
const input = {
- id: 'task-123',
- title: 'Complete project',
- description: 'Finish the implementation',
- reasoning: 'AI determined this was a task',
- dueDate: new Date('2025-06-20'),
- priority: 'high',
+ id: "task-123",
+ title: "Complete project",
+ description: "Finish the implementation",
+ reasoning: "AI determined this was a task",
+ dueDate: new Date("2025-06-20"),
+ priority: "high",
completed: false,
orderIndex: 42,
};
@@ -116,11 +115,11 @@ describe('db-encrypted helpers', () => {
expect(encrypted.orderIndex).toBe(input.orderIndex);
});
- it('does not mutate input object', () => {
+ it("does not mutate input object", () => {
const input = {
- id: 'raw-123',
- text: 'Original text',
- userId: 'user-456',
+ id: "raw-123",
+ text: "Original text",
+ userId: "user-456",
};
const originalText = input.text;
@@ -137,13 +136,13 @@ describe('db-encrypted helpers', () => {
});
});
- describe('decryptModelData behavior', () => {
- it('decrypts configured fields for RawInput', () => {
- const originalText = 'Original user input';
+ describe("decryptModelData behavior", () => {
+ it("decrypts configured fields for RawInput", () => {
+ const originalText = "Original user input";
const encryptedData = {
- id: 'raw-123',
+ id: "raw-123",
text: encryptField(originalText),
- userId: 'user-456',
+ userId: "user-456",
};
// Simulate decryption
@@ -155,10 +154,10 @@ describe('db-encrypted helpers', () => {
expect(decrypted.text).toBe(originalText);
});
- it('decrypts array fields to string[]', () => {
- const originalAttendees = ['alice@example.com', 'bob@example.com'];
+ it("decrypts array fields to string[]", () => {
+ const originalAttendees = ["alice@example.com", "bob@example.com"];
const encryptedData = {
- id: 'event-123',
+ id: "event-123",
attendees: encryptArrayField(originalAttendees),
};
@@ -167,24 +166,25 @@ describe('db-encrypted helpers', () => {
expect(Array.isArray(decrypted)).toBe(true);
});
- it('handles null values gracefully', () => {
+ it("handles null values gracefully", () => {
const data = {
- id: 'event-123',
+ id: "event-123",
reasoning: null,
attendees: null,
};
// Simulate decryption with null handling
- const decryptedReasoning = data.reasoning === null ? null : decryptField(data.reasoning);
+ const decryptedReasoning =
+ data.reasoning === null ? null : decryptField(data.reasoning);
const decryptedAttendees = decryptArrayField(data.attendees);
expect(decryptedReasoning).toBeNull();
expect(decryptedAttendees).toEqual([]);
});
- it('handles undefined values gracefully', () => {
+ it("handles undefined values gracefully", () => {
const data = {
- id: 'event-123',
+ id: "event-123",
reasoning: undefined,
attendees: undefined,
};
@@ -193,11 +193,11 @@ describe('db-encrypted helpers', () => {
expect(decryptedAttendees).toEqual([]);
});
- it('preserves non-target field types (Date, number, boolean)', () => {
- const originalDate = new Date('2025-06-15T10:00:00Z');
+ it("preserves non-target field types (Date, number, boolean)", () => {
+ const originalDate = new Date("2025-06-15T10:00:00Z");
const data = {
- id: 'event-123',
- title: 'Meeting',
+ id: "event-123",
+ title: "Meeting",
start: originalDate,
allDay: false,
confidence: 0.95,
@@ -212,16 +212,16 @@ describe('db-encrypted helpers', () => {
expect(decrypted.start).toBe(originalDate);
expect(decrypted.start instanceof Date).toBe(true);
- expect(typeof decrypted.allDay).toBe('boolean');
+ expect(typeof decrypted.allDay).toBe("boolean");
expect(decrypted.allDay).toBe(false);
- expect(typeof decrypted.confidence).toBe('number');
+ expect(typeof decrypted.confidence).toBe("number");
expect(decrypted.confidence).toBe(0.95);
});
- it('does not mutate input object', () => {
- const encryptedText = encryptField('Original text');
+ it("does not mutate input object", () => {
+ const encryptedText = encryptField("Original text");
const input = {
- id: 'raw-123',
+ id: "raw-123",
text: encryptedText,
};
@@ -239,20 +239,20 @@ describe('db-encrypted helpers', () => {
});
});
- describe('decryptWithIncludes behavior', () => {
- it('decrypts one-to-many array relations (parsedEvents)', () => {
+ describe("decryptWithIncludes behavior", () => {
+ it("decrypts one-to-many array relations (parsedEvents)", () => {
const events = [
{
- id: 'event-1',
- title: 'Event 1',
- attendees: encryptArrayField(['a@test.com']),
- reasoning: encryptField('Reason 1'),
+ id: "event-1",
+ title: "Event 1",
+ attendees: encryptArrayField(["a@test.com"]),
+ reasoning: encryptField("Reason 1"),
},
{
- id: 'event-2',
- title: 'Event 2',
- attendees: encryptArrayField(['b@test.com']),
- reasoning: encryptField('Reason 2'),
+ id: "event-2",
+ title: "Event 2",
+ attendees: encryptArrayField(["b@test.com"]),
+ reasoning: encryptField("Reason 2"),
},
];
@@ -263,16 +263,16 @@ describe('db-encrypted helpers', () => {
reasoning: decryptField(event.reasoning),
}));
- expect(decrypted[0].attendees).toEqual(['a@test.com']);
- expect(decrypted[0].reasoning).toBe('Reason 1');
- expect(decrypted[1].attendees).toEqual(['b@test.com']);
- expect(decrypted[1].reasoning).toBe('Reason 2');
+ expect(decrypted[0].attendees).toEqual(["a@test.com"]);
+ expect(decrypted[0].reasoning).toBe("Reason 1");
+ expect(decrypted[1].attendees).toEqual(["b@test.com"]);
+ expect(decrypted[1].reasoning).toBe("Reason 2");
});
- it('decrypts one-to-one object relations (rawInput)', () => {
+ it("decrypts one-to-one object relations (rawInput)", () => {
const rawInput = {
- id: 'raw-123',
- text: encryptField('User said: schedule meeting'),
+ id: "raw-123",
+ text: encryptField("User said: schedule meeting"),
};
// Simulate decrypting single related record
@@ -281,12 +281,12 @@ describe('db-encrypted helpers', () => {
text: decryptField(rawInput.text),
};
- expect(decrypted.text).toBe('User said: schedule meeting');
+ expect(decrypted.text).toBe("User said: schedule meeting");
});
- it('handles null relations', () => {
+ it("handles null relations", () => {
const parsedResult = {
- id: 'result-123',
+ id: "result-123",
rawInput: null,
parsedEvents: [],
};
@@ -296,19 +296,19 @@ describe('db-encrypted helpers', () => {
expect(parsedResult.parsedEvents).toEqual([]);
});
- it('handles deeply nested includes', () => {
+ it("handles deeply nested includes", () => {
const data = {
- id: 'result-123',
- routingReason: encryptField('Routed to calendar'),
+ id: "result-123",
+ routingReason: encryptField("Routed to calendar"),
rawInput: {
- id: 'raw-456',
- text: encryptField('Schedule meeting tomorrow'),
+ id: "raw-456",
+ text: encryptField("Schedule meeting tomorrow"),
},
parsedEvents: [
{
- id: 'event-789',
- attendees: encryptArrayField(['user@test.com']),
- reasoning: encryptField('Parsed as calendar event'),
+ id: "event-789",
+ attendees: encryptArrayField(["user@test.com"]),
+ reasoning: encryptField("Parsed as calendar event"),
},
],
};
@@ -328,20 +328,22 @@ describe('db-encrypted helpers', () => {
})),
};
- expect(decrypted.routingReason).toBe('Routed to calendar');
- expect(decrypted.rawInput.text).toBe('Schedule meeting tomorrow');
- expect(decrypted.parsedEvents[0].attendees).toEqual(['user@test.com']);
- expect(decrypted.parsedEvents[0].reasoning).toBe('Parsed as calendar event');
+ expect(decrypted.routingReason).toBe("Routed to calendar");
+ expect(decrypted.rawInput.text).toBe("Schedule meeting tomorrow");
+ expect(decrypted.parsedEvents[0].attendees).toEqual(["user@test.com"]);
+ expect(decrypted.parsedEvents[0].reasoning).toBe(
+ "Parsed as calendar event",
+ );
});
});
- describe('array field handling in queries', () => {
- it('attendees is encrypted as string, decrypted as string[]', () => {
+ describe("array field handling in queries", () => {
+ it("attendees is encrypted as string, decrypted as string[]", () => {
const originalAttendees = testArrays.emails;
// On write: array becomes encrypted string
const encrypted = encryptArrayField(originalAttendees);
- expect(typeof encrypted).toBe('string');
+ expect(typeof encrypted).toBe("string");
// On read: encrypted string becomes array
const decrypted = decryptArrayField(encrypted);
@@ -349,38 +351,49 @@ describe('db-encrypted helpers', () => {
expect(decrypted).toEqual(originalAttendees);
});
- it('handles empty attendees array', () => {
+ it("handles empty attendees array", () => {
const empty: string[] = [];
const encrypted = encryptArrayField(empty);
const decrypted = decryptArrayField(encrypted);
expect(decrypted).toEqual([]);
});
- it('handles attendees with special characters', () => {
- const specialAttendees = ['user+tag@example.com', "name.o'brien@test.com"];
+ it("handles attendees with special characters", () => {
+ const specialAttendees = [
+ "user+tag@example.com",
+ "name.o'brien@test.com",
+ ];
const encrypted = encryptArrayField(specialAttendees);
const decrypted = decryptArrayField(encrypted);
expect(decrypted).toEqual(specialAttendees);
});
});
- describe('model field configuration', () => {
- it('all expected models are configured', () => {
- const expectedModels = ['RawInput', 'ParsedEvent', 'ParsedTask', 'Event', 'ParsedResult'];
- expect(Object.keys(ENCRYPTED_FIELDS).sort()).toEqual(expectedModels.sort());
+ describe("model field configuration", () => {
+ it("all expected models are configured", () => {
+ const expectedModels = [
+ "RawInput",
+ "ParsedEvent",
+ "ParsedTask",
+ "Event",
+ "ParsedResult",
+ ];
+ expect(Object.keys(ENCRYPTED_FIELDS).sort()).toEqual(
+ expectedModels.sort(),
+ );
});
- it('RawInput encrypts text field only', () => {
- expect(ENCRYPTED_FIELDS.RawInput).toEqual(['text']);
+ it("RawInput encrypts text field only", () => {
+ expect(ENCRYPTED_FIELDS.RawInput).toEqual(["text"]);
});
- it('ParsedEvent encrypts attendees and reasoning', () => {
- expect(ENCRYPTED_FIELDS.ParsedEvent).toContain('attendees');
- expect(ENCRYPTED_FIELDS.ParsedEvent).toContain('reasoning');
+ it("ParsedEvent encrypts attendees and reasoning", () => {
+ expect(ENCRYPTED_FIELDS.ParsedEvent).toContain("attendees");
+ expect(ENCRYPTED_FIELDS.ParsedEvent).toContain("reasoning");
});
- it('Event encrypts attendees only', () => {
- expect(ENCRYPTED_FIELDS.Event).toEqual(['attendees']);
+ it("Event encrypts attendees only", () => {
+ expect(ENCRYPTED_FIELDS.Event).toEqual(["attendees"]);
});
});
});
diff --git a/lib/__tests__/encryption.test.ts b/lib/__tests__/encryption.test.ts
index 949d78db..a1d7b39a 100644
--- a/lib/__tests__/encryption.test.ts
+++ b/lib/__tests__/encryption.test.ts
@@ -1,119 +1,117 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import { randomBytes } from 'crypto';
-
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
+ testAlternativeKey,
testEncryptionKey,
testEncryptionKeyHex,
- testAlternativeKey,
testPlaintexts,
-} from '@/tests/fixtures';
+} from "@/tests/fixtures";
// Must import after env is stubbed for modules that read env at import time
-let encryptToken: typeof import('@/lib/encryption').encryptToken;
-let decryptToken: typeof import('@/lib/encryption').decryptToken;
-let getEncryptionKey: typeof import('@/lib/encryption').getEncryptionKey;
-type EncryptedToken = import('@/lib/encryption').EncryptedToken;
+let encryptToken: typeof import("@/lib/encryption").encryptToken;
+let decryptToken: typeof import("@/lib/encryption").decryptToken;
+type EncryptedToken = import("@/lib/encryption").EncryptedToken;
-describe('encryption', () => {
+describe("encryption", () => {
beforeEach(async () => {
- vi.stubEnv('ENCRYPTION_KEY', testEncryptionKey);
+ vi.stubEnv("ENCRYPTION_KEY", testEncryptionKey);
// Reset module cache to pick up new env
vi.resetModules();
- const mod = await import('@/lib/encryption');
+ const mod = await import("@/lib/encryption");
encryptToken = mod.encryptToken;
decryptToken = mod.decryptToken;
- getEncryptionKey = mod.getEncryptionKey;
});
afterEach(() => {
vi.unstubAllEnvs();
});
- describe('round-trip encryption/decryption', () => {
- it('encrypts and decrypts simple text correctly', () => {
+ describe("round-trip encryption/decryption", () => {
+ it("encrypts and decrypts simple text correctly", () => {
const encrypted = encryptToken(testPlaintexts.simple);
const decrypted = decryptToken(encrypted);
expect(decrypted).toBe(testPlaintexts.simple);
});
- it('handles empty string', () => {
+ it("handles empty string", () => {
const encrypted = encryptToken(testPlaintexts.empty);
const decrypted = decryptToken(encrypted);
expect(decrypted).toBe(testPlaintexts.empty);
});
- it('handles unicode characters correctly', () => {
+ it("handles unicode characters correctly", () => {
const encrypted = encryptToken(testPlaintexts.unicode);
const decrypted = decryptToken(encrypted);
expect(decrypted).toBe(testPlaintexts.unicode);
});
- it('handles emoji correctly', () => {
+ it("handles emoji correctly", () => {
const encrypted = encryptToken(testPlaintexts.emoji);
const decrypted = decryptToken(encrypted);
expect(decrypted).toBe(testPlaintexts.emoji);
});
- it('handles combining characters correctly', () => {
+ it("handles combining characters correctly", () => {
const encrypted = encryptToken(testPlaintexts.combiningChars);
const decrypted = decryptToken(encrypted);
expect(decrypted).toBe(testPlaintexts.combiningChars);
});
- it('handles long text correctly', () => {
+ it("handles long text correctly", () => {
const encrypted = encryptToken(testPlaintexts.longText);
const decrypted = decryptToken(encrypted);
expect(decrypted).toBe(testPlaintexts.longText);
});
});
- describe('token structure and format', () => {
- it('returns only expected fields (iv, encryptedData, authTag)', () => {
+ describe("token structure and format", () => {
+ it("returns only expected fields (iv, encryptedData, authTag)", () => {
const encrypted = encryptToken(testPlaintexts.simple);
const keys = Object.keys(encrypted).sort();
- expect(keys).toEqual(['authTag', 'encryptedData', 'iv']);
+ expect(keys).toEqual(["authTag", "encryptedData", "iv"]);
});
- it('all fields are valid base64 strings', () => {
+ it("all fields are valid base64 strings", () => {
const encrypted = encryptToken(testPlaintexts.simple);
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
- expect(typeof encrypted.iv).toBe('string');
- expect(typeof encrypted.encryptedData).toBe('string');
- expect(typeof encrypted.authTag).toBe('string');
+ expect(typeof encrypted.iv).toBe("string");
+ expect(typeof encrypted.encryptedData).toBe("string");
+ expect(typeof encrypted.authTag).toBe("string");
expect(encrypted.iv).toMatch(base64Regex);
expect(encrypted.encryptedData).toMatch(base64Regex);
expect(encrypted.authTag).toMatch(base64Regex);
});
- it('IV is 12 bytes when decoded', () => {
+ it("IV is 12 bytes when decoded", () => {
const encrypted = encryptToken(testPlaintexts.simple);
- const ivBuffer = Buffer.from(encrypted.iv, 'base64');
+ const ivBuffer = Buffer.from(encrypted.iv, "base64");
expect(ivBuffer.length).toBe(12);
});
- it('authTag is 16 bytes when decoded', () => {
+ it("authTag is 16 bytes when decoded", () => {
const encrypted = encryptToken(testPlaintexts.simple);
- const authTagBuffer = Buffer.from(encrypted.authTag, 'base64');
+ const authTagBuffer = Buffer.from(encrypted.authTag, "base64");
expect(authTagBuffer.length).toBe(16);
});
- it('encryptedData decodes to non-empty bytes for non-empty plaintext', () => {
+ it("encryptedData decodes to non-empty bytes for non-empty plaintext", () => {
const encrypted = encryptToken(testPlaintexts.simple);
- const dataBuffer = Buffer.from(encrypted.encryptedData, 'base64');
+ const dataBuffer = Buffer.from(encrypted.encryptedData, "base64");
expect(dataBuffer.length).toBeGreaterThan(0);
});
- it('encryptedData is valid for empty plaintext', () => {
+ it("encryptedData is valid for empty plaintext", () => {
const encrypted = encryptToken(testPlaintexts.empty);
// GCM may produce empty ciphertext for empty plaintext
- expect(() => Buffer.from(encrypted.encryptedData, 'base64')).not.toThrow();
+ expect(() =>
+ Buffer.from(encrypted.encryptedData, "base64"),
+ ).not.toThrow();
});
});
- describe('IV uniqueness', () => {
- it('produces different ciphertext for same plaintext', () => {
+ describe("IV uniqueness", () => {
+ it("produces different ciphertext for same plaintext", () => {
const encrypted1 = encryptToken(testPlaintexts.simple);
const encrypted2 = encryptToken(testPlaintexts.simple);
@@ -124,44 +122,46 @@ describe('encryption', () => {
});
});
- describe('key validation', () => {
- it('throws for non-32-byte key', () => {
- const shortKey = Buffer.from('too-short', 'utf8');
+ describe("key validation", () => {
+ it("throws for non-32-byte key", () => {
+ const shortKey = Buffer.from("too-short", "utf8");
// Node's crypto throws "Invalid key length" before our validateKey runs when key is provided directly
expect(() => encryptToken(testPlaintexts.simple, shortKey)).toThrow();
});
- it('throws when encryption key env is missing', async () => {
+ it("throws when encryption key env is missing", async () => {
vi.unstubAllEnvs();
vi.resetModules();
- const mod = await import('@/lib/encryption');
+ const mod = await import("@/lib/encryption");
- expect(() => mod.getEncryptionKey()).toThrow('No encryption key configured');
+ expect(() => mod.getEncryptionKey()).toThrow(
+ "No encryption key configured",
+ );
});
- it('accepts hex key from CALENDAR_CREDENTIAL_ENCRYPTION_KEY', async () => {
+ it("accepts hex key from CALENDAR_CREDENTIAL_ENCRYPTION_KEY", async () => {
vi.unstubAllEnvs();
- vi.stubEnv('CALENDAR_CREDENTIAL_ENCRYPTION_KEY', testEncryptionKeyHex);
+ vi.stubEnv("CALENDAR_CREDENTIAL_ENCRYPTION_KEY", testEncryptionKeyHex);
vi.resetModules();
- const mod = await import('@/lib/encryption');
+ const mod = await import("@/lib/encryption");
const key = mod.getEncryptionKey();
expect(key.length).toBe(32);
});
});
- describe('custom key parameter', () => {
- it('uses provided key instead of env key', () => {
+ describe("custom key parameter", () => {
+ it("uses provided key instead of env key", () => {
// Exactly 32 bytes (32 ASCII characters)
- const customKey = Buffer.from('abcdefghijklmnopqrstuvwxyz123456', 'utf8');
+ const customKey = Buffer.from("abcdefghijklmnopqrstuvwxyz123456", "utf8");
const encrypted = encryptToken(testPlaintexts.simple, customKey);
const decrypted = decryptToken(encrypted, customKey);
expect(decrypted).toBe(testPlaintexts.simple);
});
- it('decryption fails with wrong key', () => {
+ it("decryption fails with wrong key", () => {
// Both keys exactly 32 bytes (32 ASCII characters)
- const key1 = Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', 'utf8');
+ const key1 = Buffer.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456", "utf8");
const key2 = testAlternativeKey;
const encrypted = encryptToken(testPlaintexts.simple, key1);
@@ -170,46 +170,46 @@ describe('encryption', () => {
});
});
- describe('integrity (GCM auth tag)', () => {
- it('fails decryption with tampered authTag', () => {
+ describe("integrity (GCM auth tag)", () => {
+ it("fails decryption with tampered authTag", () => {
const encrypted = encryptToken(testPlaintexts.simple);
// Tamper with authTag
- const tamperedAuthTag = Buffer.from(encrypted.authTag, 'base64');
+ const tamperedAuthTag = Buffer.from(encrypted.authTag, "base64");
tamperedAuthTag[0] ^= 0xff; // Flip bits
const tampered: EncryptedToken = {
...encrypted,
- authTag: tamperedAuthTag.toString('base64'),
+ authTag: tamperedAuthTag.toString("base64"),
};
expect(() => decryptToken(tampered)).toThrow();
});
- it('fails decryption with tampered ciphertext', () => {
+ it("fails decryption with tampered ciphertext", () => {
const encrypted = encryptToken(testPlaintexts.simple);
// Tamper with encryptedData
- const tamperedData = Buffer.from(encrypted.encryptedData, 'base64');
+ const tamperedData = Buffer.from(encrypted.encryptedData, "base64");
if (tamperedData.length > 0) {
tamperedData[0] ^= 0xff;
}
const tampered: EncryptedToken = {
...encrypted,
- encryptedData: tamperedData.toString('base64'),
+ encryptedData: tamperedData.toString("base64"),
};
expect(() => decryptToken(tampered)).toThrow();
});
- it('fails decryption with tampered IV', () => {
+ it("fails decryption with tampered IV", () => {
const encrypted = encryptToken(testPlaintexts.simple);
// Tamper with IV
- const tamperedIv = Buffer.from(encrypted.iv, 'base64');
+ const tamperedIv = Buffer.from(encrypted.iv, "base64");
tamperedIv[0] ^= 0xff;
const tampered: EncryptedToken = {
...encrypted,
- iv: tamperedIv.toString('base64'),
+ iv: tamperedIv.toString("base64"),
};
expect(() => decryptToken(tampered)).toThrow();
diff --git a/lib/__tests__/events-shared.test.ts b/lib/__tests__/events-shared.test.ts
index 70f592da..21aba384 100644
--- a/lib/__tests__/events-shared.test.ts
+++ b/lib/__tests__/events-shared.test.ts
@@ -1,267 +1,282 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from "vitest";
// Mock timezone-utils to isolate payload shaping from timezone conversion logic
const mockDatetimeLocalToIso = vi.fn();
-vi.mock('@/lib/timezone-utils', () => ({
- datetimeLocalToIso: (local: string, tz: string) => mockDatetimeLocalToIso(local, tz),
+vi.mock("@/lib/timezone-utils", () => ({
+ datetimeLocalToIso: (local: string, tz: string) =>
+ mockDatetimeLocalToIso(local, tz),
}));
-import { prepareEventUpdatePayload } from '../events-shared';
+import { prepareEventUpdatePayload } from "../events-shared";
-describe('events-shared', () => {
+describe("events-shared", () => {
beforeEach(() => {
vi.clearAllMocks();
// Default mock: return valid ISO string for valid inputs
mockDatetimeLocalToIso.mockImplementation((local: string) => {
- if (!local || local.trim() === '') return null;
- return `2025-01-15T${local.replace('T', '')}:00.000Z`;
+ if (!local || local.trim() === "") return null;
+ return `2025-01-15T${local.replace("T", "")}:00.000Z`;
});
});
- describe('prepareEventUpdatePayload', () => {
-
- describe('title trimming / omission', () => {
- it('should trim whitespace from title', () => {
+ describe("prepareEventUpdatePayload", () => {
+ describe("title trimming / omission", () => {
+ it("should trim whitespace from title", () => {
const result = prepareEventUpdatePayload({
- title: ' Meeting ',
- timeZone: 'America/New_York',
+ title: " Meeting ",
+ timeZone: "America/New_York",
});
- expect(result.title).toBe('Meeting');
+ expect(result.title).toBe("Meeting");
});
- it('should omit title when it is only whitespace', () => {
+ it("should omit title when it is only whitespace", () => {
const result = prepareEventUpdatePayload({
- title: ' ',
- timeZone: 'America/New_York',
+ title: " ",
+ timeZone: "America/New_York",
});
expect(result.title).toBeUndefined();
});
- it('should include title when provided with content', () => {
+ it("should include title when provided with content", () => {
const result = prepareEventUpdatePayload({
- title: 'Team Standup',
- timeZone: 'America/New_York',
+ title: "Team Standup",
+ timeZone: "America/New_York",
});
- expect(result.title).toBe('Team Standup');
+ expect(result.title).toBe("Team Standup");
});
});
- describe('description nulling', () => {
- it('should set description to null when it is only whitespace', () => {
+ describe("description nulling", () => {
+ it("should set description to null when it is only whitespace", () => {
const result = prepareEventUpdatePayload({
- description: ' ',
- timeZone: 'America/New_York',
+ description: " ",
+ timeZone: "America/New_York",
});
expect(result.description).toBe(null);
});
- it('should trim and include description when it has content', () => {
+ it("should trim and include description when it has content", () => {
const result = prepareEventUpdatePayload({
- description: ' hi ',
- timeZone: 'America/New_York',
+ description: " hi ",
+ timeZone: "America/New_York",
});
- expect(result.description).toBe('hi');
+ expect(result.description).toBe("hi");
});
- it('should include description when provided', () => {
+ it("should include description when provided", () => {
const result = prepareEventUpdatePayload({
- description: 'Meeting notes',
- timeZone: 'America/New_York',
+ description: "Meeting notes",
+ timeZone: "America/New_York",
});
- expect(result.description).toBe('Meeting notes');
+ expect(result.description).toBe("Meeting notes");
});
});
- describe('attendees parsing + de-dupe', () => {
- it('should parse and de-duplicate attendees', () => {
+ describe("attendees parsing + de-dupe", () => {
+ it("should parse and de-duplicate attendees", () => {
const result = prepareEventUpdatePayload({
- attendees: ' a@a.com , b@b.com, a@a.com ,, ',
- timeZone: 'America/New_York',
+ attendees: " a@a.com , b@b.com, a@a.com ,, ",
+ timeZone: "America/New_York",
});
- expect(result.attendees).toEqual(['a@a.com', 'b@b.com']);
+ expect(result.attendees).toEqual(["a@a.com", "b@b.com"]);
});
- it('should return empty array when attendees is empty string', () => {
+ it("should return empty array when attendees is empty string", () => {
const result = prepareEventUpdatePayload({
- attendees: '',
- timeZone: 'America/New_York',
+ attendees: "",
+ timeZone: "America/New_York",
});
expect(result.attendees).toEqual([]);
});
- it('should handle single attendee', () => {
+ it("should handle single attendee", () => {
const result = prepareEventUpdatePayload({
- attendees: 'user@example.com',
- timeZone: 'America/New_York',
+ attendees: "user@example.com",
+ timeZone: "America/New_York",
});
- expect(result.attendees).toEqual(['user@example.com']);
+ expect(result.attendees).toEqual(["user@example.com"]);
});
- it('should trim whitespace from attendee emails', () => {
+ it("should trim whitespace from attendee emails", () => {
const result = prepareEventUpdatePayload({
- attendees: ' user1@example.com , user2@example.com ',
- timeZone: 'America/New_York',
+ attendees: " user1@example.com , user2@example.com ",
+ timeZone: "America/New_York",
});
- expect(result.attendees).toEqual(['user1@example.com', 'user2@example.com']);
+ expect(result.attendees).toEqual([
+ "user1@example.com",
+ "user2@example.com",
+ ]);
});
});
- describe('datetime behavior (invalid inputs)', () => {
- it('should omit start when datetimeLocalToIso returns null', () => {
+ describe("datetime behavior (invalid inputs)", () => {
+ it("should omit start when datetimeLocalToIso returns null", () => {
mockDatetimeLocalToIso.mockReturnValue(null);
const result = prepareEventUpdatePayload({
- startLocal: 'invalid-date',
- timeZone: 'America/New_York',
+ startLocal: "invalid-date",
+ timeZone: "America/New_York",
});
expect(result.start).toBeUndefined();
- expect(mockDatetimeLocalToIso).toHaveBeenCalledWith('invalid-date', 'America/New_York');
+ expect(mockDatetimeLocalToIso).toHaveBeenCalledWith(
+ "invalid-date",
+ "America/New_York",
+ );
});
- it('should omit end when datetimeLocalToIso returns null', () => {
+ it("should omit end when datetimeLocalToIso returns null", () => {
mockDatetimeLocalToIso.mockReturnValue(null);
const result = prepareEventUpdatePayload({
- endLocal: 'invalid-date',
- timeZone: 'America/New_York',
+ endLocal: "invalid-date",
+ timeZone: "America/New_York",
});
expect(result.end).toBeUndefined();
- expect(mockDatetimeLocalToIso).toHaveBeenCalledWith('invalid-date', 'America/New_York');
+ expect(mockDatetimeLocalToIso).toHaveBeenCalledWith(
+ "invalid-date",
+ "America/New_York",
+ );
});
- it('should include start when datetimeLocalToIso returns valid ISO string', () => {
- mockDatetimeLocalToIso.mockReturnValue('2025-01-15T14:30:00.000Z');
+ it("should include start when datetimeLocalToIso returns valid ISO string", () => {
+ mockDatetimeLocalToIso.mockReturnValue("2025-01-15T14:30:00.000Z");
const result = prepareEventUpdatePayload({
- startLocal: '2025-01-15T10:30',
- timeZone: 'America/New_York',
+ startLocal: "2025-01-15T10:30",
+ timeZone: "America/New_York",
});
- expect(result.start).toBe('2025-01-15T14:30:00.000Z');
- expect(mockDatetimeLocalToIso).toHaveBeenCalledWith('2025-01-15T10:30', 'America/New_York');
+ expect(result.start).toBe("2025-01-15T14:30:00.000Z");
+ expect(mockDatetimeLocalToIso).toHaveBeenCalledWith(
+ "2025-01-15T10:30",
+ "America/New_York",
+ );
});
- it('should include end when datetimeLocalToIso returns valid ISO string', () => {
- mockDatetimeLocalToIso.mockReturnValue('2025-01-15T15:30:00.000Z');
+ it("should include end when datetimeLocalToIso returns valid ISO string", () => {
+ mockDatetimeLocalToIso.mockReturnValue("2025-01-15T15:30:00.000Z");
const result = prepareEventUpdatePayload({
- endLocal: '2025-01-15T11:30',
- timeZone: 'America/New_York',
+ endLocal: "2025-01-15T11:30",
+ timeZone: "America/New_York",
});
- expect(result.end).toBe('2025-01-15T15:30:00.000Z');
- expect(mockDatetimeLocalToIso).toHaveBeenCalledWith('2025-01-15T11:30', 'America/New_York');
+ expect(result.end).toBe("2025-01-15T15:30:00.000Z");
+ expect(mockDatetimeLocalToIso).toHaveBeenCalledWith(
+ "2025-01-15T11:30",
+ "America/New_York",
+ );
});
- it('should handle both start and end with mixed valid/invalid', () => {
+ it("should handle both start and end with mixed valid/invalid", () => {
mockDatetimeLocalToIso
- .mockReturnValueOnce('2025-01-15T14:30:00.000Z') // start is valid
+ .mockReturnValueOnce("2025-01-15T14:30:00.000Z") // start is valid
.mockReturnValueOnce(null); // end is invalid
const result = prepareEventUpdatePayload({
- startLocal: '2025-01-15T10:30',
- endLocal: 'invalid',
- timeZone: 'America/New_York',
+ startLocal: "2025-01-15T10:30",
+ endLocal: "invalid",
+ timeZone: "America/New_York",
});
- expect(result.start).toBe('2025-01-15T14:30:00.000Z');
+ expect(result.start).toBe("2025-01-15T14:30:00.000Z");
expect(result.end).toBeUndefined();
});
});
- describe('pass-through fields', () => {
- it('should include timeZone when provided', () => {
+ describe("pass-through fields", () => {
+ it("should include timeZone when provided", () => {
const result = prepareEventUpdatePayload({
- timeZone: 'Europe/London',
+ timeZone: "Europe/London",
});
- expect(result.timeZone).toBe('Europe/London');
+ expect(result.timeZone).toBe("Europe/London");
});
- it('should include allDay when provided as true', () => {
+ it("should include allDay when provided as true", () => {
const result = prepareEventUpdatePayload({
allDay: true,
- timeZone: 'America/New_York',
+ timeZone: "America/New_York",
});
expect(result.allDay).toBe(true);
});
- it('should include allDay when provided as false', () => {
+ it("should include allDay when provided as false", () => {
const result = prepareEventUpdatePayload({
allDay: false,
- timeZone: 'America/New_York',
+ timeZone: "America/New_York",
});
expect(result.allDay).toBe(false);
});
- it('should trim and nullify recurrence when empty', () => {
+ it("should trim and nullify recurrence when empty", () => {
const result = prepareEventUpdatePayload({
- recurrence: ' ',
- timeZone: 'America/New_York',
+ recurrence: " ",
+ timeZone: "America/New_York",
});
expect(result.recurrence).toBe(null);
});
- it('should trim and include recurrence when provided', () => {
+ it("should trim and include recurrence when provided", () => {
const result = prepareEventUpdatePayload({
- recurrence: ' FREQ=DAILY ',
- timeZone: 'America/New_York',
+ recurrence: " FREQ=DAILY ",
+ timeZone: "America/New_York",
});
- expect(result.recurrence).toBe('FREQ=DAILY');
+ expect(result.recurrence).toBe("FREQ=DAILY");
});
- it('should include recurrence when provided with content', () => {
+ it("should include recurrence when provided with content", () => {
const result = prepareEventUpdatePayload({
- recurrence: 'FREQ=WEEKLY;BYDAY=MO',
- timeZone: 'America/New_York',
+ recurrence: "FREQ=WEEKLY;BYDAY=MO",
+ timeZone: "America/New_York",
});
- expect(result.recurrence).toBe('FREQ=WEEKLY;BYDAY=MO');
+ expect(result.recurrence).toBe("FREQ=WEEKLY;BYDAY=MO");
});
});
- describe('combined fields', () => {
- it('should handle all fields together', () => {
+ describe("combined fields", () => {
+ it("should handle all fields together", () => {
mockDatetimeLocalToIso
- .mockReturnValueOnce('2025-01-15T14:30:00.000Z')
- .mockReturnValueOnce('2025-01-15T15:30:00.000Z');
+ .mockReturnValueOnce("2025-01-15T14:30:00.000Z")
+ .mockReturnValueOnce("2025-01-15T15:30:00.000Z");
const result = prepareEventUpdatePayload({
- title: ' Team Meeting ',
- description: ' Weekly sync ',
- startLocal: '2025-01-15T10:30',
- endLocal: '2025-01-15T11:30',
- timeZone: 'America/New_York',
+ title: " Team Meeting ",
+ description: " Weekly sync ",
+ startLocal: "2025-01-15T10:30",
+ endLocal: "2025-01-15T11:30",
+ timeZone: "America/New_York",
allDay: false,
- attendees: ' a@a.com , b@b.com, a@a.com ',
- recurrence: ' FREQ=WEEKLY ',
+ attendees: " a@a.com , b@b.com, a@a.com ",
+ recurrence: " FREQ=WEEKLY ",
});
expect(result).toEqual({
- title: 'Team Meeting',
- description: 'Weekly sync',
- start: '2025-01-15T14:30:00.000Z',
- end: '2025-01-15T15:30:00.000Z',
- timeZone: 'America/New_York',
+ title: "Team Meeting",
+ description: "Weekly sync",
+ start: "2025-01-15T14:30:00.000Z",
+ end: "2025-01-15T15:30:00.000Z",
+ timeZone: "America/New_York",
allDay: false,
- attendees: ['a@a.com', 'b@b.com'],
- recurrence: 'FREQ=WEEKLY',
+ attendees: ["a@a.com", "b@b.com"],
+ recurrence: "FREQ=WEEKLY",
});
});
});
diff --git a/lib/__tests__/field-encryption.test.ts b/lib/__tests__/field-encryption.test.ts
index ae7dcafc..10f9a0d1 100644
--- a/lib/__tests__/field-encryption.test.ts
+++ b/lib/__tests__/field-encryption.test.ts
@@ -1,26 +1,25 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
+ testArrays,
testEncryptionKey,
testPlaintexts,
- testArrays,
-} from '@/tests/fixtures';
+} from "@/tests/fixtures";
// Must import after env is stubbed
-let isEncryptedValue: typeof import('@/lib/field-encryption').isEncryptedValue;
-let encryptField: typeof import('@/lib/field-encryption').encryptField;
-let decryptField: typeof import('@/lib/field-encryption').decryptField;
-let encryptArrayField: typeof import('@/lib/field-encryption').encryptArrayField;
-let decryptArrayField: typeof import('@/lib/field-encryption').decryptArrayField;
-let isArrayField: typeof import('@/lib/field-encryption').isArrayField;
-let getEncryptedFields: typeof import('@/lib/field-encryption').getEncryptedFields;
-let ENCRYPTED_FIELDS: typeof import('@/lib/field-encryption').ENCRYPTED_FIELDS;
-
-describe('field-encryption', () => {
+let isEncryptedValue: typeof import("@/lib/field-encryption").isEncryptedValue;
+let encryptField: typeof import("@/lib/field-encryption").encryptField;
+let decryptField: typeof import("@/lib/field-encryption").decryptField;
+let encryptArrayField: typeof import("@/lib/field-encryption").encryptArrayField;
+let decryptArrayField: typeof import("@/lib/field-encryption").decryptArrayField;
+let isArrayField: typeof import("@/lib/field-encryption").isArrayField;
+let getEncryptedFields: typeof import("@/lib/field-encryption").getEncryptedFields;
+let ENCRYPTED_FIELDS: typeof import("@/lib/field-encryption").ENCRYPTED_FIELDS;
+
+describe("field-encryption", () => {
beforeEach(async () => {
- vi.stubEnv('ENCRYPTION_KEY', testEncryptionKey);
+ vi.stubEnv("ENCRYPTION_KEY", testEncryptionKey);
vi.resetModules();
- const mod = await import('@/lib/field-encryption');
+ const mod = await import("@/lib/field-encryption");
isEncryptedValue = mod.isEncryptedValue;
encryptField = mod.encryptField;
decryptField = mod.decryptField;
@@ -35,222 +34,224 @@ describe('field-encryption', () => {
vi.unstubAllEnvs();
});
- describe('isEncryptedValue', () => {
- it('returns true for valid EncryptedToken JSON', () => {
+ describe("isEncryptedValue", () => {
+ it("returns true for valid EncryptedToken JSON", () => {
const encrypted = encryptField(testPlaintexts.simple);
expect(isEncryptedValue(encrypted)).toBe(true);
});
- it('returns false for plain string', () => {
+ it("returns false for plain string", () => {
expect(isEncryptedValue(testPlaintexts.simple)).toBe(false);
});
- it('returns false for empty string', () => {
- expect(isEncryptedValue('')).toBe(false);
+ it("returns false for empty string", () => {
+ expect(isEncryptedValue("")).toBe(false);
});
- it('returns false for non-token JSON object', () => {
+ it("returns false for non-token JSON object", () => {
expect(isEncryptedValue('{"a":1}')).toBe(false);
expect(isEncryptedValue('{"key":"value"}')).toBe(false);
});
- it('returns false for invalid JSON', () => {
- expect(isEncryptedValue('not json at all')).toBe(false);
- expect(isEncryptedValue('{invalid}')).toBe(false);
+ it("returns false for invalid JSON", () => {
+ expect(isEncryptedValue("not json at all")).toBe(false);
+ expect(isEncryptedValue("{invalid}")).toBe(false);
});
- it('returns false for non-string values', () => {
+ it("returns false for non-string values", () => {
expect(isEncryptedValue(null)).toBe(false);
expect(isEncryptedValue(undefined)).toBe(false);
expect(isEncryptedValue(123)).toBe(false);
- expect(isEncryptedValue({ iv: 'a', encryptedData: 'b', authTag: 'c' })).toBe(false);
+ expect(
+ isEncryptedValue({ iv: "a", encryptedData: "b", authTag: "c" }),
+ ).toBe(false);
});
- it('returns false for partial token structure', () => {
+ it("returns false for partial token structure", () => {
expect(isEncryptedValue('{"iv":"abc"}')).toBe(false);
expect(isEncryptedValue('{"iv":"a","encryptedData":"b"}')).toBe(false);
});
});
- describe('encryptField / decryptField', () => {
- it('encryptField returns parseable EncryptedToken JSON', () => {
+ describe("encryptField / decryptField", () => {
+ it("encryptField returns parseable EncryptedToken JSON", () => {
const encrypted = encryptField(testPlaintexts.simple);
- expect(typeof encrypted).toBe('string');
+ expect(typeof encrypted).toBe("string");
- const parsed = JSON.parse(encrypted);
- expect(parsed).toHaveProperty('iv');
- expect(parsed).toHaveProperty('encryptedData');
- expect(parsed).toHaveProperty('authTag');
+ const parsed: unknown = JSON.parse(encrypted);
+ expect(parsed).toHaveProperty("iv");
+ expect(parsed).toHaveProperty("encryptedData");
+ expect(parsed).toHaveProperty("authTag");
});
- it('round-trip encryption/decryption works', () => {
+ it("round-trip encryption/decryption works", () => {
const encrypted = encryptField(testPlaintexts.simple);
const decrypted = decryptField(encrypted);
expect(decrypted).toBe(testPlaintexts.simple);
});
- it('handles unicode correctly', () => {
+ it("handles unicode correctly", () => {
const encrypted = encryptField(testPlaintexts.unicode);
const decrypted = decryptField(encrypted);
expect(decrypted).toBe(testPlaintexts.unicode);
});
- it('handles JSON content correctly', () => {
+ it("handles JSON content correctly", () => {
const encrypted = encryptField(testPlaintexts.json);
const decrypted = decryptField(encrypted);
expect(decrypted).toBe(testPlaintexts.json);
});
});
- describe('decryptField backward compatibility', () => {
- it('returns unencrypted string as-is', () => {
+ describe("decryptField backward compatibility", () => {
+ it("returns unencrypted string as-is", () => {
const plaintext = testPlaintexts.simple;
const result = decryptField(plaintext);
expect(result).toBe(plaintext);
});
- it('returns non-token JSON string as-is', () => {
+ it("returns non-token JSON string as-is", () => {
const jsonString = '{"a":1}';
const result = decryptField(jsonString);
expect(result).toBe(jsonString);
});
- it('returns empty string as-is', () => {
- const result = decryptField('');
- expect(result).toBe('');
+ it("returns empty string as-is", () => {
+ const result = decryptField("");
+ expect(result).toBe("");
});
});
- describe('encryptArrayField / decryptArrayField', () => {
- it('round-trip encryption/decryption works for arrays', () => {
+ describe("encryptArrayField / decryptArrayField", () => {
+ it("round-trip encryption/decryption works for arrays", () => {
const encrypted = encryptArrayField(testArrays.emails);
const decrypted = decryptArrayField(encrypted);
expect(decrypted).toEqual(testArrays.emails);
});
- it('handles empty array', () => {
+ it("handles empty array", () => {
const encrypted = encryptArrayField(testArrays.empty);
const decrypted = decryptArrayField(encrypted);
expect(decrypted).toEqual([]);
});
- it('handles unicode in array elements', () => {
+ it("handles unicode in array elements", () => {
const encrypted = encryptArrayField(testArrays.unicode);
const decrypted = decryptArrayField(encrypted);
expect(decrypted).toEqual(testArrays.unicode);
});
- it('encrypted array is a string (not an array)', () => {
+ it("encrypted array is a string (not an array)", () => {
const encrypted = encryptArrayField(testArrays.emails);
- expect(typeof encrypted).toBe('string');
+ expect(typeof encrypted).toBe("string");
expect(Array.isArray(encrypted)).toBe(false);
});
});
- describe('decryptArrayField null/empty handling', () => {
- it('returns empty array for null', () => {
+ describe("decryptArrayField null/empty handling", () => {
+ it("returns empty array for null", () => {
expect(decryptArrayField(null)).toEqual([]);
});
- it('returns empty array for undefined', () => {
+ it("returns empty array for undefined", () => {
expect(decryptArrayField(undefined)).toEqual([]);
});
- it('returns empty array for empty string', () => {
- expect(decryptArrayField('')).toEqual([]);
+ it("returns empty array for empty string", () => {
+ expect(decryptArrayField("")).toEqual([]);
});
});
- describe('decryptArrayField backward compatibility', () => {
- it('handles legacy unencrypted JSON array string', () => {
+ describe("decryptArrayField backward compatibility", () => {
+ it("handles legacy unencrypted JSON array string", () => {
const legacyValue = '["alice@example.com","bob@example.com"]';
const result = decryptArrayField(legacyValue);
- expect(result).toEqual(['alice@example.com', 'bob@example.com']);
+ expect(result).toEqual(["alice@example.com", "bob@example.com"]);
});
- it('handles legacy empty array string', () => {
- const legacyValue = '[]';
+ it("handles legacy empty array string", () => {
+ const legacyValue = "[]";
const result = decryptArrayField(legacyValue);
expect(result).toEqual([]);
});
- it('returns empty array for malformed JSON (no throw)', () => {
- expect(decryptArrayField('not json')).toEqual([]);
- expect(decryptArrayField('{invalid}')).toEqual([]);
+ it("returns empty array for malformed JSON (no throw)", () => {
+ expect(decryptArrayField("not json")).toEqual([]);
+ expect(decryptArrayField("{invalid}")).toEqual([]);
expect(decryptArrayField('["unclosed')).toEqual([]);
});
- it('returns empty array for non-array JSON', () => {
+ it("returns empty array for non-array JSON", () => {
expect(decryptArrayField('{"a":1}')).toEqual([]);
expect(decryptArrayField('"just a string"')).toEqual([]);
- expect(decryptArrayField('123')).toEqual([]);
+ expect(decryptArrayField("123")).toEqual([]);
});
});
- describe('isArrayField', () => {
- it('returns true for attendees field', () => {
- expect(isArrayField('attendees')).toBe(true);
+ describe("isArrayField", () => {
+ it("returns true for attendees field", () => {
+ expect(isArrayField("attendees")).toBe(true);
});
- it('returns false for non-array encrypted fields', () => {
- expect(isArrayField('text')).toBe(false);
- expect(isArrayField('reasoning')).toBe(false);
- expect(isArrayField('routingReason')).toBe(false);
+ it("returns false for non-array encrypted fields", () => {
+ expect(isArrayField("text")).toBe(false);
+ expect(isArrayField("reasoning")).toBe(false);
+ expect(isArrayField("routingReason")).toBe(false);
});
- it('returns false for unknown fields', () => {
- expect(isArrayField('unknownField')).toBe(false);
- expect(isArrayField('')).toBe(false);
+ it("returns false for unknown fields", () => {
+ expect(isArrayField("unknownField")).toBe(false);
+ expect(isArrayField("")).toBe(false);
});
});
- describe('getEncryptedFields', () => {
- it('returns correct fields for RawInput', () => {
- const fields = getEncryptedFields('RawInput');
- expect(fields).toEqual(['text']);
+ describe("getEncryptedFields", () => {
+ it("returns correct fields for RawInput", () => {
+ const fields = getEncryptedFields("RawInput");
+ expect(fields).toEqual(["text"]);
});
- it('returns correct fields for ParsedEvent', () => {
- const fields = getEncryptedFields('ParsedEvent');
- expect(fields).toEqual(['attendees', 'reasoning']);
+ it("returns correct fields for ParsedEvent", () => {
+ const fields = getEncryptedFields("ParsedEvent");
+ expect(fields).toEqual(["attendees", "reasoning"]);
});
- it('returns correct fields for ParsedTask', () => {
- const fields = getEncryptedFields('ParsedTask');
- expect(fields).toEqual(['reasoning']);
+ it("returns correct fields for ParsedTask", () => {
+ const fields = getEncryptedFields("ParsedTask");
+ expect(fields).toEqual(["reasoning"]);
});
- it('returns correct fields for Event', () => {
- const fields = getEncryptedFields('Event');
- expect(fields).toEqual(['attendees']);
+ it("returns correct fields for Event", () => {
+ const fields = getEncryptedFields("Event");
+ expect(fields).toEqual(["attendees"]);
});
- it('returns correct fields for ParsedResult', () => {
- const fields = getEncryptedFields('ParsedResult');
- expect(fields).toEqual(['routingReason']);
+ it("returns correct fields for ParsedResult", () => {
+ const fields = getEncryptedFields("ParsedResult");
+ expect(fields).toEqual(["routingReason"]);
});
- it('returns undefined for unknown model', () => {
- const fields = getEncryptedFields('UnknownModel');
+ it("returns undefined for unknown model", () => {
+ const fields = getEncryptedFields("UnknownModel");
expect(fields).toBeUndefined();
});
});
- describe('ENCRYPTED_FIELDS configuration snapshot', () => {
- it('has exactly the expected models and fields', () => {
+ describe("ENCRYPTED_FIELDS configuration snapshot", () => {
+ it("has exactly the expected models and fields", () => {
// Explicit comparison to catch any changes
const expectedConfig = {
- RawInput: ['text'],
- ParsedEvent: ['attendees', 'reasoning'],
- ParsedTask: ['reasoning'],
- Event: ['attendees'],
- ParsedResult: ['routingReason'],
+ RawInput: ["text"],
+ ParsedEvent: ["attendees", "reasoning"],
+ ParsedTask: ["reasoning"],
+ Event: ["attendees"],
+ ParsedResult: ["routingReason"],
};
expect(ENCRYPTED_FIELDS).toEqual(expectedConfig);
});
- it('has exactly 5 encrypted models', () => {
+ it("has exactly 5 encrypted models", () => {
expect(Object.keys(ENCRYPTED_FIELDS)).toHaveLength(5);
});
});
diff --git a/lib/__tests__/parsing-auto-confirm.test.ts b/lib/__tests__/parsing-auto-confirm.test.ts
index 55e01969..42786dd8 100644
--- a/lib/__tests__/parsing-auto-confirm.test.ts
+++ b/lib/__tests__/parsing-auto-confirm.test.ts
@@ -1,93 +1,103 @@
-import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
-import { LifeCycle, IntentType } from '@prisma/client';
-
-import { prismaMock } from '@/tests/mocks/prisma';
+import { IntentType, LifeCycle } from "@prisma/client";
+import {
+ afterEach,
+ beforeEach,
+ describe,
+ expect,
+ it,
+ type Mock,
+ vi,
+} from "vitest";
+import { prismaMock } from "@/tests/mocks/prisma";
// Mock server-only (required in test environment)
-vi.mock('server-only', () => ({}));
+vi.mock("server-only", () => ({}));
// Mock external dependencies before importing the module under test
-vi.mock('@sentry/nextjs', () => ({
- startSpan: (_ctx: unknown, fn: (...args: unknown[]) => unknown) => fn(undefined),
+vi.mock("@sentry/nextjs", () => ({
+ startSpan: (_ctx: unknown, fn: (...args: unknown[]) => unknown) =>
+ fn(undefined),
}));
-vi.mock('ai', () => ({
+vi.mock("ai", () => ({
generateObject: vi.fn(),
}));
-vi.mock('@/lib/ai/factory', () => ({
+vi.mock("@/lib/ai/factory", () => ({
getParserModel: vi.fn(),
}));
-vi.mock('@/lib/audit-log', () => ({
+vi.mock("@/lib/audit-log", () => ({
createBulkAuditLogs: vi.fn(),
}));
-vi.mock('@/lib/calendar-sync-trigger', () => ({
+vi.mock("@/lib/calendar-sync-trigger", () => ({
triggerEventSync: vi.fn(() => Promise.resolve()),
}));
-vi.mock('@/lib/tasks-sync-trigger', () => ({
+vi.mock("@/lib/tasks-sync-trigger", () => ({
triggerTaskSync: vi.fn(() => Promise.resolve()),
}));
-vi.mock('@/lib/storage', () => ({
- generatePresignedDownloadUrl: vi.fn(() => Promise.resolve('https://example.com/presigned')),
+vi.mock("@/lib/storage", () => ({
+ generatePresignedDownloadUrl: vi.fn(() =>
+ Promise.resolve("https://example.com/presigned"),
+ ),
}));
-vi.mock('@/lib/ai/prompts/system', () => ({
- buildParsingSystemPrompt: vi.fn(() => 'system prompt'),
+vi.mock("@/lib/ai/prompts/system", () => ({
+ buildParsingSystemPrompt: vi.fn(() => "system prompt"),
}));
-vi.mock('@/lib/ai/prompts/context', () => ({
+vi.mock("@/lib/ai/prompts/context", () => ({
deriveTemporalContext: vi.fn(() => ({
- timeZone: 'America/New_York',
+ timeZone: "America/New_York",
fallbackUsed: false,
- isoDate: '2024-06-15',
- localTime: '12:00:00',
- weekdayName: 'Saturday',
+ isoDate: "2024-06-15",
+ localTime: "12:00:00",
+ weekdayName: "Saturday",
weekdayNumber: 6,
- monthName: 'June',
+ monthName: "June",
monthNumber: 6,
year: 2024,
})),
}));
-import { parseUserInput } from '@/lib/parsing';
-import { generateObject } from 'ai';
-import { getParserModel } from '@/lib/ai/factory';
-import { createBulkAuditLogs } from '@/lib/audit-log';
+import { generateObject } from "ai";
+import { getParserModel } from "@/lib/ai/factory";
+import { createBulkAuditLogs } from "@/lib/audit-log";
+import { parseUserInput } from "@/lib/parsing";
-describe('parseUserInput auto-confirm', () => {
- const mockUserId = 'user-123';
- const mockRawInputId = 'raw-input-456';
- const mockParsedResultId = 'parsed-result-789';
- const mockEventId = 'event-001';
- const mockTaskId = 'task-001';
+describe("parseUserInput auto-confirm", () => {
+ const mockUserId = "user-123";
+ const mockRawInputId = "raw-input-456";
+ const mockParsedResultId = "parsed-result-789";
+ const mockEventId = "event-001";
+ const mockTaskId = "task-001";
const baseParams = {
userId: mockUserId,
rawInput: {
id: mockRawInputId,
userId: mockUserId,
- text: 'Meeting tomorrow at 3pm',
- source: 'CHAT' as const,
- requestId: 'req-123',
+ text: "Meeting tomorrow at 3pm",
+ source: "CHAT" as const,
+ requestId: "req-123",
createdAt: new Date(),
attachments: [],
},
- userTimeZone: 'America/New_York',
- requestId: 'req-123',
+ userTimeZone: "America/New_York",
+ requestId: "req-123",
attachments: [],
weekStartDay: 1 as const,
};
const mockParserModel = {
- model: { modelId: 'test-model' },
- config: { modelName: 'test-model' },
+ model: { modelId: "test-model" },
+ config: { modelName: "test-model" },
decision: {
- provider: 'openai',
- reason: 'test',
+ provider: "openai",
+ reason: "test",
fallbackApplied: false,
},
};
@@ -101,8 +111,8 @@ describe('parseUserInput auto-confirm', () => {
vi.resetAllMocks();
});
- describe('no items parsed', () => {
- it('does not trigger auto-confirm when no events or tasks are parsed', async () => {
+ describe("no items parsed", () => {
+ it("does not trigger auto-confirm when no events or tasks are parsed", async () => {
// Arrange: AI returns empty arrays
(generateObject as Mock).mockResolvedValue({
object: { events: [], tasks: [] },
@@ -119,8 +129,8 @@ describe('parseUserInput auto-confirm', () => {
userId: mockUserId,
rawInputId: mockRawInputId,
intent: IntentType.CREATE,
- aiProvider: 'openai',
- routingReason: 'test',
+ aiProvider: "openai",
+ routingReason: "test",
processingDurationMs: 100,
createdAt: new Date(),
} as never);
@@ -141,21 +151,23 @@ describe('parseUserInput auto-confirm', () => {
});
});
- describe('auto-confirm disabled', () => {
- it('does not trigger auto-confirm when disabled even with items present', async () => {
+ describe("auto-confirm disabled", () => {
+ it("does not trigger auto-confirm when disabled even with items present", async () => {
// Arrange: AI returns items
(generateObject as Mock).mockResolvedValue({
object: {
- events: [{
- title: 'Meeting',
- description: 'Team sync',
- start: '2024-06-16T15:00:00',
- end: '2024-06-16T16:00:00',
- timeZone: 'America/New_York',
- allDay: false,
- confidence: 0.95,
- reasoning: 'Parsed from input',
- }],
+ events: [
+ {
+ title: "Meeting",
+ description: "Team sync",
+ start: "2024-06-16T15:00:00",
+ end: "2024-06-16T16:00:00",
+ timeZone: "America/New_York",
+ allDay: false,
+ confidence: 0.95,
+ reasoning: "Parsed from input",
+ },
+ ],
tasks: [],
},
});
@@ -170,8 +182,8 @@ describe('parseUserInput auto-confirm', () => {
userId: mockUserId,
rawInputId: mockRawInputId,
intent: IntentType.CREATE,
- aiProvider: 'openai',
- routingReason: 'test',
+ aiProvider: "openai",
+ routingReason: "test",
processingDurationMs: 100,
createdAt: new Date(),
} as never);
@@ -181,16 +193,16 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Meeting',
- description: 'Team sync',
- start: new Date('2024-06-16T19:00:00Z'),
- end: new Date('2024-06-16T20:00:00Z'),
- timeZone: 'America/New_York',
+ title: "Meeting",
+ description: "Team sync",
+ start: new Date("2024-06-16T19:00:00Z"),
+ end: new Date("2024-06-16T20:00:00Z"),
+ timeZone: "America/New_York",
allDay: false,
attendees: [],
recurrence: null,
confidence: 0.95,
- reasoning: 'Parsed from input',
+ reasoning: "Parsed from input",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
@@ -213,30 +225,34 @@ describe('parseUserInput auto-confirm', () => {
});
});
- describe('auto-confirm enabled with success', () => {
- it('auto-confirms items and creates audit logs with exact shape', async () => {
+ describe("auto-confirm enabled with success", () => {
+ it("auto-confirms items and creates audit logs with exact shape", async () => {
// Arrange: AI returns event and task
(generateObject as Mock).mockResolvedValue({
object: {
- events: [{
- title: 'Meeting',
- description: 'Team sync',
- start: '2024-06-16T15:00:00',
- end: '2024-06-16T16:00:00',
- timeZone: 'America/New_York',
- allDay: false,
- confidence: 0.95,
- reasoning: 'Parsed from input',
- }],
- tasks: [{
- title: 'Follow up',
- description: 'Send notes',
- dueDate: '2024-06-17T12:00:00',
- timeZone: 'America/New_York',
- priority: 'HIGH',
- confidence: 0.9,
- reasoning: 'Derived task',
- }],
+ events: [
+ {
+ title: "Meeting",
+ description: "Team sync",
+ start: "2024-06-16T15:00:00",
+ end: "2024-06-16T16:00:00",
+ timeZone: "America/New_York",
+ allDay: false,
+ confidence: 0.95,
+ reasoning: "Parsed from input",
+ },
+ ],
+ tasks: [
+ {
+ title: "Follow up",
+ description: "Send notes",
+ dueDate: "2024-06-17T12:00:00",
+ timeZone: "America/New_York",
+ priority: "HIGH",
+ confidence: 0.9,
+ reasoning: "Derived task",
+ },
+ ],
},
});
@@ -245,16 +261,16 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Meeting',
- description: 'Team sync',
- start: new Date('2024-06-16T19:00:00Z'),
- end: new Date('2024-06-16T20:00:00Z'),
- timeZone: 'America/New_York',
+ title: "Meeting",
+ description: "Team sync",
+ start: new Date("2024-06-16T19:00:00Z"),
+ end: new Date("2024-06-16T20:00:00Z"),
+ timeZone: "America/New_York",
allDay: false,
attendees: [],
recurrence: null,
confidence: 0.95,
- reasoning: 'Parsed from input',
+ reasoning: "Parsed from input",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
@@ -265,13 +281,13 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Follow up',
- description: 'Send notes',
- dueDate: new Date('2024-06-17T16:00:00Z'),
- timeZone: 'America/New_York',
- priority: 'HIGH',
+ title: "Follow up",
+ description: "Send notes",
+ dueDate: new Date("2024-06-17T16:00:00Z"),
+ timeZone: "America/New_York",
+ priority: "HIGH",
confidence: 0.9,
- reasoning: 'Derived task',
+ reasoning: "Derived task",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
@@ -282,8 +298,8 @@ describe('parseUserInput auto-confirm', () => {
userId: mockUserId,
rawInputId: mockRawInputId,
intent: IntentType.CREATE,
- aiProvider: 'openai',
- routingReason: 'test',
+ aiProvider: "openai",
+ routingReason: "test",
processingDurationMs: 100,
createdAt: new Date(),
};
@@ -294,8 +310,12 @@ describe('parseUserInput auto-confirm', () => {
return result;
});
- prismaMock.parsedResult.create.mockResolvedValue(mockParsedResult as never);
- prismaMock.parsedEvent.create.mockResolvedValue(mockCreatedEvent as never);
+ prismaMock.parsedResult.create.mockResolvedValue(
+ mockParsedResult as never,
+ );
+ prismaMock.parsedEvent.create.mockResolvedValue(
+ mockCreatedEvent as never,
+ );
prismaMock.parsedTask.create.mockResolvedValue(mockCreatedTask as never);
// acceptParsedItems calls: findFirst returns the result with items
@@ -313,8 +333,12 @@ describe('parseUserInput auto-confirm', () => {
prismaMock.parsedEvent.updateMany.mockResolvedValue({ count: 1 });
prismaMock.parsedTask.updateMany.mockResolvedValue({ count: 1 });
- prismaMock.event.create.mockResolvedValue({ id: 'created-event-1' } as never);
- prismaMock.task.create.mockResolvedValue({ id: 'created-task-1' } as never);
+ prismaMock.event.create.mockResolvedValue({
+ id: "created-event-1",
+ } as never);
+ prismaMock.task.create.mockResolvedValue({
+ id: "created-task-1",
+ } as never);
(createBulkAuditLogs as Mock).mockResolvedValue({ count: 2 });
@@ -342,32 +366,32 @@ describe('parseUserInput auto-confirm', () => {
// 1. In acceptParsedItems for MANUAL_ACCEPT (Event/Task)
// 2. In parseUserInput for AUTO_CONFIRM (ParsedEvent/ParsedTask)
expect(createBulkAuditLogs).toHaveBeenCalledTimes(2);
-
+
// First call: MANUAL_ACCEPT audit logs for created entities (from acceptParsedItems)
expect(createBulkAuditLogs).toHaveBeenNthCalledWith(1, [
{
userId: mockUserId,
- entity: 'Event',
- entityId: 'created-event-1',
- action: 'MANUAL_ACCEPT',
+ entity: "Event",
+ entityId: "created-event-1",
+ action: "MANUAL_ACCEPT",
metadata: { parsedResultId: mockParsedResultId },
},
{
userId: mockUserId,
- entity: 'Task',
- entityId: 'created-task-1',
- action: 'MANUAL_ACCEPT',
+ entity: "Task",
+ entityId: "created-task-1",
+ action: "MANUAL_ACCEPT",
metadata: { parsedResultId: mockParsedResultId },
},
]);
-
+
// Second call: AUTO_CONFIRM audit logs for parsed items (from parseUserInput)
expect(createBulkAuditLogs).toHaveBeenNthCalledWith(2, [
{
userId: mockUserId,
- entity: 'ParsedEvent',
+ entity: "ParsedEvent",
entityId: mockEventId,
- action: 'AUTO_CONFIRM',
+ action: "AUTO_CONFIRM",
metadata: {
confidence: 0.95,
parsedResultId: mockParsedResultId,
@@ -375,9 +399,9 @@ describe('parseUserInput auto-confirm', () => {
},
{
userId: mockUserId,
- entity: 'ParsedTask',
+ entity: "ParsedTask",
entityId: mockTaskId,
- action: 'AUTO_CONFIRM',
+ action: "AUTO_CONFIRM",
metadata: {
confidence: 0.9,
parsedResultId: mockParsedResultId,
@@ -394,30 +418,34 @@ describe('parseUserInput auto-confirm', () => {
});
});
- describe('lifecycle state validation', () => {
- it('returns ACCEPTED lifecycle for items when auto-confirm succeeds', async () => {
+ describe("lifecycle state validation", () => {
+ it("returns ACCEPTED lifecycle for items when auto-confirm succeeds", async () => {
// Arrange: AI returns event and task
(generateObject as Mock).mockResolvedValue({
object: {
- events: [{
- title: 'Meeting',
- description: 'Team sync',
- start: '2024-06-16T15:00:00',
- end: '2024-06-16T16:00:00',
- timeZone: 'America/New_York',
- allDay: false,
- confidence: 0.95,
- reasoning: 'Parsed from input',
- }],
- tasks: [{
- title: 'Follow up',
- description: 'Send notes',
- dueDate: '2024-06-17T12:00:00',
- timeZone: 'America/New_York',
- priority: 'HIGH',
- confidence: 0.9,
- reasoning: 'Derived task',
- }],
+ events: [
+ {
+ title: "Meeting",
+ description: "Team sync",
+ start: "2024-06-16T15:00:00",
+ end: "2024-06-16T16:00:00",
+ timeZone: "America/New_York",
+ allDay: false,
+ confidence: 0.95,
+ reasoning: "Parsed from input",
+ },
+ ],
+ tasks: [
+ {
+ title: "Follow up",
+ description: "Send notes",
+ dueDate: "2024-06-17T12:00:00",
+ timeZone: "America/New_York",
+ priority: "HIGH",
+ confidence: 0.9,
+ reasoning: "Derived task",
+ },
+ ],
},
});
@@ -426,16 +454,16 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Meeting',
- description: 'Team sync',
- start: new Date('2024-06-16T19:00:00Z'),
- end: new Date('2024-06-16T20:00:00Z'),
- timeZone: 'America/New_York',
+ title: "Meeting",
+ description: "Team sync",
+ start: new Date("2024-06-16T19:00:00Z"),
+ end: new Date("2024-06-16T20:00:00Z"),
+ timeZone: "America/New_York",
allDay: false,
attendees: [],
recurrence: null,
confidence: 0.95,
- reasoning: 'Parsed from input',
+ reasoning: "Parsed from input",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
@@ -446,13 +474,13 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Follow up',
- description: 'Send notes',
- dueDate: new Date('2024-06-17T16:00:00Z'),
- timeZone: 'America/New_York',
- priority: 'HIGH',
+ title: "Follow up",
+ description: "Send notes",
+ dueDate: new Date("2024-06-17T16:00:00Z"),
+ timeZone: "America/New_York",
+ priority: "HIGH",
confidence: 0.9,
- reasoning: 'Derived task',
+ reasoning: "Derived task",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
@@ -463,8 +491,8 @@ describe('parseUserInput auto-confirm', () => {
userId: mockUserId,
rawInputId: mockRawInputId,
intent: IntentType.CREATE,
- aiProvider: 'openai',
- routingReason: 'test',
+ aiProvider: "openai",
+ routingReason: "test",
processingDurationMs: 100,
createdAt: new Date(),
};
@@ -475,8 +503,12 @@ describe('parseUserInput auto-confirm', () => {
return result;
});
- prismaMock.parsedResult.create.mockResolvedValue(mockParsedResult as never);
- prismaMock.parsedEvent.create.mockResolvedValue(mockCreatedEvent as never);
+ prismaMock.parsedResult.create.mockResolvedValue(
+ mockParsedResult as never,
+ );
+ prismaMock.parsedEvent.create.mockResolvedValue(
+ mockCreatedEvent as never,
+ );
prismaMock.parsedTask.create.mockResolvedValue(mockCreatedTask as never);
// acceptParsedItems calls: findFirst returns the result with items
@@ -494,8 +526,12 @@ describe('parseUserInput auto-confirm', () => {
prismaMock.parsedEvent.updateMany.mockResolvedValue({ count: 1 });
prismaMock.parsedTask.updateMany.mockResolvedValue({ count: 1 });
- prismaMock.event.create.mockResolvedValue({ id: 'created-event-1' } as never);
- prismaMock.task.create.mockResolvedValue({ id: 'created-task-1' } as never);
+ prismaMock.event.create.mockResolvedValue({
+ id: "created-event-1",
+ } as never);
+ prismaMock.task.create.mockResolvedValue({
+ id: "created-task-1",
+ } as never);
(createBulkAuditLogs as Mock).mockResolvedValue({ count: 2 });
@@ -518,20 +554,22 @@ describe('parseUserInput auto-confirm', () => {
}
});
- it('returns PENDING lifecycle for items when auto-confirm is disabled', async () => {
+ it("returns PENDING lifecycle for items when auto-confirm is disabled", async () => {
// Arrange: AI returns event
(generateObject as Mock).mockResolvedValue({
object: {
- events: [{
- title: 'Meeting',
- description: 'Team sync',
- start: '2024-06-16T15:00:00',
- end: '2024-06-16T16:00:00',
- timeZone: 'America/New_York',
- allDay: false,
- confidence: 0.95,
- reasoning: 'Parsed from input',
- }],
+ events: [
+ {
+ title: "Meeting",
+ description: "Team sync",
+ start: "2024-06-16T15:00:00",
+ end: "2024-06-16T16:00:00",
+ timeZone: "America/New_York",
+ allDay: false,
+ confidence: 0.95,
+ reasoning: "Parsed from input",
+ },
+ ],
tasks: [],
},
});
@@ -546,8 +584,8 @@ describe('parseUserInput auto-confirm', () => {
userId: mockUserId,
rawInputId: mockRawInputId,
intent: IntentType.CREATE,
- aiProvider: 'openai',
- routingReason: 'test',
+ aiProvider: "openai",
+ routingReason: "test",
processingDurationMs: 100,
createdAt: new Date(),
} as never);
@@ -557,16 +595,16 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Meeting',
- description: 'Team sync',
- start: new Date('2024-06-16T19:00:00Z'),
- end: new Date('2024-06-16T20:00:00Z'),
- timeZone: 'America/New_York',
+ title: "Meeting",
+ description: "Team sync",
+ start: new Date("2024-06-16T19:00:00Z"),
+ end: new Date("2024-06-16T20:00:00Z"),
+ timeZone: "America/New_York",
allDay: false,
attendees: [],
recurrence: null,
confidence: 0.95,
- reasoning: 'Parsed from input',
+ reasoning: "Parsed from input",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
@@ -587,20 +625,22 @@ describe('parseUserInput auto-confirm', () => {
}
});
- it('returns PENDING lifecycle when auto-confirm fails', async () => {
+ it("returns PENDING lifecycle when auto-confirm fails", async () => {
// Arrange: AI returns event
(generateObject as Mock).mockResolvedValue({
object: {
- events: [{
- title: 'Meeting',
- description: 'Team sync',
- start: '2024-06-16T15:00:00',
- end: '2024-06-16T16:00:00',
- timeZone: 'America/New_York',
- allDay: false,
- confidence: 0.95,
- reasoning: 'Parsed from input',
- }],
+ events: [
+ {
+ title: "Meeting",
+ description: "Team sync",
+ start: "2024-06-16T15:00:00",
+ end: "2024-06-16T16:00:00",
+ timeZone: "America/New_York",
+ allDay: false,
+ confidence: 0.95,
+ reasoning: "Parsed from input",
+ },
+ ],
tasks: [],
},
});
@@ -615,8 +655,8 @@ describe('parseUserInput auto-confirm', () => {
userId: mockUserId,
rawInputId: mockRawInputId,
intent: IntentType.CREATE,
- aiProvider: 'openai',
- routingReason: 'test',
+ aiProvider: "openai",
+ routingReason: "test",
processingDurationMs: 100,
createdAt: new Date(),
} as never);
@@ -626,16 +666,16 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Meeting',
- description: 'Team sync',
- start: new Date('2024-06-16T19:00:00Z'),
- end: new Date('2024-06-16T20:00:00Z'),
- timeZone: 'America/New_York',
+ title: "Meeting",
+ description: "Team sync",
+ start: new Date("2024-06-16T19:00:00Z"),
+ end: new Date("2024-06-16T20:00:00Z"),
+ timeZone: "America/New_York",
allDay: false,
attendees: [],
recurrence: null,
confidence: 0.95,
- reasoning: 'Parsed from input',
+ reasoning: "Parsed from input",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
@@ -660,21 +700,23 @@ describe('parseUserInput auto-confirm', () => {
});
});
- describe('auto-confirm failure', () => {
- it('returns autoConfirmed false and does not create audit logs when acceptParsedItems fails', async () => {
+ describe("auto-confirm failure", () => {
+ it("returns autoConfirmed false and does not create audit logs when acceptParsedItems fails", async () => {
// Arrange: AI returns items
(generateObject as Mock).mockResolvedValue({
object: {
- events: [{
- title: 'Meeting',
- description: 'Team sync',
- start: '2024-06-16T15:00:00',
- end: '2024-06-16T16:00:00',
- timeZone: 'America/New_York',
- allDay: false,
- confidence: 0.95,
- reasoning: 'Parsed from input',
- }],
+ events: [
+ {
+ title: "Meeting",
+ description: "Team sync",
+ start: "2024-06-16T15:00:00",
+ end: "2024-06-16T16:00:00",
+ timeZone: "America/New_York",
+ allDay: false,
+ confidence: 0.95,
+ reasoning: "Parsed from input",
+ },
+ ],
tasks: [],
},
});
@@ -689,8 +731,8 @@ describe('parseUserInput auto-confirm', () => {
userId: mockUserId,
rawInputId: mockRawInputId,
intent: IntentType.CREATE,
- aiProvider: 'openai',
- routingReason: 'test',
+ aiProvider: "openai",
+ routingReason: "test",
processingDurationMs: 100,
createdAt: new Date(),
} as never);
@@ -700,16 +742,16 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Meeting',
- description: 'Team sync',
- start: new Date('2024-06-16T19:00:00Z'),
- end: new Date('2024-06-16T20:00:00Z'),
- timeZone: 'America/New_York',
+ title: "Meeting",
+ description: "Team sync",
+ start: new Date("2024-06-16T19:00:00Z"),
+ end: new Date("2024-06-16T20:00:00Z"),
+ timeZone: "America/New_York",
allDay: false,
attendees: [],
recurrence: null,
confidence: 0.95,
- reasoning: 'Parsed from input',
+ reasoning: "Parsed from input",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
@@ -733,21 +775,23 @@ describe('parseUserInput auto-confirm', () => {
});
});
- describe('auto-confirm exception', () => {
- it('handles exception gracefully and returns autoConfirmed false', async () => {
+ describe("auto-confirm exception", () => {
+ it("handles exception gracefully and returns autoConfirmed false", async () => {
// Arrange: AI returns items
(generateObject as Mock).mockResolvedValue({
object: {
- events: [{
- title: 'Meeting',
- description: 'Team sync',
- start: '2024-06-16T15:00:00',
- end: '2024-06-16T16:00:00',
- timeZone: 'America/New_York',
- allDay: false,
- confidence: 0.95,
- reasoning: 'Parsed from input',
- }],
+ events: [
+ {
+ title: "Meeting",
+ description: "Team sync",
+ start: "2024-06-16T15:00:00",
+ end: "2024-06-16T16:00:00",
+ timeZone: "America/New_York",
+ allDay: false,
+ confidence: 0.95,
+ reasoning: "Parsed from input",
+ },
+ ],
tasks: [],
},
});
@@ -762,8 +806,8 @@ describe('parseUserInput auto-confirm', () => {
userId: mockUserId,
rawInputId: mockRawInputId,
intent: IntentType.CREATE,
- aiProvider: 'openai',
- routingReason: 'test',
+ aiProvider: "openai",
+ routingReason: "test",
processingDurationMs: 100,
createdAt: new Date(),
} as never);
@@ -773,23 +817,25 @@ describe('parseUserInput auto-confirm', () => {
parseResultId: mockParsedResultId,
userId: mockUserId,
ordinal: 1,
- title: 'Meeting',
- description: 'Team sync',
- start: new Date('2024-06-16T19:00:00Z'),
- end: new Date('2024-06-16T20:00:00Z'),
- timeZone: 'America/New_York',
+ title: "Meeting",
+ description: "Team sync",
+ start: new Date("2024-06-16T19:00:00Z"),
+ end: new Date("2024-06-16T20:00:00Z"),
+ timeZone: "America/New_York",
allDay: false,
attendees: [],
recurrence: null,
confidence: 0.95,
- reasoning: 'Parsed from input',
+ reasoning: "Parsed from input",
lifeCycle: LifeCycle.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
} as never);
// acceptParsedItems throws an exception
- prismaMock.parsedResult.findFirst.mockRejectedValue(new Error('Database connection lost'));
+ prismaMock.parsedResult.findFirst.mockRejectedValue(
+ new Error("Database connection lost"),
+ );
// Act
const result = await parseUserInput({
diff --git a/lib/__tests__/quota-period.test.ts b/lib/__tests__/quota-period.test.ts
index 76f72b1a..22fdcbe9 100644
--- a/lib/__tests__/quota-period.test.ts
+++ b/lib/__tests__/quota-period.test.ts
@@ -1,19 +1,19 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Mock env before importing quota to avoid environment variable validation errors
-vi.mock('@/env.mjs', () => ({
+vi.mock("@/env.mjs", () => ({
env: {
- NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
- NEXT_PUBLIC_STRIPE_PLUS_MONTHLY_PLAN_ID: 'price_plus_monthly',
- NEXT_PUBLIC_STRIPE_PLUS_YEARLY_PLAN_ID: 'price_plus_yearly',
- NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: 'price_pro_monthly',
- NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: 'price_pro_yearly',
+ NEXT_PUBLIC_APP_URL: "http://localhost:3000",
+ NEXT_PUBLIC_STRIPE_PLUS_MONTHLY_PLAN_ID: "price_plus_monthly",
+ NEXT_PUBLIC_STRIPE_PLUS_YEARLY_PLAN_ID: "price_plus_yearly",
+ NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: "price_pro_monthly",
+ NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: "price_pro_yearly",
},
}));
-import { getQuotaPeriodBounds } from '../quota';
+import { getQuotaPeriodBounds } from "../quota";
-describe('getQuotaPeriodBounds', () => {
+describe("getQuotaPeriodBounds", () => {
beforeEach(() => {
vi.useFakeTimers();
});
@@ -22,10 +22,10 @@ describe('getQuotaPeriodBounds', () => {
vi.useRealTimers();
});
- describe('Free tier / missing subscription data', () => {
- it('should return calendar month bounds when stripeCurrentPeriodStart is null', () => {
+ describe("Free tier / missing subscription data", () => {
+ it("should return calendar month bounds when stripeCurrentPeriodStart is null", () => {
// Set "now" to Jan 15, 2025
- const now = new Date('2025-01-15T12:00:00.000Z');
+ const now = new Date("2025-01-15T12:00:00.000Z");
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -34,47 +34,47 @@ describe('getQuotaPeriodBounds', () => {
});
// Should return Jan 1 - Feb 1 (calendar month)
- expect(result.periodStart).toEqual(new Date('2025-01-01T00:00:00.000Z'));
- expect(result.periodEnd).toEqual(new Date('2025-02-01T00:00:00.000Z'));
+ expect(result.periodStart).toEqual(new Date("2025-01-01T00:00:00.000Z"));
+ expect(result.periodEnd).toEqual(new Date("2025-02-01T00:00:00.000Z"));
});
- it('should return calendar month bounds when stripeCurrentPeriodEnd is null', () => {
- const now = new Date('2025-03-20T12:00:00.000Z');
+ it("should return calendar month bounds when stripeCurrentPeriodEnd is null", () => {
+ const now = new Date("2025-03-20T12:00:00.000Z");
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
- stripeCurrentPeriodStart: new Date('2025-01-01T00:00:00.000Z'),
+ stripeCurrentPeriodStart: new Date("2025-01-01T00:00:00.000Z"),
stripeCurrentPeriodEnd: null,
});
// Should fall back to calendar month
- expect(result.periodStart).toEqual(new Date('2025-03-01T00:00:00.000Z'));
- expect(result.periodEnd).toEqual(new Date('2025-04-01T00:00:00.000Z'));
+ expect(result.periodStart).toEqual(new Date("2025-03-01T00:00:00.000Z"));
+ expect(result.periodEnd).toEqual(new Date("2025-04-01T00:00:00.000Z"));
});
});
- describe('Expired subscription', () => {
- it('should return calendar month bounds when subscription has expired', () => {
- const now = new Date('2025-06-15T12:00:00.000Z');
+ describe("Expired subscription", () => {
+ it("should return calendar month bounds when subscription has expired", () => {
+ const now = new Date("2025-06-15T12:00:00.000Z");
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
- stripeCurrentPeriodStart: new Date('2025-01-01T00:00:00.000Z'),
- stripeCurrentPeriodEnd: new Date('2025-05-31T23:59:59.000Z'), // Expired
+ stripeCurrentPeriodStart: new Date("2025-01-01T00:00:00.000Z"),
+ stripeCurrentPeriodEnd: new Date("2025-05-31T23:59:59.000Z"), // Expired
});
// Should fall back to calendar month
- expect(result.periodStart).toEqual(new Date('2025-06-01T00:00:00.000Z'));
- expect(result.periodEnd).toEqual(new Date('2025-07-01T00:00:00.000Z'));
+ expect(result.periodStart).toEqual(new Date("2025-06-01T00:00:00.000Z"));
+ expect(result.periodEnd).toEqual(new Date("2025-07-01T00:00:00.000Z"));
});
});
- describe('Monthly subscription', () => {
- it('should return monthly window matching Stripe period when now is within first month', () => {
+ describe("Monthly subscription", () => {
+ it("should return monthly window matching Stripe period when now is within first month", () => {
// Subscription starts Jan 15, ends Feb 15
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2025-02-15T00:00:00.000Z');
- const now = new Date('2025-01-20T12:00:00.000Z'); // Within first month
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2025-02-15T00:00:00.000Z");
+ const now = new Date("2025-01-20T12:00:00.000Z"); // Within first month
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -87,11 +87,11 @@ describe('getQuotaPeriodBounds', () => {
expect(result.periodEnd).toEqual(periodEnd);
});
- it('should handle subscription that started mid-month', () => {
+ it("should handle subscription that started mid-month", () => {
// Subscription starts Jan 15, ends Feb 15
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2025-02-15T00:00:00.000Z');
- const now = new Date('2025-01-25T12:00:00.000Z');
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2025-02-15T00:00:00.000Z");
+ const now = new Date("2025-01-25T12:00:00.000Z");
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -104,12 +104,12 @@ describe('getQuotaPeriodBounds', () => {
});
});
- describe('Yearly subscription with monthly quota windows', () => {
- it('should return first monthly window for yearly subscription', () => {
+ describe("Yearly subscription with monthly quota windows", () => {
+ it("should return first monthly window for yearly subscription", () => {
// Yearly subscription: Jan 15, 2025 - Jan 15, 2026
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2026-01-15T00:00:00.000Z');
- const now = new Date('2025-01-20T12:00:00.000Z'); // Within first month
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2026-01-15T00:00:00.000Z");
+ const now = new Date("2025-01-20T12:00:00.000Z"); // Within first month
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -119,14 +119,14 @@ describe('getQuotaPeriodBounds', () => {
// Should return first monthly window: Jan 15 - Feb 15
expect(result.periodStart).toEqual(periodStart);
- expect(result.periodEnd).toEqual(new Date('2025-02-15T00:00:00.000Z'));
+ expect(result.periodEnd).toEqual(new Date("2025-02-15T00:00:00.000Z"));
});
- it('should return second monthly window for yearly subscription', () => {
+ it("should return second monthly window for yearly subscription", () => {
// Yearly subscription: Jan 15, 2025 - Jan 15, 2026
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2026-01-15T00:00:00.000Z');
- const now = new Date('2025-02-20T12:00:00.000Z'); // Within second month
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2026-01-15T00:00:00.000Z");
+ const now = new Date("2025-02-20T12:00:00.000Z"); // Within second month
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -135,15 +135,15 @@ describe('getQuotaPeriodBounds', () => {
});
// Should return second monthly window: Feb 15 - Mar 15
- expect(result.periodStart).toEqual(new Date('2025-02-15T00:00:00.000Z'));
- expect(result.periodEnd).toEqual(new Date('2025-03-15T00:00:00.000Z'));
+ expect(result.periodStart).toEqual(new Date("2025-02-15T00:00:00.000Z"));
+ expect(result.periodEnd).toEqual(new Date("2025-03-15T00:00:00.000Z"));
});
- it('should return third monthly window for yearly subscription', () => {
+ it("should return third monthly window for yearly subscription", () => {
// Yearly subscription: Jan 15, 2025 - Jan 15, 2026
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2026-01-15T00:00:00.000Z');
- const now = new Date('2025-03-20T12:00:00.000Z'); // Within third month
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2026-01-15T00:00:00.000Z");
+ const now = new Date("2025-03-20T12:00:00.000Z"); // Within third month
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -152,15 +152,15 @@ describe('getQuotaPeriodBounds', () => {
});
// Should return third monthly window: Mar 15 - Apr 15
- expect(result.periodStart).toEqual(new Date('2025-03-15T00:00:00.000Z'));
- expect(result.periodEnd).toEqual(new Date('2025-04-15T00:00:00.000Z'));
+ expect(result.periodStart).toEqual(new Date("2025-03-15T00:00:00.000Z"));
+ expect(result.periodEnd).toEqual(new Date("2025-04-15T00:00:00.000Z"));
});
- it('should handle edge case at month boundary', () => {
+ it("should handle edge case at month boundary", () => {
// Yearly subscription: Jan 15, 2025 - Jan 15, 2026
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2026-01-15T00:00:00.000Z');
- const now = new Date('2025-02-15T00:00:00.000Z'); // Exactly at second month start
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2026-01-15T00:00:00.000Z");
+ const now = new Date("2025-02-15T00:00:00.000Z"); // Exactly at second month start
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -169,15 +169,15 @@ describe('getQuotaPeriodBounds', () => {
});
// Should return second monthly window: Feb 15 - Mar 15
- expect(result.periodStart).toEqual(new Date('2025-02-15T00:00:00.000Z'));
- expect(result.periodEnd).toEqual(new Date('2025-03-15T00:00:00.000Z'));
+ expect(result.periodStart).toEqual(new Date("2025-02-15T00:00:00.000Z"));
+ expect(result.periodEnd).toEqual(new Date("2025-03-15T00:00:00.000Z"));
});
- it('should handle last month of yearly subscription', () => {
+ it("should handle last month of yearly subscription", () => {
// Yearly subscription: Jan 15, 2025 - Jan 15, 2026
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2026-01-15T00:00:00.000Z');
- const now = new Date('2026-01-10T12:00:00.000Z'); // Within last month
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2026-01-15T00:00:00.000Z");
+ const now = new Date("2026-01-10T12:00:00.000Z"); // Within last month
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -186,16 +186,16 @@ describe('getQuotaPeriodBounds', () => {
});
// Should return last monthly window: Dec 15, 2025 - Jan 15, 2026
- expect(result.periodStart).toEqual(new Date('2025-12-15T00:00:00.000Z'));
+ expect(result.periodStart).toEqual(new Date("2025-12-15T00:00:00.000Z"));
expect(result.periodEnd).toEqual(periodEnd);
});
});
- describe('Edge cases', () => {
- it('should handle now exactly at period start', () => {
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2025-02-15T00:00:00.000Z');
- const now = new Date('2025-01-15T00:00:00.000Z');
+ describe("Edge cases", () => {
+ it("should handle now exactly at period start", () => {
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2025-02-15T00:00:00.000Z");
+ const now = new Date("2025-01-15T00:00:00.000Z");
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -207,10 +207,10 @@ describe('getQuotaPeriodBounds', () => {
expect(result.periodEnd).toEqual(periodEnd);
});
- it('should handle now just before period end', () => {
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2025-02-15T00:00:00.000Z');
- const now = new Date('2025-02-14T23:59:59.000Z');
+ it("should handle now just before period end", () => {
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2025-02-15T00:00:00.000Z");
+ const now = new Date("2025-02-14T23:59:59.000Z");
vi.setSystemTime(now);
const result = getQuotaPeriodBounds({
@@ -222,11 +222,11 @@ describe('getQuotaPeriodBounds', () => {
expect(result.periodEnd).toEqual(periodEnd);
});
- it('should use provided now parameter instead of system time', () => {
- const periodStart = new Date('2025-01-15T00:00:00.000Z');
- const periodEnd = new Date('2026-01-15T00:00:00.000Z');
- const systemNow = new Date('2025-06-15T12:00:00.000Z');
- const providedNow = new Date('2025-03-20T12:00:00.000Z');
+ it("should use provided now parameter instead of system time", () => {
+ const periodStart = new Date("2025-01-15T00:00:00.000Z");
+ const periodEnd = new Date("2026-01-15T00:00:00.000Z");
+ const systemNow = new Date("2025-06-15T12:00:00.000Z");
+ const providedNow = new Date("2025-03-20T12:00:00.000Z");
vi.setSystemTime(systemNow);
const result = getQuotaPeriodBounds({
@@ -237,8 +237,8 @@ describe('getQuotaPeriodBounds', () => {
// Should use providedNow (March) not systemNow (June)
// March is in the third monthly window: Mar 15 - Apr 15
- expect(result.periodStart).toEqual(new Date('2025-03-15T00:00:00.000Z'));
- expect(result.periodEnd).toEqual(new Date('2025-04-15T00:00:00.000Z'));
+ expect(result.periodStart).toEqual(new Date("2025-03-15T00:00:00.000Z"));
+ expect(result.periodEnd).toEqual(new Date("2025-04-15T00:00:00.000Z"));
});
});
});
diff --git a/lib/__tests__/quota-usage.test.ts b/lib/__tests__/quota-usage.test.ts
index 5acb222b..5f56c2f5 100644
--- a/lib/__tests__/quota-usage.test.ts
+++ b/lib/__tests__/quota-usage.test.ts
@@ -1,20 +1,20 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Mock env before importing quota to avoid environment variable validation errors
-vi.mock('@/env.mjs', () => ({
+vi.mock("@/env.mjs", () => ({
env: {
- NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
- NEXT_PUBLIC_STRIPE_PLUS_MONTHLY_PLAN_ID: 'price_plus_monthly',
- NEXT_PUBLIC_STRIPE_PLUS_YEARLY_PLAN_ID: 'price_plus_yearly',
- NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: 'price_pro_monthly',
- NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: 'price_pro_yearly',
+ NEXT_PUBLIC_APP_URL: "http://localhost:3000",
+ NEXT_PUBLIC_STRIPE_PLUS_MONTHLY_PLAN_ID: "price_plus_monthly",
+ NEXT_PUBLIC_STRIPE_PLUS_YEARLY_PLAN_ID: "price_plus_yearly",
+ NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: "price_pro_monthly",
+ NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: "price_pro_yearly",
},
}));
-import { getTierFromPriceId, getQuotaUsage } from '../quota';
-import { prismaMock } from '@/tests/mocks/prisma';
+import { getQuotaUsage, getTierFromPriceId } from "../quota";
+import { prismaMock } from "@/tests/mocks/prisma";
-describe('quota-usage', () => {
+describe("quota-usage", () => {
beforeEach(() => {
vi.useFakeTimers();
});
@@ -23,38 +23,38 @@ describe('quota-usage', () => {
vi.useRealTimers();
});
- describe('getTierFromPriceId', () => {
+ describe("getTierFromPriceId", () => {
it('should return "plus" for plus monthly price ID', () => {
- expect(getTierFromPriceId('price_plus_monthly')).toBe('plus');
+ expect(getTierFromPriceId("price_plus_monthly")).toBe("plus");
});
it('should return "plus" for plus yearly price ID', () => {
- expect(getTierFromPriceId('price_plus_yearly')).toBe('plus');
+ expect(getTierFromPriceId("price_plus_yearly")).toBe("plus");
});
it('should return "pro" for pro monthly price ID', () => {
- expect(getTierFromPriceId('price_pro_monthly')).toBe('pro');
+ expect(getTierFromPriceId("price_pro_monthly")).toBe("pro");
});
it('should return "pro" for pro yearly price ID', () => {
- expect(getTierFromPriceId('price_pro_yearly')).toBe('pro');
+ expect(getTierFromPriceId("price_pro_yearly")).toBe("pro");
});
it('should return "free" for null price ID', () => {
- expect(getTierFromPriceId(null)).toBe('free');
+ expect(getTierFromPriceId(null)).toBe("free");
});
it('should return "free" for unknown price ID', () => {
- expect(getTierFromPriceId('price_unknown')).toBe('free');
+ expect(getTierFromPriceId("price_unknown")).toBe("free");
});
});
- describe('getQuotaUsage', () => {
- const userId = 'user_123';
+ describe("getQuotaUsage", () => {
+ const userId = "user_123";
- describe('free tier shaping', () => {
- it('should return free tier with correct limits when subscription is null', async () => {
- const now = new Date('2025-01-15T12:00:00.000Z');
+ describe("free tier shaping", () => {
+ it("should return free tier with correct limits when subscription is null", async () => {
+ const now = new Date("2025-01-15T12:00:00.000Z");
vi.setSystemTime(now);
prismaMock.user.findUnique.mockResolvedValue({
@@ -68,31 +68,31 @@ describe('quota-usage', () => {
const result = await getQuotaUsage(userId);
- expect(result.tierName).toBe('free');
+ expect(result.tierName).toBe("free");
expect(result.parsingRequests).toEqual({
used: 10,
limit: 50,
percentage: 20,
- status: 'normal',
+ status: "normal",
remaining: 40,
});
expect(result.fileUploads).toEqual({
used: 0,
limit: 0,
percentage: null,
- status: 'disabled',
+ status: "disabled",
remaining: 0,
});
});
- it('should return free tier when subscription has expired', async () => {
- const now = new Date('2025-06-15T12:00:00.000Z');
+ it("should return free tier when subscription has expired", async () => {
+ const now = new Date("2025-06-15T12:00:00.000Z");
vi.setSystemTime(now);
prismaMock.user.findUnique.mockResolvedValue({
- stripePriceId: 'price_plus_monthly',
- stripeCurrentPeriodStart: new Date('2025-01-01T00:00:00.000Z'),
- stripeCurrentPeriodEnd: new Date('2025-05-31T23:59:59.000Z'), // Expired
+ stripePriceId: "price_plus_monthly",
+ stripeCurrentPeriodStart: new Date("2025-01-01T00:00:00.000Z"),
+ stripeCurrentPeriodEnd: new Date("2025-05-31T23:59:59.000Z"), // Expired
} as any);
prismaMock.parsedResult.count.mockResolvedValue(10);
@@ -100,22 +100,22 @@ describe('quota-usage', () => {
const result = await getQuotaUsage(userId);
- expect(result.tierName).toBe('free');
+ expect(result.tierName).toBe("free");
expect(result.parsingRequests.limit).toBe(50);
expect(result.fileUploads.limit).toBe(0);
- expect(result.fileUploads.status).toBe('disabled');
+ expect(result.fileUploads.status).toBe("disabled");
});
});
- describe('threshold statuses (plus tier)', () => {
+ describe("threshold statuses (plus tier)", () => {
it('should return "warning" status when parsing usage is at 80%', async () => {
- const now = new Date('2025-01-20T12:00:00.000Z');
+ const now = new Date("2025-01-20T12:00:00.000Z");
vi.setSystemTime(now);
prismaMock.user.findUnique.mockResolvedValue({
- stripePriceId: 'price_plus_monthly',
- stripeCurrentPeriodStart: new Date('2025-01-15T00:00:00.000Z'),
- stripeCurrentPeriodEnd: new Date('2025-02-15T00:00:00.000Z'),
+ stripePriceId: "price_plus_monthly",
+ stripeCurrentPeriodStart: new Date("2025-01-15T00:00:00.000Z"),
+ stripeCurrentPeriodEnd: new Date("2025-02-15T00:00:00.000Z"),
} as any);
// 400 / 500 = 80%
@@ -124,24 +124,24 @@ describe('quota-usage', () => {
const result = await getQuotaUsage(userId);
- expect(result.tierName).toBe('plus');
+ expect(result.tierName).toBe("plus");
expect(result.parsingRequests).toEqual({
used: 400,
limit: 500,
percentage: 80,
- status: 'warning',
+ status: "warning",
remaining: 100,
});
});
it('should return "critical" status when parsing usage is at 95%', async () => {
- const now = new Date('2025-01-20T12:00:00.000Z');
+ const now = new Date("2025-01-20T12:00:00.000Z");
vi.setSystemTime(now);
prismaMock.user.findUnique.mockResolvedValue({
- stripePriceId: 'price_plus_monthly',
- stripeCurrentPeriodStart: new Date('2025-01-15T00:00:00.000Z'),
- stripeCurrentPeriodEnd: new Date('2025-02-15T00:00:00.000Z'),
+ stripePriceId: "price_plus_monthly",
+ stripeCurrentPeriodStart: new Date("2025-01-15T00:00:00.000Z"),
+ stripeCurrentPeriodEnd: new Date("2025-02-15T00:00:00.000Z"),
} as any);
// 475 / 500 = 95%
@@ -150,24 +150,24 @@ describe('quota-usage', () => {
const result = await getQuotaUsage(userId);
- expect(result.tierName).toBe('plus');
+ expect(result.tierName).toBe("plus");
expect(result.parsingRequests).toEqual({
used: 475,
limit: 500,
percentage: 95,
- status: 'critical',
+ status: "critical",
remaining: 25,
});
});
it('should return "critical" status when file upload usage is at 95%', async () => {
- const now = new Date('2025-01-20T12:00:00.000Z');
+ const now = new Date("2025-01-20T12:00:00.000Z");
vi.setSystemTime(now);
prismaMock.user.findUnique.mockResolvedValue({
- stripePriceId: 'price_plus_monthly',
- stripeCurrentPeriodStart: new Date('2025-01-15T00:00:00.000Z'),
- stripeCurrentPeriodEnd: new Date('2025-02-15T00:00:00.000Z'),
+ stripePriceId: "price_plus_monthly",
+ stripeCurrentPeriodStart: new Date("2025-01-15T00:00:00.000Z"),
+ stripeCurrentPeriodEnd: new Date("2025-02-15T00:00:00.000Z"),
} as any);
// 48 / 50 = 96%
@@ -176,24 +176,24 @@ describe('quota-usage', () => {
const result = await getQuotaUsage(userId);
- expect(result.tierName).toBe('plus');
+ expect(result.tierName).toBe("plus");
expect(result.fileUploads).toEqual({
used: 48,
limit: 50,
percentage: 96,
- status: 'critical',
+ status: "critical",
remaining: 2,
});
});
it('should return "normal" status when usage is below 80%', async () => {
- const now = new Date('2025-01-20T12:00:00.000Z');
+ const now = new Date("2025-01-20T12:00:00.000Z");
vi.setSystemTime(now);
prismaMock.user.findUnique.mockResolvedValue({
- stripePriceId: 'price_plus_monthly',
- stripeCurrentPeriodStart: new Date('2025-01-15T00:00:00.000Z'),
- stripeCurrentPeriodEnd: new Date('2025-02-15T00:00:00.000Z'),
+ stripePriceId: "price_plus_monthly",
+ stripeCurrentPeriodStart: new Date("2025-01-15T00:00:00.000Z"),
+ stripeCurrentPeriodEnd: new Date("2025-02-15T00:00:00.000Z"),
} as any);
// 100 / 500 = 20%
@@ -202,21 +202,21 @@ describe('quota-usage', () => {
const result = await getQuotaUsage(userId);
- expect(result.tierName).toBe('plus');
- expect(result.parsingRequests.status).toBe('normal');
- expect(result.fileUploads.status).toBe('normal');
+ expect(result.tierName).toBe("plus");
+ expect(result.parsingRequests.status).toBe("normal");
+ expect(result.fileUploads.status).toBe("normal");
});
});
- describe('unlimited (pro) shaping', () => {
- it('should return null fields for unlimited pro tier', async () => {
- const now = new Date('2025-01-20T12:00:00.000Z');
+ describe("unlimited (pro) shaping", () => {
+ it("should return null fields for unlimited pro tier", async () => {
+ const now = new Date("2025-01-20T12:00:00.000Z");
vi.setSystemTime(now);
prismaMock.user.findUnique.mockResolvedValue({
- stripePriceId: 'price_pro_monthly',
- stripeCurrentPeriodStart: new Date('2025-01-15T00:00:00.000Z'),
- stripeCurrentPeriodEnd: new Date('2025-02-15T00:00:00.000Z'),
+ stripePriceId: "price_pro_monthly",
+ stripeCurrentPeriodStart: new Date("2025-01-15T00:00:00.000Z"),
+ stripeCurrentPeriodEnd: new Date("2025-02-15T00:00:00.000Z"),
} as any);
// Arbitrary high counts
@@ -225,31 +225,31 @@ describe('quota-usage', () => {
const result = await getQuotaUsage(userId);
- expect(result.tierName).toBe('pro');
+ expect(result.tierName).toBe("pro");
expect(result.parsingRequests).toEqual({
used: 1234,
limit: null,
percentage: null,
- status: 'normal',
+ status: "normal",
remaining: null,
});
expect(result.fileUploads).toEqual({
used: 999,
limit: null,
percentage: null,
- status: 'normal',
+ status: "normal",
remaining: null,
});
});
- it('should handle pro yearly subscription', async () => {
- const now = new Date('2025-01-20T12:00:00.000Z');
+ it("should handle pro yearly subscription", async () => {
+ const now = new Date("2025-01-20T12:00:00.000Z");
vi.setSystemTime(now);
prismaMock.user.findUnique.mockResolvedValue({
- stripePriceId: 'price_pro_yearly',
- stripeCurrentPeriodStart: new Date('2025-01-15T00:00:00.000Z'),
- stripeCurrentPeriodEnd: new Date('2026-01-15T00:00:00.000Z'),
+ stripePriceId: "price_pro_yearly",
+ stripeCurrentPeriodStart: new Date("2025-01-15T00:00:00.000Z"),
+ stripeCurrentPeriodEnd: new Date("2026-01-15T00:00:00.000Z"),
} as any);
prismaMock.parsedResult.count.mockResolvedValue(5000);
@@ -257,11 +257,11 @@ describe('quota-usage', () => {
const result = await getQuotaUsage(userId);
- expect(result.tierName).toBe('pro');
+ expect(result.tierName).toBe("pro");
expect(result.parsingRequests.limit).toBe(null);
expect(result.fileUploads.limit).toBe(null);
- expect(result.parsingRequests.status).toBe('normal');
- expect(result.fileUploads.status).toBe('normal');
+ expect(result.parsingRequests.status).toBe("normal");
+ expect(result.fileUploads.status).toBe("normal");
});
});
});
diff --git a/lib/__tests__/timezone-utils.test.ts b/lib/__tests__/timezone-utils.test.ts
index 28cf1117..5f7b9b2f 100644
--- a/lib/__tests__/timezone-utils.test.ts
+++ b/lib/__tests__/timezone-utils.test.ts
@@ -1,172 +1,190 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { describe, expect, it } from "vitest";
import {
- validateTimezone,
- getTimezones,
- resolveTimezone,
- isoToDatetimeLocal,
datetimeLocalToIso,
- toGoogleAllDayDateRange,
formatTimezoneDisplay,
getCurrentUtcDate,
-} from '../timezone-utils';
-import { testTimezones, testDates } from '@/tests/fixtures';
+ getTimezones,
+ isoToDatetimeLocal,
+ resolveTimezone,
+ toGoogleAllDayDateRange,
+ validateTimezone,
+} from "../timezone-utils";
+import { testDates, testTimezones } from "@/tests/fixtures";
-describe('timezone-utils', () => {
- describe('validateTimezone', () => {
- it('should return true for valid IANA timezone', () => {
- expect(validateTimezone('America/New_York')).toBe(true);
- expect(validateTimezone('Europe/London')).toBe(true);
- expect(validateTimezone('Asia/Tokyo')).toBe(true);
- expect(validateTimezone('UTC')).toBe(true);
+describe("timezone-utils", () => {
+ describe("validateTimezone", () => {
+ it("should return true for valid IANA timezone", () => {
+ expect(validateTimezone("America/New_York")).toBe(true);
+ expect(validateTimezone("Europe/London")).toBe(true);
+ expect(validateTimezone("Asia/Tokyo")).toBe(true);
+ expect(validateTimezone("UTC")).toBe(true);
});
- it('should return false for invalid timezone', () => {
- expect(validateTimezone('Invalid/Timezone')).toBe(false);
- expect(validateTimezone('NotATimezone')).toBe(false);
- expect(validateTimezone('')).toBe(false);
+ it("should return false for invalid timezone", () => {
+ expect(validateTimezone("Invalid/Timezone")).toBe(false);
+ expect(validateTimezone("NotATimezone")).toBe(false);
+ expect(validateTimezone("")).toBe(false);
});
- it('should handle edge cases', () => {
- expect(validateTimezone('Etc/GMT+12')).toBe(true);
- expect(validateTimezone('Pacific/Auckland')).toBe(true);
+ it("should handle edge cases", () => {
+ expect(validateTimezone("Etc/GMT+12")).toBe(true);
+ expect(validateTimezone("Pacific/Auckland")).toBe(true);
});
});
- describe('getTimezones', () => {
- it('should return a non-empty array', () => {
+ describe("getTimezones", () => {
+ it("should return a non-empty array", () => {
const timezones = getTimezones();
expect(Array.isArray(timezones)).toBe(true);
expect(timezones.length).toBeGreaterThan(0);
});
- it('should contain UTC', () => {
+ it("should contain UTC", () => {
const timezones = getTimezones();
// UTC may be included or common timezones may not have UTC explicitly
// The array should at least have valid timezones
- expect(timezones.some(tz => tz === 'UTC' || tz.includes('/'))).toBe(true);
+ expect(timezones.some((tz) => tz === "UTC" || tz.includes("/"))).toBe(
+ true,
+ );
});
- it('should contain common timezones', () => {
+ it("should contain common timezones", () => {
const timezones = getTimezones();
- expect(timezones).toContain('America/New_York');
- expect(timezones).toContain('Europe/London');
+ expect(timezones).toContain("America/New_York");
+ expect(timezones).toContain("Europe/London");
});
});
- describe('resolveTimezone', () => {
- it('should return the provided timezone when valid', () => {
- expect(resolveTimezone('America/New_York')).toBe('America/New_York');
- expect(resolveTimezone('Europe/London')).toBe('Europe/London');
+ describe("resolveTimezone", () => {
+ it("should return the provided timezone when valid", () => {
+ expect(resolveTimezone("America/New_York")).toBe("America/New_York");
+ expect(resolveTimezone("Europe/London")).toBe("Europe/London");
});
- it('should fallback to system timezone when null', () => {
+ it("should fallback to system timezone when null", () => {
const result = resolveTimezone(null);
expect(result).toBeTruthy();
- expect(typeof result).toBe('string');
+ expect(typeof result).toBe("string");
});
- it('should fallback to system timezone when undefined', () => {
+ it("should fallback to system timezone when undefined", () => {
const result = resolveTimezone(undefined);
expect(result).toBeTruthy();
- expect(typeof result).toBe('string');
+ expect(typeof result).toBe("string");
});
});
- describe('isoToDatetimeLocal', () => {
- it('should convert ISO string to datetime-local format', () => {
+ describe("isoToDatetimeLocal", () => {
+ it("should convert ISO string to datetime-local format", () => {
// 2025-06-15T14:30:00.000Z in America/New_York (EDT, UTC-4)
// Should be 10:30 local time
- const result = isoToDatetimeLocal(testDates.afternoon, testTimezones.newYork);
- expect(result).toBe('2025-06-15T10:30');
+ const result = isoToDatetimeLocal(
+ testDates.afternoon,
+ testTimezones.newYork,
+ );
+ expect(result).toBe("2025-06-15T10:30");
});
- it('should handle different timezones correctly', () => {
+ it("should handle different timezones correctly", () => {
// Same UTC time, different local result in Tokyo (UTC+9)
// 14:30 UTC = 23:30 Tokyo
- const result = isoToDatetimeLocal(testDates.afternoon, testTimezones.tokyo);
- expect(result).toBe('2025-06-15T23:30');
+ const result = isoToDatetimeLocal(
+ testDates.afternoon,
+ testTimezones.tokyo,
+ );
+ expect(result).toBe("2025-06-15T23:30");
});
- it('should return empty string for null input', () => {
- expect(isoToDatetimeLocal(null, testTimezones.newYork)).toBe('');
+ it("should return empty string for null input", () => {
+ expect(isoToDatetimeLocal(null, testTimezones.newYork)).toBe("");
});
- it('should return empty string for invalid ISO string', () => {
- expect(isoToDatetimeLocal('not-a-date', testTimezones.newYork)).toBe('');
+ it("should return empty string for invalid ISO string", () => {
+ expect(isoToDatetimeLocal("not-a-date", testTimezones.newYork)).toBe("");
});
});
- describe('datetimeLocalToIso', () => {
- it('should convert datetime-local to ISO string', () => {
+ describe("datetimeLocalToIso", () => {
+ it("should convert datetime-local to ISO string", () => {
// 10:30 in New York (EDT) = 14:30 UTC
- const result = datetimeLocalToIso('2025-06-15T10:30', testTimezones.newYork);
- expect(result).toBe('2025-06-15T14:30:00.000Z');
+ const result = datetimeLocalToIso(
+ "2025-06-15T10:30",
+ testTimezones.newYork,
+ );
+ expect(result).toBe("2025-06-15T14:30:00.000Z");
});
- it('should handle different timezones correctly', () => {
+ it("should handle different timezones correctly", () => {
// 23:30 in Tokyo (UTC+9) = 14:30 UTC
- const result = datetimeLocalToIso('2025-06-15T23:30', testTimezones.tokyo);
- expect(result).toBe('2025-06-15T14:30:00.000Z');
+ const result = datetimeLocalToIso(
+ "2025-06-15T23:30",
+ testTimezones.tokyo,
+ );
+ expect(result).toBe("2025-06-15T14:30:00.000Z");
});
- it('should return null for empty input', () => {
- expect(datetimeLocalToIso('', testTimezones.newYork)).toBeNull();
+ it("should return null for empty input", () => {
+ expect(datetimeLocalToIso("", testTimezones.newYork)).toBeNull();
expect(datetimeLocalToIso(null, testTimezones.newYork)).toBeNull();
expect(datetimeLocalToIso(undefined, testTimezones.newYork)).toBeNull();
});
});
- describe('toGoogleAllDayDateRange', () => {
- it('should return exclusive end date for same-day event', () => {
- const start = new Date('2025-06-15T00:00:00.000Z');
- const end = new Date('2025-06-15T23:59:59.000Z');
-
+ describe("toGoogleAllDayDateRange", () => {
+ it("should return exclusive end date for same-day event", () => {
+ const start = new Date("2025-06-15T00:00:00.000Z");
+ const end = new Date("2025-06-15T23:59:59.000Z");
+
const result = toGoogleAllDayDateRange(start, end, testTimezones.utc);
-
- expect(result.startDate).toBe('2025-06-15');
- expect(result.endDateExclusive).toBe('2025-06-16');
+
+ expect(result.startDate).toBe("2025-06-15");
+ expect(result.endDateExclusive).toBe("2025-06-16");
});
- it('should handle multi-day events', () => {
- const start = new Date('2025-06-15T00:00:00.000Z');
- const end = new Date('2025-06-17T23:59:59.000Z');
-
+ it("should handle multi-day events", () => {
+ const start = new Date("2025-06-15T00:00:00.000Z");
+ const end = new Date("2025-06-17T23:59:59.000Z");
+
const result = toGoogleAllDayDateRange(start, end, testTimezones.utc);
-
- expect(result.startDate).toBe('2025-06-15');
- expect(result.endDateExclusive).toBe('2025-06-18');
+
+ expect(result.startDate).toBe("2025-06-15");
+ expect(result.endDateExclusive).toBe("2025-06-18");
});
- it('should ensure minimum one-day span when end equals start', () => {
- const sameDate = new Date('2025-06-15T12:00:00.000Z');
-
- const result = toGoogleAllDayDateRange(sameDate, sameDate, testTimezones.utc);
-
- expect(result.startDate).toBe('2025-06-15');
- expect(result.endDateExclusive).toBe('2025-06-16');
+ it("should ensure minimum one-day span when end equals start", () => {
+ const sameDate = new Date("2025-06-15T12:00:00.000Z");
+
+ const result = toGoogleAllDayDateRange(
+ sameDate,
+ sameDate,
+ testTimezones.utc,
+ );
+
+ expect(result.startDate).toBe("2025-06-15");
+ expect(result.endDateExclusive).toBe("2025-06-16");
});
});
- describe('formatTimezoneDisplay', () => {
- it('should format timezone with abbreviation and offset', () => {
+ describe("formatTimezoneDisplay", () => {
+ it("should format timezone with abbreviation and offset", () => {
const result = formatTimezoneDisplay(testTimezones.newYork);
// Result should contain timezone name and offset info
- expect(result).toContain('America/New_York');
- expect(result).toContain('UTC');
+ expect(result).toContain("America/New_York");
+ expect(result).toContain("UTC");
});
- it('should handle UTC', () => {
+ it("should handle UTC", () => {
const result = formatTimezoneDisplay(testTimezones.utc);
- expect(result).toContain('UTC');
+ expect(result).toContain("UTC");
});
});
- describe('getCurrentUtcDate', () => {
- it('should return current date', () => {
+ describe("getCurrentUtcDate", () => {
+ it("should return current date", () => {
const before = Date.now();
const result = getCurrentUtcDate();
const after = Date.now();
-
+
expect(result).toBeInstanceOf(Date);
expect(result.getTime()).toBeGreaterThanOrEqual(before);
expect(result.getTime()).toBeLessThanOrEqual(after);
diff --git a/lib/__tests__/user-ai-settings.test.ts b/lib/__tests__/user-ai-settings.test.ts
index a3bdb2b6..4e274103 100644
--- a/lib/__tests__/user-ai-settings.test.ts
+++ b/lib/__tests__/user-ai-settings.test.ts
@@ -1,10 +1,9 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { getAISettings, updateAISettings } from "@/lib/user";
+import { prismaMock } from "@/tests/mocks/prisma";
-import { prismaMock } from '@/tests/mocks/prisma';
-import { getAISettings, updateAISettings } from '@/lib/user';
-
-describe('AI Settings', () => {
- const FIXED_DATE = new Date('2024-06-15T12:00:00.000Z');
+describe("AI Settings", () => {
+ const FIXED_DATE = new Date("2024-06-15T12:00:00.000Z");
beforeEach(() => {
vi.useFakeTimers();
@@ -15,21 +14,21 @@ describe('AI Settings', () => {
vi.useRealTimers();
});
- describe('getAISettings', () => {
- it('returns settings when user exists', async () => {
+ describe("getAISettings", () => {
+ it("returns settings when user exists", async () => {
prismaMock.user.findUnique.mockResolvedValue({
autoConfirmEnabled: true,
autoConfirmEnabledAt: FIXED_DATE,
} as never);
- const result = await getAISettings('user-123');
+ const result = await getAISettings("user-123");
expect(result).toEqual({
autoConfirmEnabled: true,
autoConfirmEnabledAt: FIXED_DATE,
});
expect(prismaMock.user.findUnique).toHaveBeenCalledWith({
- where: { id: 'user-123' },
+ where: { id: "user-123" },
select: {
autoConfirmEnabled: true,
autoConfirmEnabledAt: true,
@@ -37,38 +36,40 @@ describe('AI Settings', () => {
});
});
- it('returns null when user is missing', async () => {
+ it("returns null when user is missing", async () => {
prismaMock.user.findUnique.mockResolvedValue(null);
- const result = await getAISettings('nonexistent-user');
+ const result = await getAISettings("nonexistent-user");
expect(result).toBeNull();
});
- it('returns null on database error', async () => {
- prismaMock.user.findUnique.mockRejectedValue(new Error('DB error'));
+ it("returns null on database error", async () => {
+ prismaMock.user.findUnique.mockRejectedValue(new Error("DB error"));
- const result = await getAISettings('user-123');
+ const result = await getAISettings("user-123");
expect(result).toBeNull();
});
});
- describe('updateAISettings', () => {
- it('sets autoConfirmEnabledAt to fixed date when enabling', async () => {
+ describe("updateAISettings", () => {
+ it("sets autoConfirmEnabledAt to fixed date when enabling", async () => {
prismaMock.user.update.mockResolvedValue({
autoConfirmEnabled: true,
autoConfirmEnabledAt: FIXED_DATE,
} as never);
- const result = await updateAISettings('user-123', { autoConfirmEnabled: true });
+ const result = await updateAISettings("user-123", {
+ autoConfirmEnabled: true,
+ });
expect(result).toEqual({
autoConfirmEnabled: true,
autoConfirmEnabledAt: FIXED_DATE,
});
expect(prismaMock.user.update).toHaveBeenCalledWith({
- where: { id: 'user-123' },
+ where: { id: "user-123" },
data: {
autoConfirmEnabled: true,
autoConfirmEnabledAt: FIXED_DATE,
@@ -80,20 +81,22 @@ describe('AI Settings', () => {
});
});
- it('clears autoConfirmEnabledAt to null when disabling', async () => {
+ it("clears autoConfirmEnabledAt to null when disabling", async () => {
prismaMock.user.update.mockResolvedValue({
autoConfirmEnabled: false,
autoConfirmEnabledAt: null,
} as never);
- const result = await updateAISettings('user-123', { autoConfirmEnabled: false });
+ const result = await updateAISettings("user-123", {
+ autoConfirmEnabled: false,
+ });
expect(result).toEqual({
autoConfirmEnabled: false,
autoConfirmEnabledAt: null,
});
expect(prismaMock.user.update).toHaveBeenCalledWith({
- where: { id: 'user-123' },
+ where: { id: "user-123" },
data: {
autoConfirmEnabled: false,
autoConfirmEnabledAt: null,
@@ -105,34 +108,38 @@ describe('AI Settings', () => {
});
});
- it('returns null when user not found', async () => {
- prismaMock.user.update.mockRejectedValue({ code: 'P2025' });
+ it("returns null when user not found", async () => {
+ prismaMock.user.update.mockRejectedValue({ code: "P2025" });
- const result = await updateAISettings('nonexistent-user', { autoConfirmEnabled: true });
+ const result = await updateAISettings("nonexistent-user", {
+ autoConfirmEnabled: true,
+ });
expect(result).toBeNull();
});
- it('returns null on database error', async () => {
- prismaMock.user.update.mockRejectedValue(new Error('DB error'));
+ it("returns null on database error", async () => {
+ prismaMock.user.update.mockRejectedValue(new Error("DB error"));
- const result = await updateAISettings('user-123', { autoConfirmEnabled: true });
+ const result = await updateAISettings("user-123", {
+ autoConfirmEnabled: true,
+ });
expect(result).toBeNull();
});
- describe('idempotent behavior', () => {
- it('enabling twice uses current timestamp each time', async () => {
+ describe("idempotent behavior", () => {
+ it("enabling twice uses current timestamp each time", async () => {
// First enable
prismaMock.user.update.mockResolvedValueOnce({
autoConfirmEnabled: true,
autoConfirmEnabledAt: FIXED_DATE,
} as never);
- await updateAISettings('user-123', { autoConfirmEnabled: true });
+ await updateAISettings("user-123", { autoConfirmEnabled: true });
// Advance time
- const LATER_DATE = new Date('2024-06-15T13:00:00.000Z');
+ const LATER_DATE = new Date("2024-06-15T13:00:00.000Z");
vi.setSystemTime(LATER_DATE);
// Second enable
@@ -141,11 +148,11 @@ describe('AI Settings', () => {
autoConfirmEnabledAt: LATER_DATE,
} as never);
- await updateAISettings('user-123', { autoConfirmEnabled: true });
+ await updateAISettings("user-123", { autoConfirmEnabled: true });
// Verify second call used the later date
expect(prismaMock.user.update).toHaveBeenLastCalledWith({
- where: { id: 'user-123' },
+ where: { id: "user-123" },
data: {
autoConfirmEnabled: true,
autoConfirmEnabledAt: LATER_DATE,
@@ -157,14 +164,14 @@ describe('AI Settings', () => {
});
});
- it('disabling twice keeps autoConfirmEnabledAt null', async () => {
+ it("disabling twice keeps autoConfirmEnabledAt null", async () => {
// First disable
prismaMock.user.update.mockResolvedValueOnce({
autoConfirmEnabled: false,
autoConfirmEnabledAt: null,
} as never);
- await updateAISettings('user-123', { autoConfirmEnabled: false });
+ await updateAISettings("user-123", { autoConfirmEnabled: false });
// Second disable
prismaMock.user.update.mockResolvedValueOnce({
@@ -172,7 +179,9 @@ describe('AI Settings', () => {
autoConfirmEnabledAt: null,
} as never);
- const result = await updateAISettings('user-123', { autoConfirmEnabled: false });
+ const result = await updateAISettings("user-123", {
+ autoConfirmEnabled: false,
+ });
expect(result).toEqual({
autoConfirmEnabled: false,
diff --git a/lib/ai/config.ts b/lib/ai/config.ts
index aa5ca1bc..b195ba79 100644
--- a/lib/ai/config.ts
+++ b/lib/ai/config.ts
@@ -1,13 +1,16 @@
-import { env } from "@/env.mjs";
-
import type {
ProviderConfig,
ProviderType,
TranscriptionProviderConfig,
TranscriptionProviderType,
} from "./types";
+import { env } from "@/env.mjs";
-const SUPPORTED_PROVIDERS: ProviderType[] = ["openai", "cerebras", "openrouter"];
+const SUPPORTED_PROVIDERS: ProviderType[] = [
+ "openai",
+ "cerebras",
+ "openrouter",
+];
export function validateProviderType(provider: string): ProviderType {
if (SUPPORTED_PROVIDERS.includes(provider as ProviderType)) {
@@ -87,7 +90,6 @@ export function getProviderConfig(
* providers are fully configured up front.
*/
export function ensureParserRoutingEnvironment(): void {
-
const failures: string[] = [];
try {
@@ -111,8 +113,12 @@ export function ensureParserRoutingEnvironment(): void {
}
}
-export function getParserConfig(overrides?: Partial): ProviderConfig {
- const targetProvider = validateProviderType(overrides?.provider ?? env.APP_PARSER_PROVIDER);
+export function getParserConfig(
+ overrides?: Partial,
+): ProviderConfig {
+ const targetProvider = validateProviderType(
+ overrides?.provider ?? env.APP_PARSER_PROVIDER,
+ );
return getProviderConfig(targetProvider, overrides);
}
@@ -126,7 +132,9 @@ export function getTranscriptionConfig(
if (provider === "groq") {
const apiKey = overrides?.apiKey ?? env.GROQ_API_KEY;
if (!apiKey) {
- throw new Error("GROQ_API_KEY is required when using the Groq transcription provider");
+ throw new Error(
+ "GROQ_API_KEY is required when using the Groq transcription provider",
+ );
}
return {
diff --git a/lib/ai/factory.ts b/lib/ai/factory.ts
index 8af8aa18..474d2e6f 100644
--- a/lib/ai/factory.ts
+++ b/lib/ai/factory.ts
@@ -1,26 +1,25 @@
import { createGroq } from "@ai-sdk/groq";
import { createOpenAI } from "@ai-sdk/openai";
-
import {
ensureParserRoutingEnvironment,
getProviderConfig,
getTranscriptionConfig,
} from "./config";
+import {
+ createCerebrasProvider,
+ createOpenAIProvider,
+ createOpenRouterProvider,
+} from "./providers";
import type {
AIModel,
CachedTranscriptionModel,
ParserModelSelection,
+ ParserRoutingReason,
ProviderConfig,
ProviderType,
- ParserRoutingReason,
- TranscriptionProviderConfig,
TranscriptionModelSelection,
+ TranscriptionProviderConfig,
} from "./types";
-import {
- createCerebrasProvider,
- createOpenAIProvider,
- createOpenRouterProvider,
-} from "./providers";
const requestCache = new WeakMap