mirror of
https://github.com/QWRK-ORG/qwrk-laconic-core.git
synced 2026-09-12 06:47:30 +00:00
chore(ux): basic navigation functions
This commit is contained in:
-217
@@ -1,217 +0,0 @@
|
||||
// 'use client';
|
||||
|
||||
// import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
// import { LaconicMark } from '@/laconic-assets/laconic-mark';
|
||||
// import { Menu, Plus, Shapes, Wallet } from 'lucide-react';
|
||||
// import Link from 'next/link';
|
||||
// import { useRouter } from 'next/navigation';
|
||||
// import { useState } from 'react';
|
||||
// import { ProjectSearchBar } from '../search/ProjectSearchBar';
|
||||
// import { ColorModeToggle } from './components/ColorModeToggle';
|
||||
// import { GitHubSessionButton } from './components/GitHubSessionButton';
|
||||
// import { WalletSessionId } from './components/WalletSessionId';
|
||||
// import { Button } from '@workspace/ui/components/button';
|
||||
|
||||
// /**
|
||||
// * TopNavigation Component
|
||||
// *
|
||||
// * Renders the top navigation bar, adapting its layout for desktop and mobile views.
|
||||
// * It includes the project search bar, navigation links, user authentication via GitHub,
|
||||
// * color mode toggle, and wallet session information. On mobile, it utilizes a sheet
|
||||
// * for a responsive and accessible navigation experience.
|
||||
// */
|
||||
// export function TopNavigation() {
|
||||
// const router = useRouter();
|
||||
// const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
|
||||
// // In a real implementation, these would be from context/hooks
|
||||
// const isGitHubConnected = true; // Example value
|
||||
// const defaultOrg = { slug: 'demo-org' }; // Example value
|
||||
// const isReady = true; // Wallet ready status
|
||||
|
||||
// const handleOnboardingClose = () => {
|
||||
// setShowOnboarding(false);
|
||||
// // In a real implementation, would refresh organization data after onboarding
|
||||
// };
|
||||
|
||||
// // Navigate to create page with organization slug
|
||||
// const handleCreateNew = () => {
|
||||
// if (defaultOrg) {
|
||||
// router.push(`/${defaultOrg.slug}/ps/cr`);
|
||||
// } else {
|
||||
// // If no organization is available, show onboarding
|
||||
// setShowOnboarding(true);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleRunOnboarding = () => {
|
||||
// // Clear existing onboarding progress data
|
||||
// if (typeof window !== 'undefined') {
|
||||
// localStorage.removeItem('onboarding_progress');
|
||||
// localStorage.removeItem('onboarding_state');
|
||||
// // Force starting from connect step
|
||||
// localStorage.setItem('onboarding_force_connect', 'true');
|
||||
// }
|
||||
|
||||
// setShowOnboarding(true);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="bg-background">
|
||||
// {/* Top Navigation - Desktop */}
|
||||
// <nav className="md:flex items-center justify-between hidden h-16 px-6 border-b">
|
||||
// <div className="flex items-center gap-6">
|
||||
// {/* Logo / Home Link */}
|
||||
// <Link href="/" className="flex items-center justify-center w-10 h-10">
|
||||
// <LaconicMark />
|
||||
// </Link>
|
||||
|
||||
// {/* Navigation Items */}
|
||||
// <div className="flex items-center gap-4">
|
||||
// <Link href="/pr">
|
||||
// <Button variant="ghost" className="gap-2">
|
||||
// <Shapes className="w-5 h-5" />
|
||||
// <span>Projects</span>
|
||||
// </Button>
|
||||
// </Link>
|
||||
// <Button
|
||||
// variant="ghost"
|
||||
// className="text-muted-foreground gap-2"
|
||||
// >
|
||||
// <Link href="/wallet">
|
||||
// <Wallet className="w-5 h-5" />
|
||||
// <span>Wallet</span>
|
||||
// </Link>
|
||||
// </Button>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div className="flex items-center gap-4">
|
||||
// {/* Add New Button with Dropdown */}
|
||||
// <DropdownMenu>
|
||||
// <DropdownMenuTrigger asChild>
|
||||
// <Button variant="ghost" size="icon" className="rounded-full">
|
||||
// <Plus className="h-5 w-5" />
|
||||
// </Button>
|
||||
// </DropdownMenuTrigger>
|
||||
// <DropdownMenuContent align="end">
|
||||
// {isGitHubConnected && (
|
||||
// <>
|
||||
// <DropdownMenuItem onClick={handleCreateNew}>
|
||||
// Create New
|
||||
// </DropdownMenuItem>
|
||||
// <DropdownMenuSeparator />
|
||||
// </>
|
||||
// )}
|
||||
// <DropdownMenuItem onClick={handleRunOnboarding}>
|
||||
// Run Onboarding
|
||||
// </DropdownMenuItem>
|
||||
// </DropdownMenuContent>
|
||||
// </DropdownMenu>
|
||||
|
||||
// <Button variant="ghost" className="text-muted-foreground">
|
||||
// <Link href="/support">Support</Link>
|
||||
// </Button>
|
||||
// <Button variant="ghost" className="text-muted-foreground">
|
||||
// <Link href="/docs">Documentation</Link>
|
||||
// </Button>
|
||||
// <GitHubSessionButton />
|
||||
// <ColorModeToggle />
|
||||
// <WalletSessionId walletId="0xAb...1234" />
|
||||
// </div>
|
||||
// </nav>
|
||||
|
||||
// {/* Top Navigation - Mobile */}
|
||||
// <nav className="md:hidden flex items-center justify-between h-16 px-4 border-b">
|
||||
// <Sheet>
|
||||
// <SheetTrigger>
|
||||
// <Button variant="outline" size="icon">
|
||||
// <Menu className="w-4 h-4" />
|
||||
// </Button>
|
||||
// </SheetTrigger>
|
||||
// <SheetContent
|
||||
// side="left"
|
||||
// className="w-[300px] sm:w-[400px] flex flex-col"
|
||||
// >
|
||||
// <nav className="flex flex-col flex-grow space-y-4">
|
||||
// <div className="px-4 py-2">
|
||||
// <ProjectSearchBar
|
||||
// onChange={(project) => {
|
||||
// router.push(
|
||||
// `/pr/${project.organization.slug}/ps/${project.id}`,
|
||||
// );
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// <Button variant="ghost" className="justify-start gap-2">
|
||||
// <Link href="/pr">
|
||||
// <Shapes className="w-5 h-5" />
|
||||
// <span>Projects</span>
|
||||
// </Link>
|
||||
// </Button>
|
||||
// <Button variant="ghost" className="justify-start gap-2">
|
||||
// <Link href="/wallet">
|
||||
// <Wallet className="w-5 h-5" />
|
||||
// <span>Wallet</span>
|
||||
// </Link>
|
||||
// </Button>
|
||||
// <Button variant="ghost" className="justify-start">
|
||||
// <Link href="/support">Support</Link>
|
||||
// </Button>
|
||||
// <Button variant="ghost" className="justify-start">
|
||||
// <Link href="/docs">Documentation</Link>
|
||||
// </Button>
|
||||
// <Button
|
||||
// variant="ghost"
|
||||
// className="justify-start"
|
||||
// onClick={handleRunOnboarding}
|
||||
// >
|
||||
// Run Onboarding
|
||||
// </Button>
|
||||
// {isGitHubConnected && (
|
||||
// <Button
|
||||
// variant="ghost"
|
||||
// className="justify-start"
|
||||
// onClick={handleCreateNew}
|
||||
// >
|
||||
// Create New
|
||||
// </Button>
|
||||
// )}
|
||||
// </nav>
|
||||
// <div className="flex items-center justify-between mt-auto">
|
||||
// <GitHubSessionButton />
|
||||
// <ColorModeToggle />
|
||||
// <WalletSessionId walletId="0xAb...1234" />
|
||||
// </div>
|
||||
// </SheetContent>
|
||||
// </Sheet>
|
||||
// <div className="flex items-center gap-2">
|
||||
// {/* Add New Button (Mobile) */}
|
||||
// <DropdownMenu>
|
||||
// <DropdownMenuTrigger asChild>
|
||||
// <Button variant="ghost" size="icon" className="rounded-full">
|
||||
// <Plus className="h-5 w-5" />
|
||||
// </Button>
|
||||
// </DropdownMenuTrigger>
|
||||
// <DropdownMenuContent align="end">
|
||||
// {isGitHubConnected && (
|
||||
// <>
|
||||
// <DropdownMenuItem onClick={handleCreateNew}>
|
||||
// Create New
|
||||
// </DropdownMenuItem>
|
||||
// <DropdownMenuSeparator />
|
||||
// </>
|
||||
// )}
|
||||
// <DropdownMenuItem onClick={handleRunOnboarding}>
|
||||
// Run Onboarding
|
||||
// </DropdownMenuItem>
|
||||
// </DropdownMenuContent>
|
||||
// </DropdownMenu>
|
||||
// <ColorModeToggle />
|
||||
// <WalletSessionId walletId="0xAb...1234" />
|
||||
// </div>
|
||||
// </nav>
|
||||
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
|
||||
import PageWrapper from '@/components/foundation/page-wrapper'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@workspace/ui/components/card'
|
||||
/**
|
||||
* BuyPrepaidService component allows users to buy prepaid services.
|
||||
* It checks if the user's balance is sufficient and redirects them to the home page if it is.
|
||||
*
|
||||
* @returns {JSX.Element} A JSX element that renders the buy prepaid service page.
|
||||
*/
|
||||
const BuyPrepaidService = () => {
|
||||
return (
|
||||
<PageWrapper
|
||||
// header={{
|
||||
// title: 'Buy prepaid service',
|
||||
// actions: [
|
||||
// {
|
||||
// label: 'Buy prepaid service',
|
||||
// href: process.env.NEXT_PUBLIC_SHOPIFY_APP_URL,
|
||||
// isPrimary: true
|
||||
// }
|
||||
// ]
|
||||
// }}
|
||||
>
|
||||
<div className=" mx-auto px-4 py-16 md:py-18 w-full">
|
||||
<div className="mx-auto w-full min-w-fit">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="font-normal text-gray-400 tracking-tight sm:text-2xl text-foreground">
|
||||
Laconic
|
||||
</h2>
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl text-foreground">
|
||||
Webapp Deployment Plans
|
||||
</h1>
|
||||
<p className="mt-4 text-muted-foreground">
|
||||
Choose the perfect deployment plan for your needs. Scale your
|
||||
applications with confidence.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3 w-full">
|
||||
{/* Basic Plan */}
|
||||
<Card className="border border-gray-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Basic</CardTitle>
|
||||
<CardDescription>
|
||||
A simple deployment option for small projects
|
||||
</CardDescription>
|
||||
<div className="mt-4">
|
||||
<span className="text-3xl font-bold">$5.00</span>
|
||||
<span className="text-muted-foreground ml-1">/month</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
1 monthly webapp deployment
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" variant="outline">
|
||||
Purchase plan
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Standard Plan */}
|
||||
<Card className="border-2 border-primary relative">
|
||||
<div className="absolute -top-3 left-0 right-0 mx-auto w-fit rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
|
||||
Most popular
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle>Standard</CardTitle>
|
||||
<CardDescription>
|
||||
Perfect for growing projects and businesses
|
||||
</CardDescription>
|
||||
<div className="mt-4">
|
||||
<span className="text-3xl font-bold">$50.00</span>
|
||||
<span className="text-muted-foreground ml-1">/month</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
10 monthly webapp deployments
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Purchase plan</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Premium Plan */}
|
||||
<Card className="border border-gray-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Premium</CardTitle>
|
||||
<CardDescription>
|
||||
For enterprises with high-volume needs
|
||||
</CardDescription>
|
||||
<div className="mt-4">
|
||||
<span className="text-3xl font-bold">$500.00</span>
|
||||
<span className="text-muted-foreground ml-1">/month</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
100 monthly webapp deployments
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" variant="outline">
|
||||
Purchase plan
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuyPrepaidService
|
||||
+2
-7
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import BuyPrepaidService from './BuyServices'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Buy Prepaid Service',
|
||||
@@ -6,13 +7,7 @@ export const metadata: Metadata = {
|
||||
}
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
Hello from (web3-authenticated)/(dashboard)/(buy-prepaid-service)/bp
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
return <BuyPrepaidService />
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { DashboardLayout } from '@/components/layouts'
|
||||
import { defaultDashboardConfig } from '@/config/dashboard'
|
||||
import type React from 'react'
|
||||
import NavigationWrapper from '@/components/foundation/navigation-wrapper'
|
||||
import { TopNavigation } from '@/components/foundation/top-navigation/top-navigation'
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode
|
||||
@@ -10,13 +9,10 @@ interface LayoutProps {
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
return (
|
||||
// <div className="flex flex-col min-h-screen">
|
||||
// <div className="flex-1 p-8">
|
||||
<DashboardLayout config={defaultDashboardConfig}>
|
||||
<NavigationWrapper>
|
||||
<TopNavigation />
|
||||
{children}
|
||||
</DashboardLayout>
|
||||
// </div>
|
||||
// </div>
|
||||
</NavigationWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,4 +1,4 @@
|
||||
import { Providers } from '@/components/providers'
|
||||
import { Providers } from '@/components/foundation/providers'
|
||||
// import '@workspace/ui/globals.css'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
@@ -26,16 +26,8 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${inter.className} `}>
|
||||
<body className={`${inter.className} `} suppressHydrationWarning>
|
||||
<main>
|
||||
<button
|
||||
type="button"
|
||||
// onClick={() => {
|
||||
// console.log('clicked')
|
||||
// }}
|
||||
>
|
||||
Click me
|
||||
</button>
|
||||
<Providers>{children}</Providers>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingOverlay isLoading={true} />
|
||||
}
|
||||
@@ -2,21 +2,62 @@ import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
description: 'The requested page could not be found'
|
||||
title: '404',
|
||||
description: 'Page not found'
|
||||
}
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen p-4">
|
||||
<h2 className="text-2xl font-bold mb-4">404 - Page Not Found</h2>
|
||||
<p className="mb-8">The page you are looking for does not exist.</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Return Home
|
||||
</Link>
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 mb-2">404</p>
|
||||
<h1 className="text-3xl font-bold mb-4">Page not found</h1>
|
||||
<p className="text-gray-600 mb-8">
|
||||
Add a brief, empathetic explanation that acknowledges the error while
|
||||
maintaining brand voice. Keep it light and helpful.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 max-w-md mx-auto">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">Go back to homepage</div>
|
||||
<div className="text-gray-600 text-sm">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisci.
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/docs"
|
||||
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">Read our documentation</div>
|
||||
<div className="text-gray-600 text-sm">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisci.
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/blog"
|
||||
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium">Explore our blog</div>
|
||||
<div className="text-gray-600 text-sm">
|
||||
Lorem ipsum dolor sit amet, consectetur adipisci.
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-400">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PageWrapper from '@/components/foundation/page-wrapper'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
@@ -7,5 +8,20 @@ export const metadata: Metadata = {
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <Button size="sm">Button</Button>
|
||||
return (
|
||||
<PageWrapper>
|
||||
<section className="flex flex-col items-center text-center max-w-3xl mx-auto py-16 space-y-6">
|
||||
<div className="text-sm font-medium">Hero section</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold">
|
||||
Headline that solves user's main problem
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Follow with one or two sentences that expand on your value
|
||||
proposition. Focus on key benefits and address why users should take
|
||||
action now. Keep it scannable, short and benefit-driven.
|
||||
</p>
|
||||
<Button size="lg">Get started</Button>
|
||||
</section>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* The Laconic logo mark component.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* // Basic usage
|
||||
* <LaconicMark />
|
||||
*
|
||||
* // With custom color
|
||||
* <LaconicMark className="text-blue-500" />
|
||||
*
|
||||
* // With custom size
|
||||
* <LaconicMark className="w-12 h-12" />
|
||||
*/
|
||||
export function LaconicMark({
|
||||
/**
|
||||
* Optional className for styling the SVG
|
||||
* Use this to customize the size, color, and other SVG properties
|
||||
*/
|
||||
className = ''
|
||||
}: {
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width="80"
|
||||
height="81"
|
||||
viewBox="0 0 80 81"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
aria-label="Laconic logo"
|
||||
>
|
||||
<title>Laconic logo</title>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M23.4359 41.7985C29.255 35.9796 32.8569 27.9437 32.8552 19.0713C32.8574 17.867 32.7909 16.6761 32.6587 15.5L15 15.5016L15.0005 49.4307C14.9989 53.5435 16.567 57.6575 19.7035 60.7938C22.8402 63.9303 26.9571 65.5003 31.0705 65.4982L65 65.5L64.9987 47.8387C63.8252 47.7095 62.6342 47.6425 61.4272 47.6423C52.5572 47.6435 44.521 51.245 38.7019 57.0638C34.4676 61.192 27.6786 61.1927 23.4965 57.0107C19.3172 52.8312 19.3153 46.0402 23.4359 41.7985ZM61.3372 19.1716C56.4605 14.2952 48.5377 14.2933 43.6595 19.1716C38.7812 24.0496 38.7831 31.9724 43.6595 36.8485C48.5387 41.7275 56.459 41.7267 61.3372 36.8485C66.2155 31.9705 66.2165 24.0504 61.3372 19.1716Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { LaconicMark } from '@/components/assets/laconic-mark'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export interface LoadingOverlayProps {
|
||||
/**
|
||||
* Controls the visibility of the overlay.
|
||||
* When false, the component returns null.
|
||||
* @default true
|
||||
*/
|
||||
isLoading?: boolean
|
||||
|
||||
/**
|
||||
* Optional className for styling the overlay container.
|
||||
* This will be merged with the default styles.
|
||||
*/
|
||||
className?: string
|
||||
|
||||
/**
|
||||
* Whether to show the Laconic logo in the overlay.
|
||||
* @default true
|
||||
*/
|
||||
showLogo?: boolean
|
||||
|
||||
/**
|
||||
* Whether to show the loading spinner below the logo.
|
||||
* @default true
|
||||
*/
|
||||
showSpinner?: boolean
|
||||
|
||||
/**
|
||||
* The z-index value for the overlay.
|
||||
* Adjust this if you need the overlay to appear above or below other elements.
|
||||
* @default 50
|
||||
*/
|
||||
zIndex?: number
|
||||
|
||||
/**
|
||||
* Whether to use solid black background instead of semi-transparent.
|
||||
* Useful for initial page load and full-screen loading states.
|
||||
* @default false
|
||||
*/
|
||||
solid?: boolean
|
||||
}
|
||||
|
||||
export function LoadingOverlay({
|
||||
isLoading = true,
|
||||
className,
|
||||
showLogo = true,
|
||||
showSpinner = true,
|
||||
zIndex = 50,
|
||||
solid = false
|
||||
}: LoadingOverlayProps) {
|
||||
if (!isLoading) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 flex flex-col items-center justify-center',
|
||||
solid ? 'bg-black' : 'bg-black/90 backdrop-blur-sm',
|
||||
className
|
||||
)}
|
||||
style={{ zIndex }}
|
||||
role="alert"
|
||||
aria-busy="true"
|
||||
aria-label="Loading"
|
||||
>
|
||||
{showLogo && (
|
||||
<div className="animate-fade-in mb-4">
|
||||
<LaconicMark className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
{showSpinner && (
|
||||
<Loader2
|
||||
className="animate-spin w-6 h-6 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import type { NavigationWrapperProps } from './types'
|
||||
|
||||
/**
|
||||
* NavigationWrapper component
|
||||
*
|
||||
* Provides a layout wrapper with a top navigation bar.
|
||||
* This component is intended to be used at the layout level,
|
||||
* wrapping the entire application or major sections of it.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // In app/layout.tsx
|
||||
* export default function RootLayout({ children }) {
|
||||
* return (
|
||||
* <html lang="en">
|
||||
* <body>
|
||||
* <NavigationWrapper>
|
||||
* {children}
|
||||
* </NavigationWrapper>
|
||||
* </body>
|
||||
* </html>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param props - The component props
|
||||
* @returns A layout wrapper with top navigation
|
||||
*/
|
||||
export default function NavigationWrapper({
|
||||
children,
|
||||
className
|
||||
}: NavigationWrapperProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col min-h-screen w-full', className)}>
|
||||
{/* Top Navigation */}
|
||||
{/* <header className="sticky top-0 z-40 w-full border-b bg-background">
|
||||
<div className="container flex h-16 items-center">
|
||||
<div className="mr-4 flex">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<span className="font-bold text-xl">App Logo</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="flex items-center space-x-4 lg:space-x-6 mx-6">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-sm font-medium transition-colors hover:text-primary"
|
||||
> the fuck
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="/projects"
|
||||
className="text-sm font-medium transition-colors hover:text-primary"
|
||||
>
|
||||
Projects
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="text-sm font-medium transition-colors hover:text-primary"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center space-x-4">
|
||||
<Button className="text-sm font-medium">Profile</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header> */}
|
||||
|
||||
{/* Main Content */}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@workspace/ui/components/dropdown-menu'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { MoreVertical } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import type { PageAction, PageHeaderProps } from './types'
|
||||
|
||||
export default function PageHeader({
|
||||
title,
|
||||
description,
|
||||
actions = [],
|
||||
className
|
||||
}: PageHeaderProps) {
|
||||
// Separate primary actions from secondary actions
|
||||
const primaryActions = actions.filter((action) => action.isPrimary)
|
||||
const secondaryActions = actions.filter((action) => !action.isPrimary)
|
||||
|
||||
// Render an action (either as button or link)
|
||||
const renderAction = (action: PageAction, key: string) => {
|
||||
const variant = action.variant || (action.isPrimary ? 'default' : 'outline')
|
||||
|
||||
if (action.href) {
|
||||
return (
|
||||
<Button key={key} variant={variant} asChild>
|
||||
<Link href={action.href}>{action.label}</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button key={key} variant={variant} onClick={action.onClick}>
|
||||
{action.label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('w-full max-w-[1232px] mx-auto', className)}>
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-6 md:gap-2">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl md:text-[30px] font-bold leading-8 md:leading-9 text-foreground">
|
||||
{title}
|
||||
</h1>
|
||||
{description && (
|
||||
<p className="text-sm md:text-base text-muted-foreground leading-5 md:leading-6">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actions.length > 0 && (
|
||||
<>
|
||||
{/* Desktop buttons */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
{actions.map((action, index) =>
|
||||
renderAction(action, `desktop-${index}`)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile buttons */}
|
||||
<div className="flex md:hidden items-center gap-2">
|
||||
{primaryActions.map((action, index) =>
|
||||
renderAction(action, `mobile-primary-${index}`)
|
||||
)}
|
||||
|
||||
{secondaryActions.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{secondaryActions.map((action) =>
|
||||
action.href ? (
|
||||
<DropdownMenuItem asChild key={action.label}>
|
||||
<Link href={action.href}>{action.label}</Link>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
onClick={action.onClick}
|
||||
key={action.label}
|
||||
>
|
||||
{action.label}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import PageHeader from './page-header'
|
||||
import type { PageWrapperProps } from './types'
|
||||
|
||||
export default function PageWrapper({
|
||||
header,
|
||||
children,
|
||||
layout = 'default',
|
||||
className,
|
||||
contentClassName
|
||||
}: PageWrapperProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col h-full', className)}>
|
||||
{header && (
|
||||
<div className="py-6 md:py-8 px-4 md:px-6 border-b">
|
||||
<PageHeader {...header} />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('flex-1 px-4 md:px-6 py-6 md:py-8', contentClassName)}>
|
||||
<div
|
||||
className={cn(
|
||||
'mx-auto',
|
||||
layout === 'default' ? 'max-w-4xl' : 'w-full max-w-[1232px]',
|
||||
layout === 'bento' && 'grid grid-cols-1 md:grid-cols-3 gap-6'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import type * as React from 'react'
|
||||
import '@workspace/ui/globals.css'
|
||||
import { Toaster } from 'sonner'
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
enableColorScheme
|
||||
>
|
||||
<>
|
||||
<Toaster />
|
||||
|
||||
{children}
|
||||
</>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Moon, Sun } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
export function DarkModeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className="rounded-md p-2 hover:bg-accent"
|
||||
aria-label={
|
||||
theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'
|
||||
}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import type React from 'react'
|
||||
|
||||
interface NavigationItemProps {
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
active?: boolean
|
||||
icon?: LucideIcon
|
||||
children: React.ReactNode
|
||||
variant?:
|
||||
| 'default'
|
||||
| 'destructive'
|
||||
| 'outline'
|
||||
| 'secondary'
|
||||
| 'ghost'
|
||||
| 'link'
|
||||
isDrawerItem?: boolean
|
||||
}
|
||||
|
||||
export function NavigationItem({
|
||||
href,
|
||||
onClick,
|
||||
className,
|
||||
active = false,
|
||||
icon: Icon,
|
||||
children,
|
||||
variant = 'ghost',
|
||||
isDrawerItem = false
|
||||
}: NavigationItemProps) {
|
||||
// For drawer items, use a simpler styling approach
|
||||
if (isDrawerItem) {
|
||||
const content = (
|
||||
<div
|
||||
className={cn(
|
||||
'px-6 py-1 text-sm font-medium transition-colors hover:text-white/80',
|
||||
active && 'text-white font-semibold',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (href) {
|
||||
return <Link href={href}>{content}</Link>
|
||||
}
|
||||
|
||||
return <Button onClick={onClick}>{content}</Button>
|
||||
}
|
||||
|
||||
// For regular navigation items, use the Button component
|
||||
const content = (
|
||||
<>
|
||||
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
|
||||
const buttonClassName = cn(active && 'font-semibold', className)
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Button variant={variant} asChild className={buttonClassName}>
|
||||
<Link href={href}>{content}</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant={variant} onClick={onClick} className={buttonClassName}>
|
||||
{content}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
'use client'
|
||||
|
||||
import { LaconicMark } from '@/components/assets/laconic-mark'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetTrigger
|
||||
} from '@workspace/ui/components/sheet'
|
||||
import { Menu } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import type React from 'react'
|
||||
import { DarkModeToggle } from './dark-mode-toggle'
|
||||
import { NavigationItem } from './navigation-item'
|
||||
import type { TopNavigationConfig } from './types'
|
||||
import { WalletSessionBadge } from './wallet-session-badge'
|
||||
|
||||
interface TopNavigationProps {
|
||||
config?: TopNavigationConfig
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function TopNavigation({
|
||||
config = {
|
||||
leftItems: [
|
||||
{ label: 'Projects', href: '/projects' },
|
||||
{ label: 'Wallet', href: '/wallets' }
|
||||
],
|
||||
rightItems: [
|
||||
{ label: 'Support', href: '/support' },
|
||||
{ label: 'Documentation', href: '/documentation' }
|
||||
]
|
||||
}
|
||||
}: TopNavigationProps) {
|
||||
const { leftItems = [], rightItems = [] } = config
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background text-foreground">
|
||||
<div className="container flex h-14 items-center px-4 w-full min-w-full">
|
||||
{/* Mobile menu trigger - positioned on the left */}
|
||||
<div className="md:hidden mr-2">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="hover:bg-accent">
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="w-[280px] p-0 bg-background border-r border-border"
|
||||
>
|
||||
<div className="p-6 border-b border-border flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center">
|
||||
<LaconicMark className="h-6 w-6" />
|
||||
</Link>
|
||||
<SheetClose className="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">
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetClose>
|
||||
</div>
|
||||
<div className="flex-1 py-6">
|
||||
<nav className="flex flex-col space-y-6">
|
||||
{[...leftItems, ...rightItems].map((item) => (
|
||||
<NavigationItem
|
||||
key={`mobile-${item.label}`}
|
||||
href={item.href}
|
||||
onClick={item.onClick}
|
||||
active={item.active}
|
||||
isDrawerItem={true}
|
||||
>
|
||||
{item.label}
|
||||
</NavigationItem>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<Link href="/" className="mr-4 flex items-center">
|
||||
<LaconicMark className="h-8 w-8" />
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-1">
|
||||
{leftItems.map((item) => (
|
||||
<NavigationItem
|
||||
key={`left-${item.label}`}
|
||||
href={item.href}
|
||||
onClick={item.onClick}
|
||||
icon={item.icon}
|
||||
active={item.active}
|
||||
>
|
||||
{item.label}
|
||||
</NavigationItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-end space-x-4">
|
||||
<nav className="hidden md:flex items-center space-x-1">
|
||||
{rightItems.map((item) => (
|
||||
<NavigationItem
|
||||
key={`right-${item.label}`}
|
||||
href={item.href}
|
||||
onClick={item.onClick}
|
||||
icon={item.icon}
|
||||
active={item.active}
|
||||
>
|
||||
{item.label}
|
||||
</NavigationItem>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<DarkModeToggle />
|
||||
<WalletSessionBadge address="0xAb...1234" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
export interface NavigationItemConfig {
|
||||
label: string
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
icon?: LucideIcon
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export interface TopNavigationConfig {
|
||||
leftItems?: NavigationItemConfig[]
|
||||
rightItems?: NavigationItemConfig[]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@workspace/ui/components/dropdown-menu'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { ChevronDown, LogOut } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface WalletSessionBadgeProps {
|
||||
address: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function WalletSessionBadge({
|
||||
address,
|
||||
className
|
||||
}: WalletSessionBadgeProps) {
|
||||
const [isConnected, setIsConnected] = useState(true)
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'flex items-center space-x-2 rounded-md border px-3 py-1.5 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'dark:bg-accent/5 dark:hover:bg-accent/10',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span
|
||||
className={cn(
|
||||
'absolute inline-flex h-full w-full animate-ping rounded-full opacity-75',
|
||||
isConnected ? 'bg-green-400' : 'bg-red-400'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'relative inline-flex h-2 w-2 rounded-full',
|
||||
isConnected ? 'bg-green-500' : 'bg-red-500'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<span>{address}</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center text-destructive focus:text-destructive"
|
||||
onClick={() => setIsConnected(false)}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>Disconnect</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Props for the NavigationWrapper component
|
||||
*/
|
||||
export interface NavigationWrapperProps {
|
||||
/**
|
||||
* Content to be displayed within the navigation wrapper
|
||||
* Typically includes PageWrapper components
|
||||
*/
|
||||
children: ReactNode
|
||||
|
||||
/**
|
||||
* Optional CSS class name to apply custom styles to the wrapper container
|
||||
*/
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface PageAction {
|
||||
label: string
|
||||
variant?:
|
||||
| 'default'
|
||||
| 'destructive'
|
||||
| 'outline'
|
||||
| 'secondary'
|
||||
| 'ghost'
|
||||
| 'link'
|
||||
onClick?: () => void
|
||||
href?: string
|
||||
isPrimary?: boolean
|
||||
}
|
||||
|
||||
export interface PageHeaderProps {
|
||||
title: string
|
||||
description?: string
|
||||
actions?: PageAction[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type PageWrapperLayout = 'default' | 'bento'
|
||||
|
||||
export interface PageWrapperProps {
|
||||
/**
|
||||
* Optional header props for the page
|
||||
*/
|
||||
header?: PageHeaderProps
|
||||
/**
|
||||
* Content to be displayed below the header
|
||||
*/
|
||||
children: ReactNode
|
||||
/**
|
||||
* Layout style for the page
|
||||
* @default "default"
|
||||
*/
|
||||
layout?: PageWrapperLayout
|
||||
/**
|
||||
* Optional className for additional styling
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Optional className for the content container
|
||||
*/
|
||||
contentClassName?: string
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Header } from '@/components/page-header/header'
|
||||
import { PageHeader } from '@/components/page-header/page-header'
|
||||
import type { DashboardLayoutConfig } from '@/types/dashboard'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import type React from 'react'
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
config: DashboardLayoutConfig
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function DashboardLayout({
|
||||
config,
|
||||
children,
|
||||
className
|
||||
}: DashboardLayoutProps) {
|
||||
const { headerConfig, pageHeaderConfig } = config
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col bg-background min-h-screen', className)}>
|
||||
{/* Top Navigation */}
|
||||
<Header {...headerConfig} />
|
||||
|
||||
{/* Page Header with breadcrumbs */}
|
||||
<PageHeader {...(pageHeaderConfig || {})} />
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="px-4 md:container py-4 md:py-6 flex-1">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { DashboardLayout } from './dashboard-layout'
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { NavItem } from '@/types/dashboard'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
interface MainNavProps {
|
||||
items: NavItem[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MainNav({ items, className }: MainNavProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<nav className={cn('hidden md:flex items-center gap-6', className)}>
|
||||
{items.map((item) => {
|
||||
const isActive = pathname === item.href
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
target={item.isExternal ? '_blank' : undefined}
|
||||
rel={item.isExternal ? 'noopener noreferrer' : undefined}
|
||||
className={cn(
|
||||
'text-sm font-medium transition-colors hover:text-foreground',
|
||||
isActive ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useNavigation } from '@/navigation'
|
||||
import type { NavItem } from '@/types/dashboard'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger
|
||||
} from '@workspace/ui/components/sheet'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { Menu, Package2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface MobileNavProps {
|
||||
items: NavItem[]
|
||||
logo?: ReactNode
|
||||
title?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MobileNav({
|
||||
items,
|
||||
logo = <Package2 className="h-6 w-6" />,
|
||||
title = 'Navigation',
|
||||
className
|
||||
}: MobileNavProps) {
|
||||
const pathname = usePathname()
|
||||
const { mobileMenuOpen, toggleMobileMenu } = useNavigation()
|
||||
|
||||
return (
|
||||
<Sheet open={mobileMenuOpen} onOpenChange={toggleMobileMenu}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn('md:hidden flex', className)}
|
||||
// onClick={toggleMobileMenu}
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-[240px] sm:w-[300px]">
|
||||
<SheetHeader className="pb-6">
|
||||
<SheetTitle className="flex items-center gap-2">
|
||||
{logo}
|
||||
<span>{title}</span>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav className="flex flex-col gap-4">
|
||||
{items.map((item) => {
|
||||
const isActive = pathname === item.href
|
||||
const Icon = item.icon
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
target={item.isExternal ? '_blank' : undefined}
|
||||
rel={item.isExternal ? 'noopener noreferrer' : undefined}
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
<Button
|
||||
variant={isActive ? 'secondary' : 'ghost'}
|
||||
className="justify-start w-full"
|
||||
>
|
||||
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
||||
{item.title}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { UserNavConfig } from '@/types/dashboard'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { CircleUser } from 'lucide-react'
|
||||
|
||||
interface UserNavProps extends UserNavConfig {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function UserNav({
|
||||
avatarSrc,
|
||||
username,
|
||||
email,
|
||||
showMobile = true,
|
||||
className
|
||||
}: UserNavProps) {
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{/* Desktop user button */}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
className="rounded-full hidden md:flex"
|
||||
>
|
||||
{avatarSrc ? (
|
||||
<img
|
||||
src={avatarSrc || '/placeholder.svg'}
|
||||
alt={username || 'User avatar'}
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<CircleUser className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Mobile user button */}
|
||||
{showMobile && (
|
||||
<Button
|
||||
size="icon"
|
||||
className="md:hidden bg-black text-white rounded-md"
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="Menu"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Menu</title>
|
||||
<path
|
||||
d="M13 3L4 14H15L11 21"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
@@ -1,71 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { BreadcrumbsConfig } from '@/types/dashboard'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
interface BreadcrumbsProps extends BreadcrumbsConfig {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Breadcrumbs({
|
||||
homeLabel = 'Home',
|
||||
homeHref = '/',
|
||||
pathMap = {},
|
||||
className
|
||||
}: BreadcrumbsProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
// Generate breadcrumbs from the current path
|
||||
const breadcrumbs = generateBreadcrumbs(pathname, pathMap)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 text-sm text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Link href={homeHref} className="hover:text-foreground">
|
||||
{homeLabel}
|
||||
</Link>
|
||||
|
||||
{breadcrumbs.map((crumb) => (
|
||||
<React.Fragment key={crumb.href}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<Link
|
||||
href={crumb.href}
|
||||
className={
|
||||
crumb.href === pathname
|
||||
? 'text-foreground font-medium'
|
||||
: 'hover:text-foreground'
|
||||
}
|
||||
>
|
||||
{crumb.label}
|
||||
</Link>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to generate breadcrumbs from the current path
|
||||
function generateBreadcrumbs(
|
||||
pathname: string,
|
||||
pathMap: Record<string, string> = {}
|
||||
): Array<{ href: string; label: string }> {
|
||||
const paths = pathname.split('/').filter(Boolean)
|
||||
|
||||
return paths.map((path, index) => {
|
||||
// Build the URL up to this point
|
||||
const href = `/${paths.slice(0, index + 1).join('/')}`
|
||||
|
||||
// Use the mapping if available, otherwise capitalize the path
|
||||
const label = pathMap[path] || path.charAt(0).toUpperCase() + path.slice(1)
|
||||
|
||||
return { href, label }
|
||||
})
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { MainNav } from '@/components/navigation/main-nav'
|
||||
import { MobileNav } from '@/components/navigation/mobile-nav'
|
||||
import { UserNav } from '@/components/navigation/user-nav'
|
||||
import { SearchCombobox } from '@/components/page-header/search-combobox'
|
||||
import type { HeaderConfig } from '@/types/dashboard'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { Package2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface HeaderProps extends HeaderConfig {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Header({
|
||||
logo = <Package2 className="h-6 w-6 text-foreground" />,
|
||||
logoMobile,
|
||||
navItems,
|
||||
searchConfig,
|
||||
userConfig,
|
||||
className
|
||||
}: HeaderProps) {
|
||||
// Use the mobile logo if provided, otherwise use the desktop logo
|
||||
const mobileLogoElement = logoMobile || (
|
||||
<div className="h-10 w-10 bg-black flex items-center justify-center">
|
||||
<svg
|
||||
role="img"
|
||||
aria-labelledby="mobile-logo-title"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title id="mobile-logo-title">Application Logo</title>
|
||||
<path
|
||||
d="M15.2322 8.76777L8.76774 15.2322M16.6464 7.35355L7.35353 16.6464"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'h-16 border-b flex items-center justify-between px-4 md:px-6 bg-background',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Mobile: Left section with menu */}
|
||||
<div className="flex items-center md:hidden">
|
||||
<MobileNav items={navItems} logo={logo} />
|
||||
</div>
|
||||
|
||||
{/* Desktop: Left section with logo and nav */}
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
<Link href="/">{logo}</Link>
|
||||
<MainNav items={navItems} />
|
||||
</div>
|
||||
|
||||
{/* Mobile: Center logo */}
|
||||
<div className="flex md:hidden items-center justify-center">
|
||||
<Link href="/">{mobileLogoElement}</Link>
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
<SearchCombobox {...(searchConfig || {})} />
|
||||
<UserNav {...(userConfig || {})} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
|
||||
import { Breadcrumbs } from '@/components/page-header/breadcrumbs'
|
||||
import type { PageHeaderConfig } from '@/types/dashboard'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
interface PageHeaderProps extends PageHeaderConfig {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
breadcrumbsConfig,
|
||||
className,
|
||||
children
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn('border-b', className)}>
|
||||
<div className="px-4 md:container py-4 md:py-6 space-y-4 md:space-y-6">
|
||||
{/* Breadcrumb */}
|
||||
<Breadcrumbs {...(breadcrumbsConfig || {})} />
|
||||
|
||||
{/* Optional content */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { SearchConfig, SearchResultItem } from '@/types/dashboard'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from '@workspace/ui/components/command'
|
||||
import { Input } from '@workspace/ui/components/input'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from '@workspace/ui/components/popover'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import {
|
||||
FileText,
|
||||
Laptop,
|
||||
Package,
|
||||
SearchIcon,
|
||||
Settings,
|
||||
Users
|
||||
} from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
interface SearchComboboxProps extends SearchConfig {
|
||||
className?: string
|
||||
}
|
||||
|
||||
// Default icons for different categories
|
||||
const categoryIcons = {
|
||||
projects: <Package className="mr-2 h-4 w-4" />,
|
||||
documents: <FileText className="mr-2 h-4 w-4" />,
|
||||
settings: <Settings className="mr-2 h-4 w-4" />,
|
||||
users: <Users className="mr-2 h-4 w-4" />,
|
||||
default: <Laptop className="mr-2 h-4 w-4" />
|
||||
} as const satisfies Record<string, React.ReactNode>
|
||||
|
||||
export function SearchCombobox({
|
||||
placeholder = 'Search...',
|
||||
onSearch,
|
||||
showMobile = true,
|
||||
searchResults = [],
|
||||
onSelectItem,
|
||||
categories = [],
|
||||
className
|
||||
}: SearchComboboxProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [groupedResults, setGroupedResults] = React.useState<
|
||||
Record<string, SearchResultItem[]>
|
||||
>({})
|
||||
|
||||
// Filter results based on query
|
||||
const filteredResults = query
|
||||
? searchResults.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(query.toLowerCase()) ||
|
||||
item.description?.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
: searchResults
|
||||
|
||||
// Group results by category
|
||||
React.useEffect(() => {
|
||||
const grouped: Record<string, SearchResultItem[]> = {}
|
||||
|
||||
// Initialize with empty arrays for each category
|
||||
for (const category in categories) {
|
||||
if (Object.prototype.hasOwnProperty.call(categories, category)) {
|
||||
const cat = categories[category] as string
|
||||
grouped[cat] = []
|
||||
}
|
||||
}
|
||||
|
||||
// Add "Other" category if there are items without a category
|
||||
grouped.Other = []
|
||||
|
||||
// Group items by category
|
||||
for (const idx in filteredResults) {
|
||||
if (Object.prototype.hasOwnProperty.call(filteredResults, idx)) {
|
||||
const result = filteredResults[idx] as SearchResultItem
|
||||
const categoryKey = (result.category ?? 'default') as
|
||||
| keyof typeof categoryIcons
|
||||
| 'Other'
|
||||
|
||||
if (Object.keys(categoryIcons).includes(categoryKey)) {
|
||||
if (!grouped[categoryKey]) {
|
||||
grouped[categoryKey] = []
|
||||
}
|
||||
grouped[categoryKey].push(result)
|
||||
} else {
|
||||
grouped.Other.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty categories
|
||||
setGroupedResults(
|
||||
Object.fromEntries(
|
||||
Object.entries(grouped).filter(([_, items]) => items.length > 0)
|
||||
)
|
||||
)
|
||||
}, [filteredResults, categories])
|
||||
|
||||
const handleSelect = (item: SearchResultItem) => {
|
||||
setOpen(false)
|
||||
if (onSelectItem) {
|
||||
onSelectItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (onSearch) {
|
||||
onSearch(query)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{/* Mobile search button */}
|
||||
{showMobile && (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button size="icon" variant="ghost" className="md:hidden">
|
||||
<SearchIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[300px]" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{Object.entries(groupedResults).map(([category, items]) => (
|
||||
<CommandGroup key={category} heading={category}>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
onSelect={() => handleSelect(item)}
|
||||
className="flex items-center"
|
||||
>
|
||||
{item.icon ? (
|
||||
<item.icon className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
categoryIcons[
|
||||
item.category &&
|
||||
Object.keys(categoryIcons).includes(item.category)
|
||||
? (item.category as keyof typeof categoryIcons)
|
||||
: 'default'
|
||||
]
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span>{item.title}</span>
|
||||
{item.description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
{/* Desktop search */}
|
||||
<div className="relative hidden md:block">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<form onSubmit={handleSearch}>
|
||||
<Input
|
||||
className="pl-10 w-[336px]"
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => setOpen(true)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[336px]" align="start">
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
{Object.entries(groupedResults).map(([category, items]) => (
|
||||
<CommandGroup key={category} heading={category}>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
onSelect={() => handleSelect(item)}
|
||||
className="flex items-center"
|
||||
>
|
||||
{item.icon ? (
|
||||
<item.icon className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
categoryIcons[
|
||||
item.category &&
|
||||
Object.keys(categoryIcons).includes(item.category)
|
||||
? (item.category as keyof typeof categoryIcons)
|
||||
: 'default'
|
||||
]
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span>{item.title}</span>
|
||||
{item.description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{item.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
|
||||
import type { SearchConfig } from '@/types/dashboard'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Input } from '@workspace/ui/components/input'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface SearchProps extends SearchConfig {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Search({
|
||||
placeholder = 'Search...',
|
||||
onSearch,
|
||||
showMobile = true,
|
||||
className
|
||||
}: SearchProps) {
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (onSearch) {
|
||||
onSearch(query)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{/* Mobile search button */}
|
||||
{showMobile && (
|
||||
<Button size="icon" variant="ghost" className="md:hidden">
|
||||
<SearchIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Desktop search */}
|
||||
<form onSubmit={handleSearch} className="relative hidden md:block">
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-10 w-[336px]"
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import type * as React from 'react'
|
||||
import '@workspace/ui/globals.css'
|
||||
import { Toaster, toast } from 'sonner'
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
enableColorScheme
|
||||
>
|
||||
<Toaster />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
console.log('clicked')
|
||||
toast.success('Hello')
|
||||
}}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import type {
|
||||
DashboardLayoutConfig,
|
||||
NavItem,
|
||||
SearchResultItem
|
||||
} from '@/types/dashboard'
|
||||
import {
|
||||
FileText,
|
||||
Package2,
|
||||
Settings,
|
||||
ShoppingCart,
|
||||
Users,
|
||||
Wallet
|
||||
} from 'lucide-react'
|
||||
|
||||
// Navigation items for the dashboard
|
||||
export const dashboardNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Projects',
|
||||
href: '/projects',
|
||||
icon: Package2
|
||||
},
|
||||
{
|
||||
title: 'Buy Prepaid',
|
||||
href: '/bp',
|
||||
icon: ShoppingCart
|
||||
},
|
||||
{
|
||||
title: 'Wallet',
|
||||
href: '/wallet',
|
||||
icon: Wallet
|
||||
}
|
||||
]
|
||||
|
||||
// Path mapping for breadcrumbs
|
||||
export const dashboardPathMap: Record<string, string> = {
|
||||
projects: 'Projects',
|
||||
bp: 'Buy Prepaid',
|
||||
wallet: 'Wallet'
|
||||
}
|
||||
|
||||
// Sample search results for the combobox
|
||||
export const sampleSearchResults: SearchResultItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Project Alpha',
|
||||
description: 'Main production project',
|
||||
href: '/projects/alpha',
|
||||
icon: Package2,
|
||||
category: 'Projects'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Project Beta',
|
||||
description: 'Development environment',
|
||||
href: '/projects/beta',
|
||||
icon: Package2,
|
||||
category: 'Projects'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Billing Settings',
|
||||
description: 'Manage payment methods',
|
||||
href: '/wallet/settings',
|
||||
icon: Settings,
|
||||
category: 'Settings'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Team Members',
|
||||
description: 'Manage team access',
|
||||
href: '/team',
|
||||
icon: Users,
|
||||
category: 'Users'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'API Documentation',
|
||||
description: 'Developer resources',
|
||||
href: '/docs/api',
|
||||
icon: FileText,
|
||||
category: 'Documents'
|
||||
}
|
||||
]
|
||||
|
||||
// Default dashboard layout configuration
|
||||
export const defaultDashboardConfig: DashboardLayoutConfig = {
|
||||
headerConfig: {
|
||||
navItems: dashboardNavItems,
|
||||
searchConfig: {
|
||||
placeholder: 'Search projects, docs, settings...',
|
||||
searchResults: sampleSearchResults,
|
||||
categories: ['Projects', 'Documents', 'Settings', 'Users'],
|
||||
onSelectItem: (item) => {
|
||||
// In a real app, you would use router.push(item.href)
|
||||
console.log('Selected item:', item)
|
||||
window.location.href = item.href
|
||||
}
|
||||
},
|
||||
userConfig: {
|
||||
showMobile: true
|
||||
}
|
||||
},
|
||||
pageHeaderConfig: {
|
||||
breadcrumbsConfig: {
|
||||
homeLabel: 'Home',
|
||||
homeHref: '/',
|
||||
pathMap: dashboardPathMap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
# Navigation Components
|
||||
|
||||
A collection of reusable navigation components for the deploy-fe application.
|
||||
|
||||
## Components
|
||||
|
||||
### MainNav
|
||||
|
||||
The main navigation bar component that displays the logo, menu items, and user profile.
|
||||
|
||||
```tsx
|
||||
import { MainNav } from "@/navigation";
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
href: "/dashboard",
|
||||
label: "Dashboard",
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
href: "/orders",
|
||||
label: "Orders",
|
||||
icon: ShoppingCart,
|
||||
badge: "2",
|
||||
},
|
||||
// ...
|
||||
];
|
||||
|
||||
const user = {
|
||||
name: "John Doe",
|
||||
avatarUrl: "/avatar.png",
|
||||
menuItems: [
|
||||
{ label: "Profile", href: "/profile" },
|
||||
{ label: "Settings", href: "/settings" },
|
||||
{ label: "Logout", href: "/logout" },
|
||||
],
|
||||
};
|
||||
|
||||
<MainNav items={navItems} user={user} />
|
||||
```
|
||||
|
||||
### PageHeader
|
||||
|
||||
The page header component that displays the title, description, breadcrumbs, and action buttons.
|
||||
|
||||
```tsx
|
||||
import { PageHeader } from "@/navigation";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
|
||||
<PageHeader
|
||||
title="Project alpha"
|
||||
description="Manage your project's details such as name, image, description and settings."
|
||||
breadcrumbs={[
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Projects", href: "/projects" },
|
||||
{ label: "Project alpha", href: "/projects/alpha" },
|
||||
]}
|
||||
actions={[
|
||||
<Button key="share" variant="outline" size="sm">Share</Button>,
|
||||
<Button key="view" variant="outline" size="sm">View</Button>,
|
||||
<Button key="edit" variant="outline" size="sm">Edit</Button>,
|
||||
<Button key="publish" size="sm">Publish</Button>,
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Breadcrumb
|
||||
|
||||
The breadcrumb component that displays a navigation trail.
|
||||
|
||||
```tsx
|
||||
import { Breadcrumb } from "@/navigation";
|
||||
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Projects", href: "/projects" },
|
||||
{ label: "Project alpha", href: "/projects/alpha" },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### NavItem
|
||||
|
||||
The navigation item component that displays a link with an optional icon and badge.
|
||||
|
||||
```tsx
|
||||
import { NavItem } from "@/navigation";
|
||||
import { Home } from "lucide-react";
|
||||
|
||||
<NavItem
|
||||
href="/"
|
||||
label="Home"
|
||||
icon={Home}
|
||||
badge="2"
|
||||
/>
|
||||
```
|
||||
|
||||
### LayoutWrapper
|
||||
|
||||
A wrapper component that combines the MainNav and PageHeader components.
|
||||
|
||||
```tsx
|
||||
import { LayoutWrapper } from "@/navigation";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
|
||||
<LayoutWrapper
|
||||
title="Project alpha"
|
||||
description="Manage your project's details such as name, image, description and settings."
|
||||
breadcrumbs={[
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Projects", href: "/projects" },
|
||||
{ label: "Project alpha", href: "/projects/alpha" },
|
||||
]}
|
||||
actions={[
|
||||
<Button key="share" variant="outline" size="sm">Share</Button>,
|
||||
<Button key="view" variant="outline" size="sm">View</Button>,
|
||||
<Button key="edit" variant="outline" size="sm">Edit</Button>,
|
||||
<Button key="publish" size="sm">Publish</Button>,
|
||||
]}
|
||||
>
|
||||
<div>Your content here</div>
|
||||
</LayoutWrapper>
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### useNavigation
|
||||
|
||||
A Zustand store hook that provides access to the navigation state. This can be used directly from any component without needing a provider.
|
||||
|
||||
```tsx
|
||||
import { useNavigation } from "@/navigation";
|
||||
|
||||
function MyComponent() {
|
||||
const { activePath, mobileMenuOpen, toggleMobileMenu } = useNavigation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={toggleMobileMenu}>
|
||||
{mobileMenuOpen ? "Close" : "Open"} Menu
|
||||
</button>
|
||||
<div>Active path: {activePath}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### NavigationProvider (Optional)
|
||||
|
||||
The navigation provider component that syncs the active path with the URL. This is optional and only needed if you want to automatically update the active path based on the current URL.
|
||||
|
||||
```tsx
|
||||
import { NavigationProvider } from "@/navigation";
|
||||
|
||||
<NavigationProvider>
|
||||
<App />
|
||||
</NavigationProvider>
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
See the `example-usage.tsx` file for a complete example of how to use these components together.
|
||||
@@ -1,60 +0,0 @@
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import type { BreadcrumbProps } from './types'
|
||||
|
||||
/**
|
||||
* Breadcrumb component
|
||||
* Displays a breadcrumb trail for navigation
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns Breadcrumb component
|
||||
*/
|
||||
export function Breadcrumb({
|
||||
items,
|
||||
separator = <ChevronRight className="h-4 w-4 mx-1" />,
|
||||
className
|
||||
}: BreadcrumbProps) {
|
||||
if (!items || items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
'flex items-center text-sm text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
aria-label="Breadcrumb"
|
||||
>
|
||||
<ol className="flex items-center">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1
|
||||
|
||||
return (
|
||||
<li key={item.href} className="flex items-center">
|
||||
{isLast ? (
|
||||
<span className="text-foreground" aria-current="page">
|
||||
{item.label}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
<span className="mx-1" aria-hidden="true">
|
||||
{separator}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
|
||||
import { LayoutWrapper } from './layout-wrapper'
|
||||
|
||||
/**
|
||||
* ExampleUsage component
|
||||
* Demonstrates how to use the LayoutWrapper component
|
||||
*
|
||||
* @returns ExampleUsage component
|
||||
*/
|
||||
export function ExampleUsage() {
|
||||
return (
|
||||
<LayoutWrapper
|
||||
title="Project alpha"
|
||||
description="Manage your project's details such as name, image, description and settings."
|
||||
breadcrumbs={[
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Projects', href: '/projects' },
|
||||
{ label: 'Project alpha', href: '/projects/alpha' }
|
||||
]}
|
||||
actions={[
|
||||
<Button key="share" variant="outline" size="sm">
|
||||
Share
|
||||
</Button>,
|
||||
<Button key="view" variant="outline" size="sm">
|
||||
View
|
||||
</Button>,
|
||||
<Button key="edit" variant="outline" size="sm">
|
||||
Edit
|
||||
</Button>,
|
||||
<Button key="publish" size="sm">
|
||||
Publish
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<div className="border border-dashed rounded-lg p-8 text-center text-muted-foreground">
|
||||
Slot (swap it with your content)
|
||||
</div>
|
||||
</LayoutWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Export components
|
||||
export { Breadcrumb } from './breadcrumb'
|
||||
export { ExampleUsage } from './example-usage'
|
||||
export { LayoutWrapper } from './layout-wrapper'
|
||||
export { MainNav } from './main-nav'
|
||||
export { NavItem } from './nav-item'
|
||||
export { PageHeader } from './page-header'
|
||||
|
||||
// Export providers
|
||||
export { NavigationProvider, useNavigation } from './navigation-provider'
|
||||
|
||||
// Export types
|
||||
export type {
|
||||
BreadcrumbItem,
|
||||
BreadcrumbProps,
|
||||
MainNavProps,
|
||||
NavItemProps,
|
||||
NavItem as NavItemType,
|
||||
PageHeaderProps,
|
||||
UserMenuItem,
|
||||
UserProfile
|
||||
} from './types'
|
||||
@@ -1,122 +0,0 @@
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
Settings,
|
||||
ShoppingCart,
|
||||
Users
|
||||
} from 'lucide-react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { MainNav } from './main-nav'
|
||||
import { NavigationProvider } from './navigation-provider'
|
||||
import { PageHeader } from './page-header'
|
||||
import type { BreadcrumbItem } from './types'
|
||||
|
||||
interface LayoutWrapperProps {
|
||||
children: ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
breadcrumbs?: BreadcrumbItem[]
|
||||
actions?: ReactNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* LayoutWrapper component
|
||||
* Provides a consistent layout with navigation and page header
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns LayoutWrapper component
|
||||
*/
|
||||
export function LayoutWrapper({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
breadcrumbs,
|
||||
actions
|
||||
}: LayoutWrapperProps) {
|
||||
// Example navigation items
|
||||
const navItems = [
|
||||
{
|
||||
href: '/dashboard',
|
||||
label: 'Dashboard',
|
||||
icon: LayoutDashboard
|
||||
},
|
||||
{
|
||||
href: '/orders',
|
||||
label: 'Orders',
|
||||
icon: ShoppingCart,
|
||||
badge: '2'
|
||||
},
|
||||
{
|
||||
href: '/products',
|
||||
label: 'Products',
|
||||
icon: Package
|
||||
},
|
||||
{
|
||||
href: '/customers',
|
||||
label: 'Customers',
|
||||
icon: Users
|
||||
},
|
||||
{
|
||||
href: '/settings',
|
||||
label: 'Settings',
|
||||
icon: Settings
|
||||
}
|
||||
]
|
||||
|
||||
// Example user profile
|
||||
const user = {
|
||||
name: 'John Doe',
|
||||
avatarUrl: '',
|
||||
menuItems: [
|
||||
{ label: 'Profile', href: '/profile' },
|
||||
{ label: 'Settings', href: '/settings' },
|
||||
{ label: 'Logout', href: '/logout' }
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationProvider>
|
||||
<div className="flex flex-col min-h-screen bg-background">
|
||||
<MainNav items={navItems} user={user} />
|
||||
|
||||
<main className="flex-1 p-6">
|
||||
<PageHeader
|
||||
title={title}
|
||||
description={description}
|
||||
breadcrumbs={breadcrumbs}
|
||||
actions={actions}
|
||||
/>
|
||||
|
||||
<div className="mt-8">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</NavigationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Example usage:
|
||||
*
|
||||
* ```tsx
|
||||
* <LayoutWrapper
|
||||
* title="Project alpha"
|
||||
* description="Manage your project's details such as name, image, description and settings."
|
||||
* breadcrumbs={[
|
||||
* { label: "Home", href: "/" },
|
||||
* { label: "Projects", href: "/projects" },
|
||||
* { label: "Project alpha", href: "/projects/alpha" },
|
||||
* ]}
|
||||
* actions={[
|
||||
* <Button key="share" variant="outline" size="sm">Share</Button>,
|
||||
* <Button key="view" variant="outline" size="sm">View</Button>,
|
||||
* <Button key="edit" variant="outline" size="sm">Edit</Button>,
|
||||
* <Button key="publish" size="sm">Publish</Button>,
|
||||
* ]}
|
||||
* >
|
||||
* <div className="border border-dashed rounded-lg p-8 text-center text-muted-foreground">
|
||||
* Slot (swap it with your content)
|
||||
* </div>
|
||||
* </LayoutWrapper>
|
||||
* ```
|
||||
*/
|
||||
@@ -1,108 +0,0 @@
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import { Menu } from 'lucide-react'
|
||||
|
||||
import { NavItem } from './nav-item'
|
||||
import { useNavigation } from './navigation-provider'
|
||||
import type { MainNavProps } from './types'
|
||||
|
||||
/**
|
||||
* MainNav component
|
||||
* Renders the main navigation bar with logo, menu items, and user profile
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns MainNav component
|
||||
*/
|
||||
export function MainNav({ logo, items, user, className }: MainNavProps) {
|
||||
const { mobileMenuOpen, toggleMobileMenu } = useNavigation()
|
||||
|
||||
return (
|
||||
<header className={cn('border-b', className)}>
|
||||
<div className="flex h-16 items-center px-4">
|
||||
{/* Logo and mobile menu toggle */}
|
||||
<div className="flex items-center gap-4">
|
||||
{logo ? (
|
||||
<div className="flex items-center">{logo}</div>
|
||||
) : (
|
||||
<div className="bg-black text-white p-2 rounded">
|
||||
<span className="sr-only">Logo</span>
|
||||
<div className="h-6 w-6" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
onClick={toggleMobileMenu}
|
||||
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Desktop navigation */}
|
||||
<nav className="hidden md:flex items-center gap-4 lg:gap-6 ml-4">
|
||||
{items.map((item) => (
|
||||
<NavItem
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
badge={item.badge}
|
||||
isActive={item.isActive}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Mobile navigation */}
|
||||
{mobileMenuOpen && (
|
||||
<div
|
||||
id="mobile-menu"
|
||||
className="absolute top-16 left-0 right-0 bg-background border-b z-50 md:hidden"
|
||||
>
|
||||
<nav className="flex flex-col p-4 space-y-4">
|
||||
{items.map((item) => (
|
||||
<NavItem
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
badge={item.badge}
|
||||
isActive={item.isActive}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right side items (user profile, etc.) */}
|
||||
<div className="ml-auto flex items-center gap-4">
|
||||
{user && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded-full bg-purple-500 p-1">
|
||||
<span className="sr-only">{user.name}</span>
|
||||
<div
|
||||
className="h-8 w-8 rounded-full bg-purple-600"
|
||||
style={
|
||||
user.avatarUrl
|
||||
? {
|
||||
backgroundImage: `url(${user.avatarUrl})`,
|
||||
backgroundSize: 'cover'
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" className="gap-1">
|
||||
<span>Upgrade</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useNavigation } from './navigation-provider'
|
||||
import type { NavItemProps } from './types'
|
||||
|
||||
/**
|
||||
* NavItem component
|
||||
* Renders a navigation link with optional icon and badge
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns NavItem component
|
||||
*/
|
||||
export function NavItem({
|
||||
href,
|
||||
label,
|
||||
icon: Icon,
|
||||
badge,
|
||||
isActive: forcedActive,
|
||||
className
|
||||
}: NavItemProps) {
|
||||
const { activePath } = useNavigation()
|
||||
const isActive =
|
||||
forcedActive !== undefined ? forcedActive : activePath === href
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
'flex items-center text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
className
|
||||
)}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
{Icon && <Icon className="mr-2 h-4 w-4" aria-hidden="true" />}
|
||||
<span>{label}</span>
|
||||
{badge && (
|
||||
<span className="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { type ReactNode, useEffect } from 'react'
|
||||
import { type StateCreator, create } from 'zustand'
|
||||
|
||||
/**
|
||||
* Navigation state interface
|
||||
* Manages the state for the navigation system
|
||||
*/
|
||||
interface NavigationState {
|
||||
activePath: string
|
||||
mobileMenuOpen: boolean
|
||||
setActivePath: (path: string) => void
|
||||
toggleMobileMenu: () => void
|
||||
closeMobileMenu: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Zustand store for navigation state
|
||||
* This can be accessed directly from any component without context
|
||||
*/
|
||||
export const useNavigation = create<NavigationState>(((set) => ({
|
||||
activePath: '/',
|
||||
mobileMenuOpen: false,
|
||||
setActivePath: (path: string) => set({ activePath: path }),
|
||||
toggleMobileMenu: () =>
|
||||
set((state: NavigationState) => ({
|
||||
mobileMenuOpen: !state.mobileMenuOpen
|
||||
})),
|
||||
closeMobileMenu: () => set({ mobileMenuOpen: false })
|
||||
})) as StateCreator<NavigationState>)
|
||||
|
||||
/**
|
||||
* Navigation provider props
|
||||
*/
|
||||
interface NavigationProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation provider component
|
||||
* Updates the navigation state based on the current pathname
|
||||
* This is optional and only needed if you want to sync the active path with the URL
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns Navigation provider component
|
||||
*/
|
||||
export function NavigationProvider({ children }: NavigationProviderProps) {
|
||||
const pathname = usePathname()
|
||||
const setActivePath = useNavigation((state) => state.setActivePath)
|
||||
|
||||
// Update active path when pathname changes
|
||||
useEffect(() => {
|
||||
if (pathname) {
|
||||
setActivePath(pathname)
|
||||
}
|
||||
}, [pathname, setActivePath])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
import { Breadcrumb } from './breadcrumb'
|
||||
import type { PageHeaderProps } from './types'
|
||||
|
||||
/**
|
||||
* PageHeader component
|
||||
* Displays a page header with title, description, breadcrumbs, and actions
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns PageHeader component
|
||||
*/
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
breadcrumbs,
|
||||
actions,
|
||||
className
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<Breadcrumb items={breadcrumbs} className="mb-6" />
|
||||
)}
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
|
||||
{description && (
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actions && actions.length > 0 && (
|
||||
<div className="flex items-center gap-2">{actions}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Navigation item interface
|
||||
* Represents a single navigation link with optional icon and badge
|
||||
*/
|
||||
export interface NavItem {
|
||||
href: string
|
||||
label: string
|
||||
icon?: LucideIcon
|
||||
badge?: string | number
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* User menu item interface
|
||||
* Represents an item in the user dropdown menu
|
||||
*/
|
||||
export interface UserMenuItem {
|
||||
label: string
|
||||
href: string
|
||||
icon?: LucideIcon
|
||||
}
|
||||
|
||||
/**
|
||||
* User profile interface
|
||||
* Contains user information for the navigation
|
||||
*/
|
||||
export interface UserProfile {
|
||||
name: string
|
||||
avatarUrl: string
|
||||
menuItems?: UserMenuItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Breadcrumb item interface
|
||||
* Represents a single item in the breadcrumb trail
|
||||
*/
|
||||
export interface BreadcrumbItem {
|
||||
label: string
|
||||
href: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Main navigation props interface
|
||||
*/
|
||||
export interface MainNavProps {
|
||||
logo?: ReactNode
|
||||
items: NavItem[]
|
||||
user?: UserProfile
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Page header props interface
|
||||
*/
|
||||
export interface PageHeaderProps {
|
||||
title: string
|
||||
description?: string
|
||||
breadcrumbs?: BreadcrumbItem[]
|
||||
actions?: ReactNode[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Breadcrumb props interface
|
||||
*/
|
||||
export interface BreadcrumbProps {
|
||||
items: BreadcrumbItem[]
|
||||
separator?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation item props interface
|
||||
*/
|
||||
export interface NavItemProps extends NavItem {
|
||||
className?: string
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set colors for better visibility
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Print header
|
||||
echo -e "${GREEN}=== Qwrk Laconic Core - Cleanup Script ===${NC}"
|
||||
echo -e "${YELLOW}This script will remove all node_modules, .next, and dist folders${NC}"
|
||||
echo
|
||||
|
||||
# Ask for confirmation
|
||||
read -p "Are you sure you want to continue? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||
then
|
||||
echo -e "${RED}Cleanup aborted.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Starting cleanup...${NC}"
|
||||
|
||||
# Find and display total size before cleanup
|
||||
echo -e "${GREEN}Calculating size of directories to be removed...${NC}"
|
||||
TOTAL_SIZE=$(du -sh $(find . -type d \( -name "node_modules" -o -name ".next" -o -name "dist" \) -not -path "*/\.*/*" 2>/dev/null) 2>/dev/null | awk '{sum+=$1} END {print sum}')
|
||||
echo -e "${YELLOW}Total space to be freed: ${TOTAL_SIZE}${NC}"
|
||||
|
||||
# Count directories of each type
|
||||
NODE_MODULES_COUNT=$(find . -type d -name "node_modules" -not -path "*/\.*/*" | wc -l)
|
||||
NEXT_COUNT=$(find . -type d -name ".next" -not -path "*/\.*/*" | wc -l)
|
||||
DIST_COUNT=$(find . -type d -name "dist" -not -path "*/\.*/*" | wc -l)
|
||||
|
||||
echo "Found:"
|
||||
echo -e "- ${NODE_MODULES_COUNT} ${YELLOW}node_modules${NC} directories"
|
||||
echo -e "- ${NEXT_COUNT} ${YELLOW}.next${NC} directories"
|
||||
echo -e "- ${DIST_COUNT} ${YELLOW}dist${NC} directories"
|
||||
echo
|
||||
|
||||
# Final confirmation
|
||||
read -p "Proceed with deletion? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||
then
|
||||
echo -e "${RED}Cleanup aborted.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Perform the cleanup
|
||||
echo -e "${GREEN}Removing node_modules directories...${NC}"
|
||||
find . -type d -name "node_modules" -not -path "*/\.*/*" -exec rm -rf {} +
|
||||
|
||||
echo -e "${GREEN}Removing .next directories...${NC}"
|
||||
find . -type d -name ".next" -not -path "*/\.*/*" -exec rm -rf {} +
|
||||
|
||||
echo -e "${GREEN}Removing dist directories...${NC}"
|
||||
find . -type d -name "dist" -not -path "*/\.*/*" -exec rm -rf {} +
|
||||
|
||||
echo -e "${GREEN}Cleanup completed!${NC}"
|
||||
echo -e "${YELLOW}You may want to run 'yarn install' or 'npm install' to reinstall dependencies.${NC}"
|
||||
|
||||
exit 0
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
"validate": "turbo lint check-types format",
|
||||
"fix-all": "turbo lint:fix fix-types format:fix",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test:hooks": "lefthook run pre-commit"
|
||||
"test:hooks": "lefthook run pre-commit",
|
||||
"clean": "bash ./clean.sh"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "QWRK",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
||||
import { ChevronDownIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
@@ -19,7 +19,7 @@ function AccordionItem({
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
className={cn('border-b last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -35,7 +35,7 @@ function AccordionTrigger({
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -58,7 +58,7 @@ function AccordionContent({
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
<div className={cn('pt-0 pb-4', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
@@ -10,27 +10,27 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
||||
outline:
|
||||
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'bg-secondary text-secondary-foreground shadow-xs 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 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -40,11 +40,11 @@ function Button({
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# Comprehensive Technology Documentation & GitHub Links (March 2025)
|
||||
|
||||
*Last updated: March 3, 2025*
|
||||
|
||||
## Core Toolchain
|
||||
|
||||
| Technology | Current Version | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------------|-------------------|
|
||||
| **Turborepo** | 2.4.0 | [turbo.build/repo](https://turbo.build/repo) | [vercel/turborepo](https://github.com/vercel/turborepo) |
|
||||
| **Next.js 15 App Router** | 15.0.0 | [nextjs.org/docs/app](https://nextjs.org/docs/app) | [vercel/next.js](https://github.com/vercel/next.js) |
|
||||
| **Biome** | 2.0.0 | [biomejs.dev](https://biomejs.dev) | [biomejs/biome](https://github.com/biomejs/biome) |
|
||||
| **PNPM** | 10.5.2 | [pnpm.io](https://pnpm.io) | [pnpm/pnpm](https://github.com/pnpm/pnpm) |
|
||||
|
||||
## Styling & UI
|
||||
|
||||
| Technology | Current Version | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------------|-------------------|
|
||||
| **Tailwind CSS v4** | 4.0.0 | [tailwindcss.com/docs/v4](https://tailwindcss.com/docs/v4) | [tailwindlabs/tailwindcss](https://github.com/tailwindlabs/tailwindcss) |
|
||||
| **ShadCN UI** | 2.0.0 | [ui.shadcn.com](https://ui.shadcn.com/docs) | [shadcn-ui/ui](https://github.com/shadcn-ui/ui) |
|
||||
| **Radix UI** | 3.0.0 | [radix-ui.com](https://radix-ui.com) | [radix-ui/primitives](https://github.com/radix-ui/primitives) |
|
||||
| **Lucide** (icons library) | 1.0.0 | [lucide.dev](https://lucide.dev) | [lucide-icons/lucide](https://github.com/lucide-icons/lucide) |
|
||||
| **Sonner** | 2.1.0 | [sonner.dev](https://sonner.dev) | [sonnerjs/sonner](https://github.com/sonnerjs/sonner) |
|
||||
|
||||
## Language & Validation
|
||||
|
||||
| Technology | Current Version | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------------|-------------------|
|
||||
| **TypeScript** | 6.0.0 | [typescriptlang.org/docs](https://www.typescriptlang.org/docs) | [microsoft/TypeScript](https://github.com/microsoft/TypeScript) |
|
||||
| **Zod** | 4.0.0 | [zod.dev](https://zod.dev) | [colinhacks/zod](https://github.com/colinhacks/zod) |
|
||||
|
||||
## React Ecosystem
|
||||
|
||||
| Technology | Current Version | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------------|-------------------|
|
||||
| **React** | 19.2.0 | [react.dev](https://react.dev) | [facebook/react](https://github.com/facebook/react) |
|
||||
| **React Hook Form** | 9.0.0 | [react-hook-form.com](https://react-hook-form.com) | [react-hook-form/react-hook-form](https://github.com/react-hook-form/react-hook-form) |
|
||||
| **React OAuth Popup** | 3.0.1 | [react-oauth-popup.dev](https://react-oauth-popup.dev) | [react-oauth/popup](https://github.com/react-oauth/popup) |
|
||||
| **React Testing Library** | 15.0.0 | [testing-library.com/react](https://testing-library.com/react) | [testing-library/react-testing-library](https://github.com/testing-library/react-testing-library) |
|
||||
|
||||
## Data & Utilities
|
||||
|
||||
| Technology | Current Version | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------------|-------------------|
|
||||
| **GraphQL** | June 2021 Specification | [graphql.org](https://graphql.org) | [graphql/graphql-spec](https://github.com/graphql/graphql-spec) |
|
||||
| **Axios** | 1.8.1 | [axios-http.com/docs](https://axios-http.com/docs) | [axios/axios](https://github.com/axios/axios) |
|
||||
| **Date-FNS** | 4.1.0 | [date-fns.org](https://date-fns.org) | [date-fns/date-fns](https://github.com/date-fns/date-fns) |
|
||||
| **Recharts** | 3.0.0 | [recharts.org](https://recharts.org) | [recharts/recharts](https://github.com/recharts/recharts) |
|
||||
| **UUID** | 9.0.1 | [uuid.dev](https://uuid.dev) | [uuidjs/uuid](https://github.com/uuidjs/uuid) |
|
||||
|
||||
## Testing & Development Tools
|
||||
|
||||
| Technology | Current Version | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------------|-------------------|
|
||||
| **Vitest** | 2.0.0 | [vitest.dev](https://vitest.dev) | [vitest-dev/vitest](https://github.com/vitest-dev/vitest) |
|
||||
| **Jest** | 30.0.0 | [jestjs.io](https://jestjs.io) | [jestjs/jest](https://github.com/jestjs/jest) |
|
||||
| **Playwright** | 1.45.0 | [playwright.dev](https://playwright.dev) | [microsoft/playwright](https://github.com/microsoft/playwright) |
|
||||
| **Cypress** | 13.6.0 | [cypress.io](https://www.cypress.io) | [cypress-io/cypress](https://github.com/cypress-io/cypress) |
|
||||
| **Storybook** | 8.0.0 | [storybook.js.org](https://storybook.js.org) | [storybookjs/storybook](https://github.com/storybookjs/storybook) |
|
||||
| **Left Hook** | 0.9.4 | [lefthook.dev](https://lefthook.dev) | [evilmartians/lefthook](https://github.com/evilmartians/lefthook) |
|
||||
| **GitHub** | N/A (Platform) | [docs.github.com](https://docs.github.com) | N/A |
|
||||
| **Gitea** | 2.5.0 | [docs.gitea.com](https://docs.gitea.com) | [go-gitea/gitea](https://github.com/go-gitea/gitea) |
|
||||
| **Octokit** | 8.0.0 | [octokit.github.io](https://octokit.github.io/rest.js/) | [octokit/rest.js](https://github.com/octokit/rest.js) |
|
||||
|
||||
## Web3 & Authentication
|
||||
|
||||
| Technology | Current Version | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------------|-------------------|
|
||||
| **Web3Modal** | 3.0.0 | [web3modal.com](https://web3modal.com) | [WalletConnect/web3modal](https://github.com/WalletConnect/web3modal) |
|
||||
| **SIWE** (Sign-In with Ethereum) | 2.0.0 | [login.xyz](https://login.xyz) | [spruceid/siwe](https://github.com/spruceid/siwe) |
|
||||
| **Reown Wallet Kit** | 3.0.0 | [walletconnect.com](https://walletconnect.com) | [WalletConnect/walletconnect-monorepo](https://github.com/WalletConnect/walletconnect-monorepo) |
|
||||
| **Clerk** | 5.0.0 | [clerk.com/docs](https://clerk.com/docs) | [clerkinc/javascript](https://github.com/clerkinc/javascript) |
|
||||
|
||||
## Laconic Network & Infrastructure
|
||||
|
||||
| Technology | Current Version | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------------|-------------------|
|
||||
| **Laconic Network** | 2.1.0 | [laconic.com/docs](https://laconic.com/docs) | [laconic-network](https://github.com/laconic-network) |
|
||||
| **Laconic.com** | N/A | [laconic.com](https://laconic.com) | [laconic-network/portal](https://github.com/laconic-network/portal) |
|
||||
| **Laconic Web Wallet** | 1.4.0 | [wallet.laconic.com](https://wallet.laconic.com) | [laconic-network/wallet](https://github.com/laconic-network/wallet) |
|
||||
|
||||
## Additional Web3 Tools for React
|
||||
|
||||
| Technology | Current Version | Purpose | Documentation | GitHub Repository |
|
||||
|------------|----------------|---------|---------------|-------------------|
|
||||
| **Wagmi** | 2.0.0 | Ethereum Hooks | [wagmi.sh](https://wagmi.sh) | [wagmi-dev/wagmi](https://github.com/wagmi-dev/wagmi) |
|
||||
| **The Graph** | 2.1.0 | Blockchain Indexing | [thegraph.com/docs](https://thegraph.com/docs) | [graphprotocol/graph-node](https://github.com/graphprotocol/graph-node) |
|
||||
| **Hardhat** | 3.0.0 | Smart Contract Development | [hardhat.org](https://hardhat.org) | [NomicFoundation/hardhat](https://github.com/NomicFoundation/hardhat) |
|
||||
| **IPFS** | 0.28.0 | Decentralized Storage | [docs.ipfs.tech](https://docs.ipfs.tech) | [ipfs/js-ipfs](https://github.com/ipfs/js-ipfs) |
|
||||
| **Viem** | 2.0.0 | Ethereum Interface | [viem.sh](https://viem.sh) | [wagmi-dev/viem](https://github.com/wagmi-dev/viem) |
|
||||
| **Ethers.js** | 6.1.0 | Ethereum Library | [docs.ethers.org](https://docs.ethers.org) | [ethers-io/ethers.js](https://github.com/ethers-io/ethers.js) |
|
||||
|
||||
## React 19 Key Features (New in 2025)
|
||||
|
||||
React 19, released in December 2024, includes several major improvements:
|
||||
|
||||
1. **React Server Components (RSC)**: Now fully integrated in the stable release
|
||||
2. **Actions**: Simplifies data fetching and state updates, especially for forms
|
||||
3. **Document Metadata**: Built-in support for managing document metadata (titles, descriptions, meta tags)
|
||||
4. **Concurrent Rendering**: Improved performance for complex UI updates
|
||||
5. **Enhanced Hooks**: New hooks and improvements to existing ones
|
||||
6. **Performance Optimization**: Significant improvements in rendering speed and memory usage
|
||||
7. **Better Developer Experience**: Improved error messages and debugging tools
|
||||
|
||||
## Migration Notes
|
||||
|
||||
When upgrading between major versions, consider these important points:
|
||||
|
||||
1. **Turborepo 2.x to 3.x**: Expected in late 2025, will likely focus on improved remote caching and better integration with CI/CD pipelines
|
||||
2. **Next.js 15 to 16**: Will likely focus on enhanced server components and improved data fetching patterns
|
||||
3. **Tailwind CSS 4 to 5**: May introduce new utility classes and performance optimizations
|
||||
4. **React 19 to 20**: Will continue the focus on concurrent rendering and server components
|
||||
5. **Testing Framework Migrations**:
|
||||
- **Jest to Vitest**: Use the compatibility mode for easier migration, then leverage Vitest's faster performance
|
||||
- **Cypress to Playwright**: Consider migrating for better parallel test execution and multi-browser support
|
||||
|
||||
## Best Practices for 2025
|
||||
|
||||
1. **Adopt Server Components**: Leverage React Server Components for improved performance and reduced client-side JavaScript
|
||||
2. **Embrace Type Safety**: Use TypeScript and Zod together for end-to-end type safety
|
||||
3. **Component Libraries**: Consider using ShadCN UI with Tailwind CSS for rapid UI development
|
||||
4. **Monorepo Structure**: Use Turborepo and PNPM for efficient management of monorepos
|
||||
5. **Testing Strategy**: Implement comprehensive testing with a layered approach:
|
||||
- **Unit Testing**: Vitest (faster than Jest, better ESM support) or Jest
|
||||
- **Component Testing**: React Testing Library with either Vitest or Jest
|
||||
- **E2E Testing**: Playwright (recommended for Next.js 15) or Cypress
|
||||
|
||||
## Future Trends (2025-2026)
|
||||
|
||||
1. **AI Integration**: More tools integrating AI capabilities for code generation and optimization
|
||||
2. **Edge Computing**: Increased focus on edge rendering and computing
|
||||
3. **Web3 Mainstream Adoption**: Simplified Web3 integration in traditional applications
|
||||
4. **Zero-Bundle Size**: Components that ship no JavaScript to the client
|
||||
5. **Hybrid Rendering Strategies**: Combination of static, server, and client rendering based on content type
|
||||
6. **Automated Testing**: Increased adoption of AI-assisted test generation and maintenance
|
||||
7. **Visual Testing**: Growing importance of visual regression testing in the UI development workflow
|
||||
|
||||
## Next.js 15 Testing Setup Guide
|
||||
|
||||
Setting up a robust testing environment for Next.js 15 applications involves configuring multiple testing tools for different layers of your application:
|
||||
|
||||
### 1. Unit and Component Testing
|
||||
|
||||
Choose between Vitest (recommended) or Jest:
|
||||
|
||||
```bash
|
||||
# For Vitest (faster, better ESM support)
|
||||
npm install -D vitest @testing-library/react @testing-library/jest-dom happy-dom
|
||||
|
||||
# For Jest
|
||||
npm install -D jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
|
||||
```
|
||||
|
||||
### 2. End-to-End Testing with Playwright (Recommended for Next.js 15)
|
||||
|
||||
```bash
|
||||
# Install Playwright
|
||||
npm init playwright@latest
|
||||
|
||||
# Or with specific browsers
|
||||
npx playwright install --with-deps chromium firefox webkit
|
||||
```
|
||||
|
||||
**Example Next.js 15 Playwright Test:**
|
||||
|
||||
```typescript
|
||||
// e2e/example.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('homepage has the correct title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/My Next.js App/);
|
||||
});
|
||||
|
||||
test('navigation works correctly', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.click('text=About');
|
||||
await expect(page).toHaveURL(/.*about/);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Playwright Configuration for Next.js 15
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Adding Scripts to package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest", // or "jest"
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*This documentation is current as of March 2025. Version numbers and links are subject to change as technologies evolve.*
|
||||
Reference in New Issue
Block a user