feat: Implement settings dropdown and page components
- Added `SettingsDropdown` component for selecting settings sections with icons and descriptions. - Created `SettingsPage` component to manage user settings, including account details, notifications, calendar availability, privacy, and appearance. - Introduced `SettingsSwitcher` for selecting options within settings. - Integrated command and dialog components for improved user interaction. - Updated `UserDropdown` to include links for settings and logout. - Refactored button styles and card footer layout for consistency. - Added popover functionality for dropdown menus. - Updated dependencies in `yarn.lock` for new components.
This commit is contained in:
parent
0cd88d6f07
commit
bc1910fdca
14 changed files with 1202 additions and 502 deletions
|
@ -33,6 +33,7 @@
|
|||
"@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.7",
|
||||
|
@ -44,6 +45,7 @@
|
|||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"lucide-react": "^0.515.0",
|
||||
"next": "15.4.0-canary.92",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
|
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 your account details and preferences',
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
label: 'Notifications',
|
||||
value: 'notifications',
|
||||
description: 'Choose how you want to be notified',
|
||||
icon: Bell,
|
||||
},
|
||||
{
|
||||
label: 'Calendar',
|
||||
value: 'calendarAvailability',
|
||||
description: 'Manage calendar display and availability',
|
||||
icon: Calendar,
|
||||
},
|
||||
{
|
||||
label: 'Privacy',
|
||||
value: 'sharingPrivacy',
|
||||
description: 'Control who can see your calendar',
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
label: 'Appearance',
|
||||
value: 'appearance',
|
||||
description: 'Customize the look and feel',
|
||||
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-white text-black 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>
|
||||
<span className='text-xs text-muted-foreground'>
|
||||
{currentSectionData?.description}
|
||||
</span>
|
||||
</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>
|
||||
<span className='text-xs text-muted-foreground'>
|
||||
{section.description}
|
||||
</span>
|
||||
</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>
|
||||
);
|
||||
}
|
467
src/components/misc/settings-page.tsx
Normal file
467
src/components/misc/settings-page.tsx
Normal file
|
@ -0,0 +1,467 @@
|
|||
'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';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter();
|
||||
const [currentSection, setCurrentSection] = useState('general');
|
||||
|
||||
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>
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'notifications':
|
||||
return (
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'calendarAvailability':
|
||||
return (
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'sharingPrivacy':
|
||||
return (
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
|
||||
case 'appearance':
|
||||
return (
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
}
|
123
src/components/misc/settings-switcher.tsx
Normal file
123
src/components/misc/settings-switcher.tsx
Normal file
|
@ -0,0 +1,123 @@
|
|||
'use client';
|
||||
|
||||
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 } from 'lucide-react';
|
||||
|
||||
interface SettingsOption {
|
||||
label: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface SettingsSwitcherProps {
|
||||
title: string;
|
||||
options: SettingsOption[];
|
||||
defaultValue?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
searchPlaceholder?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SettingsSwitcher({
|
||||
title,
|
||||
options,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
placeholder = 'Select option...',
|
||||
searchPlaceholder = 'Search options...',
|
||||
className,
|
||||
}: SettingsSwitcherProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState(
|
||||
defaultValue || options[0]?.value || '',
|
||||
);
|
||||
|
||||
const selectedOption = options.find(
|
||||
(option) => option.value === selectedValue,
|
||||
);
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
setSelectedValue(value);
|
||||
setOpen(false);
|
||||
onValueChange?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
<label className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
|
||||
{title}
|
||||
</label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline_muted'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='w-full justify-between bg-white text-black'
|
||||
>
|
||||
<div className='flex flex-col items-start'>
|
||||
<span>{selectedOption?.label || placeholder}</span>
|
||||
{selectedOption?.description && (
|
||||
<span className='text-xs text-muted-foreground'>
|
||||
{selectedOption.description}
|
||||
</span>
|
||||
)}
|
||||
</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={searchPlaceholder} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No option found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={() => handleSelect(option.value)}
|
||||
className='flex items-center justify-between'
|
||||
>
|
||||
<div className='flex flex-col'>
|
||||
<span>{option.label}</span>
|
||||
{option.description && (
|
||||
<span className='text-xs text-muted-foreground'>
|
||||
{option.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
'ml-2 h-4 w-4',
|
||||
selectedValue === option.value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -21,7 +21,7 @@ export default function UserCard() {
|
|||
)}
|
||||
</Avatar>
|
||||
<div className='flex justify-center'>{data?.data.user.name}</div>
|
||||
<div className='flex justify-center text-text-muted'>
|
||||
<div className='flex justify-center text-text-muted text-[12px]'>
|
||||
{data?.data.user.email}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
|
|||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
// DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
// DropdownMenuLabel,
|
||||
// DropdownMenuPortal,
|
||||
|
@ -48,11 +48,13 @@ export default function UserDropdown() {
|
|||
<UserCard />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<Link href='/settings'>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Link href='/logout'>Logout</Link>
|
||||
</DropdownMenuItem>
|
||||
<Link href='/logout'>
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
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 }
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
58
yarn.lock
58
yarn.lock
|
@ -1370,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:
|
||||
|
@ -1396,7 +1396,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-dialog@npm:^1.1.14":
|
||||
"@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:
|
||||
|
@ -1550,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:
|
||||
|
@ -1620,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"
|
||||
|
@ -1688,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:
|
||||
|
@ -4184,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"
|
||||
|
@ -6681,6 +6729,7 @@ __metadata:
|
|||
"@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.7"
|
||||
|
@ -6698,6 +6747,7 @@ __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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue