Profiles page is donegit add -A!

This commit is contained in:
2025-09-04 14:23:24 -05:00
parent 500da1f8be
commit 56ea3e0904
13 changed files with 539 additions and 172 deletions

View File

@@ -1,8 +1,14 @@
'use server';
import { preloadQuery } from 'convex/nextjs';
import { api } from '~/convex/_generated/api';
import { AvatarUpload, ProfileHeader, UserInfoForm } from '@/components/layout/profile';
import { Card, Separator } from '@/components/ui';
import {
AvatarUpload,
ProfileHeader,
ResetPasswordForm,
SignOutForm,
UserInfoForm
} from '@/components/layout/profile';
const Profile = async () => {
const preloadedUser = await preloadQuery(api.auth.getUser);
@@ -13,6 +19,9 @@ const Profile = async () => {
<Separator />
<UserInfoForm preloadedUser={preloadedUser} />
<Separator />
<ResetPasswordForm />
<Separator />
<SignOutForm />
</Card>
);
};

View File

@@ -17,67 +17,56 @@ import {
FormLabel,
FormMessage,
Input,
Separator,
StatusMessage,
SubmitButton,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui';
import { toast } from 'sonner';
const signInFormSchema = z.object({
email: z.email({
message: 'Please enter a valid email address.',
}),
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters.',
password: z.string()
.regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,100}$/, {
message: 'Incorrect password. Does not meet requirements.'
})
.regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$/, {
message:
'Password must contain at least one digit, ' +
'one uppercase letter, one lowercase letter, ' +
'and one special character.',
}),
});
const signUpFormSchema = z
.object({
name: z.string().min(2, {
message: 'Name must be at least 2 characters.',
const signUpFormSchema = z.object({
name: z.string().min(2, {
message: 'Name must be at least 2 characters.',
}),
email: z.email({
message: 'Please enter a valid email address.',
}),
password: z.string()
.min(8, { message: 'Password must be at least 8 characters.' })
.max(100, { message: 'Password must be no more than 100 characters.' })
.regex(/[0-9]/, { message: 'Password must contain at least one digit.' })
.regex(/[a-z]/, {
message: 'Password must contain at least one lowercase letter.'
})
.regex(/[A-Z]/, {
message: 'Password must contain at least one uppercase letter.'
})
.regex(/[@#$%^&+=]/, {
message: 'Password must contain at least one special character.'
}),
email: z.email({
message: 'Please enter a valid email address.',
}),
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters.',
})
.regex(
/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$/,
{
message:
'Password must contain at least one digit, ' +
'one uppercase letter, one lowercase letter, ' +
'and one special character.',
},
),
confirmPassword: z.string().min(8, {
message: 'Password must be at least 8 characters.',
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match!',
path: ['confirmPassword'],
});
confirmPassword: z.string().min(8, {
message: 'Password must be at least 8 characters.',
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match!',
path: ['confirmPassword'],
});
export default function SignIn() {
const { signIn } = useAuthActions();
const [flow, setFlow] = useState<'signIn' | 'signUp'>('signIn');
const [statusMessage, setStatusMessage] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
@@ -97,25 +86,39 @@ export default function SignIn() {
});
const handleSignIn = async (values: z.infer<typeof signInFormSchema>) => {
const formData = new FormData();
formData.append('email', values.email);
formData.append('password', values.password);
formData.append('flow', flow);
setLoading(true);
try {
setLoading(true);
setStatusMessage('');
const formData = new FormData();
formData.append('email', values.email);
formData.append('password', values.password);
formData.append('flow', flow);
if (flow === 'signUp') {
formData.append('name', values.name);
if (values.confirmPassword !== values.password)
throw new ConvexError({ message: 'Passwords do not match!' });
}
await signIn('password', formData);
signInForm.reset();
router.push('/');
} catch (error) {
setStatusMessage(
`Error signing in: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
console.error('Error signing in:', error);
toast.error('Error signing in.');
} finally {
setLoading(false);
}
};
const handleSignUp = async (values: z.infer<typeof signUpFormSchema>) => {
const formData = new FormData();
formData.append('email', values.email);
formData.append('password', values.password);
formData.append('flow', flow);
formData.append('name', values.name);
setLoading(true);
try {
if (values.confirmPassword !== values.password)
throw new ConvexError('Passwords do not match.');
await signIn('password', formData);
signUpForm.reset();
router.push('/');
} catch (error) {
console.error('Error signing up:', error);
toast.error('Error signing up.');
} finally {
setLoading(false);
}
@@ -194,15 +197,6 @@ export default function SignIn() {
</FormItem>
)}
/>
{statusMessage && (
<StatusMessage
message={
statusMessage.toLowerCase().includes('error')
? { error: statusMessage }
: { success: statusMessage }
}
/>
)}
<SubmitButton
disabled={loading}
pendingText='Signing in...'
@@ -220,7 +214,7 @@ export default function SignIn() {
<CardContent>
<Form {...signUpForm}>
<form
onSubmit={signUpForm.handleSubmit(handleSignIn)}
onSubmit={signUpForm.handleSubmit(handleSignUp)}
className='flex flex-col space-y-8'
>
<FormField
@@ -301,15 +295,6 @@ export default function SignIn() {
</FormItem>
)}
/>
{statusMessage && (
<StatusMessage
message={
statusMessage.toLowerCase().includes('error')
? { error: statusMessage }
: { success: statusMessage }
}
/>
)}
<SubmitButton
disabled={loading}
pendingText='Signing Up...'

View File

@@ -52,10 +52,10 @@ const AvatarUpload = ({ preloadedUser }: AvatarUploadProps) => {
}
const uploadResponse = await result.json() as { storageId: Id<'_storage'> };
await updateUserImage({ storageId: uploadResponse.storageId });
toast('Profile picture updated.');
toast.success('Profile picture updated.');
} catch (error) {
console.error('Upload failed:', error);
toast('Upload failed. Please try again.');
toast.error('Upload failed. Please try again.');
} finally {
setIsUploading(false);
if (inputRef.current) inputRef.current.value = '';

View File

@@ -1,3 +1,5 @@
export { AvatarUpload } from './avatar-upload';
export { ProfileHeader } from './header';
export { ResetPasswordForm } from './reset-password';
export { SignOutForm } from './sign-out';
export { UserInfoForm } from './user-info';

View File

@@ -0,0 +1,164 @@
'use client';
import { useState } from 'react';
import { useAction } from 'convex/react';
import { api } from '~/convex/_generated/api';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
SubmitButton,
} from '@/components/ui';
import { toast } from 'sonner';
const formSchema = z.object({
currentPassword: z.string()
.regex(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,100}$/, {
message: 'Incorrect current password. Does not meet requirements.',
}),
newPassword: z.string()
.min(8, { message: 'New password must be at least 8 characters.' })
.max(100, { message: 'New password must be less than 100 characters.' })
.regex(/[0-9]/, { message: 'New password must contain at least one digit.' })
.regex(/[a-z]/, {
message: 'New password must contain at least one lowercase letter.'
})
.regex(/[A-Z]/, {
message: 'New password must contain at least one uppercase letter.'
})
.regex(/[@#$%^&+=]/, {
message: 'New password must contain at least one special character.'
}),
confirmPassword: z.string(),
})
.refine((data) => data.currentPassword !== data.newPassword, {
message: 'New password must be different from current password.',
path: ['newPassword'],
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: 'Passwords do not match.',
path: ['confirmPassword'],
});
export const ResetPasswordForm = () => {
const [loading, setLoading] = useState(false);
const changePassword = useAction(api.auth.updateUserPassword);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
currentPassword: '',
newPassword: '',
confirmPassword: '',
},
});
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
setLoading(true);
try {
const result = await changePassword({
currentPassword: values.currentPassword,
newPassword: values.newPassword,
});
if (result?.success) {
form.reset();
toast.success('Password updated successfully.');
}
} catch (error) {
console.error('Error updating password:', error);
toast.error('Error updating password.');
} finally {
setLoading(false)
}
};
return (
<>
<CardHeader className='pb-5'>
<CardTitle className='text-2xl'>Change Password</CardTitle>
<CardDescription>
Update your password to keep your account secure
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className='space-y-6'
>
<FormField
control={form.control}
name='currentPassword'
render={({ field }) => (
<FormItem>
<FormLabel>Current Password</FormLabel>
<FormControl>
<Input type='password' {...field} />
</FormControl>
<FormDescription>
Enter your current password.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='newPassword'
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<Input type='password' {...field} />
</FormControl>
<FormDescription>
Enter your new password. Must be at least 8 characters.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='confirmPassword'
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input type='password' {...field} />
</FormControl>
<FormDescription>
Please re-enter your new password to confirm.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className='flex justify-center'>
<SubmitButton
className='lg:w-1/3 w-2/3 text-[1.0rem]'
disabled={loading}
pendingText='Updating Password...'
>
Update Password
</SubmitButton>
</div>
</form>
</Form>
</CardContent>
</>
);
};

View File

@@ -0,0 +1,25 @@
'use client';
import { useRouter } from 'next/navigation';
import { useAuthActions } from '@convex-dev/auth/react';
import {
CardHeader,
SubmitButton,
} from '@/components/ui';
export const SignOutForm = () => {
const { signOut } = useAuthActions();
const router = useRouter();
return (
<div className='flex justify-center'>
<SubmitButton
className='lg:w-2/3 w-5/6
text-[1.0rem] font-semibold cursor-pointer
hover:bg-red-700/60 dark:hover:bg-red-300/80'
onClick={() => void signOut().then(() => router.push('/signin'))}
>
Sign Out
</SubmitButton>
</div>
);
}

View File

@@ -69,6 +69,7 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
try {
await Promise.all(ops);
form.reset({ name, email});
toast.success('Profile updated successfully.');
} catch (error) {
console.error(error);
toast.error('Error updating profile.')