Compare commits
8 commits
0c260820e2
...
8555fd919a
Author | SHA1 | Date | |
---|---|---|---|
8555fd919a | |||
6619f0a9eb | |||
7691bd2fac | |||
7f5f8642ef | |||
bc1910fdca | |||
0cd88d6f07 | |||
15015931d6 | |||
87e0577ff7 |
41 changed files with 3120 additions and 578 deletions
14
package.json
14
package.json
|
@ -27,20 +27,26 @@
|
|||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@prisma/client": "^6.9.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.13",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tanstack/react-query": "^5.80.7",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"lucide-react": "^0.515.0",
|
||||
"next": "15.4.0-canary.92",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-themes": "^0.4.6",
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { RedirectButton } from '@/components/buttons/redirect-button';
|
||||
import { ThemePicker } from '@/components/misc/theme-picker';
|
||||
import { useGetApiUserMe } from '@/generated/api/user/user';
|
||||
|
||||
export default function Home() {
|
||||
const { data, isLoading } = useGetApiUserMe();
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center h-screen'>
|
||||
<div className='absolute top-4 right-4'>{<ThemePicker />}</div>
|
||||
<div className='flex flex-col items-center justify-center h-full'>
|
||||
<div>
|
||||
<h1>
|
||||
Hello{' '}
|
23
src/app/(main)/layout.tsx
Normal file
23
src/app/(main)/layout.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import { AppSidebar } from '@/components/custom-ui/app-sidebar';
|
||||
import SidebarProviderWrapper from '@/components/wrappers/sidebar-provider';
|
||||
import Header from '@/components/misc/header';
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const defaultOpen = cookieStore.get('sidebar_state')?.value === 'true';
|
||||
return (
|
||||
<>
|
||||
<SidebarProviderWrapper defaultOpen={defaultOpen}>
|
||||
<AppSidebar></AppSidebar>
|
||||
<Header>{children}</Header>
|
||||
</SidebarProviderWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
--font-heading: 'Comfortaa', sans-serif;
|
||||
--font-label: 'Varela Round', sans-serif;
|
||||
--font-button: 'Varela Round', sans-serif;
|
||||
--font-sans: var(--font-label);
|
||||
|
||||
--transparent: transparent;
|
||||
|
||||
|
@ -28,11 +29,12 @@
|
|||
|
||||
--background: var(--neutral-800);
|
||||
--background-reversed: var(--neutral-000);
|
||||
--base: var(--neutral-800);
|
||||
--basecl: var(--neutral-800);
|
||||
--text: var(--neutral-000);
|
||||
--text-alt: var(--neutral-900);
|
||||
--text-input: var(--text);
|
||||
--text-muted-input: var(--neutral-450);
|
||||
--text-muted: var(--neutral-300);
|
||||
--muted-input: var(--neutral-600);
|
||||
--background-disabled: var(--neutral-500);
|
||||
--text-disabled: var(--neutral-700);
|
||||
|
@ -48,13 +50,27 @@
|
|||
--active-secondary: oklch(0.4254 0.133 272.15);
|
||||
--disabled-secondary: oklch(0.4937 0.1697 271.26 / 0.5);
|
||||
|
||||
--destructive: oklch(60.699% 0.20755 25.945);
|
||||
--hover-destructive: oklch(60.699% 0.20755 25.945 / 0.8);
|
||||
--active-destructive: oklch(50.329% 0.17084 25.842);
|
||||
--disabled-destructive: oklch(60.699% 0.20755 25.945 / 0.4);
|
||||
|
||||
--muted: var(--color-neutral-700);
|
||||
--hover-muted: var(--color-neutral-600);
|
||||
--active-muted: var(--color-neutral-400);
|
||||
--disabled-muted: var(--color-neutral-400);
|
||||
|
||||
--toaster-default-bg: var(--color-neutral-150);
|
||||
--toaster-success-bg: oklch(54.147% 0.09184 144.208);
|
||||
--toaster-error-bg: oklch(52.841% 0.10236 27.274);
|
||||
--toaster-info-bg: oklch(44.298% 0.05515 259.369);
|
||||
--toaster-warning-bg: oklch(61.891% 0.07539 102.943);
|
||||
--toaster-notification-bg: var(--color-neutral-150);
|
||||
|
||||
--card: var(--neutral-800);
|
||||
|
||||
--sidebar-width-icon: 32px;
|
||||
|
||||
/* ------------------- */
|
||||
|
||||
--foreground: oklch(0.13 0.028 261.692);
|
||||
|
@ -77,8 +93,6 @@
|
|||
|
||||
--accent-foreground: oklch(0.21 0.034 264.665);
|
||||
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
|
||||
--border: oklch(0.928 0.006 264.531);
|
||||
|
||||
--input: oklch(0.928 0.006 264.531);
|
||||
|
@ -95,23 +109,79 @@
|
|||
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
|
||||
--sidebar: oklch(0.985 0.002 247.839);
|
||||
--sidebar: var(--background);
|
||||
|
||||
--sidebar-foreground: oklch(0.13 0.028 261.692);
|
||||
--sidebar-foreground: var(--text);
|
||||
|
||||
--sidebar-primary: oklch(0.21 0.034 264.665);
|
||||
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-primary-foreground: var(--text);
|
||||
|
||||
--sidebar-accent: oklch(0.967 0.003 264.542);
|
||||
|
||||
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--sidebar-accent-foreground: var(--text);
|
||||
|
||||
--sidebar-border: oklch(0.928 0.006 264.531);
|
||||
|
||||
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 40px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 36px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 32px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 28px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 26px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: var(--font-label);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Comfortaa';
|
||||
font-style: normal;
|
||||
|
@ -150,11 +220,12 @@
|
|||
|
||||
--color-background: var(--neutral-750);
|
||||
--color-background-reversed: var(--background-reversed);
|
||||
--color-base: var(--neutral-800);
|
||||
--color-basecl: var(--neutral-800);
|
||||
--color-text: var(--text);
|
||||
--color-text-alt: var(--text-alt);
|
||||
--color-text-input: var(--text-input);
|
||||
--color-text-muted-input: var(--text-muted-input);
|
||||
--color-text-muted: var(--text-muted);
|
||||
--color-muted-input: var(--muted-input);
|
||||
|
||||
--color-background-disabled: var(--neutral-500);
|
||||
|
@ -171,11 +242,23 @@
|
|||
--color-active-secondary: var(--active-secondary);
|
||||
--color-disabled-secondary: var(--disabled-secondary);
|
||||
|
||||
--color-destructive: var(--destructive);
|
||||
--color-hover-destructive: var(--hover-destructive);
|
||||
--color-active-destructive: var(--active-destructive);
|
||||
--color-disabled-destructive: var(--disabled-destructive);
|
||||
|
||||
--color-muted: var(--muted);
|
||||
--color-hover-muted: var(--hover-muted);
|
||||
--color-active-muted: var(--active-muted);
|
||||
--color-disabled-muted: var(--disabled-muted);
|
||||
|
||||
--color-toaster-default-bg: var(--toaster-default-bg);
|
||||
--color-toaster-success-bg: var(--toaster-success-bg);
|
||||
--color-toaster-error-bg: var(--toaster-error-bg);
|
||||
--color-toaster-info-bg: var(--toaster-info-bg);
|
||||
--color-toaster-warning-bg: var(--toaster-warning-bg);
|
||||
--color-toaster-notification-bg: var(--toaster-notification-bg);
|
||||
|
||||
/* Custom values */
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
@ -216,8 +299,6 @@
|
|||
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
|
||||
--color-destructive: var(--destructive);
|
||||
|
||||
--color-border: var(--border);
|
||||
|
||||
--color-input: var(--input);
|
||||
|
@ -273,11 +354,12 @@
|
|||
|
||||
--background: var(--neutral-750);
|
||||
--background-reversed: var(--neutral-000);
|
||||
--base: var(--neutral-750);
|
||||
--basecl: var(--neutral-750);
|
||||
--text: var(--neutral-000);
|
||||
--text-alt: var(--neutral-900);
|
||||
--text-input: var(--text);
|
||||
--text-muted-input: var(--neutral-450);
|
||||
--text-muted: var(--neutral-300);
|
||||
--muted-input: var(--neutral-500);
|
||||
--background-disabled: var(--neutral-500);
|
||||
--text-disabled: var(--neutral-700);
|
||||
|
@ -292,11 +374,23 @@
|
|||
--active-secondary: oklch(0.4471 0.15 271.61);
|
||||
--disabled-secondary: oklch(0.6065 0.213 271.11 / 0.4);
|
||||
|
||||
--destructive: oklch(0.58 0.2149 27.13);
|
||||
--hover-destructive: oklch(0.58 0.2149 27.13 / 0.8);
|
||||
--active-destructive: oklch(45.872% 0.16648 26.855);
|
||||
--disabled-destructive: oklch(0.58 0.2149 27.13 / 0.4);
|
||||
|
||||
--muted: var(--color-neutral-650);
|
||||
--hover-muted: var(--color-neutral-500);
|
||||
--active-muted: var(--color-neutral-400);
|
||||
--disabled-muted: var(--color-neutral-400);
|
||||
|
||||
--toaster-default-bg: var(--color-neutral-150);
|
||||
--toaster-success-bg: var(--color-green-200);
|
||||
--toaster-error-bg: var(--color-red-200);
|
||||
--toaster-info-bg: var(--color-blue-200);
|
||||
--toaster-warning-bg: var(--color-yellow-200);
|
||||
--toaster-notification-bg: var(--color-neutral-150);
|
||||
|
||||
--card: var(--neutral-750);
|
||||
|
||||
/* ------------------- */
|
||||
|
@ -321,8 +415,6 @@
|
|||
|
||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
|
@ -339,17 +431,17 @@
|
|||
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
|
||||
--sidebar: oklch(0.21 0.034 264.665);
|
||||
--sidebar: var(--background);
|
||||
|
||||
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-foreground: var(--text);
|
||||
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-primary-foreground: var(--text);
|
||||
|
||||
--sidebar-accent: oklch(0.278 0.033 256.848);
|
||||
|
||||
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-accent-foreground: var(--text);
|
||||
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ThemeProvider } from '@/components/wrappers/theme-provider';
|
|||
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
import { QueryProvider } from '@/components/query-provider';
|
||||
import { QueryProvider } from '@/components/wrappers/query-provider';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'MeetUp',
|
||||
|
|
|
@ -1,482 +1,5 @@
|
|||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import SettingsPage from '@/components/misc/settings-page';
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div className='fixed inset-0 flex items-center justify-center p-4 bg-background/50 backdrop-blur-sm'>
|
||||
<div className='rounded-lg border bg-card text-card-foreground shadow-xl max-w-[700px] w-full h-auto max-h-[calc(100vh-2rem)] flex flex-col'>
|
||||
<Tabs
|
||||
defaultValue='general'
|
||||
className='w-full flex flex-col flex-grow min-h-0'
|
||||
>
|
||||
<TabsList className='grid w-full grid-cols-3 sm:grid-cols-5'>
|
||||
<TabsTrigger value='general'>Account</TabsTrigger>
|
||||
<TabsTrigger value='notifications'>Notifications</TabsTrigger>
|
||||
<TabsTrigger value='calendarAvailability'>Calendar</TabsTrigger>
|
||||
<TabsTrigger value='sharingPrivacy'>Privacy</TabsTrigger>
|
||||
<TabsTrigger value='appearance'>Appearance</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value='general' className='flex-grow overflow-hidden'>
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your account details and preferences.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='displayName'>Display Name</Label>
|
||||
<Input id='displayName' placeholder='Your Name' />
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='email'>Email Address</Label>
|
||||
<Input
|
||||
id='email'
|
||||
type='email'
|
||||
placeholder='your.email@example.com'
|
||||
readOnly
|
||||
value='user-email@example.com'
|
||||
/>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Email is managed by your SSO provider.
|
||||
</p>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='profilePicture'>Profile Picture</Label>
|
||||
<Input id='profilePicture' type='file' />
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Upload a new profile picture.
|
||||
</p>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='timezone'>Timezone</Label>
|
||||
<Input id='displayName' placeholder='Europe/Berlin' />
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='language'>Language</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='language'>
|
||||
<SelectValue placeholder='Select language' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='en'>English</SelectItem>
|
||||
<SelectItem value='de'>German</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='pt-4'>
|
||||
<Button variant='secondary'>Delete Account</Button>
|
||||
<p className='text-sm text-muted-foreground pt-1'>
|
||||
Permanently delete your account and all associated data.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
||||
<Button variant='secondary'>Exit</Button>
|
||||
<Button>Save Changes</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value='notifications'
|
||||
className='flex-grow overflow-hidden'
|
||||
>
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Notification Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Choose how you want to be notified.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='flex items-center justify-between space-x-2 p-3 rounded-md border'>
|
||||
<Label
|
||||
htmlFor='masterEmailNotifications'
|
||||
className='font-normal'
|
||||
>
|
||||
Enable All Email Notifications
|
||||
</Label>
|
||||
<Switch id='masterEmailNotifications' />
|
||||
</div>
|
||||
<div className='space-y-4 pl-2 border-l-2 ml-2'>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label
|
||||
htmlFor='newMeetingBookings'
|
||||
className='font-normal'
|
||||
>
|
||||
New Meeting Bookings
|
||||
</Label>
|
||||
<Switch id='newMeetingBookings' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label
|
||||
htmlFor='meetingConfirmations'
|
||||
className='font-normal'
|
||||
>
|
||||
Meeting Confirmations/Cancellations
|
||||
</Label>
|
||||
<Switch id='meetingConfirmations' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label
|
||||
htmlFor='enableMeetingReminders'
|
||||
className='font-normal'
|
||||
>
|
||||
Meeting Reminders
|
||||
</Label>
|
||||
<Switch id='enableMeetingReminders' />
|
||||
</div>
|
||||
<div className='space-y-2 pl-6'>
|
||||
<Label htmlFor='remindBefore'>Remind me before</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='remindBefore'>
|
||||
<SelectValue placeholder='Select reminder time' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='15m'>15 minutes</SelectItem>
|
||||
<SelectItem value='30m'>30 minutes</SelectItem>
|
||||
<SelectItem value='1h'>1 hour</SelectItem>
|
||||
<SelectItem value='1d'>1 day</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label htmlFor='friendRequests' className='font-normal'>
|
||||
Friend Requests
|
||||
</Label>
|
||||
<Switch id='friendRequests' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label htmlFor='groupUpdates' className='font-normal'>
|
||||
Group Invitations/Updates
|
||||
</Label>
|
||||
<Switch id='groupUpdates' />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
||||
<Button variant='secondary'>Exit</Button>
|
||||
<Button>Save Changes</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value='calendarAvailability'
|
||||
className='flex-grow overflow-hidden'
|
||||
>
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Calendar & Availability</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your calendar display, default availability, and iCal
|
||||
integrations.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
||||
<legend className='text-sm font-medium px-1'>
|
||||
Display
|
||||
</legend>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='defaultCalendarView'>
|
||||
Default Calendar View
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='defaultCalendarView'>
|
||||
<SelectValue placeholder='Select view' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='day'>Day</SelectItem>
|
||||
<SelectItem value='week'>Week</SelectItem>
|
||||
<SelectItem value='month'>Month</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='weekStartsOn'>Week Starts On</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='weekStartsOn'>
|
||||
<SelectValue placeholder='Select day' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='sunday'>Sunday</SelectItem>
|
||||
<SelectItem value='monday'>Monday</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label htmlFor='showWeekends' className='font-normal'>
|
||||
Show Weekends
|
||||
</Label>
|
||||
<Switch id='showWeekends' defaultChecked />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
||||
<legend className='text-sm font-medium px-1'>
|
||||
Availability
|
||||
</legend>
|
||||
<div className='space-y-2'>
|
||||
<Label>Working Hours</Label>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Define your typical available hours (e.g.,
|
||||
Monday-Friday, 9 AM - 5 PM).
|
||||
</p>
|
||||
<Button variant='outline_muted' size='sm'>
|
||||
Set Working Hours
|
||||
</Button>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='minNoticeBooking'>
|
||||
Minimum Notice for Bookings
|
||||
</Label>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Min time before a booking can be made.
|
||||
</p>
|
||||
<div className='space-y-2'>
|
||||
<Input
|
||||
id='bookingWindow'
|
||||
type='text'
|
||||
placeholder='e.g., 1h'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='bookingWindow'>
|
||||
Booking Window (days in advance)
|
||||
</Label>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Max time in advance a booking can be made.
|
||||
</p>
|
||||
<Input
|
||||
id='bookingWindow'
|
||||
type='number'
|
||||
placeholder='e.g., 30d'
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
||||
<legend className='text-sm font-medium px-1'>
|
||||
iCalendar Integration
|
||||
</legend>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='icalImport'>Import iCal Feed URL</Label>
|
||||
<Input
|
||||
id='icalImport'
|
||||
type='url'
|
||||
placeholder='https://calendar.example.com/feed.ics'
|
||||
/>
|
||||
<Button size='sm' className='mt-1'>
|
||||
Add Feed
|
||||
</Button>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label>Export Your Calendar</Label>
|
||||
<Button variant='outline_muted' size='sm'>
|
||||
Get iCal Export URL
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline_muted'
|
||||
size='sm'
|
||||
className='ml-2'
|
||||
>
|
||||
Download .ics File
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
||||
<Button variant='secondary'>Exit</Button>
|
||||
<Button>Save Changes</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value='sharingPrivacy'
|
||||
className='flex-grow overflow-hidden'
|
||||
>
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Sharing & Privacy</CardTitle>
|
||||
<CardDescription>
|
||||
Control who can see your calendar and book time with you.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='defaultVisibility'>
|
||||
Default Calendar Visibility
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='defaultVisibility'>
|
||||
<SelectValue placeholder='Select visibility' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='private'>
|
||||
Private (Only You)
|
||||
</SelectItem>
|
||||
<SelectItem value='freebusy'>
|
||||
Free/Busy for Friends
|
||||
</SelectItem>
|
||||
<SelectItem value='fulldetails'>
|
||||
Full Details for Friends
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='whoCanSeeFull'>
|
||||
Who Can See Your Full Calendar Details?
|
||||
</Label>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
(Override for Default Visibility)
|
||||
<br />
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
This setting will override the default visibility for
|
||||
your calendar. You can set specific friends or groups to
|
||||
see your full calendar details.
|
||||
</span>
|
||||
</p>
|
||||
<Select>
|
||||
<SelectTrigger id='whoCanSeeFull'>
|
||||
<SelectValue placeholder='Select audience' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='me'>Only Me</SelectItem>
|
||||
<SelectItem value='friends'>My Friends</SelectItem>
|
||||
<SelectItem value='specific'>
|
||||
Specific Friends/Groups (manage separately)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='whoCanBook'>
|
||||
Who Can Book Time With You?
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='whoCanBook'>
|
||||
<SelectValue placeholder='Select audience' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='none'>No One</SelectItem>
|
||||
<SelectItem value='friends'>My Friends</SelectItem>
|
||||
<SelectItem value='specific'>
|
||||
Specific Friends/Groups (manage separately)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label>Blocked Users</Label>
|
||||
<Button variant='outline_muted'>
|
||||
Manage Blocked Users
|
||||
</Button>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Prevent specific users from seeing your calendar or
|
||||
booking time.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
||||
<Button variant='secondary'>Exit</Button>
|
||||
<Button>Save Changes</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='appearance' className='flex-grow overflow-hidden'>
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Appearance</CardTitle>
|
||||
<CardDescription>
|
||||
Customize the look and feel of the application.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='theme'>Theme</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='theme'>
|
||||
<SelectValue placeholder='Select theme' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='light'>Light</SelectItem>
|
||||
<SelectItem value='dark'>Dark</SelectItem>
|
||||
<SelectItem value='system'>System Default</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='dateFormat'>Date Format</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='dateFormat'>
|
||||
<SelectValue placeholder='Select date format' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='ddmmyyyy'>DD/MM/YYYY</SelectItem>
|
||||
<SelectItem value='mmddyyyy'>MM/DD/YYYY</SelectItem>
|
||||
<SelectItem value='yyyymmdd'>YYYY-MM-DD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='timeFormat'>Time Format</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='timeFormat'>
|
||||
<SelectValue placeholder='Select time format' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='24h'>24-hour</SelectItem>
|
||||
<SelectItem value='12h'>12-hour</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
<CardFooter className='mt-auto border-t pt-4 flex justify-between'>
|
||||
<Button variant='secondary'>Exit</Button>
|
||||
<Button>Save Changes</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export default function Page() {
|
||||
return <SettingsPage />;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export { default as logo_colored_combo_light } from '@/assets/logo/logo_colored_combo_light.svg';
|
||||
export { default as logo_colored_combo_dark } from '@/assets/logo/logo_colored_combo_dark.svg';
|
||||
export { default as logo_colored_combo_light } from '@/assets/logo/new/logo_colored_combo_light.svg';
|
||||
export { default as logo_colored_combo_dark } from '@/assets/logo/new/logo_colored_combo_dark.svg';
|
||||
export { default as logo_colored_primary_light } from '@/assets/logo/logo_colored_primary_light.svg';
|
||||
export { default as logo_colored_primary_dark } from '@/assets/logo/logo_colored_primary_dark.svg';
|
||||
export { default as logo_colored_secondary_light } from '@/assets/logo/logo_colored_secondary_light.svg';
|
||||
|
@ -12,5 +12,5 @@ export { default as logo_mono_secondary_light } from '@/assets/logo/logo_mono_se
|
|||
export { default as logo_mono_secondary_dark } from '@/assets/logo/logo_mono_secondary_dark.svg';
|
||||
export { default as logo_mono_submark_light } from '@/assets/logo/logo_mono_submark_light.svg';
|
||||
export { default as logo_mono_submark_dark } from '@/assets/logo/logo_mono_submark_dark.svg';
|
||||
export { default as logo_colored_submark_light } from '@/assets/logo/logo_colored_submark_light.svg';
|
||||
export { default as logo_colored_submark_dark } from '@/assets/logo/logo_colored_submark_dark.svg';
|
||||
export { default as logo_colored_submark_light } from '@/assets/logo/new/logo_colored_submark_light.svg';
|
||||
export { default as logo_colored_submark_dark } from '@/assets/logo/new/logo_colored_submark_dark.svg';
|
||||
|
|
17
src/assets/logo/new/logo_colored_combo_dark.svg
Normal file
17
src/assets/logo/new/logo_colored_combo_dark.svg
Normal file
|
@ -0,0 +1,17 @@
|
|||
<svg width="402" height="100" viewBox="0 0 402 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M234.931 38.9532C235.883 38.9532 236.684 39.3271 237.335 40.0751C237.987 40.8231 238.312 41.7437 238.312 42.8369C238.312 44.8361 236.815 46.2891 234.796 46.2891L171.031 46.3754C170.129 46.3754 169.378 46.0014 168.777 45.2535C168.126 44.563 167.8 43.7 167.8 42.6643C167.8 41.6286 168.126 40.7656 168.777 40.0751C169.378 39.3847 170.129 39.0395 171.031 39.0395L234.931 38.9532Z" fill="white"/>
|
||||
<path d="M104.461 93.0499C103.487 93.0499 102.666 92.7092 102 92.0278C101.333 91.3463 101 90.5077 101 89.5117V42.5722C101 41.1569 101.487 39.9513 102.461 38.9554C103.436 37.907 104.615 37.3829 105.999 37.3829H108.538C109.563 37.3829 110.512 37.6974 111.383 38.3264C112.255 38.9554 112.871 39.7416 113.229 40.6852L130.689 85.1873L148.149 40.6852C148.559 39.7416 149.174 38.9554 149.995 38.3264C150.866 37.6974 151.815 37.3829 152.841 37.3829H155.379C156.763 37.3829 157.943 37.907 158.917 38.9554C159.891 39.9513 160.378 41.1569 160.378 42.5722V89.5117C160.378 90.5077 160.045 91.3463 159.378 92.0278C158.712 92.7092 157.891 93.0499 156.917 93.0499C155.943 93.0499 155.097 92.7092 154.379 92.0278C153.712 91.3463 153.379 90.5077 153.379 89.5117V44.8523L135.612 89.7476C135.201 90.7436 134.56 91.556 133.689 92.185C132.817 92.7616 131.817 93.0499 130.689 93.0499C129.561 93.0499 128.561 92.7616 127.689 92.185C126.818 91.556 126.177 90.7436 125.767 89.7476L107.999 44.8523V89.5117C107.999 90.5077 107.64 91.3463 106.922 92.0278C106.256 92.7092 105.435 93.0499 104.461 93.0499Z" fill="white"/>
|
||||
<path d="M234.931 61.2199C235.883 61.2199 236.684 61.5939 237.335 62.3419C237.987 63.0899 238.312 64.0105 238.312 65.1037C238.312 67.1029 236.815 68.5559 234.796 68.5559L171.031 68.6422C170.129 68.6422 169.378 68.2682 168.777 67.5202C168.126 66.8298 167.8 65.9667 167.8 64.9311C167.8 63.8954 168.126 63.0324 168.777 62.3419C169.378 61.6515 170.129 61.3062 171.031 61.3062L234.931 61.2199Z" fill="#C1830D"/>
|
||||
<path d="M234.931 85.6277C235.883 85.6277 236.684 86.0016 237.335 86.7496C237.987 87.4976 238.312 88.4182 238.312 89.5114C238.312 91.5106 236.815 92.9636 234.796 92.9636L171.031 93.0499C170.129 93.0499 169.378 92.6759 168.777 91.928C168.126 91.2375 167.8 90.3745 167.8 89.3388C167.8 88.3031 168.126 87.4401 168.777 86.7496C169.378 86.0592 170.129 85.714 171.031 85.714L234.931 85.6277Z" fill="white"/>
|
||||
<path d="M267.963 93.0499C266.995 93.0499 266.181 92.7068 265.519 92.0205C264.857 91.3342 264.526 90.4896 264.526 89.4866V44.1136H248.943C248.026 44.1136 247.262 43.7969 246.651 43.1634C246.04 42.5299 245.734 41.738 245.734 40.7878C245.734 39.8376 246.04 39.0458 246.651 38.4123C247.262 37.726 248.026 37.3829 248.943 37.3829H286.983C287.9 37.3829 288.664 37.726 289.275 38.4123C289.937 39.0458 290.268 39.8376 290.268 40.7878C290.268 41.738 289.937 42.5299 289.275 43.1634C288.664 43.7969 287.9 44.1136 286.983 44.1136H271.477V89.4866C271.477 90.4896 271.12 91.3342 270.407 92.0205C269.745 92.7068 268.931 93.0499 267.963 93.0499Z" fill="white"/>
|
||||
<path d="M338.498 3.30273C339.9 3.29168 341.204 4.05009 342.504 5.35547C344.287 7.14689 347.48 10.3467 350.621 13.4922L358.434 21.3037L358.456 21.3252L358.476 21.3486C359.393 22.4377 359.772 23.5909 359.703 24.6758C359.635 25.7528 359.129 26.695 358.402 27.375C356.958 28.7257 354.528 29.1152 352.683 27.4297L352.669 27.417L352.655 27.4033C350.775 25.4925 347.992 22.7063 345.677 20.3965C344.519 19.2419 343.48 18.2064 342.729 17.46C342.722 17.4528 342.715 17.4456 342.708 17.4385V74.9961C342.708 78.7908 342.028 82.1326 340.646 85.0029L340.647 85.0039C339.33 87.7947 337.557 90.1328 335.328 92.0059L335.32 92.0117C333.107 93.8166 330.654 95.1582 327.965 96.0342C325.293 96.9042 322.639 97.3428 320.005 97.3428C317.371 97.3428 314.716 96.9042 312.045 96.0342C309.355 95.1582 306.902 93.8166 304.689 92.0117L304.682 92.0059C302.455 90.1348 300.659 87.8002 299.29 85.0137L299.287 85.0068L299.283 84.999C297.955 82.13 297.302 78.7894 297.302 74.9961V43.5576C297.302 42.4529 297.649 41.4822 298.35 40.6836L298.494 40.5264C299.284 39.6526 300.283 39.1983 301.45 39.1982C302.613 39.1982 303.609 39.6498 304.397 40.5176C305.245 41.3395 305.675 42.3674 305.675 43.5576V75.2344C305.675 78.4181 306.366 81.0359 307.705 83.126C309.12 85.2003 310.917 86.7601 313.102 87.8184H313.101C315.307 88.8345 317.606 89.3408 320.005 89.3408C322.403 89.3408 324.702 88.8342 326.908 87.8184L327.312 87.6143C329.312 86.5634 330.947 85.0716 332.226 83.1289L332.228 83.126C333.618 81.0358 334.335 78.4183 334.335 75.2344V17.3291C333.656 18.0124 332.73 18.9438 331.672 20.0059C329.454 22.232 326.655 25.0349 324.336 27.3389L324.326 27.3486L324.315 27.3584C320.384 30.9544 315.024 25.7542 318.168 21.7617L318.193 21.7285L318.224 21.6982C321.362 18.5764 323.629 16.2939 326.017 13.8896C328.405 11.4851 330.915 8.95892 334.54 5.35254C335.811 4.08795 337.1 3.31376 338.498 3.30273Z" fill="#5770FF" stroke="#5770FF" stroke-width="1.39574"/>
|
||||
<path d="M377.073 39.2316C381.146 39.2316 384.597 39.7797 387.4 40.9054H387.399C390.214 41.961 392.474 43.418 394.146 45.295C395.846 47.0923 397.064 49.0974 397.785 51.3077L397.924 51.7179C398.597 53.7677 398.935 55.8467 398.935 57.9523C398.935 60.1981 398.55 62.4136 397.785 64.5958C397.109 66.6659 395.999 68.5781 394.466 70.3322L394.154 70.6818C392.481 72.5093 390.216 73.9697 387.395 75.0812L387.388 75.0841C384.589 76.152 381.143 76.672 377.073 76.672H363.345V93.3468C363.345 94.5647 362.897 95.6139 362.018 96.4503C361.2 97.2792 360.179 97.6974 359 97.6974C357.813 97.6973 356.786 97.2729 355.965 96.4318C355.145 95.5914 354.736 94.548 354.736 93.3468V43.6642C354.736 42.463 355.145 41.4196 355.965 40.5792C356.779 39.6913 357.804 39.2317 359 39.2316H377.073ZM363.345 68.295H376.36C380.271 68.295 383.177 67.7578 385.147 66.7521C387.166 65.6658 388.491 64.3569 389.203 62.8497C389.956 61.2025 390.325 59.5717 390.326 57.9523C390.326 56.2735 389.954 54.643 389.203 53.0548C388.493 51.5512 387.172 50.2695 385.157 49.2374L385.145 49.2306C383.179 48.1733 380.274 47.6095 376.36 47.6095H363.345V68.295Z" fill="#5770FF" stroke="#5770FF" stroke-width="1.39574"/>
|
||||
<rect x="7.14288" y="39.2857" width="75" height="53.5714" fill="#C1830D"/>
|
||||
<path d="M3.57144 15.6832C3.57144 14.9114 4.19713 14.2857 4.96896 14.2857H84.3168C85.0886 14.2857 85.7143 14.9114 85.7143 15.6832V32.1428H3.57144V15.6832Z" fill="#5770FF"/>
|
||||
<rect x="2.09627" y="12.8106" width="85.0932" height="85.0932" rx="2.09627" stroke="white" stroke-width="4.19255"/>
|
||||
<line y1="35.0155" x2="89.2857" y2="35.0155" stroke="white" stroke-width="1.39752"/>
|
||||
<rect x="19.2546" y="1.39752" width="9.53118" height="20.1191" rx="2.79503" fill="#C1830D" stroke="white" stroke-width="2.79503"/>
|
||||
<rect x="58.5403" y="1.39752" width="9.53118" height="20.1191" rx="2.79503" fill="#C1830D" stroke="white" stroke-width="2.79503"/>
|
||||
<path d="M84.6845 32.1428C85.9796 32.1428 87.0703 32.5641 87.9565 33.4066C88.8426 34.2492 89.2857 35.2862 89.2857 36.5176V95.528C89.2857 96.7594 88.8426 97.7964 87.9565 98.6389C87.0703 99.5463 85.9796 100 84.6845 100H43.6824C42.4554 100 41.4329 99.5787 40.6149 98.7362C39.7288 97.9584 39.2857 96.9863 39.2857 95.8197C39.2857 94.6531 39.7288 93.6809 40.6149 92.9032C41.4329 92.1254 42.4554 91.7366 43.6824 91.7366H79.981V70.2517H58.7396C57.5126 70.2517 56.4901 69.8304 55.6721 68.9879C54.786 68.2102 54.3429 67.238 54.3429 66.0714C54.3429 64.9048 54.786 63.9326 55.6721 63.1549C56.4901 62.3772 57.5126 61.9883 58.7396 61.9883H79.981V40.4062L43.6824 40.4062C42.4554 40.4062 41.4329 39.985 40.6149 39.1424C39.7288 38.3647 39.2857 37.3925 39.2857 36.2259C39.2857 35.0593 39.7288 34.0872 40.6149 33.3094C41.4329 32.5317 42.4554 32.1428 43.6824 32.1428H84.6845Z" fill="white"/>
|
||||
<path d="M4.60123 100C3.30607 100 2.21541 99.5788 1.32924 98.7362C0.443082 97.8937 0 96.8567 0 95.6253V36.6148C0 35.3834 0.443082 34.3465 1.32924 33.5039C2.21541 32.5966 3.30607 32.1429 4.60123 32.1429H45.6033C46.8303 32.1429 47.8528 32.5642 48.6708 33.4067C49.5569 34.1844 50 35.1566 50 36.3232C50 37.4898 49.5569 38.462 48.6708 39.2397C47.8528 40.0174 46.8303 40.4063 45.6033 40.4063H9.3047V61.8911H30.5461C31.7731 61.8911 32.7956 62.3124 33.6136 63.155C34.4997 63.9327 34.9428 64.9049 34.9428 66.0715C34.9428 67.2381 34.4997 68.2102 33.6136 68.988C32.7956 69.7657 31.7731 70.1546 30.5461 70.1546H9.3047V91.7366H45.6033C46.8303 91.7366 47.8528 92.1579 48.6708 93.0004C49.5569 93.7782 50 94.7503 50 95.9169C50 97.0835 49.5569 98.0557 48.6708 98.8334C47.8528 99.6112 46.8303 100 45.6033 100H4.60123Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 8.3 KiB |
17
src/assets/logo/new/logo_colored_combo_light.svg
Normal file
17
src/assets/logo/new/logo_colored_combo_light.svg
Normal file
|
@ -0,0 +1,17 @@
|
|||
<svg width="402" height="100" viewBox="0 0 402 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M234.931 38.9532C235.883 38.9532 236.684 39.3271 237.335 40.0751C237.987 40.8231 238.312 41.7437 238.312 42.8369C238.312 44.8361 236.815 46.2891 234.796 46.2891L171.031 46.3754C170.129 46.3754 169.378 46.0014 168.777 45.2535C168.126 44.563 167.8 43.7 167.8 42.6643C167.8 41.6286 168.126 40.7656 168.777 40.0751C169.378 39.3847 170.129 39.0395 171.031 39.0395L234.931 38.9532Z" fill="black"/>
|
||||
<path d="M104.461 93.0499C103.487 93.0499 102.666 92.7092 102 92.0278C101.333 91.3463 101 90.5077 101 89.5117V42.5722C101 41.1569 101.487 39.9513 102.461 38.9554C103.436 37.907 104.615 37.3829 105.999 37.3829H108.538C109.563 37.3829 110.512 37.6974 111.383 38.3264C112.255 38.9554 112.871 39.7416 113.229 40.6852L130.689 85.1873L148.149 40.6852C148.559 39.7416 149.174 38.9554 149.995 38.3264C150.866 37.6974 151.815 37.3829 152.841 37.3829H155.379C156.763 37.3829 157.943 37.907 158.917 38.9554C159.891 39.9513 160.378 41.1569 160.378 42.5722V89.5117C160.378 90.5077 160.045 91.3463 159.378 92.0278C158.712 92.7092 157.891 93.0499 156.917 93.0499C155.943 93.0499 155.097 92.7092 154.379 92.0278C153.712 91.3463 153.379 90.5077 153.379 89.5117V44.8523L135.612 89.7476C135.201 90.7436 134.56 91.556 133.689 92.185C132.817 92.7616 131.817 93.0499 130.689 93.0499C129.561 93.0499 128.561 92.7616 127.689 92.185C126.818 91.556 126.177 90.7436 125.767 89.7476L107.999 44.8523V89.5117C107.999 90.5077 107.64 91.3463 106.922 92.0278C106.256 92.7092 105.435 93.0499 104.461 93.0499Z" fill="black"/>
|
||||
<path d="M234.931 61.2199C235.883 61.2199 236.684 61.5939 237.335 62.3419C237.987 63.0899 238.312 64.0105 238.312 65.1037C238.312 67.1029 236.815 68.5559 234.796 68.5559L171.031 68.6422C170.129 68.6422 169.378 68.2682 168.777 67.5202C168.126 66.8298 167.8 65.9667 167.8 64.9311C167.8 63.8954 168.126 63.0324 168.777 62.3419C169.378 61.6515 170.129 61.3062 171.031 61.3062L234.931 61.2199Z" fill="#E69D11"/>
|
||||
<path d="M234.931 85.6277C235.883 85.6277 236.684 86.0016 237.335 86.7496C237.987 87.4976 238.312 88.4182 238.312 89.5114C238.312 91.5106 236.815 92.9636 234.796 92.9636L171.031 93.0499C170.129 93.0499 169.378 92.6759 168.777 91.928C168.126 91.2375 167.8 90.3745 167.8 89.3388C167.8 88.3031 168.126 87.4401 168.777 86.7496C169.378 86.0592 170.129 85.714 171.031 85.714L234.931 85.6277Z" fill="black"/>
|
||||
<path d="M267.963 93.0499C266.995 93.0499 266.181 92.7068 265.519 92.0205C264.857 91.3342 264.526 90.4896 264.526 89.4866V44.1136H248.943C248.026 44.1136 247.262 43.7969 246.651 43.1634C246.04 42.5299 245.734 41.738 245.734 40.7878C245.734 39.8376 246.04 39.0458 246.651 38.4123C247.262 37.726 248.026 37.3829 248.943 37.3829H286.983C287.9 37.3829 288.664 37.726 289.275 38.4123C289.937 39.0458 290.268 39.8376 290.268 40.7878C290.268 41.738 289.937 42.5299 289.275 43.1634C288.664 43.7969 287.9 44.1136 286.983 44.1136H271.477V89.4866C271.477 90.4896 271.12 91.3342 270.407 92.0205C269.745 92.7068 268.931 93.0499 267.963 93.0499Z" fill="black"/>
|
||||
<path d="M338.498 3.30273C339.9 3.29168 341.204 4.05009 342.504 5.35547C344.287 7.14689 347.48 10.3467 350.621 13.4922L358.434 21.3037L358.456 21.3252L358.476 21.3486C359.393 22.4377 359.772 23.5909 359.703 24.6758C359.635 25.7528 359.129 26.695 358.402 27.375C356.958 28.7257 354.528 29.1152 352.683 27.4297L352.669 27.417L352.655 27.4033C350.775 25.4925 347.992 22.7063 345.677 20.3965C344.519 19.2419 343.48 18.2064 342.729 17.46C342.722 17.4528 342.715 17.4456 342.708 17.4385V74.9961C342.708 78.7908 342.028 82.1326 340.646 85.0029L340.647 85.0039C339.33 87.7947 337.557 90.1328 335.328 92.0059L335.32 92.0117C333.107 93.8166 330.654 95.1582 327.965 96.0342C325.293 96.9042 322.639 97.3428 320.005 97.3428C317.371 97.3428 314.716 96.9042 312.045 96.0342C309.355 95.1582 306.902 93.8166 304.689 92.0117L304.682 92.0059C302.455 90.1348 300.659 87.8002 299.29 85.0137L299.287 85.0068L299.283 84.999C297.955 82.13 297.302 78.7894 297.302 74.9961V43.5576C297.302 42.4529 297.649 41.4822 298.35 40.6836L298.494 40.5264C299.284 39.6526 300.283 39.1983 301.45 39.1982C302.613 39.1982 303.609 39.6498 304.397 40.5176C305.245 41.3395 305.675 42.3674 305.675 43.5576V75.2344C305.675 78.4181 306.366 81.0359 307.705 83.126C309.12 85.2003 310.917 86.7601 313.102 87.8184H313.101C315.307 88.8345 317.606 89.3408 320.005 89.3408C322.403 89.3408 324.702 88.8342 326.908 87.8184L327.312 87.6143C329.312 86.5634 330.947 85.0716 332.226 83.1289L332.228 83.126C333.618 81.0358 334.335 78.4183 334.335 75.2344V17.3291C333.656 18.0124 332.73 18.9438 331.672 20.0059C329.454 22.232 326.655 25.0349 324.336 27.3389L324.326 27.3486L324.315 27.3584C320.384 30.9544 315.024 25.7542 318.168 21.7617L318.193 21.7285L318.224 21.6982C321.362 18.5764 323.629 16.2939 326.017 13.8896C328.405 11.4851 330.915 8.95892 334.54 5.35254C335.811 4.08795 337.1 3.31376 338.498 3.30273Z" fill="#4154C0" stroke="#4154C0" stroke-width="1.39574"/>
|
||||
<path d="M377.073 39.2316C381.146 39.2316 384.597 39.7797 387.4 40.9054H387.399C390.214 41.961 392.474 43.418 394.146 45.295C395.846 47.0923 397.064 49.0974 397.785 51.3077L397.924 51.7179C398.597 53.7677 398.935 55.8467 398.935 57.9523C398.935 60.1981 398.55 62.4136 397.785 64.5958C397.109 66.6659 395.999 68.5781 394.466 70.3322L394.154 70.6818C392.481 72.5093 390.216 73.9697 387.395 75.0812L387.388 75.0841C384.589 76.152 381.143 76.672 377.073 76.672H363.345V93.3468C363.345 94.5647 362.897 95.6139 362.018 96.4503C361.2 97.2792 360.179 97.6974 359 97.6974C357.813 97.6973 356.786 97.2729 355.965 96.4318C355.145 95.5914 354.736 94.548 354.736 93.3468V43.6642C354.736 42.463 355.145 41.4196 355.965 40.5792C356.779 39.6913 357.804 39.2317 359 39.2316H377.073ZM363.345 68.295H376.36C380.271 68.295 383.177 67.7578 385.147 66.7521C387.166 65.6658 388.491 64.3569 389.203 62.8497C389.956 61.2025 390.325 59.5717 390.326 57.9523C390.326 56.2735 389.954 54.643 389.203 53.0548C388.493 51.5512 387.172 50.2695 385.157 49.2374L385.145 49.2306C383.179 48.1733 380.274 47.6095 376.36 47.6095H363.345V68.295Z" fill="#4154C0" stroke="#4154C0" stroke-width="1.39574"/>
|
||||
<rect x="7.14288" y="39.2857" width="75" height="53.5714" fill="#E69D11"/>
|
||||
<path d="M3.57144 15.6832C3.57144 14.9114 4.19713 14.2857 4.96896 14.2857H84.3168C85.0886 14.2857 85.7143 14.9114 85.7143 15.6832V32.1428H3.57144V15.6832Z" fill="#4154C0"/>
|
||||
<rect x="2.09627" y="12.8106" width="85.0932" height="85.0932" rx="2.09627" stroke="black" stroke-width="4.19255"/>
|
||||
<line y1="35.0155" x2="89.2857" y2="35.0155" stroke="black" stroke-width="1.39752"/>
|
||||
<rect x="19.2546" y="1.39752" width="9.53118" height="20.1191" rx="2.79503" fill="#E69D11" stroke="black" stroke-width="2.79503"/>
|
||||
<rect x="58.5403" y="1.39752" width="9.53118" height="20.1191" rx="2.79503" fill="#E69D11" stroke="black" stroke-width="2.79503"/>
|
||||
<path d="M84.6845 32.1428C85.9796 32.1428 87.0703 32.5641 87.9565 33.4066C88.8426 34.2492 89.2857 35.2862 89.2857 36.5176V95.528C89.2857 96.7594 88.8426 97.7964 87.9565 98.6389C87.0703 99.5463 85.9796 100 84.6845 100H43.6824C42.4554 100 41.4329 99.5787 40.6149 98.7362C39.7288 97.9584 39.2857 96.9863 39.2857 95.8197C39.2857 94.6531 39.7288 93.6809 40.6149 92.9032C41.4329 92.1254 42.4554 91.7366 43.6824 91.7366H79.981V70.2517H58.7396C57.5126 70.2517 56.4901 69.8304 55.6721 68.9879C54.786 68.2102 54.3429 67.238 54.3429 66.0714C54.3429 64.9048 54.786 63.9326 55.6721 63.1549C56.4901 62.3772 57.5126 61.9883 58.7396 61.9883H79.981V40.4062L43.6824 40.4062C42.4554 40.4062 41.4329 39.985 40.6149 39.1424C39.7288 38.3647 39.2857 37.3925 39.2857 36.2259C39.2857 35.0593 39.7288 34.0872 40.6149 33.3094C41.4329 32.5317 42.4554 32.1428 43.6824 32.1428H84.6845Z" fill="black"/>
|
||||
<path d="M4.60123 100C3.30607 100 2.21541 99.5788 1.32924 98.7362C0.443082 97.8937 0 96.8567 0 95.6253V36.6148C0 35.3834 0.443082 34.3465 1.32924 33.5039C2.21541 32.5966 3.30607 32.1429 4.60123 32.1429H45.6033C46.8303 32.1429 47.8528 32.5642 48.6708 33.4067C49.5569 34.1844 50 35.1566 50 36.3232C50 37.4898 49.5569 38.462 48.6708 39.2397C47.8528 40.0174 46.8303 40.4063 45.6033 40.4063H9.3047V61.8911H30.5461C31.7731 61.8911 32.7956 62.3124 33.6136 63.155C34.4997 63.9327 34.9428 64.9049 34.9428 66.0715C34.9428 67.2381 34.4997 68.2102 33.6136 68.988C32.7956 69.7657 31.7731 70.1546 30.5461 70.1546H9.3047V91.7366H45.6033C46.8303 91.7366 47.8528 92.1579 48.6708 93.0004C49.5569 93.7782 50 94.7503 50 95.9169C50 97.0835 49.5569 98.0557 48.6708 98.8334C47.8528 99.6112 46.8303 100 45.6033 100H4.60123Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 8.3 KiB |
10
src/assets/logo/new/logo_colored_submark_dark.svg
Normal file
10
src/assets/logo/new/logo_colored_submark_dark.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="90" height="100" viewBox="0 0 90 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="7.14285" y="39.2857" width="75" height="53.5714" fill="#C1830D"/>
|
||||
<path d="M3.57141 15.6832C3.57141 14.9114 4.1971 14.2857 4.96893 14.2857H84.3168C85.0886 14.2857 85.7143 14.9114 85.7143 15.6832V32.1429H3.57141V15.6832Z" fill="#5770FF"/>
|
||||
<rect x="2.09627" y="12.8106" width="85.0932" height="85.0932" rx="2.09627" stroke="white" stroke-width="4.19255"/>
|
||||
<line y1="35.0156" x2="89.2857" y2="35.0156" stroke="white" stroke-width="1.39752"/>
|
||||
<rect x="19.2546" y="1.39752" width="9.53118" height="20.1191" rx="2.79503" fill="#C1830D" stroke="white" stroke-width="2.79503"/>
|
||||
<rect x="58.5403" y="1.39752" width="9.53118" height="20.1191" rx="2.79503" fill="#C1830D" stroke="white" stroke-width="2.79503"/>
|
||||
<path d="M84.6844 32.1429C85.9796 32.1429 87.0702 32.5642 87.9564 33.4067C88.8426 34.2492 89.2856 35.2862 89.2856 36.5176V95.5281C89.2856 96.7595 88.8426 97.7965 87.9564 98.639C87.0702 99.5464 85.9796 100 84.6844 100H43.6824C42.4554 100 41.4329 99.5788 40.6149 98.7362C39.7287 97.9585 39.2856 96.9863 39.2856 95.8197C39.2856 94.6531 39.7287 93.681 40.6149 92.9032C41.4329 92.1255 42.4554 91.7366 43.6824 91.7366H79.9809V70.2518H58.7396C57.5126 70.2518 56.4901 69.8305 55.6721 68.988C54.7859 68.2102 54.3428 67.2381 54.3428 66.0715C54.3428 64.9049 54.7859 63.9327 55.6721 63.155C56.4901 62.3772 57.5126 61.9884 58.7396 61.9884H79.9809V40.4063L43.6824 40.4063C42.4554 40.4063 41.4329 39.985 40.6149 39.1425C39.7287 38.3647 39.2856 37.3926 39.2856 36.226C39.2856 35.0594 39.7287 34.0872 40.6149 33.3095C41.4329 32.5318 42.4554 32.1429 43.6824 32.1429H84.6844Z" fill="white"/>
|
||||
<path d="M4.60123 100C3.30607 100 2.21541 99.5787 1.32924 98.7362C0.443082 97.8936 0 96.8567 0 95.6252V36.6148C0 35.3834 0.443082 34.3464 1.32924 33.5039C2.21541 32.5965 3.30607 32.1429 4.60123 32.1429H45.6033C46.8303 32.1429 47.8528 32.5641 48.6708 33.4067C49.5569 34.1844 50 35.1566 50 36.3232C50 37.4898 49.5569 38.4619 48.6708 39.2397C47.8528 40.0174 46.8303 40.4063 45.6033 40.4063H9.3047V61.8911H30.5461C31.7731 61.8911 32.7956 62.3124 33.6136 63.1549C34.4997 63.9327 34.9428 64.9048 34.9428 66.0714C34.9428 67.238 34.4997 68.2102 33.6136 68.9879C32.7956 69.7657 31.7731 70.1545 30.5461 70.1545H9.3047V91.7366H45.6033C46.8303 91.7366 47.8528 92.1579 48.6708 93.0004C49.5569 93.7781 50 94.7503 50 95.9169C50 97.0835 49.5569 98.0557 48.6708 98.8334C47.8528 99.6111 46.8303 100 45.6033 100H4.60123Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
10
src/assets/logo/new/logo_colored_submark_light.svg
Normal file
10
src/assets/logo/new/logo_colored_submark_light.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="90" height="100" viewBox="0 0 90 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="7.14285" y="39.2857" width="75" height="53.5714" fill="#E69D11"/>
|
||||
<path d="M3.57141 15.6832C3.57141 14.9114 4.1971 14.2857 4.96893 14.2857H84.3168C85.0886 14.2857 85.7143 14.9114 85.7143 15.6832V32.1429H3.57141V15.6832Z" fill="#4154C0"/>
|
||||
<rect x="2.09627" y="12.8106" width="85.0932" height="85.0932" rx="2.09627" stroke="black" stroke-width="4.19255"/>
|
||||
<line y1="35.0156" x2="89.2857" y2="35.0156" stroke="black" stroke-width="1.39752"/>
|
||||
<rect x="19.2546" y="1.39752" width="9.53118" height="20.1191" rx="2.79503" fill="#E69D11" stroke="black" stroke-width="2.79503"/>
|
||||
<rect x="58.5403" y="1.39752" width="9.53118" height="20.1191" rx="2.79503" fill="#E69D11" stroke="black" stroke-width="2.79503"/>
|
||||
<path d="M84.6844 32.1429C85.9796 32.1429 87.0702 32.5642 87.9564 33.4067C88.8426 34.2492 89.2856 35.2862 89.2856 36.5176V95.5281C89.2856 96.7595 88.8426 97.7965 87.9564 98.639C87.0702 99.5464 85.9796 100 84.6844 100H43.6824C42.4554 100 41.4329 99.5788 40.6149 98.7362C39.7287 97.9585 39.2856 96.9863 39.2856 95.8197C39.2856 94.6531 39.7287 93.681 40.6149 92.9032C41.4329 92.1255 42.4554 91.7366 43.6824 91.7366H79.9809V70.2518H58.7396C57.5126 70.2518 56.4901 69.8305 55.6721 68.988C54.7859 68.2102 54.3428 67.2381 54.3428 66.0715C54.3428 64.9049 54.7859 63.9327 55.6721 63.155C56.4901 62.3772 57.5126 61.9884 58.7396 61.9884H79.9809V40.4063L43.6824 40.4063C42.4554 40.4063 41.4329 39.985 40.6149 39.1425C39.7287 38.3647 39.2856 37.3926 39.2856 36.226C39.2856 35.0594 39.7287 34.0872 40.6149 33.3095C41.4329 32.5318 42.4554 32.1429 43.6824 32.1429H84.6844Z" fill="black"/>
|
||||
<path d="M4.60123 100C3.30607 100 2.21541 99.5787 1.32924 98.7362C0.443082 97.8936 0 96.8567 0 95.6252V36.6148C0 35.3834 0.443082 34.3464 1.32924 33.5039C2.21541 32.5965 3.30607 32.1429 4.60123 32.1429H45.6033C46.8303 32.1429 47.8528 32.5641 48.6708 33.4067C49.5569 34.1844 50 35.1566 50 36.3232C50 37.4898 49.5569 38.4619 48.6708 39.2397C47.8528 40.0174 46.8303 40.4063 45.6033 40.4063H9.3047V61.8911H30.5461C31.7731 61.8911 32.7956 62.3124 33.6136 63.1549C34.4997 63.9327 34.9428 64.9048 34.9428 66.0714C34.9428 67.238 34.4997 68.2102 33.6136 68.9879C32.7956 69.7657 31.7731 70.1545 30.5461 70.1545H9.3047V91.7366H45.6033C46.8303 91.7366 47.8528 92.1579 48.6708 93.0004C49.5569 93.7781 50 94.7503 50 95.9169C50 97.0835 49.5569 98.0557 48.6708 98.8334C47.8528 99.6111 46.8303 100 45.6033 100H4.60123Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
40
src/components/buttons/notification-button.tsx
Normal file
40
src/components/buttons/notification-button.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
// DropdownMenuGroup,
|
||||
// DropdownMenuItem,
|
||||
// DropdownMenuLabel,
|
||||
// DropdownMenuPortal,
|
||||
// DropdownMenuSeparator,
|
||||
// DropdownMenuShortcut,
|
||||
// DropdownMenuSub,
|
||||
// DropdownMenuSubContent,
|
||||
// DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { NDot, NotificationDot } from '@/components/misc/notification-dot';
|
||||
|
||||
export function NotificationButton({
|
||||
dotVariant,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
dotVariant?: NDot;
|
||||
children: React.ReactNode;
|
||||
} & React.ComponentProps<typeof Button>) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type='button' variant='outline_primary' {...props}>
|
||||
{children}
|
||||
<NotificationDot
|
||||
dotVariant={dotVariant}
|
||||
className='absolute ml-[30px] mt-[30px]'
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'></DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
145
src/components/custom-ui/app-sidebar.tsx
Normal file
145
src/components/custom-ui/app-sidebar.tsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
// SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
// SidebarInput,
|
||||
// SidebarInset,
|
||||
SidebarMenu,
|
||||
// SidebarMenuAction,
|
||||
// SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
// SidebarMenuSkeleton,
|
||||
// SidebarMenuSub,
|
||||
// SidebarMenuSubButton,
|
||||
// SidebarMenuSubItem,
|
||||
// SidebarProvider,
|
||||
// SidebarRail,
|
||||
// SidebarSeparator,
|
||||
// SidebarTrigger,
|
||||
// useSidebar,
|
||||
} from '@/components/custom-ui/sidebar';
|
||||
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
|
||||
import Logo from '@/components/misc/logo';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
Star,
|
||||
CalendarDays,
|
||||
User,
|
||||
Users,
|
||||
CalendarClock,
|
||||
CalendarPlus,
|
||||
} from 'lucide-react';
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: 'Calendar',
|
||||
url: '#',
|
||||
icon: CalendarDays,
|
||||
},
|
||||
{
|
||||
title: 'Friends',
|
||||
url: '#',
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
title: 'Groups',
|
||||
url: '#',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: 'Events',
|
||||
url: '/events',
|
||||
icon: CalendarClock,
|
||||
},
|
||||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
return (
|
||||
<>
|
||||
<Sidebar collapsible='icon' variant='sidebar'>
|
||||
<SidebarHeader className='overflow-hidden'>
|
||||
<Logo
|
||||
colorType='colored'
|
||||
logoType='combo'
|
||||
height={50}
|
||||
className='group-data-[collapsible=icon]:hidden min-w-[203px]'
|
||||
></Logo>
|
||||
<Logo
|
||||
colorType='colored'
|
||||
logoType='submark'
|
||||
height={50}
|
||||
className='group-data-[collapsible=]:hidden group-data-[mobile=true]/mobile:hidden'
|
||||
></Logo>
|
||||
</SidebarHeader>
|
||||
<SidebarContent className='grid grid-rows-[auto_1fr_auto]'>
|
||||
<Collapsible defaultOpen className='group/collapsible'>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel asChild>
|
||||
<CollapsibleTrigger>
|
||||
<span className='flex items-center gap-2 text-xl font-label text-neutral-100'>
|
||||
<Star className='size-8' />{' '}
|
||||
<span className='group-data-[collapsible=icon]:hidden'>
|
||||
Favorites
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown className='ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180 group-data-[collapsible=icon]:hidden text-nowrap whitespace-nowrap' />
|
||||
</CollapsibleTrigger>
|
||||
</SidebarGroupLabel>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent />
|
||||
</CollapsibleContent>
|
||||
</SidebarGroup>
|
||||
</Collapsible>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title} className='pt-2'>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href={item.url}>
|
||||
<item.icon className='size-8' />
|
||||
<span className='text-xl font-label group-data-[collapsible=icon]:hidden text-nowrap whitespace-nowrap'>
|
||||
{item.title}
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<SidebarMenuItem className='pl-[8px]'>
|
||||
<Link
|
||||
href='/events/new'
|
||||
className='flex items-center gap-2 text-xl font-label'
|
||||
>
|
||||
<CalendarPlus className='size-8' />
|
||||
<span className='group-data-[collapsible=icon]:hidden text-nowrap whitespace-nowrap'>
|
||||
New Event
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
</SidebarFooter>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import React from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
export default function LabeledInput({
|
||||
type,
|
||||
|
@ -11,7 +14,7 @@ export default function LabeledInput({
|
|||
error,
|
||||
...rest
|
||||
}: {
|
||||
type: 'text' | 'email' | 'password';
|
||||
type: 'text' | 'email' | 'password' | 'file';
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
|
@ -19,19 +22,33 @@ export default function LabeledInput({
|
|||
autocomplete?: string;
|
||||
error?: string;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
const [passwordVisible, setPasswordVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-1 gap-1'>
|
||||
<Label htmlFor={name}>{label}</Label>
|
||||
|
||||
<Input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
id={name}
|
||||
name={name}
|
||||
autoComplete={autocomplete}
|
||||
{...rest}
|
||||
/>
|
||||
<span className='relative'>
|
||||
<Input
|
||||
className={type === 'password' ? 'pr-[50px]' : ''}
|
||||
type={passwordVisible ? 'text' : type}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
id={name}
|
||||
name={name}
|
||||
autoComplete={autocomplete}
|
||||
{...rest}
|
||||
/>
|
||||
{type === 'password' && (
|
||||
<Button
|
||||
className='absolute right-0 top-0 w-[36px] h-[36px]'
|
||||
type='button'
|
||||
variant={'outline_muted'}
|
||||
onClick={() => setPasswordVisible((visible) => !visible)}
|
||||
>
|
||||
{passwordVisible ? <Eye /> : <EyeOff />}
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
{error && <p className='text-red-500 text-sm mt-1'>{error}</p>}
|
||||
</div>
|
||||
);
|
||||
|
|
725
src/components/custom-ui/sidebar.tsx
Normal file
725
src/components/custom-ui/sidebar.tsx
Normal file
|
@ -0,0 +1,725 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, VariantProps } from 'class-variance-authority';
|
||||
import { PanelLeftIcon } from 'lucide-react';
|
||||
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = '16rem';
|
||||
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
const SIDEBAR_WIDTH_ICON = '4rem';
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: 'expanded' | 'collapsed';
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error('useSidebar must be used within a SidebarProvider.');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === 'function' ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? 'expanded' : 'collapsed';
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
data-slot='sidebar-wrapper'
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': SIDEBAR_WIDTH,
|
||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = 'left',
|
||||
variant = 'sidebar',
|
||||
collapsible = 'offcanvas',
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
side?: 'left' | 'right';
|
||||
variant?: 'sidebar' | 'floating' | 'inset';
|
||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === 'none') {
|
||||
return (
|
||||
<div
|
||||
data-slot='sidebar'
|
||||
className={cn(
|
||||
'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar='sidebar'
|
||||
data-slot='sidebar'
|
||||
data-mobile='true'
|
||||
className='bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden group/mobile'
|
||||
style={
|
||||
{
|
||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className='sr-only'>
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className='flex h-full w-full flex-col'>{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='group peer text-sidebar-foreground hidden md:block'
|
||||
data-state={state}
|
||||
data-collapsible={state === 'collapsed' ? collapsible : ''}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot='sidebar'
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot='sidebar-gap'
|
||||
className={cn(
|
||||
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot='sidebar-container'
|
||||
className={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar='sidebar'
|
||||
data-slot='sidebar-inner'
|
||||
className='bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm'
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar='trigger'
|
||||
data-slot='sidebar-trigger'
|
||||
variant='muted'
|
||||
size='icon'
|
||||
className={cn('', className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className='sr-only'>Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar='rail'
|
||||
data-slot='sidebar-rail'
|
||||
aria-label='Toggle Sidebar'
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title='Toggle Sidebar'
|
||||
className={cn(
|
||||
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
||||
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
||||
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
||||
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
|
||||
return (
|
||||
<main
|
||||
data-slot='sidebar-inset'
|
||||
className={cn(
|
||||
'bg-background relative flex w-full flex-1 flex-col',
|
||||
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot='sidebar-input'
|
||||
data-sidebar='input'
|
||||
className={cn('bg-background h-8 w-full shadow-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='sidebar-header'
|
||||
data-sidebar='header'
|
||||
className={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='sidebar-footer'
|
||||
data-sidebar='footer'
|
||||
className={cn('flex flex-col gap-2 p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot='sidebar-separator'
|
||||
data-sidebar='separator'
|
||||
className={cn('bg-sidebar-border mx-2 w-auto', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='sidebar-content'
|
||||
data-sidebar='content'
|
||||
className={cn(
|
||||
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='sidebar-group'
|
||||
data-sidebar='group'
|
||||
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'div';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot='sidebar-group-label'
|
||||
data-sidebar='group-label'
|
||||
className={cn(
|
||||
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 ml-[7.5px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot='sidebar-group-action'
|
||||
data-sidebar='group-action'
|
||||
className={cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
'after:absolute after:-inset-2 md:after:hidden',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='sidebar-group-content'
|
||||
data-sidebar='group-content'
|
||||
className={cn('w-full text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot='sidebar-menu'
|
||||
data-sidebar='menu'
|
||||
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot='sidebar-menu-item'
|
||||
data-sidebar='menu-item'
|
||||
className={cn('group/menu-item relative list-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! p-0 ml-[15.5px] [&>span:last-child]:truncate [&>svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
outline:
|
||||
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 text-sm',
|
||||
sm: 'h-7 text-xs',
|
||||
lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
data-slot='sidebar-menu-button'
|
||||
data-sidebar='menu-button'
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === 'string') {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side='right'
|
||||
align='center'
|
||||
hidden={state !== 'collapsed' || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot='sidebar-menu-action'
|
||||
data-sidebar='menu-action'
|
||||
className={cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
// Increases the hit area of the button on mobile.
|
||||
'after:absolute after:-inset-2 md:after:hidden',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
showOnHover &&
|
||||
'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='sidebar-menu-badge'
|
||||
data-sidebar='menu-badge'
|
||||
className={cn(
|
||||
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none',
|
||||
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
||||
'peer-data-[size=sm]/menu-button:top-1',
|
||||
'peer-data-[size=default]/menu-button:top-1.5',
|
||||
'peer-data-[size=lg]/menu-button:top-2.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & {
|
||||
showIcon?: boolean;
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot='sidebar-menu-skeleton'
|
||||
data-sidebar='menu-skeleton'
|
||||
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className='size-4 rounded-md'
|
||||
data-sidebar='menu-skeleton-icon'
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className='h-4 max-w-(--skeleton-width) flex-1'
|
||||
data-sidebar='menu-skeleton-text'
|
||||
style={
|
||||
{
|
||||
'--skeleton-width': width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot='sidebar-menu-sub'
|
||||
data-sidebar='menu-sub'
|
||||
className={cn(
|
||||
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot='sidebar-menu-sub-item'
|
||||
data-sidebar='menu-sub-item'
|
||||
className={cn('group/menu-sub-item relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
asChild = false,
|
||||
size = 'md',
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'a'> & {
|
||||
asChild?: boolean;
|
||||
size?: 'sm' | 'md';
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'a';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot='sidebar-menu-sub-button'
|
||||
data-sidebar='menu-sub-button'
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
||||
size === 'sm' && 'text-xs',
|
||||
size === 'md' && 'text-sm',
|
||||
'group-data-[collapsible=icon]:hidden',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
51
src/components/misc/header.tsx
Normal file
51
src/components/misc/header.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { SidebarTrigger } from '@/components/custom-ui/sidebar';
|
||||
import { ThemePicker } from '@/components/misc/theme-picker';
|
||||
import { NotificationButton } from '@/components/buttons/notification-button';
|
||||
|
||||
import { BellRing, Inbox } from 'lucide-react';
|
||||
import UserDropdown from '@/components/misc/user-dropdown';
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: 'Calendar',
|
||||
url: '#',
|
||||
icon: Inbox,
|
||||
},
|
||||
{
|
||||
title: 'Friends',
|
||||
url: '#',
|
||||
icon: BellRing,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Header({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div className='w-full grid grid-rows-[50px_1fr] h-screen'>
|
||||
<header className='border-b-1 grid-cols-[1fr_3fr_1fr] grid items-center px-2 shadow-md'>
|
||||
<span className='flex justify-start'>
|
||||
<SidebarTrigger variant='outline_primary' size='icon' />
|
||||
</span>
|
||||
<span className='flex justify-center'>Search</span>
|
||||
<span className='flex gap-1 justify-end'>
|
||||
<ThemePicker />
|
||||
{items.map((item) => (
|
||||
<NotificationButton
|
||||
key={item.title}
|
||||
variant='outline_primary'
|
||||
dotVariant='hidden'
|
||||
size='icon'
|
||||
>
|
||||
<item.icon />
|
||||
</NotificationButton>
|
||||
))}
|
||||
<UserDropdown />
|
||||
</span>
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -90,6 +90,7 @@ export default function Logo({
|
|||
|
||||
return (
|
||||
<Image
|
||||
unoptimized
|
||||
src={logoVar}
|
||||
alt={alt || defaultAltText}
|
||||
className={className}
|
||||
|
|
110
src/components/misc/nav-user.tsx
Normal file
110
src/components/misc/nav-user.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/custom-ui/sidebar';
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
};
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size='lg'
|
||||
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
|
||||
>
|
||||
<Avatar className='h-8 w-8 rounded-lg'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className='rounded-lg'>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grid flex-1 text-left text-sm leading-tight'>
|
||||
<span className='truncate font-medium'>{user.name}</span>
|
||||
<span className='truncate text-xs'>{user.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className='ml-auto size-4' />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align='end'
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className='p-0 font-normal'>
|
||||
<div className='flex items-center gap-2 px-1 py-1.5 text-left text-sm'>
|
||||
<Avatar className='h-8 w-8 rounded-lg'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className='rounded-lg'>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grid flex-1 text-left text-sm leading-tight'>
|
||||
<span className='truncate font-medium'>{user.name}</span>
|
||||
<span className='truncate text-xs'>{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
35
src/components/misc/notification-dot.tsx
Normal file
35
src/components/misc/notification-dot.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { CircleSmall } from 'lucide-react';
|
||||
|
||||
const dotVariants = cva('', {
|
||||
variants: {
|
||||
variant: {
|
||||
neutral: 'fill-neutral-900',
|
||||
active: 'fill-red-600 stroke-red-600',
|
||||
hidden: 'hidden',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
function NotificationDot({
|
||||
className,
|
||||
dotVariant,
|
||||
...props
|
||||
}: {
|
||||
className: string;
|
||||
dotVariant: VariantProps<typeof dotVariants>['variant'];
|
||||
}) {
|
||||
return (
|
||||
<CircleSmall
|
||||
className={cn(dotVariants({ variant: dotVariant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type NDot = VariantProps<typeof dotVariants>['variant'];
|
||||
export { NotificationDot, dotVariants };
|
35
src/components/misc/profile-picture-upload.tsx
Normal file
35
src/components/misc/profile-picture-upload.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import Image from 'next/image';
|
||||
import { Avatar } from '../ui/avatar';
|
||||
import { useGetApiUserMe } from '@/generated/api/user/user';
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
import { Input } from '../ui/input';
|
||||
|
||||
export default function ProfilePictureUpload() {
|
||||
const { data } = useGetApiUserMe();
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-cols-1 gap-1'>
|
||||
<span className='relative flex space-6'>
|
||||
<Input
|
||||
id='pic-upload'
|
||||
type='file'
|
||||
defaultValue={data?.data.user.image ?? undefined}
|
||||
/>
|
||||
<Avatar className='flex justify-center items-center ml-6 shadow-md border h-[36px] w-[36px]'>
|
||||
{data?.data.user.image ? (
|
||||
<Image
|
||||
src={data?.data.user.image}
|
||||
alt='Avatar'
|
||||
width='20'
|
||||
height='20'
|
||||
/>
|
||||
) : (
|
||||
<User />
|
||||
)}
|
||||
</Avatar>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
158
src/components/misc/settings-dropdown.tsx
Normal file
158
src/components/misc/settings-dropdown.tsx
Normal file
|
@ -0,0 +1,158 @@
|
|||
'use client';
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
User,
|
||||
Bell,
|
||||
Calendar,
|
||||
Shield,
|
||||
Palette,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SettingsSection {
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
}
|
||||
|
||||
interface SettingsDropdownProps {
|
||||
currentSection: string;
|
||||
onSectionChange: (section: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const settingsSections: SettingsSection[] = [
|
||||
{
|
||||
label: 'Account',
|
||||
value: 'general',
|
||||
description: 'Manage account details',
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
label: 'Notifications',
|
||||
value: 'notifications',
|
||||
description: 'Choose notification Preferences',
|
||||
icon: Bell,
|
||||
},
|
||||
{
|
||||
label: 'Calendar',
|
||||
value: 'calendarAvailability',
|
||||
description: 'Manage calendar display, availability and iCal integration',
|
||||
icon: Calendar,
|
||||
},
|
||||
{
|
||||
label: 'Privacy',
|
||||
value: 'sharingPrivacy',
|
||||
description: 'Control who can see your calendar and book time with you',
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
label: 'Appearance',
|
||||
value: 'appearance',
|
||||
description: 'Customize the look and feel of the application',
|
||||
icon: Palette,
|
||||
},
|
||||
];
|
||||
|
||||
export function SettingsDropdown({
|
||||
currentSection,
|
||||
onSectionChange,
|
||||
className,
|
||||
}: SettingsDropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const currentSectionData = settingsSections.find(
|
||||
(section) => section.value === currentSection,
|
||||
);
|
||||
const CurrentIcon = currentSectionData?.icon || User;
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
onSectionChange(value);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('w-full max-w-md', className)}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline_muted'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between bg-popover text-text h-auto py-3'
|
||||
>
|
||||
<div className='flex items-center gap-3'>
|
||||
<CurrentIcon className='h-4 w-4 text-muted-foreground' />
|
||||
<div className='flex flex-col items-start text-left'>
|
||||
<span className='font-medium'>{currentSectionData?.label}</span>
|
||||
<p className='text-xs text-muted-foreground text-wrap'>
|
||||
{currentSectionData?.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-full p-0' align='start'>
|
||||
<Command>
|
||||
<CommandInput placeholder='Search settings...' />
|
||||
<CommandList>
|
||||
<CommandEmpty>No settings found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{settingsSections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={section.value}
|
||||
value={section.value}
|
||||
onSelect={() => handleSelect(section.value)}
|
||||
className='flex items-center justify-between p-3'
|
||||
>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Icon className='h-4 w-4 text-muted-foreground' />
|
||||
<div className='flex flex-col'>
|
||||
<span className='font-medium'>{section.label}</span>
|
||||
<p className='text-xs text-muted-foreground text-wrap'>
|
||||
{section.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-2 h-4 w-4',
|
||||
currentSection === section.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
514
src/components/misc/settings-page.tsx
Normal file
514
src/components/misc/settings-page.tsx
Normal file
|
@ -0,0 +1,514 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { SettingsDropdown } from '@/components/misc/settings-dropdown';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useGetApiUserMe } from '@/generated/api/user/user';
|
||||
import { ThemePicker } from './theme-picker';
|
||||
import LabeledInput from '../custom-ui/labeled-input';
|
||||
import { GroupWrapper } from '../wrappers/group-wrapper';
|
||||
import { Avatar } from '../ui/avatar';
|
||||
import Image from 'next/image';
|
||||
import { User } from 'lucide-react';
|
||||
import ProfilePictureUpload from './profile-picture-upload';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter();
|
||||
const [currentSection, setCurrentSection] = useState('general');
|
||||
const { data } = useGetApiUserMe();
|
||||
|
||||
const renderSettingsContent = () => {
|
||||
switch (currentSection) {
|
||||
case 'general':
|
||||
return (
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6 mt-2'>
|
||||
<GroupWrapper title='General Settings'>
|
||||
<div className='space-y-4'>
|
||||
<GroupWrapper>
|
||||
<div className='flex items-center justify-evenly sm:flex-row flex-col gap-6'>
|
||||
<div>
|
||||
<LabeledInput
|
||||
label='First Name'
|
||||
type='text'
|
||||
placeholder='First Name'
|
||||
defaultValue={data?.data.user.first_name ?? ''}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledInput
|
||||
label='Last Name'
|
||||
type='text'
|
||||
placeholder='Last Name'
|
||||
defaultValue={data?.data.user.last_name ?? ''}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
</div>
|
||||
</GroupWrapper>
|
||||
<div className='space-y-2'>
|
||||
<LabeledInput
|
||||
label='Display Name'
|
||||
type='text'
|
||||
placeholder='Display Name'
|
||||
defaultValue={data?.data.user.name}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
<div className='space-y-2 space-b-2'>
|
||||
<LabeledInput
|
||||
type='email'
|
||||
label='Email Address'
|
||||
placeholder='Your E-Mail'
|
||||
defaultValue={data?.data.user.email ?? ''}
|
||||
></LabeledInput>
|
||||
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
Email might be managed by your SSO provider.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</GroupWrapper>
|
||||
<GroupWrapper title='Reset Password'>
|
||||
<div className='flex items-center justify-evenly sm:flex-row flex-col gap-6'>
|
||||
<div>
|
||||
<LabeledInput
|
||||
type='password'
|
||||
label='Current Password'
|
||||
placeholder='Current Password'
|
||||
defaultValue={data?.data.user.first_name ?? ''}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledInput
|
||||
type='password'
|
||||
label='New Password'
|
||||
placeholder='New Password'
|
||||
defaultValue={data?.data.user.first_name ?? ''}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledInput
|
||||
type='password'
|
||||
label='Repeat Password'
|
||||
placeholder='Repeat Password'
|
||||
defaultValue={data?.data.user.first_name ?? ''}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
</div>
|
||||
</GroupWrapper>
|
||||
<GroupWrapper title='Profile Picture'>
|
||||
<div className='space-y-2 grid grid-cols-[1fr_auto]'>
|
||||
<ProfilePictureUpload />
|
||||
</div>
|
||||
</GroupWrapper>
|
||||
<GroupWrapper title='Regional Settings'>
|
||||
<div className='space-y-2 grid sm:grid-cols-[1fr_auto] sm:flex-row gap-4'>
|
||||
<div className='grid gap-1'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Timezone'
|
||||
placeholder='Europe/Berlin'
|
||||
defaultValue={data?.data.user.timezone}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
<div>
|
||||
<div className='grid gap-1'>
|
||||
<Label htmlFor='language'>Language</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='language'>
|
||||
<SelectValue placeholder='Select language' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='en'>English</SelectItem>
|
||||
<SelectItem value='de'>German</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GroupWrapper>
|
||||
<div className='flex items-center justify-evenly sm:flex-row flex-col gap-6'>
|
||||
<Button variant='secondary'>Delete Account</Button>
|
||||
<span className='text-sm text-muted-foreground pt-1'>
|
||||
Permanently delete your account and all associated data.
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'notifications':
|
||||
return (
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Notification Preferences</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='flex items-center justify-between space-x-2 p-3 rounded-md border'>
|
||||
<Label
|
||||
htmlFor='masterEmailNotifications'
|
||||
className='font-normal'
|
||||
>
|
||||
Enable All Email Notifications
|
||||
</Label>
|
||||
<Switch id='masterEmailNotifications' />
|
||||
</div>
|
||||
<div className='space-y-4 pl-2 border-l-2 ml-2'>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label htmlFor='newMeetingBookings' className='font-normal'>
|
||||
New Meeting Bookings
|
||||
</Label>
|
||||
<Switch id='newMeetingBookings' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label
|
||||
htmlFor='meetingConfirmations'
|
||||
className='font-normal'
|
||||
>
|
||||
Meeting Confirmations/Cancellations
|
||||
</Label>
|
||||
<Switch id='meetingConfirmations' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label
|
||||
htmlFor='enableMeetingReminders'
|
||||
className='font-normal'
|
||||
>
|
||||
Meeting Reminders
|
||||
</Label>
|
||||
<Switch id='enableMeetingReminders' />
|
||||
</div>
|
||||
<div className='space-y-2 pl-6'>
|
||||
<Label htmlFor='remindBefore'>Remind me before</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='remindBefore'>
|
||||
<SelectValue placeholder='Select reminder time' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='15m'>15 minutes</SelectItem>
|
||||
<SelectItem value='30m'>30 minutes</SelectItem>
|
||||
<SelectItem value='1h'>1 hour</SelectItem>
|
||||
<SelectItem value='1d'>1 day</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label htmlFor='friendRequests' className='font-normal'>
|
||||
Friend Requests
|
||||
</Label>
|
||||
<Switch id='friendRequests' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label htmlFor='groupUpdates' className='font-normal'>
|
||||
Group Invitations/Updates
|
||||
</Label>
|
||||
<Switch id='groupUpdates' />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'calendarAvailability':
|
||||
return (
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Calendar & Availability</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
||||
<legend className='text-sm font-medium px-1'>Display</legend>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='defaultCalendarView'>
|
||||
Default Calendar View
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='defaultCalendarView'>
|
||||
<SelectValue placeholder='Select view' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='day'>Day</SelectItem>
|
||||
<SelectItem value='week'>Week</SelectItem>
|
||||
<SelectItem value='month'>Month</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='weekStartsOn'>Week Starts On</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='weekStartsOn'>
|
||||
<SelectValue placeholder='Select day' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='sunday'>Sunday</SelectItem>
|
||||
<SelectItem value='monday'>Monday</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='flex items-center justify-between space-x-2'>
|
||||
<Label htmlFor='showWeekends' className='font-normal'>
|
||||
Show Weekends
|
||||
</Label>
|
||||
<Switch id='showWeekends' defaultChecked />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
||||
<legend className='text-sm font-medium px-1'>
|
||||
Availability
|
||||
</legend>
|
||||
<div className='space-y-2'>
|
||||
<Label>Working Hours</Label>
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
Define your typical available hours (e.g., Monday-Friday,
|
||||
9 AM - 5 PM).
|
||||
</span>
|
||||
<Button variant='outline_muted' size='sm'>
|
||||
Set Working Hours
|
||||
</Button>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='minNoticeBooking'>
|
||||
Minimum Notice for Bookings
|
||||
</Label>
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
Min time before a booking can be made.
|
||||
</span>
|
||||
<div className='space-y-2'>
|
||||
<Input
|
||||
id='bookingWindow'
|
||||
type='text'
|
||||
placeholder='e.g., 1h'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='bookingWindow'>
|
||||
Booking Window (days in advance)
|
||||
</Label>
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
Max time in advance a booking can be made.
|
||||
</span>
|
||||
<Input
|
||||
id='bookingWindow'
|
||||
type='number'
|
||||
placeholder='e.g., 30d'
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className='space-y-4 p-4 border rounded-md'>
|
||||
<legend className='text-sm font-medium px-1'>
|
||||
iCalendar Integration
|
||||
</legend>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='icalImport'>Import iCal Feed URL</Label>
|
||||
<Input
|
||||
id='icalImport'
|
||||
type='url'
|
||||
placeholder='https://calendar.example.com/feed.ics'
|
||||
/>
|
||||
<Button size='sm' className='mt-1'>
|
||||
Add Feed
|
||||
</Button>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label>Export Your Calendar</Label>
|
||||
<Button variant='outline_muted' size='sm'>
|
||||
Get iCal Export URL
|
||||
</Button>
|
||||
<Button variant='outline_muted' size='sm' className='ml-2'>
|
||||
Download .ics File
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'sharingPrivacy':
|
||||
return (
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Sharing & Privacy</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='defaultVisibility'>
|
||||
Default Calendar Visibility
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='defaultVisibility'>
|
||||
<SelectValue placeholder='Select visibility' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='private'>
|
||||
Private (Only You)
|
||||
</SelectItem>
|
||||
<SelectItem value='freebusy'>
|
||||
Free/Busy for Friends
|
||||
</SelectItem>
|
||||
<SelectItem value='fulldetails'>
|
||||
Full Details for Friends
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='whoCanSeeFull'>
|
||||
Who Can See Your Full Calendar Details?
|
||||
</Label>
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
(Override for Default Visibility)
|
||||
<br />
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
This setting will override the default visibility for your
|
||||
calendar. You can set specific friends or groups to see
|
||||
your full calendar details.
|
||||
</span>
|
||||
</span>
|
||||
<Select>
|
||||
<SelectTrigger id='whoCanSeeFull'>
|
||||
<SelectValue placeholder='Select audience' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='me'>Only Me</SelectItem>
|
||||
<SelectItem value='friends'>My Friends</SelectItem>
|
||||
<SelectItem value='specific'>
|
||||
Specific Friends/Groups (manage separately)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='whoCanBook'>
|
||||
Who Can Book Time With You?
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='whoCanBook'>
|
||||
<SelectValue placeholder='Select audience' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='none'>No One</SelectItem>
|
||||
<SelectItem value='friends'>My Friends</SelectItem>
|
||||
<SelectItem value='specific'>
|
||||
Specific Friends/Groups (manage separately)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label>Blocked Users</Label>
|
||||
<Button variant='outline_muted'>Manage Blocked Users</Button>
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
Prevent specific users from seeing your calendar or booking
|
||||
time.
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'appearance':
|
||||
return (
|
||||
<Card className='h-full flex flex-col border-0 shadow-none rounded-none'>
|
||||
<ScrollableSettingsWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>Appearance</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='theme'>Theme</Label>
|
||||
<ThemePicker />
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='dateFormat'>Date Format</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='dateFormat'>
|
||||
<SelectValue placeholder='Select date format' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='ddmmyyyy'>DD/MM/YYYY</SelectItem>
|
||||
<SelectItem value='mmddyyyy'>MM/DD/YYYY</SelectItem>
|
||||
<SelectItem value='yyyymmdd'>YYYY-MM-DD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='timeFormat'>Time Format</Label>
|
||||
<Select>
|
||||
<SelectTrigger id='timeFormat'>
|
||||
<SelectValue placeholder='Select time format' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='24h'>24-hour</SelectItem>
|
||||
<SelectItem value='12h'>12-hour</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</ScrollableSettingsWrapper>
|
||||
</Card>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 flex items-center justify-center p-4 bg-background/50 backdrop-blur-sm'>
|
||||
<div className='rounded-lg border bg-card text-card-foreground shadow-xl max-w-[700px] w-full h-auto max-h-[calc(100vh-2rem)] flex flex-col'>
|
||||
<div className='p-6 border-b'>
|
||||
<div className='flex items-center justify-between mb-4'>
|
||||
<h1 className='text-2xl font-semibold'>Settings</h1>
|
||||
</div>
|
||||
<SettingsDropdown
|
||||
currentSection={currentSection}
|
||||
onSectionChange={setCurrentSection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex-grow overflow-auto'>{renderSettingsContent()}</div>
|
||||
<div>
|
||||
<CardFooter className='border-t h-[60px] flex content-center justify-between'>
|
||||
<Button onClick={() => router.back()} variant='secondary'>
|
||||
Exit
|
||||
</Button>
|
||||
<Button variant='primary'>Save Changes</Button>
|
||||
</CardFooter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
29
src/components/misc/user-card.tsx
Normal file
29
src/components/misc/user-card.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { useGetApiUserMe } from '@/generated/api/user/user';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import Image from 'next/image';
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
export default function UserCard() {
|
||||
const { data } = useGetApiUserMe();
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<Avatar className='flex justify-center items-center'>
|
||||
{data?.data.user.image ? (
|
||||
<Image
|
||||
className='border-2 rounded-md'
|
||||
src={data?.data.user.image}
|
||||
alt='Avatar'
|
||||
width='50'
|
||||
height='50'
|
||||
/>
|
||||
) : (
|
||||
<User />
|
||||
)}
|
||||
</Avatar>
|
||||
<div className='flex justify-center'>{data?.data.user.name}</div>
|
||||
<div className='flex justify-center text-text-muted text-[12px]'>
|
||||
{data?.data.user.email}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
62
src/components/misc/user-dropdown.tsx
Normal file
62
src/components/misc/user-dropdown.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
'use client';
|
||||
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
// DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
// DropdownMenuLabel,
|
||||
// DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
// DropdownMenuShortcut,
|
||||
// DropdownMenuSub,
|
||||
// DropdownMenuSubContent,
|
||||
// DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useGetApiUserMe } from '@/generated/api/user/user';
|
||||
import { ChevronDown, User } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import UserCard from '@/components/misc/user-card';
|
||||
|
||||
export default function UserDropdown() {
|
||||
const { data } = useGetApiUserMe();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='outline_primary'>
|
||||
<Avatar className='flex justify-center items-center'>
|
||||
{data?.data.user.image ? (
|
||||
<Image
|
||||
src={data?.data.user.image}
|
||||
alt='Avatar'
|
||||
width='20'
|
||||
height='20'
|
||||
/>
|
||||
) : (
|
||||
<User />
|
||||
)}
|
||||
</Avatar>
|
||||
<ChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem>
|
||||
<UserCard />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<Link href='/settings'>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
<Link href='/logout'>
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
53
src/components/ui/avatar.tsx
Normal file
53
src/components/ui/avatar.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot='avatar'
|
||||
className={cn(
|
||||
'relative flex shrink-0 overflow-hidden rounded-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot='avatar-image'
|
||||
className={cn('aspect-square size-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot='avatar-fallback'
|
||||
className={cn(
|
||||
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"radius-lg inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-button transition-all cursor-pointer 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",
|
||||
"radius-lg inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-button transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-nonef",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
@ -126,7 +126,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|||
return (
|
||||
<div
|
||||
data-slot='card-footer'
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
className={cn('flex items-center px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
33
src/components/ui/collapsible.tsx
Normal file
33
src/components/ui/collapsible.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
'use client';
|
||||
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot='collapsible' {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot='collapsible-trigger'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot='collapsible-content'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
184
src/components/ui/command.tsx
Normal file
184
src/components/ui/command.tsx
Normal file
|
@ -0,0 +1,184 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
48
src/components/ui/popover.tsx
Normal file
48
src/components/ui/popover.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
|
@ -13,10 +13,13 @@ function Separator({
|
|||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot='separator-root'
|
||||
data-slot='separator'
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn('shrink-0', className)}
|
||||
className={cn(
|
||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
139
src/components/ui/sheet.tsx
Normal file
139
src/components/ui/sheet.tsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot='sheet' {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot='sheet-trigger' {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot='sheet-close' {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot='sheet-portal' {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot='sheet-overlay'
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = 'right',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot='sheet-content'
|
||||
className={cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
side === 'right' &&
|
||||
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||
side === 'left' &&
|
||||
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||
side === 'top' &&
|
||||
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||
side === 'bottom' &&
|
||||
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className='ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none'>
|
||||
<XIcon className='size-4' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='sheet-header'
|
||||
className={cn('flex flex-col gap-1.5 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='sheet-footer'
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot='sheet-title'
|
||||
className={cn('text-foreground font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot='sheet-description'
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
13
src/components/ui/skeleton.tsx
Normal file
13
src/components/ui/skeleton.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot='skeleton'
|
||||
className={cn('bg-accent animate-pulse rounded-md', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
61
src/components/ui/tooltip.tsx
Normal file
61
src/components/ui/tooltip.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot='tooltip-provider'
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot='tooltip' {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot='tooltip-trigger' {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot='tooltip-content'
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className='bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]' />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
23
src/components/wrappers/group-wrapper.tsx
Normal file
23
src/components/wrappers/group-wrapper.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import type * as React from 'react';
|
||||
|
||||
interface ScrollableSettingsWrapperProps {
|
||||
className?: string;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function GroupWrapper({
|
||||
className,
|
||||
title,
|
||||
children,
|
||||
}: ScrollableSettingsWrapperProps) {
|
||||
return (
|
||||
<fieldset
|
||||
className={cn('space-t-4 p-4 border rounded-md shadow-md', className)}
|
||||
>
|
||||
<legend className='text-sm font-medium px-1'>{title}</legend>
|
||||
{children}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type * as React from 'react';
|
||||
|
||||
interface ScrollableContentWrapperProps {
|
||||
children: React.ReactNode;
|
||||
interface ScrollableSettingsWrapperProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ScrollableSettingsWrapper: React.FC<
|
||||
ScrollableContentWrapperProps
|
||||
> = ({ children, className = '' }) => {
|
||||
export function ScrollableSettingsWrapper({
|
||||
className,
|
||||
children,
|
||||
}: ScrollableSettingsWrapperProps) {
|
||||
return (
|
||||
<div className={`h-[500px] overflow-y-auto space-y-2 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={cn('overflow-y-auto h-full', className)}>{children}</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
23
src/components/wrappers/sidebar-provider.tsx
Normal file
23
src/components/wrappers/sidebar-provider.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { SidebarProvider } from '../custom-ui/sidebar';
|
||||
|
||||
export default function SidebarProviderWrapper({
|
||||
defaultOpen,
|
||||
children,
|
||||
}: {
|
||||
defaultOpen: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(defaultOpen);
|
||||
return (
|
||||
<SidebarProvider
|
||||
defaultOpen={defaultOpen}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
21
src/hooks/use-mobile.ts
Normal file
21
src/hooks/use-mobile.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react';
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener('change', onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener('change', onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
268
yarn.lock
268
yarn.lock
|
@ -543,15 +543,15 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"@gerrit0/mini-shiki@npm:^3.2.2":
|
||||
version: 3.6.0
|
||||
resolution: "@gerrit0/mini-shiki@npm:3.6.0"
|
||||
version: 3.7.0
|
||||
resolution: "@gerrit0/mini-shiki@npm:3.7.0"
|
||||
dependencies:
|
||||
"@shikijs/engine-oniguruma": "npm:^3.6.0"
|
||||
"@shikijs/langs": "npm:^3.6.0"
|
||||
"@shikijs/themes": "npm:^3.6.0"
|
||||
"@shikijs/types": "npm:^3.6.0"
|
||||
"@shikijs/engine-oniguruma": "npm:^3.7.0"
|
||||
"@shikijs/langs": "npm:^3.7.0"
|
||||
"@shikijs/themes": "npm:^3.7.0"
|
||||
"@shikijs/types": "npm:^3.7.0"
|
||||
"@shikijs/vscode-textmate": "npm:^10.0.2"
|
||||
checksum: 10c0/347456c9da8a1fadd3c1f63097da459a5f930ef4bca6431cce913a379012c551e061d0a94ff7a0f307215b87f2418b7c198a55fba888fc97fb02ab36247adf6b
|
||||
checksum: 10c0/eb3f4900d841338077d839ebbc7f8722b13876a586cff7abc73295e956683724dd3371a9f990900184a2d069461965951b2604d677991badf3474262e7811384
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -1299,6 +1299,55 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-avatar@npm:^1.1.10":
|
||||
version: 1.1.10
|
||||
resolution: "@radix-ui/react-avatar@npm:1.1.10"
|
||||
dependencies:
|
||||
"@radix-ui/react-context": "npm:1.1.2"
|
||||
"@radix-ui/react-primitive": "npm:2.1.3"
|
||||
"@radix-ui/react-use-callback-ref": "npm:1.1.1"
|
||||
"@radix-ui/react-use-is-hydrated": "npm:0.1.0"
|
||||
"@radix-ui/react-use-layout-effect": "npm:1.1.1"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/9fb0cf9a9d0fdbeaa2efda476402fc09db2e6ff9cd9aa3ea1d315d9c9579840722a4833725cb196c455e0bd775dfe04221a4f6855685ce89d2133c42e2b07e5f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-collapsible@npm:^1.1.11":
|
||||
version: 1.1.11
|
||||
resolution: "@radix-ui/react-collapsible@npm:1.1.11"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": "npm:1.1.2"
|
||||
"@radix-ui/react-compose-refs": "npm:1.1.2"
|
||||
"@radix-ui/react-context": "npm:1.1.2"
|
||||
"@radix-ui/react-id": "npm:1.1.1"
|
||||
"@radix-ui/react-presence": "npm:1.1.4"
|
||||
"@radix-ui/react-primitive": "npm:2.1.3"
|
||||
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
|
||||
"@radix-ui/react-use-layout-effect": "npm:1.1.1"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/fa2de539ef06e2b2d18acebb12a34ce1534ca88bd484b7359aac05534d1e551fe83eaafbf60915c00161bb370f0dc9fc303903133510dea0a59fd018155b7db5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-collection@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@radix-ui/react-collection@npm:1.1.7"
|
||||
|
@ -1321,7 +1370,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-compose-refs@npm:1.1.2":
|
||||
"@radix-ui/react-compose-refs@npm:1.1.2, @radix-ui/react-compose-refs@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "@radix-ui/react-compose-refs@npm:1.1.2"
|
||||
peerDependencies:
|
||||
|
@ -1347,6 +1396,38 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-dialog@npm:^1.1.14, @radix-ui/react-dialog@npm:^1.1.6":
|
||||
version: 1.1.14
|
||||
resolution: "@radix-ui/react-dialog@npm:1.1.14"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": "npm:1.1.2"
|
||||
"@radix-ui/react-compose-refs": "npm:1.1.2"
|
||||
"@radix-ui/react-context": "npm:1.1.2"
|
||||
"@radix-ui/react-dismissable-layer": "npm:1.1.10"
|
||||
"@radix-ui/react-focus-guards": "npm:1.1.2"
|
||||
"@radix-ui/react-focus-scope": "npm:1.1.7"
|
||||
"@radix-ui/react-id": "npm:1.1.1"
|
||||
"@radix-ui/react-portal": "npm:1.1.9"
|
||||
"@radix-ui/react-presence": "npm:1.1.4"
|
||||
"@radix-ui/react-primitive": "npm:2.1.3"
|
||||
"@radix-ui/react-slot": "npm:1.2.3"
|
||||
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
|
||||
aria-hidden: "npm:^1.2.4"
|
||||
react-remove-scroll: "npm:^2.6.3"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/ab7bc783510ed8fccfe91020b214f4a571d5a1d46d398faa33f4c151bc9f586c47483b307e72b67687b06694c194b3aa80dd1de728460fa765db9f3057690ba3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-direction@npm:1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "@radix-ui/react-direction@npm:1.1.1"
|
||||
|
@ -1383,7 +1464,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-dropdown-menu@npm:^2.1.14":
|
||||
"@radix-ui/react-dropdown-menu@npm:^2.1.15":
|
||||
version: 2.1.15
|
||||
resolution: "@radix-ui/react-dropdown-menu@npm:2.1.15"
|
||||
dependencies:
|
||||
|
@ -1469,7 +1550,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-id@npm:1.1.1":
|
||||
"@radix-ui/react-id@npm:1.1.1, @radix-ui/react-id@npm:^1.1.0":
|
||||
version: 1.1.1
|
||||
resolution: "@radix-ui/react-id@npm:1.1.1"
|
||||
dependencies:
|
||||
|
@ -1539,6 +1620,39 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-popover@npm:^1.1.14":
|
||||
version: 1.1.14
|
||||
resolution: "@radix-ui/react-popover@npm:1.1.14"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": "npm:1.1.2"
|
||||
"@radix-ui/react-compose-refs": "npm:1.1.2"
|
||||
"@radix-ui/react-context": "npm:1.1.2"
|
||||
"@radix-ui/react-dismissable-layer": "npm:1.1.10"
|
||||
"@radix-ui/react-focus-guards": "npm:1.1.2"
|
||||
"@radix-ui/react-focus-scope": "npm:1.1.7"
|
||||
"@radix-ui/react-id": "npm:1.1.1"
|
||||
"@radix-ui/react-popper": "npm:1.2.7"
|
||||
"@radix-ui/react-portal": "npm:1.1.9"
|
||||
"@radix-ui/react-presence": "npm:1.1.4"
|
||||
"@radix-ui/react-primitive": "npm:2.1.3"
|
||||
"@radix-ui/react-slot": "npm:1.2.3"
|
||||
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
|
||||
aria-hidden: "npm:^1.2.4"
|
||||
react-remove-scroll: "npm:^2.6.3"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/04e557bfcaab4887694d119555b101e16b8a4e99595541ff2cbe805c551be853cb02882a2ada04e6507ffc45bc092bc2b89704b7b79f5025251767d0b4f3230a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-popper@npm:1.2.7":
|
||||
version: 1.2.7
|
||||
resolution: "@radix-ui/react-popper@npm:1.2.7"
|
||||
|
@ -1607,7 +1721,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-primitive@npm:2.1.3":
|
||||
"@radix-ui/react-primitive@npm:2.1.3, @radix-ui/react-primitive@npm:^2.0.2":
|
||||
version: 2.1.3
|
||||
resolution: "@radix-ui/react-primitive@npm:2.1.3"
|
||||
dependencies:
|
||||
|
@ -1719,7 +1833,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-separator@npm:^1.1.6":
|
||||
"@radix-ui/react-separator@npm:^1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@radix-ui/react-separator@npm:1.1.7"
|
||||
dependencies:
|
||||
|
@ -1738,7 +1852,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.2.2":
|
||||
"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@radix-ui/react-slot@npm:1.2.3"
|
||||
dependencies:
|
||||
|
@ -1804,6 +1918,36 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-tooltip@npm:^1.2.7":
|
||||
version: 1.2.7
|
||||
resolution: "@radix-ui/react-tooltip@npm:1.2.7"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": "npm:1.1.2"
|
||||
"@radix-ui/react-compose-refs": "npm:1.1.2"
|
||||
"@radix-ui/react-context": "npm:1.1.2"
|
||||
"@radix-ui/react-dismissable-layer": "npm:1.1.10"
|
||||
"@radix-ui/react-id": "npm:1.1.1"
|
||||
"@radix-ui/react-popper": "npm:1.2.7"
|
||||
"@radix-ui/react-portal": "npm:1.1.9"
|
||||
"@radix-ui/react-presence": "npm:1.1.4"
|
||||
"@radix-ui/react-primitive": "npm:2.1.3"
|
||||
"@radix-ui/react-slot": "npm:1.2.3"
|
||||
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
|
||||
"@radix-ui/react-visually-hidden": "npm:1.2.3"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/28798d576c6ffec4f11120cd563aa9d5ab9afb9a37dc18778176442756d026c8c46eec1ddc647b2b5914045495fcb89f82530106e91acb55776b7d6b1a10fb57
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-callback-ref@npm:1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1"
|
||||
|
@ -1863,6 +2007,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-is-hydrated@npm:0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "@radix-ui/react-use-is-hydrated@npm:0.1.0"
|
||||
dependencies:
|
||||
use-sync-external-store: "npm:^1.5.0"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/635079bafe32829fc7405895154568ea94a22689b170489fd6d77668e4885e72ff71ed6d0ea3d602852841ef0f1927aa400fee2178d5dfbeb8bc9297da7d6498
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-layout-effect@npm:1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1"
|
||||
|
@ -1966,7 +2125,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@shikijs/engine-oniguruma@npm:^3.6.0":
|
||||
"@shikijs/engine-oniguruma@npm:^3.7.0":
|
||||
version: 3.7.0
|
||||
resolution: "@shikijs/engine-oniguruma@npm:3.7.0"
|
||||
dependencies:
|
||||
|
@ -1976,7 +2135,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@shikijs/langs@npm:^3.6.0":
|
||||
"@shikijs/langs@npm:^3.7.0":
|
||||
version: 3.7.0
|
||||
resolution: "@shikijs/langs@npm:3.7.0"
|
||||
dependencies:
|
||||
|
@ -1985,7 +2144,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@shikijs/themes@npm:^3.6.0":
|
||||
"@shikijs/themes@npm:^3.7.0":
|
||||
version: 3.7.0
|
||||
resolution: "@shikijs/themes@npm:3.7.0"
|
||||
dependencies:
|
||||
|
@ -1994,7 +2153,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@shikijs/types@npm:3.7.0, @shikijs/types@npm:^3.6.0":
|
||||
"@shikijs/types@npm:3.7.0, @shikijs/types@npm:^3.7.0":
|
||||
version: 3.7.0
|
||||
resolution: "@shikijs/types@npm:3.7.0"
|
||||
dependencies:
|
||||
|
@ -3662,7 +3821,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8":
|
||||
"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9":
|
||||
version: 3.1.9
|
||||
resolution: "array-includes@npm:3.1.9"
|
||||
dependencies:
|
||||
|
@ -3699,7 +3858,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"array.prototype.findlastindex@npm:^1.2.5":
|
||||
"array.prototype.findlastindex@npm:^1.2.6":
|
||||
version: 1.2.6
|
||||
resolution: "array.prototype.findlastindex@npm:1.2.6"
|
||||
dependencies:
|
||||
|
@ -3714,7 +3873,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2":
|
||||
"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.3":
|
||||
version: 1.3.3
|
||||
resolution: "array.prototype.flat@npm:1.3.3"
|
||||
dependencies:
|
||||
|
@ -4058,6 +4217,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cmdk@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "cmdk@npm:1.1.1"
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs": "npm:^1.1.1"
|
||||
"@radix-ui/react-dialog": "npm:^1.1.6"
|
||||
"@radix-ui/react-id": "npm:^1.1.0"
|
||||
"@radix-ui/react-primitive": "npm:^2.0.2"
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||
checksum: 10c0/5605ac4396ec9bc65c82f954da19dd89a0636a54026df72780e2470da1381f9d57434a80a53f2d57eaa4e759660a3ebba9232b74258dc09970576591eae03116
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color-convert@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "color-convert@npm:2.0.1"
|
||||
|
@ -4792,7 +4966,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-module-utils@npm:^2.12.0":
|
||||
"eslint-module-utils@npm:^2.12.1":
|
||||
version: 2.12.1
|
||||
resolution: "eslint-module-utils@npm:2.12.1"
|
||||
dependencies:
|
||||
|
@ -4805,31 +4979,31 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"eslint-plugin-import@npm:^2.31.0":
|
||||
version: 2.31.0
|
||||
resolution: "eslint-plugin-import@npm:2.31.0"
|
||||
version: 2.32.0
|
||||
resolution: "eslint-plugin-import@npm:2.32.0"
|
||||
dependencies:
|
||||
"@rtsao/scc": "npm:^1.1.0"
|
||||
array-includes: "npm:^3.1.8"
|
||||
array.prototype.findlastindex: "npm:^1.2.5"
|
||||
array.prototype.flat: "npm:^1.3.2"
|
||||
array.prototype.flatmap: "npm:^1.3.2"
|
||||
array-includes: "npm:^3.1.9"
|
||||
array.prototype.findlastindex: "npm:^1.2.6"
|
||||
array.prototype.flat: "npm:^1.3.3"
|
||||
array.prototype.flatmap: "npm:^1.3.3"
|
||||
debug: "npm:^3.2.7"
|
||||
doctrine: "npm:^2.1.0"
|
||||
eslint-import-resolver-node: "npm:^0.3.9"
|
||||
eslint-module-utils: "npm:^2.12.0"
|
||||
eslint-module-utils: "npm:^2.12.1"
|
||||
hasown: "npm:^2.0.2"
|
||||
is-core-module: "npm:^2.15.1"
|
||||
is-core-module: "npm:^2.16.1"
|
||||
is-glob: "npm:^4.0.3"
|
||||
minimatch: "npm:^3.1.2"
|
||||
object.fromentries: "npm:^2.0.8"
|
||||
object.groupby: "npm:^1.0.3"
|
||||
object.values: "npm:^1.2.0"
|
||||
object.values: "npm:^1.2.1"
|
||||
semver: "npm:^6.3.1"
|
||||
string.prototype.trimend: "npm:^1.0.8"
|
||||
string.prototype.trimend: "npm:^1.0.9"
|
||||
tsconfig-paths: "npm:^3.15.0"
|
||||
peerDependencies:
|
||||
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
|
||||
checksum: 10c0/e21d116ddd1900e091ad120b3eb68c5dd5437fe2c930f1211781cd38b246f090a6b74d5f3800b8255a0ed29782591521ad44eb21c5534960a8f1fb4040fd913a
|
||||
checksum: 10c0/bfb1b8fc8800398e62ddfefbf3638d185286edfed26dfe00875cc2846d954491b4f5112457831588b757fa789384e1ae585f812614c4797f0499fa234fd4a48b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -5758,7 +5932,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0":
|
||||
"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1":
|
||||
version: 2.16.1
|
||||
resolution: "is-core-module@npm:2.16.1"
|
||||
dependencies:
|
||||
|
@ -6454,12 +6628,12 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lucide-react@npm:^0.511.0":
|
||||
version: 0.511.0
|
||||
resolution: "lucide-react@npm:0.511.0"
|
||||
"lucide-react@npm:^0.515.0":
|
||||
version: 0.515.0
|
||||
resolution: "lucide-react@npm:0.515.0"
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/bf09dd73cf2233abea90506ad31a91739555d761062722acbe045cb73e274f035b196472de0971a8a8f0645b2b54e3f21b8c1980fe87c909ca93171a9c28428a
|
||||
checksum: 10c0/00485e09ab3d0bbb34797b1f368c269e8708522b6e2f46fd84dd5bd99741546487be9a65a260f274e8049b81cc37687566e26132f5752352c8d9bc8e5d0b3dea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -6549,15 +6723,20 @@ __metadata:
|
|||
"@fortawesome/react-fontawesome": "npm:^0.2.2"
|
||||
"@hookform/resolvers": "npm:^5.0.1"
|
||||
"@prisma/client": "npm:^6.9.0"
|
||||
"@radix-ui/react-dropdown-menu": "npm:^2.1.14"
|
||||
"@radix-ui/react-avatar": "npm:^1.1.10"
|
||||
"@radix-ui/react-collapsible": "npm:^1.1.11"
|
||||
"@radix-ui/react-dialog": "npm:^1.1.14"
|
||||
"@radix-ui/react-dropdown-menu": "npm:^2.1.15"
|
||||
"@radix-ui/react-hover-card": "npm:^1.1.13"
|
||||
"@radix-ui/react-label": "npm:^2.1.6"
|
||||
"@radix-ui/react-popover": "npm:^1.1.14"
|
||||
"@radix-ui/react-scroll-area": "npm:^1.2.8"
|
||||
"@radix-ui/react-select": "npm:^2.2.4"
|
||||
"@radix-ui/react-separator": "npm:^1.1.6"
|
||||
"@radix-ui/react-slot": "npm:^1.2.2"
|
||||
"@radix-ui/react-separator": "npm:^1.1.7"
|
||||
"@radix-ui/react-slot": "npm:^1.2.3"
|
||||
"@radix-ui/react-switch": "npm:^1.2.4"
|
||||
"@radix-ui/react-tabs": "npm:^1.1.11"
|
||||
"@radix-ui/react-tooltip": "npm:^1.2.7"
|
||||
"@tailwindcss/postcss": "npm:4.1.10"
|
||||
"@tanstack/react-query": "npm:^5.80.7"
|
||||
"@types/node": "npm:22.15.32"
|
||||
|
@ -6568,11 +6747,12 @@ __metadata:
|
|||
bcryptjs: "npm:^3.0.2"
|
||||
class-variance-authority: "npm:^0.7.1"
|
||||
clsx: "npm:^2.1.1"
|
||||
cmdk: "npm:^1.1.1"
|
||||
dotenv-cli: "npm:8.0.0"
|
||||
eslint: "npm:9.29.0"
|
||||
eslint-config-next: "npm:15.3.4"
|
||||
eslint-config-prettier: "npm:10.1.5"
|
||||
lucide-react: "npm:^0.511.0"
|
||||
lucide-react: "npm:^0.515.0"
|
||||
next: "npm:15.4.0-canary.92"
|
||||
next-auth: "npm:^5.0.0-beta.25"
|
||||
next-themes: "npm:^0.4.6"
|
||||
|
@ -7185,7 +7365,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1":
|
||||
"object.values@npm:^1.1.6, object.values@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "object.values@npm:1.2.1"
|
||||
dependencies:
|
||||
|
@ -8569,7 +8749,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9":
|
||||
"string.prototype.trimend@npm:^1.0.9":
|
||||
version: 1.0.9
|
||||
resolution: "string.prototype.trimend@npm:1.0.9"
|
||||
dependencies:
|
||||
|
@ -9294,7 +9474,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-sync-external-store@npm:^1.4.0":
|
||||
"use-sync-external-store@npm:^1.4.0, use-sync-external-store@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "use-sync-external-store@npm:1.5.0"
|
||||
peerDependencies:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue