Skip to content

Commit

Permalink
Merge pull request #340 from kpi-ua/KB-187-Implement-profile-page
Browse files Browse the repository at this point in the history
KB-187 implement profile page
  • Loading branch information
niravzi authored Jan 14, 2025
2 parents 3e923e3 + 9e76218 commit 3115d0c
Show file tree
Hide file tree
Showing 33 changed files with 4,134 additions and 4,688 deletions.
7,309 changes: 2,678 additions & 4,631 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.4",
"@radix-ui/react-tooltip": "^1.1.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
Expand Down
6 changes: 3 additions & 3 deletions src/actions/auth.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function loginWithCredentials(username: string, password: string, r
return null;
}

const userResponse = await campusFetch('account/info', {
const userResponse = await campusFetch('profile', {
method: 'GET',
headers: {
Authorization: `Bearer ${jsonResponse.access_token}`,
Expand All @@ -47,7 +47,7 @@ export async function loginWithCredentials(username: string, password: string, r
return null;
}

const user = await userResponse.json();
const user: User = await userResponse.json();

const maxAge = rememberMe ? COOKIE_MAX_AGE : undefined;

Expand Down Expand Up @@ -90,7 +90,7 @@ export async function resetPassword(username: string, recaptchaToken: string) {
}

export async function getUserDetails(): Promise<User | null> {
const userResponse = await campusFetch('account/info', {
const userResponse = await campusFetch('profile', {
method: 'GET',
});

Expand Down
108 changes: 108 additions & 0 deletions src/actions/profile.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use server';

import { Contact, ContactType } from '@/types/contact';
import { campusFetch } from '@/lib/client';
import { revalidatePath } from 'next/cache';
import { getUserDetails } from '@/actions/auth.actions';

export async function getContacts(): Promise<Contact[]> {
try {
const response = await campusFetch('profile/contacts');

return response.json();
} catch (error) {
return [];
}
}

export async function getContactTypes(): Promise<ContactType[]> {
try {
const response = await campusFetch('profile/contacts/types');

return response.json();
} catch (error) {
return [];
}
}

export async function createContact(typeId: number, value: string) {
try {
await campusFetch('profile/contacts', {
method: 'POST',
body: JSON.stringify({ typeId, value }),
});
revalidatePath('/profile');
} catch (error) {
throw new Error('Error while creating contact');
}
}

export async function updateContact(id: number, typeId: number, value: string) {
try {
await campusFetch(`profile/contacts/${id}`, {
method: 'PUT',
body: JSON.stringify({ typeId, value }),
});
revalidatePath('/profile');
} catch (error) {
throw new Error('Error while updating contact');
}
}

export async function deleteContact(id: number) {
try {
await campusFetch(`profile/contacts/${id}`, {
method: 'DELETE',
});
revalidatePath('/profile');
} catch (error) {
throw new Error('Error while deleting contact');
}
}

export async function setIntellectAgreement(agree: boolean) {
try {
await campusFetch('profile/intellect/agreement', {
method: 'POST',
body: JSON.stringify({ agree }),
});
return getUserDetails();
} catch (error) {
throw new Error('Error while setting intellect agreement');
}
}

export async function updateEnglishFullName(fullNameEnglish: string) {
try {
await campusFetch('profile', {
method: 'PUT',
body: JSON.stringify({ fullNameEnglish }),
});
return getUserDetails();
} catch (error) {
throw new Error('Error while updating English full name');
}
}

export async function updateIntellectInfo(credo: string, scientificInterests: string) {
try {
await campusFetch('profile/intellect', {
method: 'PUT',
body: JSON.stringify({ credo, scientificInterests }),
});
return getUserDetails();
} catch (error) {
throw new Error('Error while updating intellect info');
}
}

export async function acceptCodeOfHonor() {
try {
await campusFetch('profile/code-of-honor', {
method: 'PUT',
});
return getUserDetails();
} catch (error) {
throw new Error('Error while accepting code of honor');
}
}
67 changes: 67 additions & 0 deletions src/app/[locale]/(private)/profile/components/code-of-honor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client';

import { Heading6 } from '@/components/typography/headers';
import { Separator } from '@/components/ui/separator';
import { Paragraph } from '@/components/typography/paragraph';
import { Button } from '@/components/ui/button';
import { useIsMobile } from '@/hooks/use-mobile';
import { useLocalStorage } from '@/hooks/use-storage';
import { User } from '@/types/user';
import { acceptCodeOfHonor } from '@/actions/profile.actions';
import React, { useState } from 'react';
import { useTranslations } from 'next-intl';
import { Check } from '@/app/images';
import { useServerErrorToast } from '@/hooks/use-server-error-toast';
import { Link } from '@/i18n/routing';

export function CodeOfHonor() {
const { errorToast } = useServerErrorToast();

const isMobile = useIsMobile();

const [user, setUser] = useLocalStorage<User>('user');

const [loading, setLoading] = useState(false);

const t = useTranslations('private.profile');

const handleAcceptCodeOfHonor = async () => {
setLoading(true);
const res = await acceptCodeOfHonor();
setLoading(false);

if (!res) {
errorToast();
return;
}
setUser(res);
};

return (
<div className="flex flex-col gap-3">
<Heading6>{t('codeOfHonor.title')}</Heading6>
<Separator />
{t.rich('codeOfHonor.content', {
documentsLink: (chunks) => <Link href="/kpi-documents">{chunks}</Link>,
paragraph: (chunks) => <Paragraph className="m-0 text-lg">{chunks}</Paragraph>,
})}
{user?.codeOfHonorSignDate ? (
<div className="flex flex-col gap-1">
<Paragraph>{t('codeOfHonor.agreement')}</Paragraph>
<Paragraph className="m-0">{user?.codeOfHonorSignDate}</Paragraph>
</div>
) : (
<Button
className="ml-auto w-fit"
loading={loading}
onClick={handleAcceptCodeOfHonor}
size={isMobile ? 'medium' : 'big'}
icon={<Check />}
iconPosition="end"
>
{t('button.agree')}
</Button>
)}
</div>
);
}
134 changes: 134 additions & 0 deletions src/app/[locale]/(private)/profile/components/contacts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use client';

import { Heading6 } from '@/components/typography/headers';
import { Separator } from '@/components/ui/separator';
import { Input } from '@/components/ui/input';
import { Contact, ContactType } from '@/types/contact';
import { createContact, deleteContact, updateContact } from '@/actions/profile.actions';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form';
import * as z from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useTranslations } from 'next-intl';
import { EditableField } from '@/app/[locale]/(private)/profile/components/editable-field';
import { Fragment } from 'react';

interface Props {
contacts: Contact[];
contactTypes: ContactType[];
}

export function Contacts({ contacts, contactTypes }: Props) {
const t = useTranslations('private.profile');

const FormSchema = z.object({
contactValue: z.string().trim().min(1),
typeId: z.string().min(1),
});

type FormData = z.infer<typeof FormSchema>;

const form = useForm({
resolver: zodResolver(FormSchema),
defaultValues: {
contactValue: '',
typeId: '',
},
});

const handleFormSubmit = async (data: FormData) => {
await createContact(parseInt(data.typeId), data.contactValue);
form.reset();
};

const handleDeleteContact = async (id: number) => {
await deleteContact(id);
form.reset();
};

const handleUpdateContact = async (id: number, typeId: number, value: string) => {
await updateContact(id, typeId, value);
};

return (
<div className="flex flex-col">
<div className="flex w-full flex-col gap-3">
<Heading6>{t('contact.title')}</Heading6>
<Separator />
<div className="flex w-full flex-col gap-4">
{contacts.map((contact) => (
<Fragment key={contact.id}>
<EditableField
label={contact.type.name}
value={contact.value}
onSave={(newValue) => handleUpdateContact(contact.id, contact.type.id, newValue)}
onDelete={() => handleDeleteContact(contact.id)}
/>
</Fragment>
))}
</div>
</div>

<div className="mt-6 flex flex-col gap-3">
<Heading6>{t('contact.add-contact')}</Heading6>
<Separator />
<Form {...form}>
<form className="flex flex-col" onSubmit={form.handleSubmit(handleFormSubmit)}>
<div className="flex flex-col gap-5 md:flex-row">
<FormField
control={form.control}
name="typeId"
render={({ field }) => (
<FormItem className="w-full gap-2">
<FormLabel className="text-base" htmlFor="typeId">
{t('contact.contact-type')}
</FormLabel>
<Select onValueChange={field.onChange} {...field}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('contact.choose-contact-type')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
{contactTypes.map((type) => (
<SelectItem key={type.id} value={type.id.toString()}>
{type.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="contactValue"
render={({ field }) => (
<FormItem className="w-full gap-2">
<FormLabel className="text-base" htmlFor="contactValue">
{t('contact.title')}
</FormLabel>
<Input {...field} value={field.value || ''} />
</FormItem>
)}
/>
</div>
<Button
type="submit"
className="ml-auto mt-3 w-fit"
variant="secondary"
disabled={!form.formState.isValid}
loading={form.formState.isSubmitting}
>
{t('button.add-new')}
</Button>
</form>
</Form>
</div>
</div>
);
}
Loading

0 comments on commit 3115d0c

Please sign in to comment.