add tv mode provider

This commit is contained in:
2025-09-03 09:03:34 -05:00
parent 64b3b0a854
commit 35215d34a1
30 changed files with 515 additions and 508 deletions

View File

@@ -30,42 +30,49 @@ const signInFormSchema = z.object({
email: z.email({
message: 'Please enter a valid email address.',
}),
password: z.string()
.min(8, {
message: 'Password must be at least 8 characters.',
})
.regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$/, {
message: 'Password must contain at least one digit, ' +
'one uppercase letter, one lowercase letter, ' +
'and one special character.'
})
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters.',
})
.regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$/, {
message:
'Password must contain at least one digit, ' +
'one uppercase letter, one lowercase letter, ' +
'and one special character.',
}),
});
const signUpFormSchema = z.object({
name: z.string()
.min(2, {
message: 'Name must be at least 2 characters.',
}),
email: z.email({
message: 'Please enter a valid email address.',
}),
password: z.string()
.min(8, {
message: 'Password must be at least 8 characters.',
const signUpFormSchema = z
.object({
name: z.string().min(2, {
message: 'Name must be at least 2 characters.',
}),
email: z.email({
message: 'Please enter a valid email address.',
}),
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters.',
})
.regex(
/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$/,
{
message:
'Password must contain at least one digit, ' +
'one uppercase letter, one lowercase letter, ' +
'and one special character.',
},
),
confirmPassword: z.string().min(8, {
message: 'Password must be at least 8 characters.',
}),
})
.regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$/, {
message: 'Password must contain at least one digit, ' +
'one uppercase letter, one lowercase letter, ' +
'and one special character.'
}),
confirmPassword: z.string().min(8, {
message: 'Password must be at least 8 characters.',
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match!',
path: ['confirmPassword'],
});
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match!',
path: ['confirmPassword'],
});
export default function SignIn() {
const { signIn } = useAuthActions();
@@ -100,13 +107,15 @@ export default function SignIn() {
if (flow === 'signUp') {
formData.append('name', values.name);
if (values.confirmPassword !== values.password)
throw new ConvexError({message: 'Passwords do not match!'});
throw new ConvexError({ message: 'Passwords do not match!' });
}
await signIn('password', formData);
signInForm.reset();
router.push('/');
} catch (error) {
setStatusMessage(`Error signing in: ${error instanceof Error ? error.message : 'Unknown error'}`);
setStatusMessage(
`Error signing in: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
} finally {
setLoading(false);
}
@@ -147,9 +156,7 @@ export default function SignIn() {
name='email'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>
Email
</FormLabel>
<FormLabel className='text-xl'>Email</FormLabel>
<FormControl>
<Input
type='email'
@@ -169,10 +176,10 @@ export default function SignIn() {
render={({ field }) => (
<FormItem>
<div className='flex justify-between'>
<FormLabel className='text-xl'>
Password
</FormLabel>
<Link href='/forgot-password'>Forgot Password?</Link>
<FormLabel className='text-xl'>Password</FormLabel>
<Link href='/forgot-password'>
Forgot Password?
</Link>
</div>
<FormControl>
<Input
@@ -221,9 +228,7 @@ export default function SignIn() {
name='name'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>
Name
</FormLabel>
<FormLabel className='text-xl'>Name</FormLabel>
<FormControl>
<Input
type='text'
@@ -242,9 +247,7 @@ export default function SignIn() {
name='email'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>
Email
</FormLabel>
<FormLabel className='text-xl'>Email</FormLabel>
<FormControl>
<Input
type='email'
@@ -263,9 +266,7 @@ export default function SignIn() {
name='password'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>
Password
</FormLabel>
<FormLabel className='text-xl'>Password</FormLabel>
<FormControl>
<Input
type='password'
@@ -325,4 +326,4 @@ export default function SignIn() {
</Card>
</div>
);
};
}

View File

@@ -8,7 +8,7 @@ import {
ConvexClientProvider,
ThemeProvider,
TVModeProvider,
} from '@/components/providers'
} from '@/components/providers';
import * as Sentry from '@sentry/nextjs';
import { generateMetadata } from '@/lib/metadata';
import PlausibleProvider from 'next-plausible';
@@ -26,36 +26,33 @@ const geistMono = Geist_Mono({
});
const metadata: Metadata = generateMetadata();
metadata.title = `Error | Tech Tracker`;
export {metadata};
export { metadata };
type GlobalErrorProps = {
error: Error & { digest?: string}
error: Error & { digest?: string };
reset?: () => void;
};
const GlobalError = ({error, reset = undefined }: GlobalErrorProps) => {
const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
useEffect(() => {
Sentry.captureException(error);
}, [error])
}, [error]);
return (
<ConvexClientProvider>
<PlausibleProvider
domain='techtracker.gbrown.org'
customDomain='https://plausible.gbrown.org'
>
<html
lang='en'
suppressHydrationWarning
>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
<html lang='en' suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<ConvexClientProvider>
<TVModeProvider>
<Header />
@@ -68,10 +65,9 @@ const GlobalError = ({error, reset = undefined }: GlobalErrorProps) => {
</main>
</TVModeProvider>
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
</ThemeProvider>
</body>
</html>
</PlausibleProvider>
</ConvexClientProvider>
);

View File

@@ -33,26 +33,26 @@ export default function RootLayout({
domain='techtracker.gbrown.org'
customDomain='https://plausible.gbrown.org'
>
<html lang='en'>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
<html lang='en'>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ConvexClientProvider>
<TVModeProvider>
<Header />
{children}
<Toaster />
</TVModeProvider>
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<ConvexClientProvider>
<TVModeProvider>
<Header />
{children}
<Toaster />
</TVModeProvider>
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
</PlausibleProvider>
</ConvexAuthNextjsServerProvider>
);

View File

@@ -7,9 +7,6 @@ import { useAuthActions } from '@convex-dev/auth/react';
import { useRouter } from 'next/navigation';
const Home = () => {
return (
<main>
</main>
);
return <main></main>;
};
export default Home;

View File

@@ -4,7 +4,6 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
BasedAvatar,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -13,17 +12,19 @@ import {
DropdownMenuTrigger,
} from '@/components/ui';
import { useConvexAuth, useQuery } from 'convex/react';
import { useTVMode } from '@/components/providers';
import { useAuthActions } from '@convex-dev/auth/react';
import { api } from '~/convex/_generated/api';
export const AvatarDropdown = () => {
const router = useRouter();
const { isLoading, isAuthenticated } = useConvexAuth();
const { signOut} = useAuthActions();
const { signOut } = useAuthActions();
const user = useQuery(api.auth.getUser);
const { tvMode, toggleTVMode } = useTVMode();
if (isLoading) return <BasedAvatar className='animate-pulse' />;
if (!isAuthenticated) return <div/>;
if (!isAuthenticated) return <div />;
return (
<DropdownMenu>
<DropdownMenuTrigger>
@@ -31,7 +32,7 @@ export const AvatarDropdown = () => {
src={user?.image}
fullName={user?.name}
className='lg:h-10 lg:w-10 my-auto'
fallbackProps={{ className:'text-xl font-semibold' }}
fallbackProps={{ className: 'text-xl font-semibold' }}
userIconProps={{ size: 32 }}
/>
</DropdownMenuTrigger>
@@ -44,6 +45,15 @@ export const AvatarDropdown = () => {
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem asChild>
<button
onClick={toggleTVMode}
className='w-full justify-center cursor-pointer'
>
{tvMode ? 'Normal Mode' : 'TV Mode'}
</button>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href='/profile'
@@ -54,17 +64,16 @@ export const AvatarDropdown = () => {
</DropdownMenuItem>
<DropdownMenuSeparator className='h-[2px]' />
<DropdownMenuItem asChild>
<Button
<button
onClick={() =>
void signOut().then(() => {
router.push('/signin');
})
}
className='w-full justify-center cursor-pointer'
variant='ghost'
>
Sign Out
</Button>
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -1,8 +1,5 @@
'use client';
import {
ThemeToggle,
type ThemeToggleProps,
} from '@/components/providers';
import { ThemeToggle, type ThemeToggleProps } from '@/components/providers';
import { AvatarDropdown } from './AvatarDropdown';
export const Controls = (themeToggleProps?: ThemeToggleProps) => {

View File

@@ -1,9 +1,7 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import {
useTVMode,
} from '@/components/providers';
import { useTVMode } from '@/components/providers';
import { cn } from '@/lib/utils';
import { type ComponentProps } from 'react';
import { Controls } from './controls';
@@ -11,13 +9,12 @@ import { Controls } from './controls';
const Header = (headerProps: ComponentProps<'header'>) => {
const { tvMode } = useTVMode();
if (tvMode) {
if (tvMode)
return (
<div className='absolute top-10 right-37'>
<div className='absolute top-16 right-20'>
<Controls />
</div>
);
}
return (
<header
@@ -29,9 +26,7 @@ const Header = (headerProps: ComponentProps<'header'>) => {
>
<div className='flex items-center justify-between'>
{/* Left spacer for perfect centering */}
<div className='flex flex-1 justify-start'>
<div className='sm:w-[120px] md:w-[160px]' />
</div>
<div className='flex flex-1 justify-start' />
{/* Centered logo and title */}
<div className='flex-shrink-0'>

View File

@@ -6,14 +6,10 @@ import { type ReactNode } from 'react';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export const ConvexClientProvider = ({
children,
}: {
children: ReactNode;
}) => {
export const ConvexClientProvider = ({ children }: { children: ReactNode }) => {
return (
<ConvexAuthNextjsProvider client={convex}>
{children}
</ConvexAuthNextjsProvider>
);
}
};

View File

@@ -42,80 +42,80 @@ const useTVMode = () => {
const TVIcon = ({ tvMode, size = 25 }: { tvMode: boolean; size?: number }) => {
return (
<div
className="relative transition-all duration-300 ease-in-out"
className='relative transition-all duration-300 ease-in-out'
style={{ width: size, height: size }}
>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
className="transition-all duration-300 ease-in-out"
viewBox='0 0 24 24'
fill='none'
className='transition-all duration-300 ease-in-out'
>
{/* TV Screen */}
<rect
x="3"
y="6"
width="18"
height="12"
rx="2"
x='3'
y='6'
width='18'
height='12'
rx='2'
className={cn(
"stroke-current stroke-2 fill-none transition-all duration-300",
tvMode
? "stroke-blue-500 animate-pulse"
: "stroke-current"
'stroke-current stroke-2 fill-none transition-all duration-300',
tvMode ? 'stroke-blue-500 animate-pulse' : 'stroke-current',
)}
/>
{/* TV Stand */}
<path
d="M8 18h8M12 18v2"
className="stroke-current stroke-2 transition-all duration-300"
d='M8 18h8M12 18v2'
className='stroke-current stroke-2 transition-all duration-300'
/>
{/* Corner arrows - animate based on mode */}
<g className={cn(
"transition-all duration-300 ease-in-out origin-center",
tvMode ? "scale-75 opacity-100" : "scale-100 opacity-70"
)}>
<g
className={cn(
'transition-all duration-300 ease-in-out origin-center',
tvMode ? 'scale-75 opacity-100' : 'scale-100 opacity-70',
)}
>
{tvMode ? (
// Exit fullscreen arrows (pointing inward)
<>
<path
d="M6 8l2 2M6 8h2M6 8v2"
className="stroke-current stroke-1.5 transition-all duration-300"
d='M6 8l2 2M6 8h2M6 8v2'
className='stroke-current stroke-1.5 transition-all duration-300'
/>
<path
d="M18 8l-2 2M18 8h-2M18 8v2"
className="stroke-current stroke-1.5 transition-all duration-300"
d='M18 8l-2 2M18 8h-2M18 8v2'
className='stroke-current stroke-1.5 transition-all duration-300'
/>
<path
d="M6 16l2-2M6 16h2M6 16v-2"
className="stroke-current stroke-1.5 transition-all duration-300"
d='M6 16l2-2M6 16h2M6 16v-2'
className='stroke-current stroke-1.5 transition-all duration-300'
/>
<path
d="M18 16l-2-2M18 16h-2M18 16v-2"
className="stroke-current stroke-1.5 transition-all duration-300"
d='M18 16l-2-2M18 16h-2M18 16v-2'
className='stroke-current stroke-1.5 transition-all duration-300'
/>
</>
) : (
// Enter fullscreen arrows (pointing outward)
<>
<path
d="M8 6l-2 2M8 6v2M8 6h-2"
className="stroke-current stroke-1.5 transition-all duration-300"
d='M8 6l-2 2M8 6v2M8 6h-2'
className='stroke-current stroke-1.5 transition-all duration-300'
/>
<path
d="M16 6l2 2M16 6v2M16 6h2"
className="stroke-current stroke-1.5 transition-all duration-300"
d='M16 6l2 2M16 6v2M16 6h2'
className='stroke-current stroke-1.5 transition-all duration-300'
/>
<path
d="M8 18l-2-2M8 18v-2M8 18h-2"
className="stroke-current stroke-1.5 transition-all duration-300"
d='M8 18l-2-2M8 18v-2M8 18h-2'
className='stroke-current stroke-1.5 transition-all duration-300'
/>
<path
d="M16 18l2-2M16 18v-2M16 18h2"
className="stroke-current stroke-1.5 transition-all duration-300"
d='M16 18l2-2M16 18v-2M16 18h2'
className='stroke-current stroke-1.5 transition-all duration-300'
/>
</>
)}
@@ -123,14 +123,12 @@ const TVIcon = ({ tvMode, size = 25 }: { tvMode: boolean; size?: number }) => {
{/* Optional: Screen content indicator */}
<circle
cx="12"
cy="12"
r="1"
cx='12'
cy='12'
r='1'
className={cn(
"transition-all duration-300",
tvMode
? "fill-blue-400 animate-ping"
: "fill-current opacity-30"
'transition-all duration-300',
tvMode ? 'fill-blue-400 animate-ping' : 'fill-current opacity-30',
)}
/>
</svg>
@@ -153,7 +151,7 @@ const TVToggle = ({
onClick={toggleTVMode}
className={cn(
'my-auto cursor-pointer transition-all duration-200 hover:scale-105 active:scale-95',
buttonClassName
buttonClassName,
)}
aria-label={tvMode ? 'Exit TV Mode' : 'Enter TV Mode'}
{...buttonProps}

View File

@@ -1,3 +1,7 @@
export { ConvexClientProvider } from './ConvexClientProvider';
export { ThemeProvider, ThemeToggle, type ThemeToggleProps } from './ThemeProvider';
export {
ThemeProvider,
ThemeToggle,
type ThemeToggleProps,
} from './ThemeProvider';
export { TVModeProvider, useTVMode, TVToggle } from './TVModeProvider';

View File

@@ -1,9 +1,9 @@
"use client"
'use client';
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Avatar({
className,
@@ -11,14 +11,14 @@ function Avatar({
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-slot='avatar'
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className,
)}
{...props}
/>
)
);
}
function AvatarImage({
@@ -27,11 +27,11 @@ function AvatarImage({
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
data-slot='avatar-image'
className={cn('aspect-square size-full', className)}
{...props}
/>
)
);
}
function AvatarFallback({
@@ -40,14 +40,14 @@ function AvatarFallback({
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
data-slot='avatar-fallback'
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
'bg-muted flex size-full items-center justify-center rounded-full',
className,
)}
{...props}
/>
)
);
}
export { Avatar, AvatarImage, AvatarFallback }
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
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 { cn } from '@/lib/utils';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@@ -10,30 +10,30 @@ const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
},
},
defaultVariants: {
variant: "default",
size: "default",
variant: 'default',
size: 'default',
},
}
)
},
);
function Button({
className,
@@ -41,19 +41,19 @@ function Button({
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : 'button';
return (
<Comp
data-slot="button"
data-slot='button'
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,84 +1,84 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Card({ className, ...props }: React.ComponentProps<"div">) {
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
data-slot='card'
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className,
)}
{...props}
/>
)
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
data-slot='card-header'
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
)}
{...props}
/>
)
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
data-slot='card-title'
className={cn('leading-none font-semibold', className)}
{...props}
/>
)
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
data-slot='card-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
data-slot='card-action'
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props}
/>
)
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
data-slot='card-content'
className={cn('px-6', className)}
{...props}
/>
)
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
data-slot='card-footer'
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
)
);
}
export {
@@ -89,4 +89,4 @@ export {
CardAction,
CardDescription,
CardContent,
}
};

View File

@@ -1,23 +1,23 @@
"use client"
'use client';
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
<DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
);
}
function DropdownMenuTrigger({
@@ -25,10 +25,10 @@ function DropdownMenuTrigger({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
data-slot='dropdown-menu-trigger'
{...props}
/>
)
);
}
function DropdownMenuContent({
@@ -39,47 +39,47 @@ function DropdownMenuContent({
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
data-slot='dropdown-menu-content'
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
<DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
variant = 'default',
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
inset?: boolean;
variant?: 'default' | 'destructive';
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-slot='dropdown-menu-item'
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
/>
)
);
}
function DropdownMenuCheckboxItem({
@@ -90,22 +90,22 @@ function DropdownMenuCheckboxItem({
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-slot='dropdown-menu-checkbox-item'
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
<CheckIcon className='size-4' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
);
}
function DropdownMenuRadioGroup({
@@ -113,10 +113,10 @@ function DropdownMenuRadioGroup({
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
data-slot='dropdown-menu-radio-group'
{...props}
/>
)
);
}
function DropdownMenuRadioItem({
@@ -126,21 +126,21 @@ function DropdownMenuRadioItem({
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-slot='dropdown-menu-radio-item'
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
<CircleIcon className='size-2 fill-current' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
);
}
function DropdownMenuLabel({
@@ -148,19 +148,19 @@ function DropdownMenuLabel({
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-slot='dropdown-menu-label'
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
)}
{...props}
/>
)
);
}
function DropdownMenuSeparator({
@@ -169,33 +169,33 @@ function DropdownMenuSeparator({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
data-slot='dropdown-menu-separator'
className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props}
/>
)
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
}: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
data-slot='dropdown-menu-shortcut'
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
)}
{...props}
/>
)
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />;
}
function DropdownMenuSubTrigger({
@@ -204,22 +204,22 @@ function DropdownMenuSubTrigger({
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-slot='dropdown-menu-sub-trigger'
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
<ChevronRightIcon className='ml-auto size-4' />
</DropdownMenuPrimitive.SubTrigger>
)
);
}
function DropdownMenuSubContent({
@@ -228,14 +228,14 @@ function DropdownMenuSubContent({
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
data-slot='dropdown-menu-sub-content'
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
)}
{...props}
/>
)
);
}
export {
@@ -254,4 +254,4 @@ export {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
};

View File

@@ -1,8 +1,8 @@
"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 * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import {
Controller,
FormProvider,
@@ -11,23 +11,23 @@ import {
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
} from 'react-hook-form';
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
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<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
@@ -39,21 +39,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext
const { id } = itemContext;
return {
id,
@@ -62,54 +62,55 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
};
};
type FormItemContextValue = {
id: string
}
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
{} as FormItemContextValue,
);
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
data-slot='form-item'
className={cn('grid gap-2', className)}
{...props}
/>
</FormItemContext.Provider>
)
);
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-slot='form-label'
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
className={cn('data-[error=true]:text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
)
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
data-slot='form-control'
id={formItemId}
aria-describedby={
!error
@@ -119,40 +120,40 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
aria-invalid={!!error}
{...props}
/>
)
);
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
data-slot='form-description'
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
);
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? '') : props.children;
if (!body) {
return null
return null;
}
return (
<p
data-slot="form-message"
data-slot='form-message'
id={formMessageId}
className={cn("text-destructive text-sm", className)}
className={cn('text-destructive text-sm', className)}
{...props}
>
{body}
</p>
)
);
}
export {
@@ -164,4 +165,4 @@ export {
FormDescription,
FormMessage,
FormField,
}
};

View File

@@ -16,7 +16,7 @@ export {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
DropdownMenuTrigger,
} from './dropdown-menu';
export {
useFormField,
@@ -33,10 +33,5 @@ export { Label } from './label';
export { Separator } from './separator';
export { StatusMessage } from './status-message';
export { SubmitButton } from './submit-button';
export {
Tabs,
TabsList,
TabsTrigger,
TabsContent
} from './tabs';
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
export { Toaster } from './sonner';

View File

@@ -1,21 +1,21 @@
import * as React from "react"
import * as React from 'react';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
data-slot='input'
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className,
)}
{...props}
/>
)
);
}
export { Input }
export { Input };

View File

@@ -1,9 +1,9 @@
"use client"
'use client';
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Label({
className,
@@ -11,14 +11,14 @@ function Label({
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
data-slot='label'
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className,
)}
{...props}
/>
)
);
}
export { Label }
export { Label };

View File

@@ -1,28 +1,28 @@
"use client"
'use client';
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Separator({
className,
orientation = "horizontal",
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
data-slot='separator'
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
)}
{...props}
/>
)
);
}
export { Separator }
export { Separator };

View File

@@ -1,25 +1,25 @@
"use client"
'use client';
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
import { useTheme } from 'next-themes';
import { Toaster as Sonner, ToasterProps } from 'sonner';
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const { theme = 'system' } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
theme={theme as ToasterProps['theme']}
className='toaster group'
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
} as React.CSSProperties
}
{...props}
/>
)
}
);
};
export { Toaster }
export { Toaster };

View File

@@ -17,7 +17,8 @@ export const StatusMessage = ({
return (
<div className='flex flex-col items-center w-full'>
{'success' in message && (
<div {...containerProps}
<div
{...containerProps}
className={cn(
'flex flex-col items-center w-11/12 rounded-md p-2',
'dark:bg-green-500/20 bg-green-700/20 border-2',
@@ -29,7 +30,8 @@ export const StatusMessage = ({
</div>
)}
{'error' in message && (
<div {...containerProps}
<div
{...containerProps}
className={cn(
'flex flex-col items-center w-11/12 rounded-md p-2',
'bg-destructive/20 border-2 border-destructive/80',
@@ -40,7 +42,8 @@ export const StatusMessage = ({
</div>
)}
{'message' in message && (
<div {...containerProps}
<div
{...containerProps}
className={cn(
'flex flex-col items-center w-11/12 rounded-md p-2',
'bg-accent/20 border-2 border-primary/80',

View File

@@ -36,7 +36,8 @@ export const SubmitButton = ({
{...loaderProps}
className={cn('mr-2 h-4 w-4 animate-spin', loaderProps?.className)}
/>
<p {...pendingTextProps}
<p
{...pendingTextProps}
className={cn('text-sm font-medium', pendingTextProps?.className)}
>
{pendingText}

View File

@@ -1,9 +1,9 @@
"use client"
'use client';
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from "@/lib/utils"
import { cn } from '@/lib/utils';
function Tabs({
className,
@@ -11,11 +11,11 @@ function Tabs({
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
data-slot='tabs'
className={cn('flex flex-col gap-2', className)}
{...props}
/>
)
);
}
function TabsList({
@@ -24,14 +24,14 @@ function TabsList({
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-slot='tabs-list'
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
className,
)}
{...props}
/>
)
);
}
function TabsTrigger({
@@ -40,14 +40,14 @@ function TabsTrigger({
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
data-slot='tabs-trigger'
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
/>
)
);
}
function TabsContent({
@@ -56,11 +56,11 @@ function TabsContent({
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
data-slot='tabs-content'
className={cn('flex-1 outline-none', className)}
{...props}
/>
)
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -3,7 +3,8 @@ import { z } from 'zod';
export const env = createEnv({
server: {
NODE_ENV: z.enum(['development', 'test', 'production'])
NODE_ENV: z
.enum(['development', 'test', 'production'])
.default('development'),
SKIP_ENV_VALIDATION: z.boolean().default(false),
CONVEX_SELF_HOSTED_URL: z.string(),
@@ -31,7 +32,8 @@ export const env = createEnv({
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_SENTRY_URL: process.env.NEXT_PUBLIC_SENTRY_URL,
NEXT_PUBLIC_SENTRY_ORG: process.env.NEXT_PUBLIC_SENTRY_ORG,
NEXT_PUBLIC_SENTRY_PROJECT_NAME: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
NEXT_PUBLIC_SENTRY_PROJECT_NAME:
process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
},
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
emptyStringAsUndefined: true,

View File

@@ -1,9 +1,9 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
};
return twMerge(clsx(inputs));
}
export const ccn = ({
context,

View File

@@ -24,6 +24,8 @@ export const config = {
// except static assets.
matcher: [
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
'/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'
'/((?!.*\\..*|_next).*)',
'/',
'/(api|trpc)(.*)',
],
};