feat: tempcommit (broken input)

This commit is contained in:
Maximilian Liebmann 2025-06-29 22:09:18 +02:00
parent 7d2d5c55e8
commit ba6d21eec5
6 changed files with 335 additions and 203 deletions

View file

@ -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;
icon?: ForwardRefExoticComponent<
Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>
>;
children: React.ReactNode;
} & React.ComponentProps<typeof Button>) {
return (
<Button type='button' variant='secondary' {...props}>
<FontAwesomeIcon icon={icon} className='mr-2' />
{icon && React.createElement(icon, { className: 'mr-2' })}
{children}
</Button>
);

View file

@ -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}

View file

@ -62,6 +62,7 @@ export function AppSidebar() {
<>
<Sidebar collapsible='icon' variant='sidebar'>
<SidebarHeader className='overflow-hidden'>
<Link href='/home'>
<Logo
colorType='colored'
logoType='combo'
@ -74,6 +75,7 @@ export function AppSidebar() {
height={50}
className='group-data-[collapsible=]:hidden group-data-[mobile=true]/mobile:hidden'
></Logo>
</Link>
</SidebarHeader>
<SidebarContent className='grid grid-rows-[auto_1fr_auto]'>
<Collapsible defaultOpen className='group/collapsible'>

View file

@ -1,8 +1,8 @@
import { Input, Textarea } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import React from 'react';
import React, { ForwardRefExoticComponent, RefAttributes } from 'react';
import { Button } from '../ui/button';
import { Eye, EyeOff } from 'lucide-react';
import { Eye, EyeOff, LucideProps } from 'lucide-react';
import { cn } from '@/lib/utils';
export default function LabeledInput({
@ -12,6 +12,7 @@ export default function LabeledInput({
placeholder,
value,
name,
icon,
variantSize = 'default',
autocomplete,
error,
@ -22,11 +23,22 @@ export default function LabeledInput({
placeholder?: string;
value?: string;
name?: string;
icon?: ForwardRefExoticComponent<
Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>
>;
variantSize?: 'default' | 'big' | 'textarea';
autocomplete?: string;
error?: string;
} & React.InputHTMLAttributes<HTMLInputElement>) {
const [passwordVisible, setPasswordVisible] = React.useState(false);
const [inputValue, setInputValue] = React.useState(value || '');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
if (rest.onChange) {
rest.onChange(e);
}
};
return (
<div className='grid grid-cols-1 gap-1'>
@ -50,18 +62,32 @@ export default function LabeledInput({
className={cn(
type === 'password' ? 'pr-[50px]' : '',
variantSize === 'big'
? 'h-12 file:h-10 text-lg gplaceholder:text-lg sm:text-2xl sm:placeholder:text-2xl'
? 'h-12 file:h-10 text-lg placeholder:text-lg sm:text-2xl sm:placeholder:text-2xl'
: '',
icon && inputValue === '' ? 'pl-10' : '',
'transition-all duration-300 ease-in-out',
)}
type={passwordVisible ? 'text' : type}
placeholder={placeholder}
defaultValue={value}
value={inputValue}
id={name}
name={name}
autoComplete={autocomplete}
onChange={handleInputChange}
{...rest}
/>
{icon && (
<span
className={cn(
'absolute left-3 top-1/2 -translate-y-1/2 text-muted-input transition-all duration-300 ease-in-out',
inputValue === ''
? 'opacity-100 scale-100'
: 'opacity-0 scale-75 pointer-events-none',
)}
>
{React.createElement(icon)}
</span>
)}
{type === 'password' && (
<Button
className='absolute right-0 top-0 w-[36px] h-[36px]'
@ -74,7 +100,6 @@ export default function LabeledInput({
)}
</span>
)}
{error && <p className='text-red-500 text-sm mt-1'>{error}</p>}
</div>
);

View file

@ -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({
<LabeledInput
type='text'
label='E-Mail or Username'
icon={UserCheck}
placeholder='What you are known as'
error={formState.errors.email?.message}
{...register('email')}
@ -64,16 +75,22 @@ function LoginFormElement({
<LabeledInput
type='password'
label='Password'
icon={FileKey}
placeholder="Let's hope you remember it"
error={formState.errors.password?.message}
{...register('password')}
data-cy='password-input'
/>
<div className='grid grid-rows-2 gap-2'>
<Button type='submit' variant='primary' data-cy='login-button'>
<IconButton
type='submit'
variant='primary'
data-cy='login-button'
icon={LogIn}
>
Login
</Button>
<Button
</IconButton>
<IconButton
type='button'
variant='outline_primary'
onClick={() => {
@ -81,9 +98,10 @@ function LoginFormElement({
setIsSignUp((v) => !v);
}}
data-cy='register-switch'
icon={UserPlus}
>
Sign Up
</Button>
</IconButton>
</div>
<div>
{formState.errors.root?.message && (
@ -159,6 +177,7 @@ function RegisterFormElement({
<LabeledInput
type='email'
label='E-Mail'
icon={MailOpen}
placeholder='Your email address'
autocomplete='email'
error={formState.errors.email?.message}
@ -168,6 +187,7 @@ function RegisterFormElement({
<LabeledInput
type='text'
label='Username'
icon={UserPen}
placeholder='Your username'
autocomplete='username'
error={formState.errors.username?.message}
@ -177,6 +197,7 @@ function RegisterFormElement({
<LabeledInput
type='password'
label='Password'
icon={FileKey2}
placeholder='Create a password'
autocomplete='new-password'
error={formState.errors.password?.message}
@ -186,6 +207,7 @@ function RegisterFormElement({
<LabeledInput
type='password'
label='Confirm Password'
icon={RotateCcwKey}
placeholder='Repeat your password'
autocomplete='new-password'
error={formState.errors.confirmPassword?.message}
@ -193,19 +215,25 @@ function RegisterFormElement({
data-cy='confirm-password-input'
/>
<div className='grid grid-rows-2 gap-2'>
<Button type='submit' variant='primary' data-cy='register-button'>
<IconButton
type='submit'
variant='primary'
data-cy='register-button'
icon={UserPlus}
>
Sign Up
</Button>
<Button
</IconButton>
<IconButton
type='button'
variant='outline_primary'
onClick={() => {
formRef?.current?.reset();
setIsSignUp((v) => !v);
}}
icon={LogIn}
>
Back to Login
</Button>
</IconButton>
</div>
<div>
{formState.errors.root?.message && (

View file

@ -9,7 +9,7 @@ import {
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';
@ -28,8 +28,23 @@ import LabeledInput from '../custom-ui/labeled-input';
import { GroupWrapper } from '../wrappers/group-wrapper';
import ProfilePictureUpload from './profile-picture-upload';
import { LogOut } from 'lucide-react';
import { signOut } from '@/auth';
import {
CalendarArrowDown,
CalendarArrowUp,
CalendarCheck,
CalendarClock,
CalendarCog,
CalendarPlus,
CalendarPlus2,
ClockAlert,
ClockFading,
FileKey,
FileKey2,
MailOpen,
RotateCcwKey,
UserLock,
} from 'lucide-react';
import { IconButton } from '../buttons/icon-button';
export default function SettingsPage() {
const router = useRouter();
@ -46,7 +61,8 @@ export default function SettingsPage() {
<CardHeader>
<CardTitle>Account Settings</CardTitle>
</CardHeader>
<CardContent className='space-y-6 mt-2'>
<CardContent className='space-y-6 my-2'>
{/*-------------------- General Settings --------------------*/}
<GroupWrapper title='General Settings'>
<div className='space-y-4'>
<div>
@ -77,6 +93,7 @@ export default function SettingsPage() {
<LabeledInput
type='email'
label='Email Address'
icon={MailOpen}
placeholder='Your E-Mail'
defaultValue={data?.data.user.email ?? ''}
></LabeledInput>
@ -87,39 +104,42 @@ export default function SettingsPage() {
</div>
</div>
</GroupWrapper>
{/*-------------------- General Settings --------------------*/}
{/*-------------------- Reset Password --------------------*/}
<GroupWrapper title='Reset Password'>
<div className='flex items-center justify-evenly sm:flex-row flex-col gap-6'>
<div>
<LabeledInput
type='password'
label='Current Password'
placeholder='Current Password'
defaultValue={data?.data.user.first_name ?? ''}
icon={FileKey}
></LabeledInput>
</div>
<div>
<LabeledInput
type='password'
label='New Password'
placeholder='New Password'
defaultValue={data?.data.user.first_name ?? ''}
icon={FileKey2}
></LabeledInput>
</div>
<div>
<LabeledInput
type='password'
label='Repeat Password'
placeholder='Repeat Password'
defaultValue={data?.data.user.first_name ?? ''}
icon={RotateCcwKey}
></LabeledInput>
</div>
</div>
</GroupWrapper>
{/*-------------------- Reset Password --------------------*/}
{/*-------------------- Profile Picture --------------------*/}
<GroupWrapper title='Profile Picture'>
<div className='space-y-2 grid grid-cols-[1fr_auto]'>
<ProfilePictureUpload className='file:border file:rounded-md file:hover:bg-disabled-destructive' />
</div>
</GroupWrapper>
{/*-------------------- Profile Picture --------------------*/}
{/*-------------------- Regional Settings --------------------*/}
<GroupWrapper title='Regional Settings'>
<div className='space-y-2 grid sm:grid-cols-[1fr_auto] sm:flex-row gap-4'>
<div className='grid gap-1'>
@ -127,6 +147,7 @@ export default function SettingsPage() {
type='text'
label='Timezone'
placeholder='Europe/Berlin'
icon={CalendarClock}
defaultValue={data?.data.user.timezone ?? ''}
></LabeledInput>
</div>
@ -146,6 +167,8 @@ export default function SettingsPage() {
</div>
</div>
</GroupWrapper>
{/*-------------------- Regional Settings --------------------*/}
<GroupWrapper title='DANGER ZONE - INSTANT DELETE - NO CONFIRMATION'>
<div className='flex items-center justify-evenly sm:flex-row flex-col gap-6'>
<Button
onClick={() => {
@ -163,6 +186,7 @@ export default function SettingsPage() {
Permanently delete your account and all associated data.
</span>
</div>
</GroupWrapper>
</CardContent>
</ScrollableSettingsWrapper>
</Card>
@ -175,8 +199,9 @@ export default function SettingsPage() {
<CardHeader>
<CardTitle>Notification Preferences</CardTitle>
</CardHeader>
<CardContent className='space-y-6'>
<GroupWrapper>
<CardContent className='space-y-6 my-2'>
{/*-------------------- All --------------------*/}
<GroupWrapper title='All'>
<div className='flex items-center justify-between'>
<Label
htmlFor='masterEmailNotifications'
@ -187,7 +212,8 @@ export default function SettingsPage() {
<Switch id='masterEmailNotifications' />
</div>
</GroupWrapper>
{/*-------------------- All --------------------*/}
{/*-------------------- Meetings --------------------*/}
<GroupWrapper title='Meetings'>
<div className='space-y-4'>
<div className='flex items-center justify-between space-x-2'>
@ -239,6 +265,8 @@ export default function SettingsPage() {
</div>
</div>
</GroupWrapper>
{/*-------------------- Meetings --------------------*/}
{/*-------------------- Social --------------------*/}
<GroupWrapper title='Social'>
<div className='space-y-4'>
<div className='flex items-center justify-between space-x-2'>
@ -255,6 +283,7 @@ export default function SettingsPage() {
</div>
</div>
</GroupWrapper>
{/*-------------------- Social --------------------*/}
</CardContent>
</ScrollableSettingsWrapper>
</Card>
@ -267,8 +296,39 @@ export default function SettingsPage() {
<CardHeader>
<CardTitle>Calendar & Availability</CardTitle>
</CardHeader>
<CardContent className='space-y-6'>
{/*--------------------* Calendar --------------------*/}
<CardContent className='space-y-6 my-2'>
{/*-------------------- Date & Time Format --------------------*/}
<GroupWrapper title='Date & Time Format'>
<div className='flex flex-col space-y-4'>
<div className='grid grid-cols-1 gap-1'>
<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='grid grid-cols-1 gap-1'>
<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>
</div>
</GroupWrapper>
{/*-------------------- Date & Time Format --------------------*/}
{/*-------------------- Calendar --------------------*/}
<GroupWrapper title='Calendar'>
<div className='space-y-4'>
<div className='grid grid-cols-1 gap-1'>
@ -306,9 +366,8 @@ export default function SettingsPage() {
</div>
</div>
</GroupWrapper>
{/*--------------------* Calendar --------------------*/}
{/*--------------------* Availability --------------------*/}
{/*-------------------- Calendar --------------------*/}
{/*-------------------- Availability --------------------*/}
<GroupWrapper title='Availability'>
<div className='space-y-4'>
<div className='space-y-2'>
@ -325,8 +384,9 @@ export default function SettingsPage() {
<LabeledInput
type='text'
label='Minimum Notice for Bookings'
icon={ClockAlert}
subtext='Min time before a booking can be made.'
placeholder='e.g., 1h'
placeholder='e.g. 1h'
defaultValue={''}
></LabeledInput>
</div>
@ -334,43 +394,65 @@ export default function SettingsPage() {
<LabeledInput
type='text'
label='Booking Window (days in advance)'
icon={ClockFading}
subtext='Max time in advance a booking can be made.'
placeholder='e.g., 30d'
placeholder='e.g. 30d'
defaultValue={''}
></LabeledInput>
</div>
</div>
</GroupWrapper>
{/*--------------------* Availability --------------------*/}
{/*--------------------* iCalendar Integration --------------------*/}
<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'>
{/*-------------------- Availability --------------------*/}
{/*-------------------- iCalendar Integration --------------------*/}
<GroupWrapper title='iCalendar Integration'>
<div className='space-y-4'>
<form
onSubmit={(e) => {
e.preventDefault();
}}
className='space-y-2'
>
<LabeledInput
type='url'
label='Import iCal Feed URL'
subtext='Max time in advance a booking can be made.'
icon={CalendarCheck}
placeholder='https://calendar.example.com/feed.ics'
defaultValue={''}
name='icalUrl'
required
></LabeledInput>
<Button size='sm' className='mt-1'>
<IconButton
type='submit'
size='sm'
className='mt-1'
variant={'secondary'}
icon={CalendarPlus}
title='Submit iCal URL'
>
Add Feed
</Button>
</div>
</IconButton>
</form>
<div className='space-y-2'>
<Label>Export Your Calendar</Label>
<Button variant='outline_muted' size='sm'>
<IconButton
variant='outline_muted'
size='sm'
icon={CalendarArrowUp}
>
Get iCal Export URL
</Button>
<Button variant='outline_muted' size='sm' className='ml-2'>
</IconButton>
<IconButton
variant='outline_muted'
size='sm'
className='ml-2'
icon={CalendarArrowDown}
>
Download .ics File
</Button>
</IconButton>
</div>
</fieldset>
{/*--------------------* iCalendar Integration --------------------*/}
</div>
</GroupWrapper>
{/*-------------------- iCalendar Integration --------------------*/}
</CardContent>
</ScrollableSettingsWrapper>
</Card>
@ -383,11 +465,17 @@ export default function SettingsPage() {
<CardHeader>
<CardTitle>Sharing & Privacy</CardTitle>
</CardHeader>
<CardContent className='space-y-6'>
<div className='space-y-2'>
<CardContent className='space-y-6 my-2'>
{/*-------------------- Privacy Settigs --------------------*/}
<GroupWrapper title='Privacy Settings'>
<div className='flex flex-col space-y-4'>
<div className='grid grid-cols-1 gap-1'>
<Label htmlFor='defaultVisibility'>
Default Calendar Visibility
</Label>
<span className='text-sm text-muted-foreground'>
Default setting for new friends.
</span>
<Select>
<SelectTrigger id='defaultVisibility'>
<SelectValue placeholder='Select visibility' />
@ -396,16 +484,14 @@ export default function SettingsPage() {
<SelectItem value='private'>
Private (Only You)
</SelectItem>
<SelectItem value='freebusy'>
Free/Busy for Friends
</SelectItem>
<SelectItem value='freebusy'>Free/Busy</SelectItem>
<SelectItem value='fulldetails'>
Full Details for Friends
Full Details
</SelectItem>
</SelectContent>
</Select>
</div>
<div className='space-y-2'>
<div className='grid grid-cols-1 gap-1'>
<Label htmlFor='whoCanSeeFull'>
Who Can See Your Full Calendar Details?
</Label>
@ -413,9 +499,9 @@ export default function SettingsPage() {
(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.
This setting will override the default visibility for
your calendar. You can set specific friends or groups
to see your full calendar details.
</span>
</span>
<Select>
@ -431,7 +517,7 @@ export default function SettingsPage() {
</SelectContent>
</Select>
</div>
<div className='space-y-2'>
<div className='grid grid-cols-1 gap-1'>
<Label htmlFor='whoCanBook'>
Who Can Book Time With You?
</Label>
@ -448,14 +534,25 @@ export default function SettingsPage() {
</SelectContent>
</Select>
</div>
<div className='space-y-2'>
<div className='space-y-4'>
<div className='grid grid-cols-1 gap-1'>
<Label>Blocked Users</Label>
<Button variant='outline_muted'>Manage Blocked Users</Button>
<span className='text-sm text-muted-foreground'>
Prevent specific users from seeing your calendar or booking
time.
Prevent specific users from seeing your calendar or
booking time.
</span>
<IconButton
variant='outline_muted'
size='sm'
icon={UserLock}
>
Manage Blocked Users
</IconButton>
</div>
</div>
</div>
</GroupWrapper>
{/*-------------------- Privacy Settigs --------------------*/}
</CardContent>
</ScrollableSettingsWrapper>
</Card>
@ -468,36 +565,15 @@ export default function SettingsPage() {
<CardHeader>
<CardTitle>Appearance</CardTitle>
</CardHeader>
<CardContent className='space-y-6'>
<CardContent className='space-y-6 my-2'>
{/*-------------------- Change Theme --------------------*/}
<GroupWrapper title='Change Theme'>
<div className='space-y-2'>
<Label htmlFor='theme'>Theme</Label>
<ThemePicker />
</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>
</GroupWrapper>
{/*-------------------- Change Theme --------------------*/}
</CardContent>
</ScrollableSettingsWrapper>
</Card>