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 e0ad2a5..a2c5b35 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,4 +1,4 @@ -import SettingsPage from '@/components/misc/settings-page'; +import SettingsPage from '@/components/settings/settings-page'; export default function Page() { return ; diff --git a/src/auth.ts b/src/auth.ts index 405b729..cb8ae40 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -88,7 +88,19 @@ const providers: Provider[] = [ } }, }), - process.env.AUTH_AUTHENTIK_ID && Authentik, + process.env.AUTH_AUTHENTIK_ID && + Authentik({ + 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/misc/settings-page.tsx b/src/components/misc/settings-page.tsx deleted file mode 100644 index 2f51d7b..0000000 --- a/src/components/misc/settings-page.tsx +++ /dev/null @@ -1,725 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card'; - -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 { - useDeleteApiUserMe, - useGetApiUserMe, - usePatchApiUserMePassword, -} from '@/generated/api/user/user'; -import { ThemePicker } from './theme-picker'; -import LabeledInput from '../custom-ui/labeled-input'; -import { GroupWrapper } from '../wrappers/group-wrapper'; - -import ProfilePictureUpload from './profile-picture-upload'; -import { - BookKey, - CalendarArrowDown, - CalendarArrowUp, - CalendarCheck, - CalendarClock, - CalendarCog, - CalendarPlus, - CalendarPlus2, - ClockAlert, - ClockFading, - FileKey, - FileKey2, - MailOpen, - RotateCcwKey, - UserLock, - UserPen, -} from 'lucide-react'; -import { IconButton } from '../buttons/icon-button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '../ui/dialog'; -import useZodForm from '@/lib/hooks/useZodForm'; -import { updateUserPasswordServerSchema } from '@/app/api/user/me/validation'; - -export default function SettingsPage() { - const router = useRouter(); - const [currentSection, setCurrentSection] = useState('general'); - const { data } = useGetApiUserMe(); - const deleteUser = useDeleteApiUserMe(); - const updatePassword = usePatchApiUserMePassword(); - - const { handleSubmit, formState, register, setError } = useZodForm( - updateUserPasswordServerSchema, - ); - - const onSubmit = handleSubmit(async (data) => { - await updatePassword.mutateAsync( - { - data: data, - }, - { - onSuccess: () => { - router.refresh(); - }, - onError: (error) => { - if (error instanceof Error) { - setError('root', { - message: error.response?.data.message, - }); - } else { - setError('root', { - message: 'An unknown error occurred.', - }); - } - }, - }, - ); - }); - - if (!data) { - return ( -
-
-
-

Loading Settings...

-
-
-
- ); - } - - const renderSettingsContent = () => { - switch (currentSection) { - case 'general': - return ( - - - - Account Settings - - - {/*-------------------- General Settings --------------------*/} - -
-
- -
-
- -
-
- -
-
- - - - Email might be managed by your SSO provider. - -
-
-
- {/*-------------------- General Settings --------------------*/} - {/*-------------------- Reset Password --------------------*/} - -
-
-
- -
-
- -
-
- -
-
- -
-
- {formState.errors.root && ( -

- {formState.errors.root.message} -

- )} -
-
- {/*-------------------- Reset Password --------------------*/} - {/*-------------------- Profile Picture --------------------*/} - -
- -
-
- {/*-------------------- Profile Picture --------------------*/} - {/*-------------------- Regional Settings --------------------*/} - -
-
- -
-
-
- - -
-
-
-
- {/*-------------------- Regional Settings --------------------*/} - {/*-------------------- DANGER ZONE --------------------*/} - -
- - - - - - -
- Are you absolutely sure? -
- - This action cannot be undone. This will - permanently delete your account and remove your - data from our servers. - - -
-
-
-
-
- - Permanently delete your account and all associated data. - -
-
- {/*-------------------- DANGER ZONE --------------------*/} -
-
-
- ); - - case 'notifications': - return ( - - - - Notification Preferences - - - {/*-------------------- All --------------------*/} - -
- - -
-
- {/*-------------------- All --------------------*/} - {/*-------------------- Meetings --------------------*/} - -
-
- - -
-
- - -
-
- -
- -
-
- -
- - -
-
-
- {/*-------------------- Meetings --------------------*/} - {/*-------------------- Social --------------------*/} - -
-
- - -
-
- - -
-
-
- {/*-------------------- Social --------------------*/} -
-
-
- ); - - case 'calendarAvailability': - return ( - - - - Calendar & Availability - - - {/*-------------------- Date & Time Format --------------------*/} - -
-
- - -
-
- - -
-
-
- {/*-------------------- Date & Time Format --------------------*/} - {/*-------------------- Calendar --------------------*/} - -
-
- - -
-
- - -
-
- - -
-
-
- {/*-------------------- Calendar --------------------*/} - {/*-------------------- Availability --------------------*/} - -
-
- - - Define your typical available hours (e.g., - Monday-Friday, 9 AM - 5 PM). - - -
-
- -
-
- -
-
-
- {/*-------------------- Availability --------------------*/} - {/*-------------------- iCalendar Integration --------------------*/} - -
-
{ - e.preventDefault(); - }} - className='space-y-2' - > - - - Add Feed - -
-
- - - Get iCal Export URL - - - Download .ics File - -
-
-
- {/*-------------------- iCalendar Integration --------------------*/} -
-
-
- ); - - case 'sharingPrivacy': - return ( - - - - Sharing & Privacy - - - {/*-------------------- Privacy Settigs --------------------*/} - -
-
- - - Default setting for new friends. - - -
-
- - - (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. - - - Manage Blocked Users - -
-
-
-
- {/*-------------------- Privacy Settigs --------------------*/} -
-
-
- ); - - case 'appearance': - return ( - - - - Appearance - - - {/*-------------------- Change Theme --------------------*/} - -
- - -
-
- {/*-------------------- Change Theme --------------------*/} -
-
-
- ); - - default: - return null; - } - }; - - return ( -
-
-
-
-

Settings

-
- -
- -
{renderSettingsContent()}
-
- - - - -
-
-
- ); -} diff --git a/src/components/misc/settings-dropdown.tsx b/src/components/settings/settings-dropdown.tsx similarity index 97% rename from src/components/misc/settings-dropdown.tsx rename to src/components/settings/settings-dropdown.tsx index 6eaae8d..6c23d07 100644 --- a/src/components/misc/settings-dropdown.tsx +++ b/src/components/settings/settings-dropdown.tsx @@ -26,6 +26,7 @@ import { Calendar, Shield, Palette, + Key, } from 'lucide-react'; interface SettingsSection { @@ -48,6 +49,12 @@ const settingsSections: SettingsSection[] = [ description: 'Manage account details', icon: User, }, + { + label: 'Password', + value: 'password', + description: 'Manage your password', + icon: Key, + }, { label: 'Notifications', value: 'notifications', diff --git a/src/components/settings/settings-page.tsx b/src/components/settings/settings-page.tsx new file mode 100644 index 0000000..26eced2 --- /dev/null +++ b/src/components/settings/settings-page.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useState } from 'react'; + +import { SettingsDropdown } from '@/components/settings/settings-dropdown'; + +import AccountTab from './tabs/account'; +import NotificationsTab from './tabs/notifications'; +import CalendarTab from './tabs/calendar'; +import PrivacyTab from './tabs/privacy'; +import AppearanceTab from './tabs/appearance'; +import PasswordTab from './tabs/password'; + +export default function SettingsPage() { + const [currentSection, setCurrentSection] = useState('general'); + + const renderSettingsContent = () => { + switch (currentSection) { + case 'general': + return ; + + case 'password': + return ; + + case 'notifications': + return ; + + case 'calendarAvailability': + return ; + + case 'sharingPrivacy': + return ; + + case 'appearance': + return ; + + default: + return null; + } + }; + + return ( +
+
+ {/* TODO: Fix overflow */} +
+
+

Settings

+
+ +
+ {renderSettingsContent()} +
+
+ ); +} diff --git a/src/components/settings/tabs/account.tsx b/src/components/settings/tabs/account.tsx new file mode 100644 index 0000000..96c7bc5 --- /dev/null +++ b/src/components/settings/tabs/account.tsx @@ -0,0 +1,263 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +import { Label } from '@/components/ui/label'; +import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + useDeleteApiUserMe, + useGetApiUserMe, + usePatchApiUserMe, +} from '@/generated/api/user/user'; +import LabeledInput from '@/components/custom-ui/labeled-input'; +import { GroupWrapper } from '@/components/wrappers/group-wrapper'; + +import ProfilePictureUpload from '@/components/misc/profile-picture-upload'; +import { CalendarClock, MailOpen, UserPen } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import useZodForm from '@/lib/hooks/useZodForm'; +import { updateUserClientSchema } from '@/app/api/user/me/validation'; +import { useRouter } from 'next/navigation'; + +export default function AccountTab() { + const router = useRouter(); + const { data } = useGetApiUserMe(); + const deleteUser = useDeleteApiUserMe(); + const updateAccount = usePatchApiUserMe(); + + const { handleSubmit, formState, register, setError } = useZodForm( + updateUserClientSchema, + ); + + const onSubmit = handleSubmit(async (submitData) => { + await updateAccount.mutateAsync( + { + data: { + first_name: + submitData?.first_name !== data?.data.user.first_name + ? submitData?.first_name + : undefined, + last_name: + submitData?.last_name !== data?.data.user.last_name + ? submitData?.last_name + : undefined, + email: + submitData?.email !== data?.data.user.email + ? submitData?.email + : undefined, + image: + submitData?.image !== data?.data.user.image + ? submitData?.image + : undefined, + timezone: + submitData?.timezone !== data?.data.user.timezone + ? submitData?.timezone + : undefined, + }, + }, + { + onSuccess: () => { + router.refresh(); + }, + onError: (error) => { + if (error instanceof Error) { + setError('root', { + message: error.response?.data.message, + }); + } else { + setError('root', { + message: 'An unknown error occurred.', + }); + } + }, + }, + ); + }); + + if (!data) { + return ( +
+
+
+

Loading Settings...

+
+
+
+ ); + } + + return ( +
+ + + + Account Settings + + + {/*-------------------- General Settings --------------------*/} + +
+
+ +
+
+ +
+
+ +
+
+ + + + Email might be managed by your SSO provider. + +
+
+ {formState.errors.root && ( +

+ {formState.errors.root.message} +

+ )} +
+ {/*-------------------- General Settings --------------------*/} + {/*-------------------- Profile Picture --------------------*/} + +
+ +
+
+ {/*-------------------- Profile Picture --------------------*/} + {/*-------------------- Regional Settings --------------------*/} + +
+
+ +
+
+
+ + +
+
+
+
+ {/*-------------------- Regional Settings --------------------*/} + {/*-------------------- DANGER ZONE --------------------*/} + +
+ + + + + + +
+ Are you absolutely sure? +
+ + This action cannot be undone. This will permanently + delete your account and remove your data from our + servers. + + +
+
+
+
+
+ + Permanently delete your account and all associated data. + +
+
+ {/*-------------------- DANGER ZONE --------------------*/} +
+
+ + + + +
+
+ ); +} diff --git a/src/components/settings/tabs/appearance.tsx b/src/components/settings/tabs/appearance.tsx new file mode 100644 index 0000000..c67c893 --- /dev/null +++ b/src/components/settings/tabs/appearance.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +import { Label } from '@/components/ui/label'; +import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; +import { GroupWrapper } from '@/components/wrappers/group-wrapper'; +import { useRouter } from 'next/navigation'; +import { ThemePicker } from '@/components/misc/theme-picker'; + +export default function AppearanceTab() { + const router = useRouter(); + return ( + <> +
+ + + + Appearance + + + {/*-------------------- Change Theme --------------------*/} + +
+ + +
+
+ {/*-------------------- Change Theme --------------------*/} +
+
+
+
+
+ + + + +
+ + ); +} diff --git a/src/components/settings/tabs/calendar.tsx b/src/components/settings/tabs/calendar.tsx new file mode 100644 index 0000000..fd949d4 --- /dev/null +++ b/src/components/settings/tabs/calendar.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +import { Label } from '@/components/ui/label'; +import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { GroupWrapper } from '@/components/wrappers/group-wrapper'; +import { Switch } from '@/components/ui/switch'; +import { useRouter } from 'next/navigation'; +import LabeledInput from '@/components/custom-ui/labeled-input'; +import { + CalendarArrowDown, + CalendarArrowUp, + CalendarCheck, + CalendarPlus, + ClockAlert, + ClockFading, +} from 'lucide-react'; +import { IconButton } from '@/components/buttons/icon-button'; + +export default function CalendarTab() { + const router = useRouter(); + return ( + <> +
+ + + + Calendar & Availability + + + {/*-------------------- Date & Time Format --------------------*/} + +
+
+ + +
+
+ + +
+
+
+ {/*-------------------- Date & Time Format --------------------*/} + {/*-------------------- Calendar --------------------*/} + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ {/*-------------------- Calendar --------------------*/} + {/*-------------------- Availability --------------------*/} + +
+
+ + + Define your typical available hours (e.g., Monday-Friday, + 9 AM - 5 PM). + + +
+
+ +
+
+ +
+
+
+ {/*-------------------- Availability --------------------*/} + {/*-------------------- iCalendar Integration --------------------*/} + +
+
{ + e.preventDefault(); + }} + className='space-y-2' + > + + + Add Feed + +
+
+ + + Get iCal Export URL + + + Download .ics File + +
+
+
+ {/*-------------------- iCalendar Integration --------------------*/} +
+
+
+
+
+ + + + +
+ + ); +} diff --git a/src/components/settings/tabs/notifications.tsx b/src/components/settings/tabs/notifications.tsx new file mode 100644 index 0000000..f1c2652 --- /dev/null +++ b/src/components/settings/tabs/notifications.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +import { Label } from '@/components/ui/label'; +import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { GroupWrapper } from '@/components/wrappers/group-wrapper'; +import { Switch } from '@/components/ui/switch'; +import { useRouter } from 'next/navigation'; + +export default function NotificationsTab() { + const router = useRouter(); + return ( + <> +
+ + + + Notification Preferences + + + {/*-------------------- All --------------------*/} + +
+ + +
+
+ {/*-------------------- All --------------------*/} + {/*-------------------- Meetings --------------------*/} + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+ + +
+
+
+ {/*-------------------- Meetings --------------------*/} + {/*-------------------- Social --------------------*/} + +
+
+ + +
+
+ + +
+
+
+ {/*-------------------- Social --------------------*/} +
+
+
+
+
+ + + + +
+ + ); +} diff --git a/src/components/settings/tabs/password.tsx b/src/components/settings/tabs/password.tsx new file mode 100644 index 0000000..c21d506 --- /dev/null +++ b/src/components/settings/tabs/password.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; +import { + useGetApiUserMe, + usePatchApiUserMePassword, +} from '@/generated/api/user/user'; +import LabeledInput from '@/components/custom-ui/labeled-input'; +import { GroupWrapper } from '@/components/wrappers/group-wrapper'; + +import { BookKey, FileKey, FileKey2, RotateCcwKey } from 'lucide-react'; +import useZodForm from '@/lib/hooks/useZodForm'; +import { updateUserPasswordServerSchema } from '@/app/api/user/me/validation'; +import { useRouter } from 'next/navigation'; + +export default function PasswordTab() { + const router = useRouter(); + const { data } = useGetApiUserMe(); + const updatePassword = usePatchApiUserMePassword(); + + const { handleSubmit, formState, register, setError } = useZodForm( + updateUserPasswordServerSchema, + ); + + const onSubmit = handleSubmit(async (data) => { + await updatePassword.mutateAsync( + { + data: data, + }, + { + onSuccess: () => { + router.refresh(); + }, + onError: (error) => { + if (error instanceof Error) { + setError('root', { + message: error.response?.data.message, + }); + } else { + setError('root', { + message: 'An unknown error occurred.', + }); + } + }, + }, + ); + }); + + if (!data) { + return ( +
+
+
+

Loading Settings...

+
+
+
+ ); + } + + return ( +
+
+ + + + Password Settings + + + {/*-------------------- Reset Password --------------------*/} + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {formState.errors.root && ( +

+ {formState.errors.root.message} +

+ )} +
+
+ {/*-------------------- Reset Password --------------------*/} +
+
+
+
+
+ + + + +
+
+ ); +} diff --git a/src/components/settings/tabs/privacy.tsx b/src/components/settings/tabs/privacy.tsx new file mode 100644 index 0000000..ed11cf2 --- /dev/null +++ b/src/components/settings/tabs/privacy.tsx @@ -0,0 +1,138 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +import { Label } from '@/components/ui/label'; +import { ScrollableSettingsWrapper } from '@/components/wrappers/settings-scroll'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { GroupWrapper } from '@/components/wrappers/group-wrapper'; +import { useRouter } from 'next/navigation'; +import { IconButton } from '@/components/buttons/icon-button'; +import { UserLock } from 'lucide-react'; + +export default function PrivacyTab() { + const router = useRouter(); + return ( + <> +
+ + + + Sharing & Privacy + + + {/*-------------------- Privacy Settigs --------------------*/} + +
+
+ + + Default setting for new friends. + + +
+
+ + + (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. + + + Manage Blocked Users + +
+
+
+
+ {/*-------------------- Privacy Settigs --------------------*/} +
+
+
+
+
+ + + + +
+ + ); +}