-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c3b656f
commit b852bf7
Showing
8 changed files
with
342 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
'use server'; | ||
|
||
import { campusFetch } from '@/lib/client'; | ||
|
||
export async function changeEmail(email: string) { | ||
try { | ||
await campusFetch('account/info', { | ||
method: 'PUT', | ||
body: JSON.stringify({ email }), | ||
}); | ||
} catch (error) { | ||
throw new Error('Error changing email.'); | ||
} | ||
} | ||
|
||
export async function changePhoto(file: File) { | ||
try { | ||
const formData = new FormData(); | ||
formData.append('file', file, file.name); | ||
|
||
await campusFetch('profile/photo', { | ||
method: 'POST', | ||
body: formData, | ||
}); | ||
} catch (error) { | ||
throw new Error('Error changing photo.'); | ||
} | ||
} | ||
|
||
export async function changePassword(newPassword: string, currentPassword: string) { | ||
try { | ||
const response = await campusFetch('account/info', { | ||
method: 'POST', | ||
body: JSON.stringify({ newPassword, currentPassword }), | ||
}); | ||
|
||
return response.json(); | ||
} catch (error) { | ||
throw new Error('Error changing password.'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { Heading1 } from '@/components/typography/headers'; | ||
import { SubLayout } from '../sub-layout'; | ||
import { useTranslations } from 'next-intl'; | ||
import { getTranslations } from 'next-intl/server'; | ||
import SettingsForm from '@/app/[locale]/(private)/settings/settings-form'; | ||
import { Paragraph } from '@/components/typography/paragraph'; | ||
|
||
const INTL_NAMESPACE = 'private.settings'; | ||
|
||
export async function generateMetadata({ params: { locale } }: any) { | ||
const t = await getTranslations({ locale, namespace: INTL_NAMESPACE }); | ||
|
||
return { | ||
title: t('title'), | ||
}; | ||
} | ||
|
||
export default function SettingsPage() { | ||
const t = useTranslations(INTL_NAMESPACE); | ||
|
||
return ( | ||
<SubLayout pageTitle={t('title')}> | ||
<div className="col-span-6"> | ||
<Heading1>{t('title')}</Heading1> | ||
<Paragraph className="text-neutral-700">{t('subtitle')}</Paragraph> | ||
<SettingsForm /> | ||
</div> | ||
</SubLayout> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
'use client'; | ||
|
||
import { Heading5 } from '@/components/typography/headers'; | ||
import { useTranslations } from 'next-intl'; | ||
import { ProfilePicture } from '@/app/[locale]/(private)/profile-picture'; | ||
import { Input } from '@/components/ui/input'; | ||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; | ||
import PasswordInput from '@/components/ui/password-input'; | ||
import { Button } from '@/components/ui/button'; | ||
import * as z from 'zod'; | ||
import { useForm } from 'react-hook-form'; | ||
import { zodResolver } from '@hookform/resolvers/zod'; | ||
import { useLocalStorage } from '@/hooks/use-storage'; | ||
import { useServerErrorToast } from '@/hooks/use-server-error-toast'; | ||
import { cn } from '@/lib/utils'; | ||
import { Card, CardContent } from '@/components/ui/card'; | ||
import { User } from '@/types/user'; | ||
import { useRef, useState } from 'react'; | ||
import { useToast } from '@/hooks/use-toast'; | ||
import { changeEmail, changePassword, changePhoto } from '@/actions/settings.actions'; | ||
import { useIsMobile } from '@/hooks/use-mobile'; | ||
|
||
const INTL_NAMESPACE = 'private.settings'; | ||
|
||
interface SettingsFormProps { | ||
className?: string; | ||
} | ||
|
||
export default function SettingsForm({ className }: SettingsFormProps) { | ||
const t = useTranslations(INTL_NAMESPACE); | ||
const { toast } = useToast(); | ||
const { errorToast } = useServerErrorToast(); | ||
|
||
const isMobile = useIsMobile(); | ||
|
||
const inputRef = useRef<HTMLInputElement>(null); | ||
|
||
const [user] = useLocalStorage<User>('user'); | ||
const [file, setFile] = useState<File | null>(null); | ||
const [previewImage, setPreviewImage] = useState(user?.photo); | ||
|
||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const files = e.target.files; | ||
const file = files?.[0]; | ||
if (!files || !file) { | ||
return; | ||
} | ||
setFile(file); | ||
setPreviewImage(URL.createObjectURL(file)); | ||
}; | ||
|
||
const handleButtonClick = () => { | ||
if (!inputRef || !inputRef.current) return; | ||
inputRef.current.click(); | ||
}; | ||
|
||
const FormSchema = z | ||
.object({ | ||
email: z | ||
.string() | ||
.min(1) | ||
.email({ message: t('error.invalid-email') }), | ||
currentPassword: z.string().optional(), | ||
newPassword: z.string().optional(), | ||
confirmPassword: z.string().optional(), | ||
}) | ||
.refine((data) => data.newPassword === data.confirmPassword, { | ||
message: t('error.password-mismatch'), | ||
path: ['confirmPassword'], | ||
}); | ||
|
||
type FormData = z.infer<typeof FormSchema>; | ||
|
||
const form = useForm({ | ||
resolver: zodResolver(FormSchema), | ||
defaultValues: { | ||
email: user?.email || '', | ||
currentPassword: '', | ||
newPassword: '', | ||
confirmPassword: '', | ||
}, | ||
}); | ||
|
||
const handleFormSubmit = async (data: FormData) => { | ||
try { | ||
form.clearErrors(); | ||
|
||
if (user?.email.trim() !== data.email.trim()) { | ||
await changeEmail(data.email.trim()); | ||
} | ||
|
||
if (file) { | ||
await changePhoto(file); | ||
} | ||
|
||
if (data.newPassword?.trim() && data.currentPassword?.trim()) { | ||
await changePassword(data.newPassword, data.currentPassword); | ||
} | ||
toast({ | ||
title: t('success.update'), | ||
}); | ||
} catch (error) { | ||
errorToast(); | ||
} | ||
}; | ||
|
||
return ( | ||
<Card className={cn(className)}> | ||
<CardContent className="flex flex-col gap-8 space-y-1.5 p-10"> | ||
<Heading5>{t('section.photo')}</Heading5> | ||
<div className="flex items-center gap-4"> | ||
<ProfilePicture variant="xl" src={previewImage} /> | ||
<Button className="h-fit" variant="secondary" onClick={handleButtonClick}> | ||
{t('button.edit')} | ||
</Button> | ||
<input ref={inputRef} type="file" hidden onChange={handleFileUpload} /> | ||
</div> | ||
<Form {...form}> | ||
<form onSubmit={form.handleSubmit(handleFormSubmit)} className="flex flex-col items-start space-y-4"> | ||
<Heading5>{t('section.contacts')}</Heading5> | ||
<FormField | ||
control={form.control} | ||
name="email" | ||
render={({ field }) => ( | ||
<FormItem className="my-6 grid w-full items-center gap-2"> | ||
<FormLabel htmlFor="email">E-mail</FormLabel> | ||
<FormControl> | ||
<Input {...field} placeholder="[email protected]" /> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
|
||
<Heading5>{t('section.password')}</Heading5> | ||
<FormField | ||
control={form.control} | ||
name="currentPassword" | ||
render={({ field }) => ( | ||
<FormItem className="my-6 grid w-full items-center gap-2"> | ||
<FormLabel htmlFor="currentPassword">{t('field.current-password')}</FormLabel> | ||
<PasswordInput {...field} value={field.value || ''} /> | ||
</FormItem> | ||
)} | ||
/> | ||
<FormField | ||
control={form.control} | ||
name="newPassword" | ||
render={({ field }) => ( | ||
<FormItem className="my-6 grid w-full items-center gap-2"> | ||
<FormLabel htmlFor="newPassword">{t('field.new-password')}</FormLabel> | ||
<PasswordInput {...field} value={field.value || ''} /> | ||
</FormItem> | ||
)} | ||
/> | ||
<FormField | ||
control={form.control} | ||
name="confirmPassword" | ||
render={({ field }) => ( | ||
<FormItem className="my-6 grid w-full items-center gap-2"> | ||
<FormLabel htmlFor="confirmPassword">{t('field.confirm-password')}</FormLabel> | ||
<PasswordInput {...field} value={field.value || ''} /> | ||
{form.formState.errors.confirmPassword && ( | ||
<span className="text-red-500">{form.formState.errors.confirmPassword?.message}</span> | ||
)} | ||
</FormItem> | ||
)} | ||
/> | ||
<FormMessage className="text-red-500">{form.formState.errors.root?.message}</FormMessage> | ||
<Button | ||
size={isMobile ? 'medium' : 'big'} | ||
className="my-4 ml-auto" | ||
type="submit" | ||
// disabled={!form.formState.isValid} | ||
loading={form.formState.isSubmitting} | ||
> | ||
{t('button.save-changes')} | ||
</Button> | ||
</form> | ||
</Form> | ||
</CardContent> | ||
</Card> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters