feat: sidebar, mobile sidebar, top nav

This commit is contained in:
mehedi-hasan 2024-04-12 17:16:50 +06:00
parent 13af296b77
commit 8e7bad0092
31 changed files with 3460 additions and 292 deletions

17
components.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

1139
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,19 +9,34 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.367.0",
"next": "14.1.4",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"next": "14.1.4"
"react-resizable-panels": "^2.0.16",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.4",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"eslint": "^8",
"eslint-config-next": "14.1.4"
"typescript": "^5"
}
}

View File

@ -0,0 +1,9 @@
import Image from "next/image";
export default function Home() {
return (
<div className="border" >
hello
</div>
);
}

View File

@ -0,0 +1,39 @@
// import { SyncActiveOrgFromUrl } from "./sync-active-org-from-url";
import { Dashboard } from "@/components/dashboard";
import { TopNav } from "@/components/top-nav";
import { topNavItems } from "@/config";
import { cookies } from "next/headers";
export default function WorkspaceLayout(props: {
children: React.ReactNode;
params: { workspaceId: string };
}) {
const layout = cookies().get("react-resizable-panels:layout");
const collapsed = cookies().get("react-resizable-panels:collapsed");
const defaultLayout = layout?.value ? JSON.parse(layout.value) : undefined;
const defaultCollapsed = collapsed?.value ? JSON.parse(collapsed.value) : undefined;
console.log("defaultL ", defaultLayout)
console.log("defaultC ", defaultCollapsed)
return (
<>
{/* TODO: Nuke it when we can do it serverside in Clerk! */}
{/* <SyncActiveOrgFromUrl /> */}
<div className="min-h-screen rounded-[0.5rem]">
<TopNav navItems={topNavItems}/>
<main className="min-h-[calc(100vh-53px)] flex-1 space-y-4">
<Dashboard
defaultLayout={defaultLayout}
defaultCollapsed={defaultCollapsed}
navCollapsedSize={4}
>
{props.children}
</Dashboard>
</main>
</div>
</>
);
}

View File

@ -2,32 +2,58 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
@apply bg-background text-foreground;
}
}

View File

@ -1,12 +1,13 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Skilld Admin Dashboard",
description: "Skilld Admin Dashboard",
};
export default function RootLayout({
@ -16,7 +17,16 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}

View File

@ -1,113 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">src/app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{" "}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore starter templates for Next.js.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{" "}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
);
}

View File

@ -0,0 +1,90 @@
"use client";
import * as React from "react";
import { HelpCircle, Settings } from "lucide-react";
import { cn } from "@/lib/utils";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { TooltipProvider } from "@/components/ui/tooltip";
import { SidebarNav } from "./sidebar-nav";
import { TopNav } from "./top-nav";
import { docsConfig, topNavItems } from "@/config";
import { DocsSidebarNav } from "./docs-sidebar-nav";
// import { AddButton } from "@/components/buttons/AddButton";
// import { AddAssetFlow } from "@/components/modals/add-asset-flow";
// import { WorkspaceSwitcher } from "@/app/(dashboard)/_components/workspace-switcher";
// import { SidebarNav } from "@/app/(dashboard)/(workspaceId)/_components/sidebar-nav";
// import { Nav } from "./nav";
// import { CardsStats } from "./stats";
// import { TopCategoriesTable } from "./top-categories-table";
// import { TransactionsReviewTable } from "./transaction-review-table";
interface DashboardProps {
defaultLayout: number[] | undefined;
defaultCollapsed?: boolean;
navCollapsedSize: number;
children: React.ReactNode;
}
export function Dashboard({
defaultLayout = [20, 40, 40],
defaultCollapsed = false,
navCollapsedSize,
children,
}: DashboardProps) {
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed);
return (
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes: number[]) => {
document.cookie = `react-resizable-panels:layout=${JSON.stringify(
sizes
)}`;
}}
className="h-full items-stretch"
>
<ResizablePanel
defaultSize={defaultLayout[0]}
collapsedSize={navCollapsedSize}
collapsible={true}
minSize={11}
maxSize={12}
onCollapse={() => {
setIsCollapsed(true);
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(
true
)}`;
}}
onExpand={() => {
setIsCollapsed(false);
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(
false
)}`;
}}
className={cn(
isCollapsed &&
"min-w-[50px] transition-all duration-300 ease-in-out",
"hidden lg:block"
)}
>
<SidebarNav isCollapsed={isCollapsed} />
{/* <DocsSidebarNav items={docsConfig.sidebarNav} /> */}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={defaultLayout[1]} minSize={30}>
{/* <TopNav navItems={topNavItems}/> */}
{children}
</ResizablePanel>
</ResizablePanelGroup>
</TooltipProvider>
);
}

