Profiles page is donegit add -A!
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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...'
|
||||
|
@@ -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 = '';
|
||||
|
@@ -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';
|
||||
|
164
src/components/layout/profile/reset-password.tsx
Normal file
164
src/components/layout/profile/reset-password.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
25
src/components/layout/profile/sign-out.tsx
Normal file
25
src/components/layout/profile/sign-out.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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.')
|
||||
|
Reference in New Issue
Block a user