From 8b2452abe462c3a8724a30e6cef84619aa32c144 Mon Sep 17 00:00:00 2001 From: Maximilian Liebmann Date: Mon, 30 Jun 2025 23:32:10 +0200 Subject: [PATCH] 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. --- src/app/api/logout/route.ts | 8 + src/app/api/user/me/validation.ts | 11 + src/app/settings/page.tsx | 483 +----------------- src/auth.ts | 16 +- src/components/buttons/icon-button.tsx | 13 +- src/components/buttons/sso-login-button.tsx | 4 +- src/components/custom-ui/app-sidebar.tsx | 26 +- src/components/custom-ui/labeled-input.tsx | 83 ++- src/components/forms/login-form.tsx | 64 ++- .../misc/profile-picture-upload.tsx | 36 ++ src/components/misc/user-card.tsx | 2 +- src/components/misc/user-dropdown.tsx | 11 +- src/components/settings/settings-dropdown.tsx | 165 ++++++ src/components/settings/settings-page.tsx | 59 +++ src/components/settings/tabs/account.tsx | 282 ++++++++++ src/components/settings/tabs/appearance.tsx | 55 ++ src/components/settings/tabs/calendar.tsx | 226 ++++++++ .../settings/tabs/notifications.tsx | 134 +++++ src/components/settings/tabs/password.tsx | 151 ++++++ src/components/settings/tabs/privacy.tsx | 143 ++++++ src/components/ui/button.tsx | 2 +- src/components/ui/card.tsx | 2 +- src/components/ui/dialog.tsx | 2 +- src/components/wrappers/group-wrapper.tsx | 23 + src/components/wrappers/settings-scroll.tsx | 20 +- 25 files changed, 1469 insertions(+), 552 deletions(-) create mode 100644 src/app/api/logout/route.ts create mode 100644 src/components/misc/profile-picture-upload.tsx create mode 100644 src/components/settings/settings-dropdown.tsx create mode 100644 src/components/settings/settings-page.tsx create mode 100644 src/components/settings/tabs/account.tsx create mode 100644 src/components/settings/tabs/appearance.tsx create mode 100644 src/components/settings/tabs/calendar.tsx create mode 100644 src/components/settings/tabs/notifications.tsx create mode 100644 src/components/settings/tabs/password.tsx create mode 100644 src/components/settings/tabs/privacy.tsx create mode 100644 src/components/wrappers/group-wrapper.tsx diff --git a/src/app/api/logout/route.ts b/src/app/api/logout/route.ts new file mode 100644 index 0000000..ba89440 --- /dev/null +++ b/src/app/api/logout/route.ts @@ -0,0 +1,8 @@ +import { signOut } from '@/auth'; +import { NextResponse } from 'next/server'; + +export const GET = async () => { + await signOut(); + + return NextResponse.redirect('/login'); +}; diff --git a/src/app/api/user/me/validation.ts b/src/app/api/user/me/validation.ts index 66f07cc..4a1d20e 100644 --- a/src/app/api/user/me/validation.ts +++ b/src/app/api/user/me/validation.ts @@ -1,11 +1,13 @@ import zod from 'zod/v4'; import { + emailSchema, firstNameSchema, lastNameSchema, newUserEmailServerSchema, newUserNameServerSchema, passwordSchema, timezoneSchema, + userNameSchema, } from '@/app/api/user/validation'; // ---------------------------------------- @@ -22,6 +24,15 @@ export const updateUserServerSchema = zod.object({ timezone: timezoneSchema.optional(), }); +export const updateUserClientSchema = zod.object({ + name: userNameSchema.optional(), + first_name: firstNameSchema.optional(), + last_name: lastNameSchema.optional(), + email: emailSchema.optional(), + image: zod.url().optional(), + timezone: timezoneSchema.optional(), +}); + export const updateUserPasswordServerSchema = zod .object({ current_password: zod.string().min(1, 'Current password is required'), diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 563ebab..a2c5b35 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -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/settings/settings-page'; -export default function SettingsPage() { - return ( -
-
- - - Account - Notifications - Calendar - Privacy - Appearance - - - - - - - Account Settings - - Manage your account details and preferences. - - - -
- - -
-
- - -

- Email is managed by your SSO provider. -

-
-
- - -

- Upload a new profile picture. -

-
-
- - -
- -
- - -
-
- -

- 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..c745114 100644 --- a/src/components/custom-ui/labeled-input.tsx +++ b/src/components/custom-ui/labeled-input.tsx @@ -1,29 +1,54 @@ 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' ? (