View File

@ -0,0 +1,84 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import { SidebarNavItem } from "@/types/nav"
export interface DocsSidebarNavProps {
items: SidebarNavItem[]
}
export function DocsSidebarNav({ items }: DocsSidebarNavProps) {
const pathname = usePathname()
return items.length ? (
<div className="w-full">
{items.map((item, index) => (
<div key={index} className={cn("pb-4")}>
<h4 className="mb-1 rounded-md px-2 py-1 text-sm font-semibold">
{item.title}
</h4>
{item?.items?.length && (
<DocsSidebarNavItems items={item.items} pathname={pathname} />
)}
</div>
))}
</div>
) : null
}
interface DocsSidebarNavItemsProps {
items: SidebarNavItem[]
pathname: string | null
}
export function DocsSidebarNavItems({
items,
pathname,
}: DocsSidebarNavItemsProps) {
return items?.length ? (
<div className="grid grid-flow-row auto-rows-max text-sm">
{items.map((item, index) =>
item.href && !item.disabled ? (
<Link
key={index}
href={item.href}
className={cn(
"group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline",
item.disabled && "cursor-not-allowed opacity-60",
pathname === item.href
? "font-medium text-foreground"
: "text-muted-foreground"
)}
target={item.external ? "_blank" : ""}
rel={item.external ? "noreferrer" : ""}
>
{item.title}
{item.label && (
<span className="ml-2 rounded-md bg-[#adfa1d] px-1.5 py-0.5 text-xs leading-none text-[#000000] no-underline group-hover:no-underline">
{item.label}
</span>
)}
</Link>
) : (
<span
key={index}
className={cn(
"flex w-full cursor-not-allowed items-center rounded-md p-2 text-muted-foreground hover:underline",
item.disabled && "cursor-not-allowed opacity-60"
)}
>
{item.title}
{item.label && (
<span className="ml-2 rounded-md bg-muted px-1.5 py-0.5 text-xs leading-none text-muted-foreground no-underline group-hover:no-underline">
{item.label}
</span>
)}
</span>
)
)}
</div>
) : null
}

View File

@ -0,0 +1,67 @@
"use client";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { AlignJustify } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { useParams, usePathname } from "next/navigation";
import { Separator } from "@/components/ui/separator";
import { Icons } from "./icons";
import { sideNavItems, siteConfig } from "@/config";
import { ExpandedItem } from "./sidebar-nav";
import { Fragment } from "react";
export default function HamburgerMenu() {
const params = useParams<{ workspaceId: string }>();
const path = usePathname();
const pathname = path.replace(`/${params.workspaceId}`, "") || "/";
const [_, currentPath] = pathname.split("/");
return (
<div className="flex lg:hidden">
<Sheet>
<SheetTrigger>
<AlignJustify />
</SheetTrigger>
<SheetContent side={"left"}>
<SheetHeader>
<SheetTitle className="flex items-center space-x-2">
<Icons.logo />
<span className="inline-block font-urban text-xl font-bold">
{siteConfig.name}
</span>
</SheetTitle>
</SheetHeader>
{sideNavItems.map((group, i) => {
return (
<Fragment key={i}>
<Separator className="mb-2 mt-2" />
<div className="flex flex-col gap-1 p-2" key={group.group}>
{group.items.map((link, idx) => {
return (
<ExpandedItem
key={link.href + idx}
item={link}
currentPath={"/" + currentPath}
/>
);
})}
</div>
</Fragment>
);
})}
</SheetContent>
</Sheet>
</div>
);
}

113
src/components/icons.tsx Normal file
View File

@ -0,0 +1,113 @@
import {
AlertTriangle,
ArrowRight,
BrainCircuit,
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
CreditCard,
File,
FileText,
HelpCircle,
Home,
Image,
Laptop,
Loader2,
LucideIcon,
LucideProps,
Moon,
MoreVertical,
PieChart,
Plus,
Puzzle,
Search,
Settings,
SunMedium,
Trash,
User,
X,
} from "lucide-react";
export type Icon = LucideIcon;
export const Icons = {
add: Plus,
arrowRight: ArrowRight,
billing: CreditCard,
chevronLeft: ChevronLeft,
chevronRight: ChevronRight,
check: Check,
close: X,
ellipsis: MoreVertical,
help: HelpCircle,
laptop: Laptop,
logo: Puzzle,
media: Image,
moon: Moon,
page: File,
post: FileText,
search: Search,
settings: Settings,
spinner: Loader2,
sun: SunMedium,
trash: Trash,
user: User,
warning: AlertTriangle,
home: Home,
piechart: PieChart,
chevrondown: ChevronDown,
brain: BrainCircuit,
gitHub: ({ ...props }: LucideProps) => (
<svg
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="github"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 496 512"
{...props}
>
<path
fill="currentColor"
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"
></path>
</svg>
),
google: ({ ...props }: LucideProps) => (
<svg
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="google"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
{...props}
>
<path
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
fill="currentColor"
/>
</svg>
),
twitter: ({ ...props }: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="twitter"
role="img"
{...props}
>
<path
d="M14.258 10.152L23.176 0h-2.113l-7.747 8.813L7.133 0H0l9.352 13.328L0 23.973h2.113l8.176-9.309 6.531 9.309h7.133zm-2.895 3.293l-.949-1.328L2.875 1.56h3.246l6.086 8.523.945 1.328 7.91 11.078h-3.246zm0 0"
fill="currentColor"
/>
</svg>
),
};

View File

@ -0,0 +1,149 @@
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { buttonVariants } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
// import { WorkspaceSwitcher } from "../../_components/workspace-switcher";
import { NavItem, sideNavItems } from "@/config";
import { Fragment } from "react";
import { ScrollArea, ScrollBar } from "./ui/scroll-area";
const CollapsedItem = ({
item,
currentPath,
}: {
item: NavItem;
currentPath: string;
}) => {
return (
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Link
href={item.href}
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"h-9 w-9",
currentPath === item.href
? "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground"
: "transparent",
"soon" === item.badge && "cursor-not-allowed opacity-80"
)}
>
{item.icon && <item.icon className="h-4 w-4" />}
<span className="sr-only">{item.title}</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-4">
{item.title}
{item?.badge ? <Badge>Coming soon</Badge> : null}
</TooltipContent>
</Tooltip>
);
};
const ExpandedItem = ({
item,
currentPath,
}: {
item: NavItem;
currentPath: string;
}) => {
return (
<Link
href={item.href}
className={cn(
buttonVariants({ variant: "ghost" }),
"justify-start",
currentPath === item.href
? "bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground"
: "transparent",
// "soon" === item.badge && "cursor-not-allowed opacity-80"
)}
>
{item.icon && <item.icon className="mr-2 h-4 w-4" />}
{item.title}
<span className={cn("ml-auto")}>
{item?.badge && <Badge>{item.badge}</Badge>}
</span>
</Link>
);
};
// TODO: idx not needed as key when all items have unique hrefs
// also, the active link should be filtered by href and not idx
export function SidebarNav({ isCollapsed }: { isCollapsed: boolean }) {
console.log("collapsed ", isCollapsed);
const params = useParams<{ workspaceId: string }>();
const path = usePathname();
// remove the workspaceId from the path when comparing active links in sidebar
const pathname = path.replace(`/${params.workspaceId}`, "") || "/";
const [_, currentPath] = pathname.split("/");
return (
<ScrollArea className="h-[calc(100vh-53px)] flex-1">
<nav
className={cn("flex flex-col", {
"items-center justify-center": isCollapsed,
})}
>
{/* <div className="p-2">
workspace
<WorkspaceSwitcher isCollapsed={isCollapsed} />
</div> */}
{sideNavItems.map((group, i) => {
return (
<Fragment key={i}>
{/* <Separator /> */}
<div
className="flex flex-col gap-1 p-2"
// key={group.group}
// key={i}
>
{/* <div>{group.group}</div> */}
{!isCollapsed ? (
<h4 className="rounded-md px-2 mt-2 text-sm font-semibold">
{group.group}
</h4>
) : (
i !== 0 && <Separator />
)}
{group.items.map((link, idx) => {
return isCollapsed ? (
<CollapsedItem
// key={link.href + idx}
item={link}
currentPath={"/" + currentPath}
/>
) : (
<ExpandedItem
// key={link.href + idx}
key={idx}
item={link}
currentPath={"/" + currentPath}
/>
);
})}
</div>
</Fragment>
);
})}
</nav>
<ScrollBar orientation="vertical" />
</ScrollArea>
);
}
export { ExpandedItem };

View File

