From 53cc8cb2b74d7f804bc46a27e4fbadd5bfe8dba8 Mon Sep 17 00:00:00 2001 From: SomeCodecat <88855796+SomeCodecat@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:01:20 +0200 Subject: [PATCH 1/5] feat: add Radix UI components and implement sidebar functionality - Added new Radix UI components: Dialog, Tooltip, Separator, and updated existing components. - Introduced a Sidebar component with collapsible functionality and mobile responsiveness. - Implemented a custom hook `useIsMobile` to manage mobile state. - Updated package dependencies in package.json and yarn.lock for new components. - Created utility components such as Button, Skeleton, and Input for consistent styling. feat: add AppSidebar component with collapsible functionality and sidebar menu - Introduced AppSidebar component for a customizable sidebar layout. - Implemented collapsible sections using Radix UI's Collapsible component. - Added sidebar menu items with icons and links for navigation. - Created Sidebar UI components including SidebarHeader, SidebarFooter, and SidebarMenu. - Integrated ThemePicker for theme selection within the sidebar. - Updated sidebar styles and layout for better responsiveness. chore: add @radix-ui/react-collapsible dependency - Added @radix-ui/react-collapsible package to manage collapsible UI elements. --- src/components/ui/sidebar.tsx | 725 ++++++++++++++++++++++++++++++++++ yarn.lock | 18 +- 2 files changed, 734 insertions(+), 9 deletions(-) create mode 100644 src/components/ui/sidebar.tsx diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..6b68b8f --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -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(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( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + ); + + return ( + + +
+ {children} +
+
+
+ ); +} + +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 ( +
+ {children} +
+ ); + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ); + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ); +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps) { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +} + +function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) { + const { toggleSidebar } = useSidebar(); + + return ( + -

- Permanently delete your account and all associated data. -

-
- - - - - - - - - - - - - - Notification Preferences - - Choose how you want to be notified. - - - -
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - - - -
-
- - - - - - Calendar & Availability - - Manage your calendar display, default availability, and iCal - integrations. - - - -
- - Display - -
- - -
-
- - -
-
- - -
-
- -
- - Availability - -
- -

- Define your typical available hours (e.g., - Monday-Friday, 9 AM - 5 PM). -

- -
-
- -

- Min time before a booking can be made. -

-
- -
-
-
- -

- Max time in advance a booking can be made. -

- -
-
- -
- - iCalendar Integration - -
- - - -
-
- - - -
-
-
-
- - - - -
-
- - - - - - Sharing & Privacy - - Control who can see your calendar and book time with you. - - - -
- - -
-
- -

- (Override for Default Visibility) -
- - This setting will override the default visibility for - your calendar. You can set specific friends or groups to - see your full calendar details. - -

- -
-
- - -
-
- - -

- Prevent specific users from seeing your calendar or - booking time. -

-
-
-
- - - - -
-
- - - - - - Appearance - - Customize the look and feel of the application. - - - -
- - -
-
- - -
-
- - -
-
-
- - - - -
-
- - - - ); +export default function Page() { + return ; } diff --git a/src/auth.ts b/src/auth.ts index 51c2e9c..18b3b2d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -95,13 +95,27 @@ const providers: Provider[] = [ } }, }), - process.env.AUTH_AUTHENTIK_ID && AuthentikProvider, + process.env.AUTH_DISCORD_ID && DiscordProvider, process.env.AUTH_FACEBOOK_ID && FacebookProvider, process.env.AUTH_GITHUB_ID && GithubProvider, process.env.AUTH_GITLAB_ID && GitlabProvider, process.env.AUTH_GOOGLE_ID && GoogleProvider, process.env.AUTH_KEYCLOAK_ID && KeycloakProvider, + + process.env.AUTH_AUTHENTIK_ID && + AuthentikProvider({ + profile(profile) { + return { + id: profile.sub, + name: profile.preferred_username, + first_name: profile.given_name.split(' ')[0] || '', + last_name: profile.given_name.split(' ')[1] || '', + email: profile.email, + image: profile.picture, + }; + }, + }), ].filter(Boolean) as Provider[]; export const providerMap = providers diff --git a/src/components/buttons/icon-button.tsx b/src/components/buttons/icon-button.tsx index 17f9945..4b50e90 100644 --- a/src/components/buttons/icon-button.tsx +++ b/src/components/buttons/icon-button.tsx @@ -1,19 +1,20 @@ import { Button } from '@/components/ui/button'; - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { LucideProps } from 'lucide-react'; +import React, { ForwardRefExoticComponent, RefAttributes } from 'react'; export function IconButton({ icon, children, ...props }: { - icon: IconProp; - children: React.ReactNode; + icon?: ForwardRefExoticComponent< + Omit & RefAttributes + >; + children?: React.ReactNode; } & React.ComponentProps) { return ( ); diff --git a/src/components/buttons/sso-login-button.tsx b/src/components/buttons/sso-login-button.tsx index 013ef73..ae0238a 100644 --- a/src/components/buttons/sso-login-button.tsx +++ b/src/components/buttons/sso-login-button.tsx @@ -1,6 +1,6 @@ import { signIn } from '@/auth'; import { IconButton } from '@/components/buttons/icon-button'; -import { faOpenid } from '@fortawesome/free-brands-svg-icons'; +import { Fingerprint, ScanEye } from 'lucide-react'; export default function SSOLogin({ provider, @@ -22,7 +22,7 @@ export default function SSOLogin({ className='w-full' type='submit' variant='secondary' - icon={faOpenid} + icon={Fingerprint} {...props} > Login with {providerDisplayName} diff --git a/src/components/custom-ui/app-sidebar.tsx b/src/components/custom-ui/app-sidebar.tsx index 50e88c2..37fa84f 100644 --- a/src/components/custom-ui/app-sidebar.tsx +++ b/src/components/custom-ui/app-sidebar.tsx @@ -62,18 +62,20 @@ export function AppSidebar() { <> - - + + + + diff --git a/src/components/custom-ui/labeled-input.tsx b/src/components/custom-ui/labeled-input.tsx index 4746a31..5505e14 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -1,29 +1,56 @@ import { Input, Textarea } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import React, { ForwardRefExoticComponent, RefAttributes } from 'react'; +import { Button } from '../ui/button'; +import { Eye, EyeOff, LucideProps } from 'lucide-react'; +import { cn } from '@/lib/utils'; export default function LabeledInput({ type, label, + subtext, placeholder, value, + defaultValue, name, + icon, variantSize = 'default', autocomplete, error, ...rest }: { - type: 'text' | 'email' | 'password'; label: string; + subtext?: string; placeholder?: string; value?: string; name?: string; + icon?: ForwardRefExoticComponent< + Omit & RefAttributes + >; variantSize?: 'default' | 'big' | 'textarea'; autocomplete?: string; error?: string; } & React.InputHTMLAttributes) { + const [passwordVisible, setPasswordVisible] = React.useState(false); + const [inputValue, setInputValue] = React.useState( + value || defaultValue || '', + ); + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + if (rest.onChange) { + rest.onChange(e); + } + }; + return (
+ {subtext && ( + + )} {variantSize === 'textarea' ? (