Update form to look better & add automatic lunch toggle

This commit is contained in:
2025-09-17 21:13:02 -05:00
parent 3bff31c07a
commit 3d85e0c2e9
14 changed files with 231 additions and 165 deletions

View File

@@ -16,6 +16,7 @@
"@convex-dev/auth": "^0.0.81",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",

View File

@@ -60,5 +60,5 @@ const RootLayout = async ({
</PlausibleProvider>
</ConvexAuthNextjsServerProvider>
);
}
};
export default RootLayout;

View File

@@ -10,13 +10,14 @@ import {
} from 'convex/react';
import { api } from '~/convex/_generated/api';
import {
Avatar,
AvatarImage,
BasedAvatar,
Button,
CardContent,
ImageCrop,
ImageCropApply,
ImageCropContent,
ImageCropReset,
Input,
} from '@/components/ui';
import { toast } from 'sonner';
@@ -121,7 +122,7 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
<BasedAvatar
src={currentImageUrl ?? undefined}
fullName={user?.name}
className='h-32 w-32'
className='h-42 w-42'
fallbackProps={{ className: 'text-4xl font-semibold' }}
userIconProps={{ size: 100 }}
/>
@@ -173,7 +174,6 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
<ImageCropContent className='max-w-sm' />
<div className='flex items-center gap-2'>
<ImageCropApply />
<ImageCropReset />
<Button
onClick={handleReset}
size='icon'
@@ -190,19 +190,17 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
{/* Cropped preview + actions */}
{croppedImage && (
<div className='flex flex-col items-center gap-3'>
<Image
alt='Cropped preview'
className='overflow-hidden rounded-full'
height={128}
src={croppedImage}
unoptimized
width={128}
/>
<div className='flex items-center gap-2'>
<Avatar className='h-42 w-42'>
<AvatarImage
alt='Cropped preview'
src={croppedImage}
/>
</Avatar>
<div className='flex items-center gap-1'>
<Button
onClick={handleSave}
disabled={isUploading}
className='px-6'
className='px-4'
>
{isUploading ? (
<span className='inline-flex items-center gap-2'>
@@ -217,7 +215,10 @@ export const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
onClick={handleReset}
size='icon'
type='button'
variant='ghost'
className='dark:bg-red-500/30 bg-red-400/80
hover:dark:text-red-300/60 hover:text-red-800/80
hover:dark:bg-accent'
variant='secondary'
>
<XIcon className='size-4' />
</Button>

View File

@@ -97,7 +97,7 @@ export const ResetPasswordForm = () => {
return (
<>
<CardHeader className='pb-5'>
<CardHeader>
<CardTitle className='text-2xl'>Change Password</CardTitle>
<CardDescription>
Update your password to keep your account secure

View File

@@ -7,6 +7,10 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
Checkbox,
Form,
FormControl,
FormDescription,
@@ -35,7 +39,8 @@ const formSchema = z.object({
lunchTime: z
.string()
.trim()
.min(3, { message: 'Must be a valid 24-hour time. Example: 13:00'}),
.min(3, { message: 'Must be a valid 24-hour time. Example: 13:00' }),
automaticLunch: z.boolean(),
});
type UserInfoFormProps = {
@@ -49,13 +54,16 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
const updateUserName = useMutation(api.auth.updateUserName);
const updateUserEmail = useMutation(api.auth.updateUserEmail);
const updateUserLunchtime = useMutation(api.auth.updateUserLunchtime);
const updateUserAutomaticLunch = useMutation(api.auth.updateUserAutomaticLunch);
const initialValues = useMemo<z.infer<typeof formSchema>>(
() => ({
name: user?.name ?? '',
email: user?.email ?? '',
lunchTime: user?.lunchTime ?? '',
}), [user?.name, user?.email, user?.lunchTime]
lunchTime: user?.lunchTime ?? '12:00',
automaticLunch: user?.automaticLunch ?? false,
}),
[user?.name, user?.email, user?.lunchTime, user?.automaticLunch],
);
const form = useForm<z.infer<typeof formSchema>>({
@@ -68,10 +76,13 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
const name = values.name.trim();
const email = values.email.trim().toLowerCase();
const lunchTime = values.lunchTime.trim();
const automaticLunch = values.automaticLunch;
if (name !== (user?.name ?? '')) ops.push(updateUserName({ name }));
if (email !== (user?.email ?? '')) ops.push(updateUserEmail({ email }));
if (lunchTime !== (user?.lunchTime ?? ''))
ops.push(updateUserLunchtime({ lunchTime }))
ops.push(updateUserLunchtime({ lunchTime }));
if (automaticLunch !== (user?.automaticLunch))
ops.push(updateUserAutomaticLunch({ automaticLunch }));
if (ops.length === 0) return;
setLoading(true);
try {
@@ -87,65 +98,103 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
};
return (
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-6'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<>
<CardHeader>
<CardTitle className='text-2xl'>Account Information</CardTitle>
<CardDescription>
Update your account information here.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-6'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Your email address associated with your account.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Your email address associated with your account.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className='flex flex-row justify-center space-x-6'>
<FormField
control={form.control}
name='lunchTime'
render={({ field }) => (
<FormItem className='w-2/5'>
<FormLabel>Lunch Time</FormLabel>
<FormControl>
<Input
type='time'
className='max-w-26'
{...field}
/>
</FormControl>
<FormDescription>
Your regular lunch time.
</FormDescription>
<FormDescription className='dark:text-red-300/60 text-red-800/80'>
{!user?.lunchTime && 'Not currently set.'}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='automaticLunch'
render={({ field }) => (
<FormItem className='w-2/5'>
<div className='flex flex-row space-x-2 my-auto'>
<FormControl>
<Checkbox
className='border-solid border-primary'
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>Automatic Lunch</FormLabel>
</div>
<FormDescription>
Automatically take your lunch at the time you specify.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='lunchTime'
render={({ field }) => (
<FormItem>
<FormLabel>Regular Lunch Time</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Enter the time you take your lunch most often in 24 hour time.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className='flex justify-center'>
<SubmitButton disabled={loading} pendingText='Saving...'>
Save Changes
</SubmitButton>
</div>
</form>
</Form>
</CardContent>
<div className='flex justify-center'>
<SubmitButton disabled={loading} pendingText='Saving...'>
Save Changes
</SubmitButton>
</div>
</form>
</Form>
</CardContent>
</>
);
};

View File

@@ -23,7 +23,7 @@ export const LunchReminder = () => {
const schedule = () => {
if (!lunchTime) return;
const ms = nextOccurrenceMs(lunchTime);
console.log('Ms = ', ms)
console.log('Ms = ', ms);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(() => {
@@ -31,7 +31,7 @@ export const LunchReminder = () => {
try {
if (
'Notification' in window &&
Notification.permission === 'granted'
Notification.permission === 'granted'
) {
new Notification('Lunch time?', {
body: 'Update your status to "At lunch"?',
@@ -41,8 +41,7 @@ export const LunchReminder = () => {
} catch {}
toast('Lunch time?', {
description:
'Would you like to set your status to "At lunch"?',
description: 'Would you like to set your status to "At lunch"?',
action: {
label: 'Set to lunch',
onClick: () => void setStatus({ message: 'At lunch' }),

View File

@@ -23,8 +23,7 @@ export const NotificationsPermission = () => {
if (Notification.permission === 'default') {
toast('Enable system notifications?', {
id: 'enable-notifications',
description:
'Get a native notification at lunch time (optional).',
description: 'Get a native notification at lunch time (optional).',
action: {
label: 'Enable',
onClick: () => {

View File

@@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -11,6 +11,7 @@ export {
CardDescription,
CardContent,
} from './card';
export { Checkbox } from './checkbox';
export {
Drawer,
DrawerPortal,

View File

@@ -19,7 +19,7 @@ export default convexAuthNextjsMiddleware(
return nextjsMiddlewareRedirect(request, '/signin');
}
},
{ cookieConfig: { maxAge: 60 * 60 * 24 * 30 }},
{ cookieConfig: { maxAge: 60 * 60 * 24 * 30 } },
);
export const config = {