@ -0,0 +1,9 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@ -0,0 +1,40 @@
"use client"
import * as React from "react"
import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@ -0,0 +1,55 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
import { UserNav } from "@/components/user-nav";
import { Icons } from "./icons";
import { NavItem, siteConfig } from "@/config";
import HamburgerMenu from "./hamburger-menu";
export const TopNav = ({ navItems }: { navItems: NavItem[] }) => {
return (
<nav className="flex h-[52px] items-center justify-between p-4 border-b">
<div className="flex items-center gap-4">
<HamburgerMenu />
<h1 className="font-cal hidden text-xl font-semibold capitalize leading-none md:inline ml-4">
{/* {props.title} */}
Skilld
</h1>
{/* {props.breadcrumbItems && (
<Breadcrumbs items={props.breadcrumbItems} />
)} */}
</div>
{/* {props.headerAction} */}
hello
</nav>
// <nav className="sticky top-0 z-10 flex h-16 items-center gap-10 border-b bg-background/60 px-4 backdrop-blur-xl transition-all">
// <Link href="/" className="flex items-center space-x-2">
// <Icons.logo />
// <span className="inline-block font-urban text-xl font-bold">
// {siteConfig.name}
// </span>
// </Link>
// <nav className="hidden gap-6 md:flex">
// {navItems?.map((item, index) => (
// <Link
// key={index}
// href={item.href}
// className={cn(
// "flex items-center text-lg font-medium text-foreground/60 transition-colors hover:text-foreground/80 sm:text-sm",
// // TODO: active link css
// // item.href.startsWith(`/${segment}`)
// // ? "text-foreground"
// // : "text-foreground/60",
// )}
// >
// {item.title}
// </Link>
// ))}
// </nav>
// <div className="ml-auto flex items-center space-x-4">
// {/* <Search /> */}
// <UserNav />
// </div>
// </nav>
);
};

View File

@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border 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",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,57 @@
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"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,205 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
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
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
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
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -0,0 +1,45 @@
"use client"
import { DragHandleDots2Icon } from "@radix-ui/react-icons"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<DragHandleDots2Icon className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

140
src/components/ui/sheet.tsx Normal file
View File

@ -0,0 +1,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
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",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

108
src/components/user-nav.tsx Normal file
View File

