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.
-
-
-
-
- Display Name
-
-
-
-
Email Address
-
-
- Email is managed by your SSO provider.
-
-
-
-
Profile Picture
-
-
- Upload a new profile picture.
-
-
-
- Timezone
-
-
-
-
- Language
-
-
-
-
-
- English
- German
-
-
-
-
-
Delete Account
-
- Permanently delete your account and all associated data.
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
-
-
- Notification Preferences
-
- Choose how you want to be notified.
-
-
-
-
-
- Enable All Email Notifications
-
-
-
-
-
-
- New Meeting Bookings
-
-
-
-
-
- Meeting Confirmations/Cancellations
-
-
-
-
-
- Meeting Reminders
-
-
-
-
- Remind me before
-
-
-
-
-
- 15 minutes
- 30 minutes
- 1 hour
- 1 day
-
-
-
-
-
- Friend Requests
-
-
-
-
-
- Group Invitations/Updates
-
-
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
-
-
- Calendar & Availability
-
- Manage your calendar display, default availability, and iCal
- integrations.
-
-
-
-
-
- Display
-
-
-
- Default Calendar View
-
-
-
-
-
-
- Day
- Week
- Month
-
-
-
-
- Week Starts On
-
-
-
-
-
- Sunday
- Monday
-
-
-
-
-
- Show Weekends
-
-
-
-
-
-
-
- Availability
-
-
-
Working Hours
-
- Define your typical available hours (e.g.,
- Monday-Friday, 9 AM - 5 PM).
-
-
- Set Working Hours
-
-
-
-
- Minimum Notice for Bookings
-
-
- Min time before a booking can be made.
-
-
-
-
-
-
-
- Booking Window (days in advance)
-
-
- Max time in advance a booking can be made.
-
-
-
-
-
-
-
- iCalendar Integration
-
-
- Import iCal Feed URL
-
-
- Add Feed
-
-
-
- Export Your Calendar
-
- Get iCal Export URL
-
-
- Download .ics File
-
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
-
-
- Sharing & Privacy
-
- Control who can see your calendar and book time with you.
-
-
-
-
-
- Default Calendar Visibility
-
-
-
-
-
-
-
- Private (Only You)
-
-
- Free/Busy for Friends
-
-
- Full Details for Friends
-
-
-
-
-
-
- Who Can See Your Full Calendar Details?
-
-
- (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.
-
-
-
-
-
-
-
- Only Me
- My Friends
-
- Specific Friends/Groups (manage separately)
-
-
-
-
-
-
- Who Can Book Time With You?
-
-
-
-
-
-
- No One
- My Friends
-
- Specific Friends/Groups (manage separately)
-
-
-
-
-
-
Blocked Users
-
- Manage Blocked Users
-
-
- Prevent specific users from seeing your calendar or
- booking time.
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
-
-
- Appearance
-
- Customize the look and feel of the application.
-
-
-
-
- Theme
-
-
-
-
-
- Light
- Dark
- System Default
-
-
-
-
- Date Format
-
-
-
-
-
- DD/MM/YYYY
- MM/DD/YYYY
- YYYY-MM-DD
-
-
-
-
- Time Format
-
-
-
-
-
- 24-hour
- 12-hour
-
-
-
-
-
-
- Exit
- Save Changes
-
-
-
-
-
-
- );
+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 (
-
+ {icon && React.createElement(icon, { className: 'mr-2' })}
{children}
);
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 (
{label}
+ {subtext && (
+
+ {subtext}
+
+ )}
{variantSize === 'textarea' ? (
) : (
-
+
+
+ {icon && (
+
+ {React.createElement(icon)}
+
+ )}
+ {type === 'password' && (
+ setPasswordVisible((visible) => !visible)}
+ >
+ {passwordVisible ? : }
+
+ )}
+
)}
{error &&
{error}
}
diff --git a/src/components/forms/login-form.tsx b/src/components/forms/login-form.tsx
index c1139b4..fdcceac 100644
--- a/src/components/forms/login-form.tsx
+++ b/src/components/forms/login-form.tsx
@@ -4,11 +4,21 @@ import React, { useState, useRef } from 'react';
import { useRouter } from 'next/navigation';
import LabeledInput from '@/components/custom-ui/labeled-input';
-import { Button } from '@/components/ui/button';
import useZodForm from '@/lib/hooks/useZodForm';
import { loginSchema, registerSchema } from '@/lib/auth/validation';
import { loginAction } from '@/lib/auth/login';
import { registerAction } from '@/lib/auth/register';
+import { IconButton } from '../buttons/icon-button';
+import {
+ FileKey,
+ FileKey2,
+ LogIn,
+ MailOpen,
+ RotateCcwKey,
+ UserCheck,
+ UserPen,
+ UserPlus,
+} from 'lucide-react';
function LoginFormElement({
setIsSignUp,
@@ -56,6 +66,7 @@ function LoginFormElement({
-
+
Login
-
-
+ {
@@ -81,9 +98,10 @@ function LoginFormElement({
setIsSignUp((v) => !v);
}}
data-cy='register-switch'
+ icon={UserPlus}
>
Sign Up
-
+
{formState.errors.root?.message && (
@@ -156,27 +174,30 @@ function RegisterFormElement({
{...register('lastName')}
data-cy='last-name-input'
/>
-
+
-
+
Sign Up
-
-
+ {
formRef?.current?.reset();
setIsSignUp((v) => !v);
}}
+ icon={LogIn}
>
Back to Login
-
+
{formState.errors.root?.message && (
diff --git a/src/components/misc/profile-picture-upload.tsx b/src/components/misc/profile-picture-upload.tsx
new file mode 100644
index 0000000..d773873
--- /dev/null
+++ b/src/components/misc/profile-picture-upload.tsx
@@ -0,0 +1,36 @@
+import Image from 'next/image';
+import { Avatar } from '../ui/avatar';
+import { useGetApiUserMe } from '@/generated/api/user/user';
+import { User } from 'lucide-react';
+
+import { Input } from '../ui/input';
+
+export default function ProfilePictureUpload({
+ className,
+ ...props
+}: {
+ className?: string;
+} & React.InputHTMLAttributes
) {
+ const { data } = useGetApiUserMe();
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/src/components/misc/user-card.tsx b/src/components/misc/user-card.tsx
index faefc35..457d0fc 100644
--- a/src/components/misc/user-card.tsx
+++ b/src/components/misc/user-card.tsx
@@ -21,7 +21,7 @@ export default function UserCard() {
)}
{data?.data.user.name}
-
+
{data?.data.user.email}
diff --git a/src/components/misc/user-dropdown.tsx b/src/components/misc/user-dropdown.tsx
index e55f4bb..d9af19d 100644
--- a/src/components/misc/user-dropdown.tsx
+++ b/src/components/misc/user-dropdown.tsx
@@ -17,6 +17,7 @@ import UserCard from '@/components/misc/user-card';
export default function UserDropdown() {
const { data } = useGetApiUserMe();
+
return (
@@ -41,11 +42,13 @@ export default function UserDropdown() {
- Settings
+
+ Settings
+
-
- Logout
-
+
+ Logout
+
);
diff --git a/src/components/settings/settings-dropdown.tsx b/src/components/settings/settings-dropdown.tsx
new file mode 100644
index 0000000..6c23d07
--- /dev/null
+++ b/src/components/settings/settings-dropdown.tsx
@@ -0,0 +1,165 @@
+'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,
+ Key,
+} from 'lucide-react';
+
+interface SettingsSection {
+ label: string;
+ value: string;
+ description: string;
+ icon: React.ComponentType<{ className?: string }>;
+}
+
+interface SettingsDropdownProps {
+ currentSection: string;
+ onSectionChange: (section: string) => void;
+ className?: string;
+}
+
+const settingsSections: SettingsSection[] = [
+ {
+ label: 'Account',
+ value: 'general',
+ description: 'Manage account details',
+ icon: User,
+ },
+ {
+ label: 'Password',
+ value: 'password',
+ description: 'Manage your password',
+ icon: Key,
+ },
+ {
+ label: 'Notifications',
+ value: 'notifications',
+ description: 'Choose notification Preferences',
+ icon: Bell,
+ },
+ {
+ label: 'Calendar',
+ value: 'calendarAvailability',
+ description: 'Manage calendar display, availability and iCal integration',
+ icon: Calendar,
+ },
+ {
+ label: 'Privacy',
+ value: 'sharingPrivacy',
+ description: 'Control who can see your calendar and book time with you',
+ icon: Shield,
+ },
+ {
+ label: 'Appearance',
+ value: 'appearance',
+ description: 'Customize the look and feel of the application',
+ icon: Palette,
+ },
+];
+
+export function SettingsDropdown({
+ currentSection,
+ onSectionChange,
+ className,
+}: SettingsDropdownProps) {
+ const [open, setOpen] = useState(false);
+
+ const currentSectionData = settingsSections.find(
+ (section) => section.value === currentSection,
+ );
+ const CurrentIcon = currentSectionData?.icon || User;
+
+ const handleSelect = (value: string) => {
+ onSectionChange(value);
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
{currentSectionData?.label}
+
+ {currentSectionData?.description}
+
+
+
+
+
+
+
+
+
+
+ No settings found.
+
+ {settingsSections.map((section) => {
+ const Icon = section.icon;
+ return (
+ handleSelect(section.value)}
+ className='flex items-center justify-between p-3'
+ >
+
+
+
+
{section.label}
+
+ {section.description}
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ );
+}
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 */}
+
+ {renderSettingsContent()}
+
+
+ );
+}
diff --git a/src/components/settings/tabs/account.tsx b/src/components/settings/tabs/account.tsx
new file mode 100644
index 0000000..0b8fe60
--- /dev/null
+++ b/src/components/settings/tabs/account.tsx
@@ -0,0 +1,287 @@
+'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';
+import { toast } from 'sonner';
+import { ToastInner } from '@/components/misc/toast-inner';
+
+export default function AccountTab() {
+ const router = useRouter();
+ const { data, refetch } = useGetApiUserMe();
+ const deleteUser = useDeleteApiUserMe();
+ const updateAccount = usePatchApiUserMe();
+
+ const { handleSubmit, formState, register } = 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,
+ name:
+ submitData?.name !== data?.data.user.name
+ ? submitData?.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: () => {
+ refetch();
+ toast.custom((t) => (
+
+ ));
+ },
+ onError: (error) => {
+ toast.custom((t) => (
+
+ ));
+ },
+ },
+ );
+ });
+
+ if (!data) {
+ return (
+
+
+
+
Loading Settings...
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/settings/tabs/appearance.tsx b/src/components/settings/tabs/appearance.tsx
new file mode 100644
index 0000000..46113ab
--- /dev/null
+++ b/src/components/settings/tabs/appearance.tsx
@@ -0,0 +1,55 @@
+'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 --------------------*/}
+
+
+ Theme
+
+
+
+ {/*-------------------- Change Theme --------------------*/}
+
+
+
+
+
+
+ router.back()}
+ variant='secondary'
+ type='button'
+ >
+ Exit
+
+ Save Changes
+
+
+ >
+ );
+}
diff --git a/src/components/settings/tabs/calendar.tsx b/src/components/settings/tabs/calendar.tsx
new file mode 100644
index 0000000..02feb47
--- /dev/null
+++ b/src/components/settings/tabs/calendar.tsx
@@ -0,0 +1,226 @@
+'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 Format
+
+
+
+
+
+ DD/MM/YYYY
+ MM/DD/YYYY
+ YYYY-MM-DD
+
+
+
+
+ Time Format
+
+
+
+
+
+ 24-hour
+ 12-hour
+
+
+
+
+
+ {/*-------------------- Date & Time Format --------------------*/}
+ {/*-------------------- Calendar --------------------*/}
+
+
+
+
+ Default Calendar View
+
+
+
+
+
+
+ Day
+ Week
+ Month
+
+
+
+
+ Week Starts On
+
+
+
+
+
+ Sunday
+ Monday
+
+
+
+
+
+ Show Weekends
+
+
+
+
+
+ {/*-------------------- Calendar --------------------*/}
+ {/*-------------------- Availability --------------------*/}
+
+
+
+ Working Hours
+
+ Define your typical available hours (e.g., Monday-Friday,
+ 9 AM - 5 PM).
+
+
+ Set Working Hours
+
+
+
+
+
+
+
+
+
+
+ {/*-------------------- Availability --------------------*/}
+ {/*-------------------- iCalendar Integration --------------------*/}
+
+
+
+
+ Export Your Calendar
+
+ Get iCal Export URL
+
+
+ Download .ics File
+
+
+
+
+ {/*-------------------- iCalendar Integration --------------------*/}
+
+
+
+
+
+
+ router.back()}
+ variant='secondary'
+ type='button'
+ >
+ Exit
+
+ Save Changes
+
+
+ >
+ );
+}
diff --git a/src/components/settings/tabs/notifications.tsx b/src/components/settings/tabs/notifications.tsx
new file mode 100644
index 0000000..f2862a1
--- /dev/null
+++ b/src/components/settings/tabs/notifications.tsx
@@ -0,0 +1,134 @@
+'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 --------------------*/}
+
+
+
+ Enable All Email Notifications
+
+
+
+
+ {/*-------------------- All --------------------*/}
+ {/*-------------------- Meetings --------------------*/}
+
+
+
+
+ New Meeting Bookings
+
+
+
+
+
+ Meeting Confirmations/Cancellations
+
+
+
+
+
+ Meeting Reminders
+
+
+
+
+
+
+
+
+ Remind me before
+
+
+
+
+
+
+ 15 minutes
+ 30 minutes
+ 1 hour
+ 1 day
+
+
+
+
+
+ {/*-------------------- Meetings --------------------*/}
+ {/*-------------------- Social --------------------*/}
+
+
+
+
+ Friend Requests
+
+
+
+
+
+ Group Invitations/Updates
+
+
+
+
+
+ {/*-------------------- Social --------------------*/}
+
+
+
+
+
+
+ router.back()}
+ variant='secondary'
+ type='button'
+ >
+ Exit
+
+ Save Changes
+
+
+ >
+ );
+}
diff --git a/src/components/settings/tabs/password.tsx b/src/components/settings/tabs/password.tsx
new file mode 100644
index 0000000..53203c4
--- /dev/null
+++ b/src/components/settings/tabs/password.tsx
@@ -0,0 +1,151 @@
+'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 (
+
+ );
+}
diff --git a/src/components/settings/tabs/privacy.tsx b/src/components/settings/tabs/privacy.tsx
new file mode 100644
index 0000000..a0c5f2e
--- /dev/null
+++ b/src/components/settings/tabs/privacy.tsx
@@ -0,0 +1,143 @@
+'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 Calendar Visibility
+
+
+ Default setting for new friends.
+
+
+
+
+
+
+
+ Private (Only You)
+
+ Free/Busy
+
+ Full Details
+
+
+
+
+
+
+ Who Can See Your Full Calendar Details?
+
+
+ (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.
+
+
+
+
+
+
+
+ Only Me
+ My Friends
+
+ Specific Friends/Groups (manage separately)
+
+
+
+
+
+
+ Who Can Book Time With You?
+
+
+
+
+
+
+ No One
+ My Friends
+
+ Specific Friends/Groups (manage separately)
+
+
+
+
+
+
+ Blocked Users
+
+ Prevent specific users from seeing your calendar or
+ booking time.
+
+
+ Manage Blocked Users
+
+
+
+
+
+ {/*-------------------- Privacy Settigs --------------------*/}
+
+
+
+
+
+
+ router.back()}
+ variant='secondary'
+ type='button'
+ >
+ Exit
+
+ Save Changes
+
+
+ >
+ );
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index a5eec23..b6c034f 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -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: {
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
index 7ba53ea..940a845 100644
--- a/src/components/ui/card.tsx
+++ b/src/components/ui/card.tsx
@@ -126,7 +126,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
);
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index e481c76..5f6b7c5 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -69,7 +69,7 @@ function DialogContent({
{showCloseButton && (
Close
diff --git a/src/components/wrappers/group-wrapper.tsx b/src/components/wrappers/group-wrapper.tsx
new file mode 100644
index 0000000..3006c9a
--- /dev/null
+++ b/src/components/wrappers/group-wrapper.tsx
@@ -0,0 +1,23 @@
+import { cn } from '@/lib/utils';
+import type * as React from 'react';
+
+interface ScrollableSettingsWrapperProps {
+ className?: string;
+ title?: string;
+ children: React.ReactNode;
+}
+
+export function GroupWrapper({
+ className,
+ title,
+ children,
+}: ScrollableSettingsWrapperProps) {
+ return (
+
+ {title}
+ {children}
+
+ );
+}
diff --git a/src/components/wrappers/settings-scroll.tsx b/src/components/wrappers/settings-scroll.tsx
index e0f7251..a647493 100644
--- a/src/components/wrappers/settings-scroll.tsx
+++ b/src/components/wrappers/settings-scroll.tsx
@@ -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 (
-
- {children}
-
+ {children}
);
-};
+}