Added stuff idek

This commit is contained in:
2025-09-02 16:30:37 -05:00
parent 1d32d31550
commit 64b3b0a854
26 changed files with 1102 additions and 467 deletions

View File

@@ -0,0 +1,14 @@
import type { Metadata } from 'next';
export const generateMetadata = (): Metadata => {
return {
title: 'Profile',
};
};
const ProfileLayout = ({
children,
}: Readonly<{ children: React.ReactNode }>) => {
return <div>{children}</div>;
};
export default ProfileLayout;

View File

@@ -0,0 +1,4 @@
const Profile = () => {
return <div></div>;
};
export default Profile;

79
src/app/global-error.tsx Normal file
View File

@@ -0,0 +1,79 @@
'use client';
import type { Metadata } from 'next';
import NextError from 'next/error';
import { Geist, Geist_Mono } from 'next/font/google';
import '@/styles/globals.css';
import {
ConvexClientProvider,
ThemeProvider,
TVModeProvider,
} from '@/components/providers'
import * as Sentry from '@sentry/nextjs';
import { generateMetadata } from '@/lib/metadata';
import PlausibleProvider from 'next-plausible';
import Header from '@/components/layout/header';
import { useEffect } from 'react';
import { Button, Toaster } from '@/components/ui';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
const metadata: Metadata = generateMetadata();
metadata.title = `Error | Tech Tracker`;
export {metadata};
type GlobalErrorProps = {
error: Error & { digest?: string}
reset?: () => void;
};
const GlobalError = ({error, reset = undefined }: GlobalErrorProps) => {
useEffect(() => {
Sentry.captureException(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
>
<ConvexClientProvider>
<TVModeProvider>
<Header />
<main className='min-h-[90vh] flex flex-col items-center'>
<NextError statusCode={0} />
{reset !== undefined && (
<Button onClick={() => reset()}>Try Again</Button>
)}
<Toaster />
</main>
</TVModeProvider>
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
</PlausibleProvider>
</ConvexClientProvider>
);
};
export default GlobalError;

View File

@@ -2,20 +2,24 @@ import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import '@/styles/globals.css';
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
import { ConvexClientProvider, ThemeProvider } from '@/components/providers';
import {
ConvexClientProvider,
ThemeProvider,
TVModeProvider,
} from '@/components/providers';
import PlausibleProvider from 'next-plausible';
import { generateMetadata } from '@/lib/metadata';
import { Toaster } from '@/components/ui';
import Header from '@/components/layout/header';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = generateMetadata();
export default function RootLayout({
@@ -39,9 +43,14 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<ConvexClientProvider>{children}</ConvexClientProvider>
<ConvexClientProvider>
<TVModeProvider>
<Header />
{children}
<Toaster />
</TVModeProvider>
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
</PlausibleProvider>

View File

@@ -6,154 +6,10 @@ import Link from 'next/link';
import { useAuthActions } from '@convex-dev/auth/react';
import { useRouter } from 'next/navigation';
export default function Home() {
const Home = () => {
return (
<>
<header className='sticky top-0 z-10 bg-background p-4 border-b-2 border-slate-200 dark:border-slate-800 flex flex-row justify-between items-center'>
Convex + Next.js + Convex Auth
<SignOutButton />
</header>
<main className='p-8 flex flex-col gap-8'>
<h1 className='text-4xl font-bold text-center'>
Convex + Next.js + Convex Auth
</h1>
<Content />
</main>
</>
<main>
</main>
);
}
function SignOutButton() {
const { isAuthenticated } = useConvexAuth();
const { signOut } = useAuthActions();
const router = useRouter();
return (
<>
{isAuthenticated && (
<button
className='bg-slate-200 dark:bg-slate-800 text-foreground rounded-md px-2 py-1'
onClick={() =>
void signOut().then(() => {
router.push('/signin');
})
}
>
Sign out
</button>
)}
</>
);
}
function Content() {
const { viewer, numbers } =
useQuery(api.myFunctions.listNumbers, {
count: 10,
}) ?? {};
const addNumber = useMutation(api.myFunctions.addNumber);
if (viewer === undefined || numbers === undefined) {
return (
<div className='mx-auto'>
<p>loading... (consider a loading skeleton)</p>
</div>
);
}
return (
<div className='flex flex-col gap-8 max-w-lg mx-auto'>
<p>Welcome {viewer ?? 'Anonymous'}!</p>
<p>
Click the button below and open this page in another window - this data
is persisted in the Convex cloud database!
</p>
<p>
<button
className='bg-foreground text-background text-sm px-4 py-2 rounded-md'
onClick={() => {
void addNumber({ value: Math.floor(Math.random() * 10) });
}}
>
Add a random number
</button>
</p>
<p>
Numbers:{' '}
{numbers?.length === 0
? 'Click the button!'
: (numbers?.join(', ') ?? '...')}
</p>
<p>
Edit{' '}
<code className='text-sm font-bold font-mono bg-slate-200 dark:bg-slate-800 px-1 py-0.5 rounded-md'>
convex/myFunctions.ts
</code>{' '}
to change your backend
</p>
<p>
Edit{' '}
<code className='text-sm font-bold font-mono bg-slate-200 dark:bg-slate-800 px-1 py-0.5 rounded-md'>
app/page.tsx
</code>{' '}
to change your frontend
</p>
<p>
See the{' '}
<Link href='/server' className='underline hover:no-underline'>
/server route
</Link>{' '}
for an example of loading data in a server component
</p>
<div className='flex flex-col'>
<p className='text-lg font-bold'>Useful resources:</p>
<div className='flex gap-2'>
<div className='flex flex-col gap-2 w-1/2'>
<ResourceCard
title='Convex docs'
description='Read comprehensive documentation for all Convex features.'
href='https://docs.convex.dev/home'
/>
<ResourceCard
title='Stack articles'
description='Learn about best practices, use cases, and more from a growing
collection of articles, videos, and walkthroughs.'
href='https://www.typescriptlang.org/docs/handbook/2/basic-types.html'
/>
</div>
<div className='flex flex-col gap-2 w-1/2'>
<ResourceCard
title='Templates'
description='Browse our collection of templates to get started quickly.'
href='https://www.convex.dev/templates'
/>
<ResourceCard
title='Discord'
description='Join our developer community to ask questions, trade tips & tricks,
and show off your projects.'
href='https://www.convex.dev/community'
/>
</div>
</div>
</div>
</div>
);
}
function ResourceCard({
title,
description,
href,
}: {
title: string;
description: string;
href: string;
}) {
return (
<div className='flex flex-col gap-2 bg-slate-200 dark:bg-slate-800 p-4 rounded-md h-28 overflow-auto'>
<a href={href} className='text-sm underline hover:no-underline'>
{title}
</a>
<p className='text-xs'>{description}</p>
</div>
);
}
};
export default Home;

View File

@@ -1,31 +0,0 @@
'use client';
import { type Preloaded, useMutation, usePreloadedQuery } from 'convex/react';
import { api } from '~/convex/_generated/api';
export default function Home({
preloaded,
}: {
preloaded: Preloaded<typeof api.myFunctions.listNumbers>;
}) {
const data = usePreloadedQuery(preloaded);
const addNumber = useMutation(api.myFunctions.addNumber);
return (
<>
<div className='flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md'>
<h2 className='text-xl font-bold'>Reactive client-loaded data</h2>
<code>
<pre>{JSON.stringify(data, null, 2)}</pre>
</code>
</div>
<button
className='bg-foreground text-background px-4 py-2 rounded-md mx-auto'
onClick={() => {
void addNumber({ value: Math.floor(Math.random() * 10) });
}}
>
Add a random number
</button>
</>
);
}

View File

@@ -1,24 +0,0 @@
import Home from './inner';
import { preloadQuery, preloadedQueryResult } from 'convex/nextjs';
import { api } from '~/convex/_generated/api';
export default async function ServerPage() {
const preloaded = await preloadQuery(api.myFunctions.listNumbers, {
count: 3,
});
const data = preloadedQueryResult(preloaded);
return (
<main className='p-8 flex flex-col gap-4 mx-auto max-w-2xl'>
<h1 className='text-4xl font-bold text-center'>Convex + Next.js</h1>
<div className='flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md'>
<h2 className='text-xl font-bold'>Non-reactive server-loaded data</h2>
<code>
<pre>{JSON.stringify(data, null, 2)}</pre>
</code>
</div>
<Home preloaded={preloaded} />
</main>
);
}

View File

@@ -0,0 +1,72 @@
'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import {
BasedAvatar,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui';
import { useConvexAuth, useQuery } from 'convex/react';
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 user = useQuery(api.auth.getUser);
if (isLoading) return <BasedAvatar className='animate-pulse' />;
if (!isAuthenticated) return <div/>;
return (
<DropdownMenu>
<DropdownMenuTrigger>
<BasedAvatar
src={user?.image}
fullName={user?.name}
className='lg:h-10 lg:w-10 my-auto'
fallbackProps={{ className:'text-xl font-semibold' }}
userIconProps={{ size: 32 }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
{(user?.name ?? user?.email) && (
<>
<DropdownMenuLabel className='font-bold text-center'>
{user.name?.trim() ?? user.email?.trim()}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem asChild>
<Link
href='/profile'
className='w-full justify-center cursor-pointer'
>
Edit Profile
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className='h-[2px]' />
<DropdownMenuItem asChild>
<Button
onClick={() =>
void signOut().then(() => {
router.push('/signin');
})
}
className='w-full justify-center cursor-pointer'
variant='ghost'
>
Sign Out
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,23 @@
'use client';
import {
ThemeToggle,
type ThemeToggleProps,
} from '@/components/providers';
import { AvatarDropdown } from './AvatarDropdown';
export const Controls = (themeToggleProps?: ThemeToggleProps) => {
return (
<div className='flex flex-row items-center'>
<ThemeToggle
size={1.2}
buttonProps={{
variant: 'secondary',
size: 'sm',
className: 'mr-4 py-5',
...themeToggleProps?.buttonProps,
}}
/>
<AvatarDropdown />
</div>
);
};

View File

@@ -0,0 +1,69 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import {
useTVMode,
} from '@/components/providers';
import { cn } from '@/lib/utils';
import { type ComponentProps } from 'react';
import { Controls } from './controls';
const Header = (headerProps: ComponentProps<'header'>) => {
const { tvMode } = useTVMode();
if (tvMode) {
return (
<div className='absolute top-10 right-37'>
<Controls />
</div>
);
}
return (
<header
{...headerProps}
className={cn(
'w-full min-h-[10vh] px-4 md:px-6 lg:px-20 my-8',
headerProps?.className,
)}
>
<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>
{/* Centered logo and title */}
<div className='flex-shrink-0'>
<Link
href='/'
scroll={false}
className='flex flex-row items-center justify-center px-4'
>
<Image
src='/favicon.png'
alt='Tech Tracker Logo'
width={100}
height={100}
className='max-w-[40px] md:max-w-[120px]'
/>
<h1
className='title-text text-sm md:text-4xl lg:text-8xl
bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground
dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
>
Tech Tracker
</h1>
</Link>
</div>
{/* Right-aligned controls */}
<div className='flex-1 flex justify-end'>
<Controls />
</div>
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,166 @@
'use client';
import React, { createContext, useContext, useState } from 'react';
import type { ReactNode } from 'react';
import { Button } from '@/components/ui';
import { type ComponentProps } from 'react';
import { cn } from '@/lib/utils';
type TVModeContextProps = {
tvMode: boolean;
toggleTVMode: () => void;
};
type TVToggleProps = {
buttonClassName?: ComponentProps<typeof Button>['className'];
buttonProps?: Omit<ComponentProps<typeof Button>, 'className'>;
size?: number;
};
const TVModeContext = createContext<TVModeContextProps | undefined>(undefined);
const TVModeProvider = ({ children }: { children: ReactNode }) => {
const [tvMode, setTVMode] = useState(false);
const toggleTVMode = () => {
setTVMode((prev) => !prev);
};
return (
<TVModeContext.Provider value={{ tvMode, toggleTVMode }}>
{children}
</TVModeContext.Provider>
);
};
const useTVMode = () => {
const context = useContext(TVModeContext);
if (!context) {
throw new Error('useTVMode must be used within a TVModeProvider');
}
return context;
};
// TV Icon Component with animations
const TVIcon = ({ tvMode, size = 25 }: { tvMode: boolean; size?: number }) => {
return (
<div
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"
>
{/* TV Screen */}
<rect
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"
)}
/>
{/* TV Stand */}
<path
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"
)}>
{tvMode ? (
// Exit fullscreen arrows (pointing inward)
<>
<path
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"
/>
<path
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"
/>
</>
) : (
// Enter fullscreen arrows (pointing outward)
<>
<path
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"
/>
<path
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"
/>
</>
)}
</g>
{/* Optional: Screen content indicator */}
<circle
cx="12"
cy="12"
r="1"
className={cn(
"transition-all duration-300",
tvMode
? "fill-blue-400 animate-ping"
: "fill-current opacity-30"
)}
/>
</svg>
</div>
);
};
const TVToggle = ({
buttonClassName,
buttonProps = {
variant: 'outline',
size: 'default',
},
size = 25,
}: TVToggleProps) => {
const { tvMode, toggleTVMode } = useTVMode();
return (
<Button
onClick={toggleTVMode}
className={cn(
'my-auto cursor-pointer transition-all duration-200 hover:scale-105 active:scale-95',
buttonClassName
)}
aria-label={tvMode ? 'Exit TV Mode' : 'Enter TV Mode'}
{...buttonProps}
>
<TVIcon tvMode={tvMode} size={size} />
</Button>
);
};
export { TVModeProvider, useTVMode, TVToggle };

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
'use client';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { User } from 'lucide-react';
import { cn } from '@/lib/utils';
import { AvatarImage } from '@/components/ui/avatar';
import { type ComponentProps } from 'react';
type BasedAvatarProps = ComponentProps<typeof AvatarPrimitive.Root> & {
src?: string | null;
fullName?: string | null;
imageProps?: Omit<ComponentProps<typeof AvatarImage>, 'data-slot'>;
fallbackProps?: ComponentProps<typeof AvatarPrimitive.Fallback>;
userIconProps?: ComponentProps<typeof User>;
};
const BasedAvatar = ({
src = null,
fullName = null,
imageProps,
fallbackProps,
userIconProps = {
size: 32,
},
className,
...props
}: BasedAvatarProps) => {
return (
<AvatarPrimitive.Root
data-slot='avatar'
className={cn(
'cursor-pointer relative flex size-8 shrink-0 overflow-hidden rounded-full',
className,
)}
{...props}
>
{src ? (
<AvatarImage
{...imageProps}
src={src}
className={imageProps?.className}
/>
) : (
<AvatarPrimitive.Fallback
{...fallbackProps}
data-slot='avatar-fallback'
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
fallbackProps?.className,
)}
>
{fullName ? (
fullName
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
) : (
<User
{...userIconProps}
className={cn('', userIconProps?.className)}
/>
)}
</AvatarPrimitive.Fallback>
)}
</AvatarPrimitive.Root>
);
};
export { BasedAvatar };

View File

@@ -0,0 +1,257 @@
"use client"
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"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
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} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.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
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.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
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
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
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
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
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.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
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"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} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
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
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
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
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@@ -1,3 +1,5 @@
export { Avatar, AvatarImage, AvatarFallback } from './avatar';
export { BasedAvatar } from './based-avatar';
export { Button, buttonVariants } from './button';
export {
Card,
@@ -8,6 +10,14 @@ export {
CardDescription,
CardContent,
} from './card';
export {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from './dropdown-menu';
export {
useFormField,
Form,
@@ -29,3 +39,4 @@ export {
TabsTrigger,
TabsContent
} from './tabs';
export { Toaster } from './sonner';

View File

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