@ -0,0 +1,108 @@
import Link from "next/link";
// import { currentUser } from "@clerk/nextjs";
import {
CreditCard,
LayoutDashboard,
LogOut,
Settings,
User,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export async function UserNav() {
// const user = await currentUser();
// if (!user) {
// return (
// <Link href="/signin">
// <Button className="relative rounded-lg">Sign In</Button>
// </Link>
// );
// }
// const fullname = `${user.firstName} ${user.lastName}`;
// const initials = fullname
// .split(" ")
// .map((n) => n[0])
// .join("");
// const email = user.emailAddresses.find(
// (e) => e.id === user.primaryEmailAddressId,
// )?.emailAddress;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
<AvatarImage src={""} alt={""} />
<AvatarFallback>MH</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{/* {user.firstName} {user.lastName} */}
John Doe
</p>
<p className="text-xs leading-none text-muted-foreground">
{/* {email} */}
johndoe@gmail.com
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
{/* <DropdownMenuItem asChild>
<Link href="/dashboard">
<LayoutDashboard className="mr-2 h-4 w-4" />
<span>Dashboard</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`#`}>
<User className="mr-2 h-4 w-4" />
<span>Profile</span>
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</Link>
</DropdownMenuItem> */}
{/* <DropdownMenuItem asChild>
<Link href={`#`}>
<CreditCard className="mr-2 h-4 w-4" />
<span>Billing</span>
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`#`}>
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</Link>
</DropdownMenuItem> */}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="#">
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,359 @@
// "use client";
// import * as React from "react";
// import Link from "next/link";
// import { useRouter } from "next/navigation";
// import { api } from "@/trpc/client";
// import { useOrganization, useOrganizationList, useUser } from "@clerk/nextjs";
// import { toDecimal } from "dinero.js";
// import { Check, ChevronDown, ChevronsUpDown, PlusCircle } from "lucide-react";
// import { env } from "@projectx/stripe/env";
// import type { ExtendedPlanInfo, PlansResponse } from "@projectx/stripe/plans";
// import type { PurchaseOrg } from "@projectx/validators";
// import { purchaseOrgSchema } from "@projectx/validators";
// import { currencySymbol } from "@/lib/currency";
// import { cn } from "@/lib/utils";
// import { useZodForm } from "@/lib/zod-form";
// import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
// import { Button } from "@/components/ui/button";
// import {
// Command,
// CommandGroup,
// CommandInput,
// CommandItem,
// CommandList,
// CommandSeparator,
// } from "@/components/ui/command";
// import {
// Dialog,
// DialogContent,
// DialogDescription,
// DialogFooter,
// DialogHeader,
// DialogTitle,
// DialogTrigger,
// } from "@/components/ui/dialog";
// import {
// Form,
// FormControl,
// FormField,
// FormItem,
// FormLabel,
// FormMessage,
// } from "@/components/ui/form";
// import { Input } from "@/components/ui/input";
// import {
// Popover,
// PopoverContent,
// PopoverTrigger,
// } from "@/components/ui/popover";
// import {
// Select,
// SelectContent,
// SelectItem,
// SelectTrigger,
// SelectValue,
// } from "@/components/ui/select";
// import { useToast } from "@/components/ui/use-toast";
// type WorkspaceSwitcherProps = {
// isCollapsed: boolean;
// };
// export function WorkspaceSwitcher({ isCollapsed }: WorkspaceSwitcherProps) {
// const router = useRouter();
// const [switcherOpen, setSwitcherOpen] = React.useState(false);
// const [newOrgDialogOpen, setNewOrgDialogOpen] = React.useState(false);
// const orgs = useOrganizationList({
// userMemberships: {
// infinite: true,
// },
// });
// const org = useOrganization();
// const { user, isSignedIn, isLoaded } = useUser();
// if (isLoaded && !isSignedIn) throw new Error("How did you get here???");
// const activeOrg = org.organization ?? user;
// if (
// !orgs.isLoaded ||
// !org.isLoaded ||
// !activeOrg ||
// orgs.userMemberships.isLoading
// ) {
// // Skeleton loader
// return (
// <Button
// variant="ghost"
// size="sm"
// role="combobox"
// aria-expanded={switcherOpen}
// aria-label="Select a workspace"
// className="w-52 justify-between opacity-50"
// >
// <Avatar className="mr-2 h-5 w-5">
// <AvatarFallback>Ac</AvatarFallback>
// </Avatar>
// <div className={cn("flex justify-between", isCollapsed && "hidden")}>
// <span>Select a workspace</span>
// <ChevronsUpDown className="ml-auto h-4 w-4 shrink-0" />
// </div>
// </Button>
// );
// }
// const normalizedObject = {
// id: activeOrg.id,
// name: "name" in activeOrg ? activeOrg.name : activeOrg.fullName,
// image: activeOrg.imageUrl,
// };
// return (
// <Dialog open={newOrgDialogOpen} onOpenChange={setNewOrgDialogOpen}>
// <Popover open={switcherOpen} onOpenChange={setSwitcherOpen}>
// <PopoverTrigger asChild>
// <Button
// variant="ghost"
// size="sm"
// role="combobox"
// aria-expanded={switcherOpen}
// aria-label="Select a workspace"
// className={cn(
// "flex w-full items-center gap-2 border [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0",
// isCollapsed &&
// "flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden",
// )}
// >
// <div className="flex justify-between">
// <Avatar className={cn("h-5 w-5", !isCollapsed && "mr-2")}>
// <AvatarImage src={normalizedObject?.image ?? ""} />
// <AvatarFallback>
// {normalizedObject.name?.substring(0, 2)}
// </AvatarFallback>
// </Avatar>
// <span
// className={cn("whitespace-nowrap", isCollapsed && "hidden")}
// >
// {normalizedObject.name}
// </span>
// </div>
// <ChevronDown
// className={cn(
// "ml-auto h-4 w-4 shrink-0 opacity-50",
// isCollapsed && "hidden",
// )}
// />
// </Button>
// </PopoverTrigger>
// <PopoverContent className="ml-2 w-52 p-0">
// <Command>
// <CommandList>
// <CommandInput placeholder="Search workspace..." />
// <CommandGroup heading="Personal account">
// <CommandItem
// onSelect={async () => {
// if (!user?.id) return;
// normalizedObject.id = user.id ?? "";
// await orgs.setActive?.({ organization: null });
// setSwitcherOpen(false);
// router.push(`/${user.id}`);
// }}
// className="cursor-pointer text-sm"
// >
// <Avatar className="mr-2 h-5 w-5">
// <AvatarImage
// src={user?.imageUrl}
// alt={user?.fullName ?? ""}
// />
// <AvatarFallback>
// {`${user?.firstName?.[0]}${user?.lastName?.[0]}` ?? "JD"}
// </AvatarFallback>
// </Avatar>
// {user?.fullName}
// <Check
// className={cn(
// "ml-auto h-4 w-4",
// org.organization === null ? "opacity-100" : "opacity-0",
// )}
// />
// </CommandItem>
// </CommandGroup>
// <CommandGroup heading="Organizations">
// {orgs.userMemberships.data?.map(({ organization: org }) => (
// <CommandItem
// key={org.name}
// onSelect={async () => {
// await orgs.setActive({ organization: org });
// setSwitcherOpen(false);
// router.push(`/${org.id}`);
// }}
// className="cursor-pointer text-sm"
// >
// <Avatar className="mr-2 h-5 w-5">
// <AvatarImage
// src={org.imageUrl ?? "/images/placeholder.png"}
// alt={org.name}
// />
// <AvatarFallback>
// {org.name.substring(0, 2)}
// </AvatarFallback>
// </Avatar>
// {org.name}
// <Check
// className={cn(
// "ml-auto h-4 w-4",
// normalizedObject?.id === org.id
// ? "opacity-100"
// : "opacity-0",
// )}
// />
// </CommandItem>
// ))}
// </CommandGroup>
// </CommandList>
// <CommandSeparator />
// <CommandList>
// <CommandGroup>
// <DialogTrigger asChild>
// <CommandItem
// onSelect={() => {
// setSwitcherOpen(false);
// setNewOrgDialogOpen(true);
// }}
// className="cursor-pointer"
// >
// <PlusCircle className="mr-2 h-5 w-5" />
// Create Organization
// </CommandItem>
// </DialogTrigger>
// </CommandGroup>
// </CommandList>
// </Command>
// </PopoverContent>
// </Popover>
// <React.Suspense>
// <NewOrganizationDialog closeDialog={() => setNewOrgDialogOpen(false)} />
// </React.Suspense>
// </Dialog>
// );
// }
// function NewOrganizationDialog(props: { closeDialog: () => void }) {
// const useStripe = env.USE_STRIPE === "true";
// let plans: any | null = null;
// if (useStripe) {
// plans = api.stripe.plans.query();
// }
// const form = useZodForm({ schema: purchaseOrgSchema });
// const toaster = useToast();
// async function handleCreateOrg(data: PurchaseOrg) {
// const response = await api.stripe.purchaseOrg
// .mutate(data)
// .catch(() => ({ success: false as const }));
// if (response?.success) window.location.href = response.url as string;
// else
// toaster.toast({
// title: "Error",
// description:
// "There was an error setting up your organization. Please try again.",
// variant: "destructive",
// });
// }
// return (
// <DialogContent>
// <Form {...form}>
// <form
// onSubmit={form.handleSubmit(handleCreateOrg)}
// className="space-y-4"
// >
// <DialogHeader>
// <DialogTitle>Create organization</DialogTitle>
// <DialogDescription>
// Add a new organization to manage products and customers.
// </DialogDescription>
// </DialogHeader>
// <FormField
// control={form.control}
// name="orgName"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Organization name *</FormLabel>
// <FormControl>
// <Input {...field} placeholder="Acme Inc." />
// </FormControl>
// <FormMessage />
// </FormItem>
// )}
// />
// {useStripe && (
// <FormField
// control={form.control}
// name="planId"
// render={({ field }) => (
// <FormItem>
// <div className="flex justify-between">
// <FormLabel>Subscription plan *</FormLabel>
// <Link
// href="/pricing"
// className="text-xs text-muted-foreground hover:underline"
// >
// What&apos;s included in each plan?
// </Link>
// </div>
// <Select
// onValueChange={field.onChange}
// defaultValue={field.value}
// >
// <FormControl>
// <SelectTrigger>
// <SelectValue placeholder="Select a plan" />
// </SelectTrigger>
// </FormControl>
// <SelectContent>
// {plans?.map((plan: ExtendedPlanInfo) => (
// <SelectItem key={plan.priceId} value={plan.priceId}>
// <span className="font-medium">{plan.name}</span> -{" "}
// <span className="text-muted-foreground">
// {toDecimal(
// plan.price,
// ({ value, currency }) =>
// `${currencySymbol(currency.code)}${value}`,
// )}{" "}
// per month
// </span>
// </SelectItem>
// ))}
// </SelectContent>
// </Select>
// <FormMessage />
// </FormItem>
// )}
// />
// )}
// <DialogFooter>
// <Button variant="outline" onClick={() => props.closeDialog()}>
// Cancel
// </Button>
// <Button type="submit">Continue</Button>
// </DialogFooter>
// </form>
// </Form>
// </DialogContent>
// );
// }

498
src/config.ts Normal file
View File

@ -0,0 +1,498 @@
import type { Route } from "next";
import {
BarChart,
BarChart4,
Bolt,
BookA,
BookText,
Building,
CircleUser,
Cloudy,
CreditCard,
FlaskConical,
FlaskRound,
GitCommitHorizontal,
HelpCircle,
Key,
Layers,
LayoutDashboard,
LucideIcon,
Ruler,
Settings,
Sparkle,
Sparkles,
Sprout,
Star,
Users,
Wrench,
} from "lucide-react";
import { MainNavItem, SidebarNavItem } from "./types/nav";
export type NavItem = {
href: Route;
title: string;
badge?: string;
icon?: LucideIcon;
};
export type GroupedNavItems = {
group: string;
items: NavItem[];
};
export const sideNavItems: GroupedNavItems[] = [
{
group: "EVALUATION",
items: [
{
href: "/new-analysis",
title: "Create New Analysis",
icon: FlaskConical,
},
{
href: "/analysis",
title: "Analysis",
icon: FlaskRound,
},
],
},
{
group: "COURSE",
items: [
{
href: "/course",
title: "Your Course",
// badge: "soon",
icon: BookText,
},
],
},
{
group: "ADMIN",
items: [
{
href: "/users",
title: "Users",
icon: Users,
},
{
href: "/organization",
title: "Organization",
icon: Building,
},
{
href: "/account",
title: "Account",
icon: CircleUser,
},
{
href: "/preferences",
title: "Preferences",
icon: Bolt,
},
{
href: "/configuration",
title: "Configuration",
icon: Wrench,
},
{
href: "/api-communication",
title: "Api Communication",
icon: Cloudy,
},
{
href: "/course-admin",
title: "Course Admin",
icon: BookA,
},
],
},
{
group: "ACCOUNT",
items: [
{
href: "/change-password",
title: "Change Password",
icon: Key,
},
],
},
{
group: "EVALUATIONS",
items: [
{
href: "/criteria",
title: "Criteria",
// badge: "soon",
icon: Ruler,
},
{
href: "/skill",
title: "Skill",
// badge: "soon",
icon: Sparkles,
},
{
href: "/evaluation",
title: "Evaluation",
// badge: "soon",
icon: BarChart4,
},
{
href: "/commit",
title: "Commit",
// badge: "soon",
icon: GitCommitHorizontal,
},
],
},
];
export const siteConfig = {
name: "Skilld Admin",
description:
"Empower your financial management with AI-driven insights, making tracking and optimizing your finances effortless.",
github: "https://github.com/projectx-codehagen/Badget",
twitter: "https://twitter.com/codehagen",
};
export const topNavItems = [
{
href: "/docs",
title: "Documentation",
},
{
href: "/support",
title: "Support",
},
] satisfies NavItem[];
interface DocsConfig {
mainNav: MainNavItem[];
sidebarNav: SidebarNavItem[];
}
export const docsConfig: DocsConfig = {
mainNav: [
{
title: "Documentation",
href: "/docs",
},
{
title: "Components",
href: "/docs/components/accordion",
},
{
title: "Themes",
href: "/themes",
},
{
title: "Examples",
href: "/examples",
},
{
title: "Blocks",
href: "/blocks",
},
],
sidebarNav: [
{
title: "Getting Started",
items: [
{
title: "Introduction",
href: "/docs",
items: [],
},
{
title: "Installation",
href: "/docs/installation",
items: [],
},
{
title: "components.json",
href: "/docs/components-json",
items: [],
},
{
title: "Theming",
href: "/docs/theming",
items: [],
},
{
title: "Dark mode",
href: "/docs/dark-mode",
items: [],
},
{
title: "CLI",
href: "/docs/cli",
items: [],
},
{
title: "Typography",
href: "/docs/components/typography",
items: [],
},
{
title: "Figma",
href: "/docs/figma",
items: [],
},
{
title: "Changelog",
href: "/docs/changelog",
items: [],
},
],
},
{
title: "Components",
items: [
{
title: "Accordion",
href: "/docs/components/accordion",
items: [],
},
{
title: "Alert",
href: "/docs/components/alert",
items: [],
},
{
title: "Alert Dialog",
href: "/docs/components/alert-dialog",
items: [],
},
{
title: "Aspect Ratio",
href: "/docs/components/aspect-ratio",
items: [],
},
{
title: "Avatar",
href: "/docs/components/avatar",
items: [],
},
{
title: "Badge",
href: "/docs/components/badge",
items: [],
},
{
title: "Breadcrumb",
href: "/docs/components/breadcrumb",
items: [],
label: "New",
},
{
title: "Button",
href: "/docs/components/button",
items: [],
},
{
title: "Calendar",
href: "/docs/components/calendar",
items: [],
},
{
title: "Card",
href: "/docs/components/card",
items: [],
},
{
title: "Carousel",
href: "/docs/components/carousel",
items: [],
},
{
title: "Checkbox",
href: "/docs/components/checkbox",
items: [],
},
{
title: "Collapsible",
href: "/docs/components/collapsible",
items: [],
},
{
title: "Combobox",
href: "/docs/components/combobox",
items: [],
},
{
title: "Command",
href: "/docs/components/command",
items: [],
},
{
title: "Context Menu",
href: "/docs/components/context-menu",
items: [],
},
{
title: "Data Table",
href: "/docs/components/data-table",
items: [],
},
{
title: "Date Picker",
href: "/docs/components/date-picker",
items: [],
},
{
title: "Dialog",
href: "/docs/components/dialog",
items: [],
},
{
title: "Drawer",
href: "/docs/components/drawer",
items: [],
},
{
title: "Dropdown Menu",
href: "/docs/components/dropdown-menu",
items: [],
},
{
title: "Form",
href: "/docs/components/form",
items: [],
},
{
title: "Hover Card",
href: "/docs/components/hover-card",
items: [],
},
{
title: "Input",
href: "/docs/components/input",
items: [],
},
{
title: "Input OTP",
href: "/docs/components/input-otp",
items: [],
label: "New",
},
{
title: "Label",
href: "/docs/components/label",
items: [],
},
{
title: "Menubar",
href: "/docs/components/menubar",
items: [],
},
{
title: "Navigation Menu",
href: "/docs/components/navigation-menu",
items: [],
},
{
title: "Pagination",
href: "/docs/components/pagination",
items: [],
},
{
title: "Popover",
href: "/docs/components/popover",
items: [],
},
{
title: "Progress",
href: "/docs/components/progress",
items: [],
},
{
title: "Radio Group",
href: "/docs/components/radio-group",
items: [],
},
{
title: "Resizable",
href: "/docs/components/resizable",
items: [],
},
{
title: "Scroll Area",
href: "/docs/components/scroll-area",
items: [],
},
{
title: "Select",
href: "/docs/components/select",
items: [],
},
{
title: "Separator",
href: "/docs/components/separator",
items: [],
},
{
title: "Sheet",
href: "/docs/components/sheet",
items: [],
},
{
title: "Skeleton",
href: "/docs/components/skeleton",
items: [],
},
{
title: "Slider",
href: "/docs/components/slider",
items: [],
},
{
title: "Sonner",
href: "/docs/components/sonner",
items: [],
},
{
title: "Switch",
href: "/docs/components/switch",
items: [],
},
{
title: "Table",
href: "/docs/components/table",
items: [],
},
{
title: "Tabs",
href: "/docs/components/tabs",
items: [],
},
{
title: "Textarea",
href: "/docs/components/textarea",
items: [],
},
{
title: "Toast",
href: "/docs/components/toast",
items: [],
},
{
title: "Toggle",
href: "/docs/components/toggle",
items: [],
},
{
title: "Toggle Group",
href: "/docs/components/toggle-group",
items: [],
},
{
title: "Tooltip",
href: "/docs/components/tooltip",
items: [],
},
],
},
],
};

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

18
src/types/nav.ts Normal file
View File

@ -0,0 +1,18 @@
import { Icons } from "@/components/icons";
export interface NavItem {
title: string;
href?: string;
disabled?: boolean;
external?: boolean;
icon?: keyof typeof Icons;
label?: string;
}
export interface NavItemWithChildren extends NavItem {
items: NavItemWithChildren[];
}
export interface MainNavItem extends NavItem {}
export interface SidebarNavItem extends NavItemWithChildren {}

View File

@ -1,20 +1,80 @@
import type { Config } from "tailwindcss";
import type { Config } from "tailwindcss"
const config: Config = {
const config = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [],
};
export default config;
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config