diff --git a/messages/en.json b/messages/en.json
index 58aafc1..1c54f5a 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -96,7 +96,8 @@
"find": "Find",
"search": "Search for an Expert",
"sort": "Sort",
- "language": "Language"
+ "language": "Language",
+ "direction": "Direction"
},
"list": {
"price": "0€",
diff --git a/package-lock.json b/package-lock.json
index 48e3528..168e126 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,9 @@
"@ant-design/cssinjs": "^1.18.1",
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.1",
+ "antd-img-crop": "^4.21.0",
"axios": "^1.6.5",
+ "dayjs": "^1.11.10",
"lodash": "^4.17.21",
"next": "14.0.3",
"next-intl": "^3.3.1",
@@ -961,6 +963,21 @@
"react-dom": ">=16.9.0"
}
},
+ "node_modules/antd-img-crop": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/antd-img-crop/-/antd-img-crop-4.21.0.tgz",
+ "integrity": "sha512-YA5GUMfwoDoSJNWinGOmtYFDFLf+t7Rhfg7ZusbHgFpKCq8n9W0005LeCWgSP4C0iK3vxNHAT3DaRa3rTgKFlQ==",
+ "dependencies": {
+ "compare-versions": "6.1.0",
+ "react-easy-crop": "^5.0.4",
+ "tslib": "^2.6.2"
+ },
+ "peerDependencies": {
+ "antd": ">=4.0.0",
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -1450,6 +1467,11 @@
"node": ">= 0.8"
}
},
+ "node_modules/compare-versions": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz",
+ "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg=="
+ },
"node_modules/compute-scroll-into-view": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
@@ -3454,6 +3476,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/normalize-wheel": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+ "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA=="
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4391,6 +4418,24 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-easy-crop": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.5.tgz",
+ "integrity": "sha512-GH7Jw3898ytSaN4i4Oxi7j3BKzapZ2pVgnKIl+gFIUjA+NsDgdBSIpiBQUrPFIvHzSnPmz0kGCpX95X6NgsDzA==",
+ "dependencies": {
+ "normalize-wheel": "^1.0.1",
+ "tslib": "2.0.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.4.0",
+ "react-dom": ">=16.4.0"
+ }
+ },
+ "node_modules/react-easy-crop/node_modules/tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -5965,6 +6010,16 @@
"throttle-debounce": "^5.0.0"
}
},
+ "antd-img-crop": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/antd-img-crop/-/antd-img-crop-4.21.0.tgz",
+ "integrity": "sha512-YA5GUMfwoDoSJNWinGOmtYFDFLf+t7Rhfg7ZusbHgFpKCq8n9W0005LeCWgSP4C0iK3vxNHAT3DaRa3rTgKFlQ==",
+ "requires": {
+ "compare-versions": "6.1.0",
+ "react-easy-crop": "^5.0.4",
+ "tslib": "^2.6.2"
+ }
+ },
"anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -6307,6 +6362,11 @@
"delayed-stream": "~1.0.0"
}
},
+ "compare-versions": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz",
+ "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg=="
+ },
"compute-scroll-into-view": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
@@ -7794,6 +7854,11 @@
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true
},
+ "normalize-wheel": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
+ "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA=="
+ },
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -8432,6 +8497,22 @@
"scheduler": "^0.23.0"
}
},
+ "react-easy-crop": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.0.5.tgz",
+ "integrity": "sha512-GH7Jw3898ytSaN4i4Oxi7j3BKzapZ2pVgnKIl+gFIUjA+NsDgdBSIpiBQUrPFIvHzSnPmz0kGCpX95X6NgsDzA==",
+ "requires": {
+ "normalize-wheel": "^1.0.1",
+ "tslib": "2.0.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+ "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+ }
+ }
+ },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/package.json b/package.json
index f34c878..25dc63f 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,9 @@
"@ant-design/cssinjs": "^1.18.1",
"@ant-design/icons": "^5.2.6",
"antd": "^5.12.1",
+ "antd-img-crop": "^4.21.0",
"axios": "^1.6.5",
+ "dayjs": "^1.11.10",
"lodash": "^4.17.21",
"next": "14.0.3",
"next-intl": "^3.3.1",
diff --git a/public/images/forbidden.png b/public/images/forbidden.png
new file mode 100644
index 0000000..fc72c66
Binary files /dev/null and b/public/images/forbidden.png differ
diff --git a/public/images/not-found.png b/public/images/not-found.png
new file mode 100644
index 0000000..ac0d63a
Binary files /dev/null and b/public/images/not-found.png differ
diff --git a/public/images/server-error.png b/public/images/server-error.png
new file mode 100644
index 0000000..5a277d3
Binary files /dev/null and b/public/images/server-error.png differ
diff --git a/src/actions/experts.ts b/src/actions/experts.ts
index 7ac8d57..ac97497 100644
--- a/src/actions/experts.ts
+++ b/src/actions/experts.ts
@@ -1,9 +1,9 @@
import { apiClient } from '../lib/apiClient';
-import { Filter, ExpertsData, ExpertDetails } from '../types/experts';
+import { GeneralFilter, ExpertsData, ExpertDetails } from '../types/experts';
-export const getExpertsList = async (filter: Filter, locale: string) => {
+export const getExpertsList = async (filter: GeneralFilter, locale: string) => {
const response = await apiClient.post(
- '/home/coachsearch',
+ '/home/coachsearch1',
{ ...filter },
{
headers: {
@@ -13,7 +13,7 @@ export const getExpertsList = async (filter: Filter, locale: string) => {
}
);
- return response.data?.coaches as ExpertsData || null;
+ return response.data as ExpertsData || null;
};
export const getExpertById = async (id: string, locale: string) => {
diff --git a/src/actions/hooks/useProfileSettings.ts b/src/actions/hooks/useProfileSettings.ts
new file mode 100644
index 0000000..14a67d1
--- /dev/null
+++ b/src/actions/hooks/useProfileSettings.ts
@@ -0,0 +1,40 @@
+'use client'
+
+import { useCallback, useEffect, useState } from 'react';
+import { Profile } from '../../types/profile';
+import { getPersonalData } from '../profile';
+import { useLocalStorage } from '../../hooks/useLocalStorage';
+import { AUTH_TOKEN_KEY } from '../../constants/common';
+
+export const useProfileSettings = (locale: string) => {
+ const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
+ const [profileSettings, setProfileSettings] = useState();
+ const [fetchLoading, setFetchLoading] = useState(false);
+ const [saveLoading, setSaveLoading] = useState(false);
+
+ useEffect(() => {
+ if (jwt) {
+ getPersonalData(locale, jwt)
+ .then(({ data }) => {
+ setProfileSettings(data);
+ })
+ .catch((err) => {
+
+ })
+ .finally(() => {
+ setFetchLoading(false);
+ });
+ }
+ }, []);
+
+ const save = useCallback(() => {
+
+ }, []);
+
+ return {
+ fetchLoading,
+ save,
+ saveLoading,
+ profileSettings
+ };
+};
diff --git a/src/actions/profile.ts b/src/actions/profile.ts
index 8fcde21..53462bf 100644
--- a/src/actions/profile.ts
+++ b/src/actions/profile.ts
@@ -1,7 +1,9 @@
import { AxiosResponse } from 'axios';
import { apiClient } from '../lib/apiClient';
+import { Profile } from '../types/profile';
+import { Session, SessionsFilter } from '../types/sessions';
-export const setPersonData = (person: { login: string, password: string, role: string }, locale: string, jwt: string): Promise => (
+export const setPersonData = (person: { login: string, password: string, role: string, languagesLinks: any[] }, locale: string, jwt: string): Promise> => (
apiClient.post(
'/home/applyperson1',
{ ...person },
@@ -13,3 +15,61 @@ export const setPersonData = (person: { login: string, password: string, role: s
}
)
);
+
+export const getPersonalData = (locale: string, jwt: string): Promise> => (
+ apiClient.post(
+ '/home/userdata',
+ {},
+ {
+ headers: {
+ 'X-User-Language': locale,
+ Authorization: `Bearer ${jwt}`
+ }
+ }
+ )
+);
+
+export const getUpcomingSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise> => (
+ apiClient.post(
+ '/home/upcomingsessionsall',
+ {
+ sessionType: 'session',
+ ...(filter || {})
+ },
+ {
+ headers: {
+ 'X-User-Language': locale,
+ Authorization: `Bearer ${jwt}`
+ }
+ }
+ )
+);
+
+export const getRequestedSessions = (locale: string, jwt: string): Promise> => (
+ apiClient.post(
+ '/home/coachhomedata',
+ {},
+ {
+ headers: {
+ 'X-User-Language': locale,
+ Authorization: `Bearer ${jwt}`
+ }
+ }
+ )
+);
+
+export const getRecentSessions = (locale: string, jwt: string, filter?: SessionsFilter): Promise> => (
+ apiClient.post(
+ '/home/historicalmeetings ',
+ {
+ sessionType: 'session',
+ ...(filter || {})
+ },
+ {
+ headers: {
+ 'X-User-Language': locale,
+ Authorization: `Bearer ${jwt}`
+ }
+ }
+ )
+);
diff --git a/src/actions/tags.ts b/src/actions/tags.ts
index e1726a3..93eb016 100644
--- a/src/actions/tags.ts
+++ b/src/actions/tags.ts
@@ -1,5 +1,5 @@
import { apiClient } from '../lib/apiClient';
-import { SearchData } from '../types/tags';
+import { SearchData, Languages } from '../types/tags';
export const getTagList = async (locale: string) => {
const response = await apiClient.post(
@@ -15,3 +15,17 @@ export const getTagList = async (locale: string) => {
return response.data as SearchData || null;
};
+
+export const getLanguages = async (locale: string) => {
+ const response = await apiClient.get(
+ '/home/languages',
+ {
+ headers: {
+ 'X-User-Language': locale,
+ Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJuYW1laWQiOiIxNzIiLCJuYmYiOjE3MDM2ODMyMDgsImV4cCI6MTczNTIxOTIwOCwiaWF0IjoxNzAzNjgzMjA4fQ.KgnYfKO7oVFLlDuKhfyNN6RAaXKdeSzJd7F4r6_15AA'
+ }
+ }
+ );
+
+ return response.data as Languages || null;
+};
diff --git a/src/app/[locale]/account/(account)/layout.tsx b/src/app/[locale]/account/(account)/layout.tsx
index 2c2d53d..b154c58 100644
--- a/src/app/[locale]/account/(account)/layout.tsx
+++ b/src/app/[locale]/account/(account)/layout.tsx
@@ -1,30 +1,14 @@
+'use client'
+
import React, { ReactNode } from 'react';
-import { Metadata } from 'next';
-import { useTranslations } from 'next-intl';
import { AccountMenu } from '../../../../components/Account';
+import { i18nText } from '../../../../i18nKeys';
type AccountInnerLayoutProps = {
children: ReactNode;
- params: Record;
+ params: { locale: string };
};
-export const metadata: Metadata = {
- title: 'Bbuddy User Account'
-};
-
-export function generateStaticParams({
- params: { locale },
-}: { params: { locale: string } }) {
- const result: { locale: string, userId: string }[] = [];
- const users = [{ userId: '1' }, { userId: '2' }];
-
- users.forEach(({ userId }) => {
- result.push({ locale, userId });
- });
-
- return result;
-}
-
const ROUTES = ['sessions', 'notifications', 'support', 'information', 'settings', 'messages', 'work-with-us'];
const COUNTS: Record = {
sessions: 12,
@@ -33,12 +17,10 @@ const COUNTS: Record = {
};
-export default function AccountInnerLayout({ children }: AccountInnerLayoutProps) {
- const t = useTranslations('Account');
-
+export default function AccountInnerLayout({ children, params: { locale } }: AccountInnerLayoutProps) {
const getMenuConfig = () => ROUTES.map((path) => ({
path,
- title: t(`menu.${path}`),
+ title: i18nText(`accountMenu.${path}`, locale),
count: COUNTS[path] || undefined
}));
diff --git a/src/app/[locale]/account/(account)/sessions/page.tsx b/src/app/[locale]/account/(account)/sessions/page.tsx
index 7fc4acf..148dcbb 100644
--- a/src/app/[locale]/account/(account)/sessions/page.tsx
+++ b/src/app/[locale]/account/(account)/sessions/page.tsx
@@ -8,12 +8,13 @@ export const metadata: Metadata = {
description: 'Bbuddy desc sessions'
};
-export default function Sessions() {
+export default function Sessions({ params: { locale } }: { params: { locale: string } }) {
const t = useTranslations('Account.Sessions');
return (
Loading...
}>
{t('title')}
-
+
+
+
>
);
};
diff --git a/src/app/[locale]/experts/[expertId]/page.tsx b/src/app/[locale]/experts/[expertId]/page.tsx
index 57c1195..dac9296 100644
--- a/src/app/[locale]/experts/[expertId]/page.tsx
+++ b/src/app/[locale]/experts/[expertId]/page.tsx
@@ -30,7 +30,7 @@ export async function generateStaticParams({
"durationTo": null
}, locale);
- experts?.forEach(({ id }) => {
+ experts?.coaches?.forEach(({ id }) => {
result.push({ locale, expertId: id.toString() });
});
diff --git a/src/app/fonts.ts b/src/app/fonts.ts
new file mode 100644
index 0000000..55e865c
--- /dev/null
+++ b/src/app/fonts.ts
@@ -0,0 +1,15 @@
+import { Comfortaa, Inter } from 'next/font/google';
+
+export const comfortaa = Comfortaa({
+ weight: ['300', '400', '500', '600', '700'],
+ subsets: ['latin', 'cyrillic'],
+ variable: '--font-comfortaa',
+ display: 'swap',
+});
+
+export const inter = Inter({
+ weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
+ subsets: ['latin', 'cyrillic'],
+ variable: '--font-inter',
+ display: 'swap',
+});
diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx
new file mode 100644
index 0000000..612defe
--- /dev/null
+++ b/src/app/global-error.tsx
@@ -0,0 +1,65 @@
+'use client'
+
+import React from 'react';
+import Link from 'next/link';
+import { Button, ConfigProvider } from 'antd';
+import { comfortaa, inter } from './fonts';
+import StyledRegistry from '../lib/StyleRegistry';
+import StyledComponentsRegistry from '../lib/AntdRegistry';
+import theme from '../constants/theme';
+
+export default function GlobalError({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string }
+ reset: () => void
+}) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Something wrong
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 959699b..06f7fdd 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
-import { Comfortaa, Inter } from 'next/font/google';
+import { comfortaa, inter } from './fonts';
import '../styles/style.scss';
type RootLayoutProps = {
@@ -7,20 +7,6 @@ type RootLayoutProps = {
params: { locale: string };
};
-const comfortaa = Comfortaa({
- weight: ['300', '400', '500', '600', '700'],
- subsets: ['latin', 'cyrillic'],
- variable: '--font-comfortaa',
- display: 'swap',
-});
-
-const inter = Inter({
- weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
- subsets: ['latin', 'cyrillic'],
- variable: '--font-inter',
- display: 'swap',
-});
-
export default function RootLayout({ children, params: { locale } }: RootLayoutProps) {
return (
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index c61e98d..9405a5c 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -1,9 +1,57 @@
import React from 'react';
+import { ConfigProvider } from 'antd';
+import Link from 'next/link';
+import { comfortaa, inter } from './fonts';
+import StyledComponentsRegistry from '../lib/AntdRegistry';
+import StyledRegistry from '../lib/StyleRegistry';
+import theme from '../constants/theme';
+import { BackButton } from '../components/view';
export default function NotFound() {
return (
-
- not found
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
404
+
We can't seem to find a page you're looking for
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/components/Account/AccountMenu.tsx b/src/components/Account/AccountMenu.tsx
index 9738897..e4d089d 100644
--- a/src/components/Account/AccountMenu.tsx
+++ b/src/components/Account/AccountMenu.tsx
@@ -5,7 +5,7 @@ import styled from 'styled-components';
import { Button } from 'antd';
import { useSelectedLayoutSegment, usePathname } from 'next/navigation';
import { Link } from '../../navigation';
-import { AUTH_TOKEN_KEY } from '../../constants/common';
+import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common';
import { deleteStorageKey } from '../../hooks/useLocalStorage';
const Logout = styled(Button)`
@@ -26,9 +26,14 @@ export const AccountMenu = ({ menu }: { menu: { path: string, title: string, cou
const onLogout = () => {
deleteStorageKey(AUTH_TOKEN_KEY);
+ deleteStorageKey(AUTH_USER);
window?.location?.replace(`/${paths.split('/')[1]}/`);
};
+ const onDeleteAccount = () => {
+ console.log('delete');
+ };
+
return (
{menu.map(({ path, title, count }) => (
@@ -49,6 +54,14 @@ export const AccountMenu = ({ menu }: { menu: { path: string, title: string, cou
Log Out
+ -
+
+ Delete account
+
+
- )
+ );
};
diff --git a/src/components/Account/ProfileSettings.tsx b/src/components/Account/ProfileSettings.tsx
new file mode 100644
index 0000000..133f179
--- /dev/null
+++ b/src/components/Account/ProfileSettings.tsx
@@ -0,0 +1,113 @@
+'use client';
+
+import React, { FC, useEffect, useState } from 'react';
+import { Form, Upload, Button } from 'antd';
+import type { UploadFile, UploadProps, GetProp } from 'antd';
+import ImgCrop from 'antd-img-crop';
+import { CameraOutlined } from '@ant-design/icons';
+import { Link } from '../../navigation';
+import { CustomInput } from '../view';
+import { Profile } from '../../types/profile';
+import { useProfileSettings } from '../../actions/hooks/useProfileSettings';
+
+type ProfileSettingsProps = {
+ locale: string;
+ photoDesc?: string;
+ placeholderName?: string;
+ placeholderSurname?: string;
+ placeholderBirthday?: string;
+ placeholderEmail?: string;
+ changePasswordLink?: string;
+ saveButton?: string;
+};
+
+type FileType = Parameters>[0];
+
+export const ProfileSettings: FC = ({
+ locale,
+ photoDesc,
+ placeholderName,
+ placeholderSurname,
+ placeholderBirthday,
+ placeholderEmail,
+ changePasswordLink,
+ saveButton
+}) => {
+ const [form] = Form.useForm();
+ const { profileSettings } = useProfileSettings(locale);
+
+ useEffect(() => {
+ if (profileSettings) {
+ form.setFieldsValue(profileSettings);
+ }
+ }, [profileSettings]);
+
+ const [fileList, setFileList] = useState();
+
+ const onChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
+ setFileList(newFileList);
+ };
+
+ const onPreview = async (file: UploadFile) => {
+ let src = file.url as string;
+ if (!src) {
+ src = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file.originFileObj as FileType);
+ reader.onload = () => resolve(reader.result as string);
+ });
+ }
+ const image = new Image();
+ image.src = src;
+ const imgWindow = window.open(src);
+ imgWindow?.document.write(image.outerHTML);
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/Account/SessionsTabs.tsx b/src/components/Account/SessionsTabs.tsx
index 4fae902..9356aaa 100644
--- a/src/components/Account/SessionsTabs.tsx
+++ b/src/components/Account/SessionsTabs.tsx
@@ -1,20 +1,66 @@
'use client';
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
-import { CustomSelect } from '../view';
+import * as dayjs from 'dayjs';
+import * as isToday from 'dayjs/plugin/isToday';
+import 'dayjs/locale/ru';
+import 'dayjs/locale/en';
+import 'dayjs/locale/de';
+import 'dayjs/locale/it';
+import 'dayjs/locale/fr';
+import 'dayjs/locale/es';
+import { CustomSelect, Loader } from '../view';
+import { useLocalStorage } from '../../hooks/useLocalStorage';
+import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common';
+import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../actions/profile';
+import { Session, Sessions, SessionType } from '../../types/sessions';
+
+dayjs.extend(isToday);
const Tab = styled.div``;
-export const SessionsTabs = ({ intlConfig }: { intlConfig: Record }) => {
+export const SessionsTabs = ({ intlConfig, locale }: { intlConfig: Record, locale: string }) => {
const [activeTab, setActiveTab] = useState(0);
const [sort, setSort] = useState();
+ const [sessions, setSessions] = useState();
+ const [loading, setLoading] = useState(true);
+ const [errorData, setErrorData] = useState();
+ const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
+ const [userId] = useLocalStorage(AUTH_USER, '');
+
+ const fetchData = () => {
+ setErrorData(undefined);
+ setLoading(true);
+ Promise.all([
+ getUpcomingSessions(locale, jwt),
+ getRequestedSessions(locale, jwt),
+ getRecentSessions(locale, jwt)
+ ])
+ .then(([upcoming, requested, recent]) => {
+ setSessions({
+ [SessionType.UPCOMING]: upcoming.data || [],
+ [SessionType.REQUESTED]: requested.data?.requestedSessions || [],
+ [SessionType.RECENT]: recent.data || []
+ });
+ })
+ .catch((err) => {
+ setErrorData(err);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
const onChangeSort = useCallback((value: string) => {
setSort(value);
}, [sort]);
- const getChildren = () => (
+ const getChildren = (list?: Session[]) => (
<>
@@ -32,92 +78,74 @@ export const SessionsTabs = ({ intlConfig }: { intlConfig: Record
-
-
-
-
-
-
-
Matthew Weeks
-
- Personal Growth Course
-
-
- Today 10:00 AM - 10:30 AM
+ {list?.length > 0 ? list?.map(({ id, scheduledStartAtUtc, scheduledEndAtUtc, title, coach, clients }) => {
+ const client = clients?.length ? clients[0] : null;
+ const current = +userId !== client?.id ? client : coach;
+ const startDate = dayjs(scheduledStartAtUtc).locale(locale);
+ const endDate = dayjs(scheduledEndAtUtc).locale(locale);
+ const today = startDate.isToday();
+
+ return (
+
+
+
+
+
+
+
+
{`${current?.name} ${current?.surname || ''}`}
+
{title}
+
+ {today
+ ? `Today ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}`
+ : `${startDate.format('D MMMM')} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}`}
+
+
+
-
-
-
-
-
-
-
-
-
Matthew Weeks
-
- Personal Growth Course
-
-
- 8 december at 10:00 AM - 10:30 AM
-
-
-
-
-
-
-
-
-
-
-
Matthew Weeks
-
- Personal Growth Course
-
-
- 8 december at 10:00 AM - 10:30 AM
-
-
-
-
+ )
+ }) : (
+
not found
+ )}
>
);
const tabs = [
{
- key: 'upcoming',
+ key: SessionType.UPCOMING,
label: (
<>
{intlConfig?.upcoming || 'Tab 1'}
-
3
+ {sessions?.upcoming?.length > 0 ? (
{sessions?.upcoming.length}) : null}
>
),
- children: getChildren()
+ children: getChildren(sessions?.upcoming)
},
{
- key: 'requested',
+ key: SessionType.REQUESTED,
label: (
<>
{intlConfig?.requested || 'Tab 2'}
-
2
+ {sessions?.requested?.length > 0 ? (
{sessions?.requested.length}) : null}
>
),
- children: getChildren()
+ children: getChildren(sessions?.requested)
},
{
- key: 'recent',
- label: (
- <>
- {intlConfig?.recent || 'Tab 3'}
- >
- ),
- children: getChildren()
+ key: SessionType.RECENT,
+ label: intlConfig?.recent || 'Tab 3',
+ children: getChildren(sessions?.recent)
}
];
return (
- <>
+
{tabs.map((tab, index) => (
{tabs[activeTab].children}
- >
+
);
};
diff --git a/src/components/Account/index.ts b/src/components/Account/index.ts
index 9f820ac..28b7629 100644
--- a/src/components/Account/index.ts
+++ b/src/components/Account/index.ts
@@ -1,2 +1,3 @@
export { AccountMenu } from './AccountMenu';
export { SessionsTabs } from './SessionsTabs';
+export { ProfileSettings } from './ProfileSettings';
diff --git a/src/components/Experts/AdditionalFilter.tsx b/src/components/Experts/AdditionalFilter.tsx
index ed3f0db..df97946 100644
--- a/src/components/Experts/AdditionalFilter.tsx
+++ b/src/components/Experts/AdditionalFilter.tsx
@@ -1,74 +1,53 @@
'use client';
-import React, { useCallback, useState } from 'react';
-import { Button } from 'antd';
+import React, { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
+import debounce from 'lodash/debounce';
import { useRouter } from '../../navigation';
import { AdditionalFilter } from '../../types/experts';
-import { LOCALES } from '../../constants/locale';
import { getObjectByFilter, getObjectByAdditionalFilter } from '../../utils/filter';
-import { CustomInput, CustomSelect, CustomMultiSelect } from '../view';
+import { CustomInput, CustomSelect } from '../view';
type ExpertAdditionalFilterProps = {
searchPlaceholder: string;
sortLabel: string;
- langLabel: string;
- buttonFind: string;
basePath: string;
};
export const ExpertsAdditionalFilter = ({
searchPlaceholder,
sortLabel,
- langLabel,
- buttonFind,
basePath,
}: ExpertAdditionalFilterProps) => {
const searchParams = useSearchParams();
const router = useRouter();
const [filter, setFilter] = useState(getObjectByAdditionalFilter(searchParams));
- const onChangeInput = useCallback((e: any) => {
- if (e?.target?.value) {
- setFilter({
- ...filter,
- text: e.target.value
- });
- } else {
- if (filter?.text) {
- const newFilter = { ...filter };
- delete newFilter.text;
+ const onChangeInput = useCallback(debounce((e: any) => {
+ const newFilter: AdditionalFilter = { ...filter };
- setFilter(newFilter);
- }
+ if (e?.target?.value) {
+ newFilter.text = e.target.value;
+ } else {
+ delete newFilter?.text;
}
- }, [filter]);
+
+ setFilter(newFilter);
+ }, 300), [filter]);
const onChangeSort = useCallback((value: string) => {
const newFilter: AdditionalFilter = { ...filter };
if (value) {
- newFilter.sort = value;
+ newFilter.coachSort = value;
} else {
- delete newFilter?.sort;
+ delete newFilter?.coachSort;
}
setFilter(newFilter);
}, [filter]);
- const onChangeLang = useCallback((value: string[]) => {
- const newFilter: AdditionalFilter = { ...filter };
-
- if (value.length > 0) {
- newFilter.language = value;
- } else {
- delete newFilter?.language;
- }
-
- setFilter(newFilter);
- }, [filter]);
-
- const goToFilterPage = useCallback(() => {
+ useEffect(() => {
router.push({
pathname: basePath as any,
query: {
@@ -76,7 +55,7 @@ export const ExpertsAdditionalFilter = ({
...filter
}
})
- }, [filter, searchParams, router]);
+ }, [filter]);
return (
@@ -85,35 +64,22 @@ export const ExpertsAdditionalFilter = ({
placeholder={searchPlaceholder}
defaultValue={filter?.text}
onChange={onChangeInput}
+ allowClear
/>
-
- ({ value, label }))}
- />
-
-
);
};
diff --git a/src/components/Experts/Experts.tsx b/src/components/Experts/Experts.tsx
index 3ca44a9..18dd2ae 100644
--- a/src/components/Experts/Experts.tsx
+++ b/src/components/Experts/Experts.tsx
@@ -1,7 +1,8 @@
import React from 'react';
import { getTranslations } from 'next-intl/server';
-import { getTagList } from '../../actions/tags';
+import { DEFAULT_PAGE_SIZE } from '../../constants/common';
import { getFilter } from '../../utils/filter';
+import { getTagList, getLanguages } from '../../actions/tags';
import { getExpertsList } from '../../actions/experts';
import { ExpertsFilter } from './Filter';
import { ExpertsAdditionalFilter } from './AdditionalFilter';
@@ -10,12 +11,14 @@ import { ExpertsList } from './ExpertsList';
type ExpertsProps = {
basePath?: string;
locale: string;
+ pageSize?: number;
};
-export const Experts = async ({ basePath = '/', locale }: ExpertsProps) => {
+export const Experts = async ({ basePath = '/', locale, pageSize = DEFAULT_PAGE_SIZE }: ExpertsProps) => {
const t = await getTranslations('Experts');
const searchData = await getTagList(locale);
- const filter = getFilter(searchData);
+ const languages = await getLanguages(locale);
+ const filter = getFilter({ searchData, pageSize });
const experts = await getExpertsList(filter, locale);
return (
@@ -23,6 +26,7 @@ export const Experts = async ({ basePath = '/', locale }: ExpertsProps) => {
{
- )
+ );
};
diff --git a/src/components/Experts/ExpertsList.tsx b/src/components/Experts/ExpertsList.tsx
index 69ef4f5..0ceb5eb 100644
--- a/src/components/Experts/ExpertsList.tsx
+++ b/src/components/Experts/ExpertsList.tsx
@@ -6,9 +6,9 @@ import { List, Tag } from 'antd';
import { RightOutlined } from '@ant-design/icons';
import isEqual from 'lodash/isEqual';
import Image from 'next/image';
-import { Link } from '../../navigation';
-import { ExpertsData, Filter } from '../../types/experts';
-import { getObjectByFilter } from '../../utils/filter';
+import { Link, useRouter } from '../../navigation';
+import { ExpertsData, Filter, GeneralFilter } from '../../types/experts';
+import { getObjectByFilter, getObjectByAdditionalFilter } from '../../utils/filter';
import { getExpertsList } from '../../actions/experts';
import { CustomPagination, CustomSpin } from '../view';
@@ -19,6 +19,8 @@ type ExpertListProps = {
detailButton: string;
locale: string;
baseFilter: Filter;
+ pageSize: number;
+ basePath: string;
};
export const ExpertsList = ({
@@ -27,35 +29,66 @@ export const ExpertsList = ({
durationTitle,
detailButton,
locale,
- baseFilter
+ baseFilter,
+ pageSize,
+ basePath
}: ExpertListProps) => {
const searchParams = useSearchParams();
+ const router = useRouter();
const getTitle = (str: string, value?: any): string => (value ? str.replace('0', value) : str);
const [experts, setExperts] = useState
();
+ const [loading, setLoading] = useState(true);
useEffect(() => {
const filter = {
...baseFilter,
- ...getObjectByFilter(searchParams)
+ ...getObjectByFilter(searchParams),
+ ...getObjectByAdditionalFilter(searchParams)
};
if (!isEqual(baseFilter, filter)) {
+ setLoading(true);
getExpertsList(filter, locale)
.then((experts) => {
setExperts(experts);
- });
+ })
+ .finally(() => {
+ setLoading(false);
+ })
+ ;
} else {
+ setLoading(false);
setExperts(data);
}
}, [searchParams]);
- return experts ? (
+ const onChangePage = (page: number) => {
+ const newFilter: GeneralFilter = {
+ ...getObjectByFilter(searchParams),
+ ...getObjectByAdditionalFilter(searchParams)
+ };
+
+ if (page !== 1) {
+ newFilter.page = page;
+ } else {
+ delete newFilter.page;
+ }
+
+ router.push({
+ pathname: basePath as any,
+ query: newFilter
+ });
+ };
+
+ const currentPage = searchParams.has('page') ? Number(searchParams.get('page')) : baseFilter.page;
+
+ return !loading ? experts?.coaches && (
<>
(
-
- {`${item.name} ${item?.surname || ''}`}
-
-
- {getTitle(priceTitle, item?.sessionCost)}
/ {getTitle(durationTitle, item?.sessionDuration)}
+
+
+
{`${item.name} ${item?.surname || ''}`}
+
+
+ {getTitle(priceTitle, item?.sessionCost)} / {getTitle(durationTitle, item?.sessionDuration)}
+
+
+
+ {item?.coachLanguages?.map((lang) => (
+ {lang}
+ ))}
)}
@@ -101,7 +141,13 @@ export const ExpertsList = ({
)}
/>
-
+ {experts.total > pageSize && (
+ )}
>
) : ;
};
diff --git a/src/components/Experts/Filter.tsx b/src/components/Experts/Filter.tsx
index 3ba5aa3..4dc9cc5 100644
--- a/src/components/Experts/Filter.tsx
+++ b/src/components/Experts/Filter.tsx
@@ -1,16 +1,18 @@
'use client';
-import React, { useCallback, useState } from 'react';
-import { Button, List } from 'antd';
-import { useSearchParams, usePathname } from 'next/navigation';
+import React, { useCallback, useEffect, useState } from 'react';
+import { Button, Collapse, List } from 'antd';
+import type { CollapseProps } from 'antd';
+import { useSearchParams } from 'next/navigation';
import { useRouter } from '../../navigation';
-import { Filter, GeneralFilter } from '../../types/experts';
-import { SearchData, Tag } from '../../types/tags';
-import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
-import { CustomSwitch, CustomSlider } from '../view';
+import { Filter } from '../../types/experts';
+import { Languages, SearchData, Tag } from '../../types/tags';
+import { getObjectByFilter, getObjectByAdditionalFilter } from '../../utils/filter';
+import { CustomSwitch, CustomSlider, CustomInput } from '../view';
type ExpertsFilterProps = {
searchData?: SearchData;
+ languages?: Languages;
basePath: string;
priceTitle: string;
durationTitle: string;
@@ -19,6 +21,7 @@ type ExpertsFilterProps = {
export const ExpertsFilter = ({
searchData,
+ languages,
basePath,
priceTitle,
durationTitle,
@@ -26,23 +29,43 @@ export const ExpertsFilter = ({
}: ExpertsFilterProps) => {
const searchParams = useSearchParams();
const router = useRouter();
- const pathname = usePathname();
const [filter, setFilter] = useState(getObjectByFilter(searchParams));
+ const [openedTabs, setOpenedTabs] = useState([]);
+ const [filteredTags, setFilteredTags] = useState([]);
+ const [searchLang, setSearchLang] = useState();
+ const [searchTags, setSearchTags] = useState();
- const onChangeTags = useCallback((id: number, checked: boolean) => {
- let themesTagIds = filter?.themesTagIds || [];
+ useEffect(() => {
+ const tags = searchData?.themesGroups
+ ? searchData.themesGroups.reduce((acc, item) => {
+ if (item?.tags) {
+ return [
+ ...acc,
+ ...item.tags.map((tag) => ({ ...tag, group: item.name }))
+ ]
+ }
- if (checked && !themesTagIds?.includes(id)) {
- themesTagIds.push(id);
+ return acc;
+ }, [])
+ : [];
+
+ setFilteredTags(tags);
+ }, [searchData?.themesGroups]);
+
+ const onChangeFilterArr = useCallback((field: string, id: number | string, checked: boolean) => {
+ let arr = (filter && filter[field]) || [];
+
+ if (checked && !arr?.includes(id)) {
+ arr.push(id);
}
- if (!checked && themesTagIds?.includes(id)) {
- themesTagIds = themesTagIds.filter((tId) => tId != id);
+ if (!checked && arr?.includes(id)) {
+ arr = arr.filter((tId) => tId != id);
}
setFilter({
- ...filter,
- themesTagIds
+ ...(filter || {}),
+ [field]: arr
});
}, [filter, searchParams, searchData]);
@@ -100,7 +123,19 @@ export const ExpertsFilter = ({
})
}, [filter, searchParams, searchData]);
- const getList = useCallback((data: Tag[]) => (
+ const getSelectedLanguage = useCallback((): string => {
+ const cur = languages ? languages.filter(({ code }) => filter?.userLanguages ? filter.userLanguages.includes(code) : false) : [];
+
+ return cur ? cur.map(({ nativeSpelling }) => nativeSpelling).join(', ') : '';
+ }, [filter, languages]);
+
+ const getSelectedTags = useCallback((): string => {
+ const cur = filteredTags.filter(({ id }) => filter?.themesTagIds ? filter.themesTagIds.includes(id) : false);
+
+ return cur ? cur.map(({ name }) => name).join(', ') : '';
+ }, [filter, filteredTags]);
+
+ const getList = useCallback((fieldName: string, data: { id: number | string, name: string }[]) => (
{name}
onChangeTags(id, checked)}
+ defaultChecked={(filter && filter[fieldName]?.includes(id)) || false}
+ onChange={(checked: boolean) => onChangeFilterArr(fieldName, id, checked)}
/>
@@ -123,35 +158,120 @@ export const ExpertsFilter = ({
), [filter, searchParams, searchData]);
+ const getLangList = () => {
+ const reg = searchLang ? new RegExp(searchLang, 'ig') : '';
+ const langList = reg ? (languages || []).filter(({ code, nativeSpelling }) => reg.test(code) || reg.test(nativeSpelling)) : languages;
+ return langList?.length && getList('userLanguages', langList.map(({ code, nativeSpelling }) => ({ id: code, name: nativeSpelling })))
+ };
+
+ const getTagsList = () => {
+ const reg = searchTags ? new RegExp(searchTags, 'ig') : '';
+
+ if (reg) {
+ const tagsList = filteredTags.filter(({ name, group }) => reg.test(name) || reg.test(group));
+ return getList('themesTagIds', tagsList);
+ }
+
+ return searchData?.themesGroups?.length ? searchData.themesGroups.map(({ id, name, tags }) => (
+
+
{name}
+ {getList('themesTagIds', tags)}
+
+ )) : null;
+ };
+
+ const getCollapsedPanels: CollapseProps['items'] = [
+ {
+ key: 'userLanguages',
+ label: (
+ <>
+
Session Language
+ {!openedTabs.includes('userLanguages') && filter?.userLanguages?.length > 0 && (
+
{getSelectedLanguage()}
+ )}
+ >
+ ),
+ children: (
+ <>
+
setSearchLang(e.target?.value)}
+ allowClear
+ />
+
+ {getLangList()}
+
+ >
+ ),
+ },
+ {
+ key: 'themesTagIds',
+ label: (
+ <>
+ Direction
+ {!openedTabs.includes('themesTagIds') && filter?.themesTagIds?.length > 0 && (
+ {getSelectedTags()}
+ )}
+ >
+ ),
+ children: (
+ <>
+ setSearchTags(e.target?.value)}
+ allowClear
+ />
+ {getTagsList()}
+ >
+ ),
+ }
+ ];
+
+ const onChangeTab = (keys: (string | string[])) => {
+ if (Array.isArray(keys)) {
+ setOpenedTabs(keys);
+ if (!keys.includes('userLanguages') && searchLang) setSearchLang('');
+ if (!keys.includes('themesTagIds') && searchTags) setSearchTags('');
+ }
+ };
+
return (
- {searchData?.themesGroups?.length && searchData.themesGroups.map(({ id, name, tags }) => (
-
-
{name}
- {getList(tags)}
+
+
+
Price
+
+
- ))}
-
{priceTitle}
-
-
{durationTitle}
-
-
+
+
Duration
+
+
+
+
{durationTitle}
+ ) : undefined}
+ />
+ );
+ }
+
+ return children;
+};
diff --git a/src/components/view/index.ts b/src/components/view/index.ts
index 4175069..2600fb6 100644
--- a/src/components/view/index.ts
+++ b/src/components/view/index.ts
@@ -12,3 +12,6 @@ export * from './CustomSpin';
export * from './FilledButton';
export * from './OutlinedButton';
export * from './LinkButton';
+export * from './BackButton';
+export * from './WithError';
+export * from './Loader';
diff --git a/src/constants/common.ts b/src/constants/common.ts
index b19517c..c5abf3b 100644
--- a/src/constants/common.ts
+++ b/src/constants/common.ts
@@ -1,2 +1,6 @@
export const BASE_URL = process.env.NEXT_PUBLIC_SERVER_BASE_URL || 'https://api.bbuddy.expert/api';
export const AUTH_TOKEN_KEY = 'bbuddy_token';
+export const AUTH_USER = 'bbuddy_auth_user';
+
+export const DEFAULT_PAGE_SIZE = 5;
+export const DEFAULT_PAGE = 1;
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
index 831e12a..9221a91 100644
--- a/src/hooks/useLocalStorage.ts
+++ b/src/hooks/useLocalStorage.ts
@@ -1,13 +1,13 @@
import { useState, useEffect } from 'react';
-function getStorageValue(key: string, defaultValue: any) {
+function getStorageValue (key: string, defaultValue: any) {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem(key);
return saved || defaultValue;
}
};
-export function deleteStorageKey(key: string) {
+export function deleteStorageKey (key: string) {
if (typeof window !== 'undefined') {
localStorage.removeItem(key);
}
diff --git a/src/i18nKeys/en.ts b/src/i18nKeys/en.ts
new file mode 100644
index 0000000..5a7b01c
--- /dev/null
+++ b/src/i18nKeys/en.ts
@@ -0,0 +1,11 @@
+export default {
+ accountMenu: {
+ sessions: 'Upcoming & Recent Sessions',
+ notifications: 'Notification',
+ support: 'Help & Support',
+ information: 'Legal Information',
+ settings: 'Profile Settings',
+ messages: 'Messages',
+ 'work-with-us': 'Work With Us'
+ }
+}
diff --git a/src/i18nKeys/index.ts b/src/i18nKeys/index.ts
new file mode 100644
index 0000000..fe23003
--- /dev/null
+++ b/src/i18nKeys/index.ts
@@ -0,0 +1,25 @@
+import { Locale } from '../types/locale';
+import en from './en';
+import ru from './ru';
+
+const MESSAGES = {
+ [Locale.en]: en,
+ [Locale.ru]: ru,
+ [Locale.de]: en,
+ [Locale.fr]: en,
+ [Locale.it]: en,
+ [Locale.es]: en
+};
+
+const getValue = (keys: string[], dictionary: any) => {
+ const keysClone = [...keys];
+ const firstElement = keysClone.shift();
+
+ if (firstElement && dictionary[firstElement]) {
+ return typeof dictionary[firstElement] === 'string' ? dictionary[firstElement] : getValue(keysClone, dictionary[firstElement]);
+ } else {
+ return '';
+ }
+};
+
+export const i18nText = (key: string, locale?: string): string => getValue(key.split('.'), MESSAGES[locale || Locale.en]);
diff --git a/src/i18nKeys/ru.ts b/src/i18nKeys/ru.ts
new file mode 100644
index 0000000..5a7b01c
--- /dev/null
+++ b/src/i18nKeys/ru.ts
@@ -0,0 +1,11 @@
+export default {
+ accountMenu: {
+ sessions: 'Upcoming & Recent Sessions',
+ notifications: 'Notification',
+ support: 'Help & Support',
+ information: 'Legal Information',
+ settings: 'Profile Settings',
+ messages: 'Messages',
+ 'work-with-us': 'Work With Us'
+ }
+}
diff --git a/src/styles/_default.scss b/src/styles/_default.scss
index c30a04c..c4a6580 100644
--- a/src/styles/_default.scss
+++ b/src/styles/_default.scss
@@ -155,7 +155,7 @@ a {
}
&__link {
- color: #FF8A00;
+ color: #FF8A00 !important;
@include rem(15);
font-style: normal;
font-weight: 400;
@@ -295,12 +295,84 @@ a {
width: 100%;
}
+ .ant-collapse-content-box {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .title-h5 {
+ color: #003B46;
+ font-size: 14px;
+ line-height: 24px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ }
+
+ .title-h3 {
+ color: #003B46;
+ font-size: 18px;
+ line-height: 32px;
+ font-weight: bold;
+ }
+
+ .ant-collapse {
+ margin-bottom: 16px;
+ }
+
+ .ant-collapse-item {
+ border-bottom: 1px solid #C4DFE6;
+ border-radius: 0 !important;
+ }
+
+ .ant-collapse-content-box {
+ padding: 0 !important;
+ }
+
+ .ant-collapse-header {
+ padding: 16px 0 !important;
+ align-items: center !important;
+
+ .ant-collapse-expand-icon {
+ height: 32px !important;
+ width: 32px;
+ padding-inline-start: 0 !important;
+ justify-content: center;
+ color: #2C7873;
+ }
+ }
+
+ &__content {
+ height: 265px;
+ overflow-y: auto;
+ width: 100%;
+ margin-bottom: 16px;
+ padding-right: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ // scrollbar-width: thin;
+ // scrollbar-color: #2C7873 #C4DFE6;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+ &::-webkit-scrollbar-track {
+ background: #C4DFE6;
+ border-radius: 8px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background-color: #2C7873;
+ border-radius: 8px;
+ }
+ }
+
&__inner {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
- margin-bottom: 16px;
}
&__range {
@@ -320,7 +392,7 @@ a {
&__slider {
width: 100%;
position: relative;
- margin: 8px 0 16px -5px;
+ margin: 0 0 0 -5px;
}
&__title {
@@ -331,6 +403,19 @@ a {
line-height: 160%;
text-transform: capitalize;
}
+
+ &__collapsed {
+ &__title {
+ font-size: 18px;
+ font-weight: bold;
+ line-height: 32px;
+ color: #003B46;
+ }
+
+ &__desc {
+ color: #6FB98F;
+ }
+ }
}
.base-btn {
@@ -510,6 +595,12 @@ a {
border-block-end: 1px solid #C4DFE6 !important;
margin: 0 0 16px !important;
+ &.session {
+ &__item {
+ margin: 0 !important;
+ }
+ }
+
&__header {
display: flex;
padding-bottom: 8px;
@@ -518,6 +609,10 @@ a {
align-self: stretch;
margin-block-end: 0 !important;
+ .ant-list-item-meta-avatar {
+ margin-inline-end: 0 !important;
+ }
+
&__portrait {
width: 86px;
height: 86px;
@@ -538,10 +633,20 @@ a {
&__inner {
display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 4px;
+ align-items: center;
+ gap: 20px;
flex: 1 0 0;
+ justify-content: space-between;
+
+ & > div {
+ display: flex;
+ gap: 4px;
+
+ &:first-child {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ }
}
&__name {
diff --git a/src/styles/_error.scss b/src/styles/_error.scss
new file mode 100644
index 0000000..a81ca58
--- /dev/null
+++ b/src/styles/_error.scss
@@ -0,0 +1,62 @@
+.page-error {
+ position: relative;
+ padding: 88px 0;
+
+ .b-inner {
+ display: flex;
+ gap: 50px;
+ justify-content: center;
+
+ & > div {
+ flex-basis: 50%;
+ }
+ }
+
+ .error {
+ &__image {
+ img {
+ display: block;
+ margin: auto;
+ width: 400px;
+ }
+ }
+
+ &__description {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ justify-content: center;
+ text-align: center;
+ }
+
+ &__code {
+ font-size: 180px;
+ line-height: 180px;
+ font-weight: bold;
+ color: #c4c4c4;
+ }
+
+ &__subtitle {
+ color: #6FB98F;
+ }
+
+ &__button {
+ width: 300px;
+ margin: 0 auto;
+ }
+ }
+
+ @media (min-width: 768px) {
+ padding-bottom: 55px;
+ }
+
+ @media (min-width: 992px) {
+ .b-inner {
+
+ }
+ }
+
+ @media (min-width: 1200px) {
+
+ }
+}
diff --git a/src/styles/_main.scss b/src/styles/_main.scss
index 4575b30..37e38ba 100644
--- a/src/styles/_main.scss
+++ b/src/styles/_main.scss
@@ -529,6 +529,24 @@
padding-right: 8px;
position: relative;
+ &__block {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 0 0 16px;
+ margin-bottom: 16px;
+ border-bottom: 1px solid #C4DFE6;
+ }
+
+ &__description {
+ color: #6FB98F;
+ font-size: 14px;
+ line-height: 14px;
+ display: flex;
+ justify-content: flex-end;
+ width: 100%;
+ }
+
@media (min-width: 992px) {
&:before {
content: "";
diff --git a/src/styles/style.scss b/src/styles/style.scss
index 2322f18..8423a3a 100644
--- a/src/styles/style.scss
+++ b/src/styles/style.scss
@@ -4,6 +4,7 @@
@import "_header.scss";
@import "_menu-mobile.scss";
@import "_main.scss";
+@import "_error.scss";
@import "_bb-experts.scss";
@import "_bb-clients.scss";
@import "_faq.scss";
diff --git a/src/types/experts.ts b/src/types/experts.ts
index fb2d0bb..20a7417 100644
--- a/src/types/experts.ts
+++ b/src/types/experts.ts
@@ -1,6 +1,4 @@
-import { Tag } from './tags';
-
-// export type Filter = Record
;
+import {Tag, ThemeGroups} from './tags';
export type GeneralFilter = Filter & AdditionalFilter;
@@ -10,12 +8,15 @@ export type Filter = {
priceTo?: number | null;
durationFrom?: number | null;
durationTo?: number | null;
+ userLanguages?: string[];
+ pageSize?: number;
+ page?: number;
+ tagLanguage?: string;
};
export type AdditionalFilter = {
text?: string;
- sort?: string;
- language?: string[];
+ coachSort?: string;
};
export type File = {
@@ -83,9 +84,18 @@ export interface ExpertItem {
speciality?: string;
specialityDesc?: string;
description?: string;
+ coachLanguages?: string[]
}
-export type ExpertsData = ExpertItem[];
+export type ExpertsData = {
+ coaches: ExpertItem[];
+ themesGroups?: ThemeGroups[],
+ sessionCostMin?: number;
+ sessionCostMax?: number;
+ sessionDurationMin?: number;
+ sessionDurationMax?: number;
+ total: number;
+};
export type ExpertDetails = {
publicCoachDetails: ExpertItem & {
diff --git a/src/types/profile.ts b/src/types/profile.ts
new file mode 100644
index 0000000..c606879
--- /dev/null
+++ b/src/types/profile.ts
@@ -0,0 +1,12 @@
+export type Profile = {
+ id: number;
+ username?: string;
+ surname?: string;
+ fillProgress?: string;
+ faceImageUrl?: string;
+ role?: string;
+ login?: string;
+ hasPassword?: boolean;
+ hasExternalLogin?: boolean;
+ isTestMode?: boolean;
+};
diff --git a/src/types/sessions.ts b/src/types/sessions.ts
new file mode 100644
index 0000000..9400c13
--- /dev/null
+++ b/src/types/sessions.ts
@@ -0,0 +1,57 @@
+type User = {
+ id: number;
+ login?: string;
+ name?: string;
+ surname?: string;
+ faceImageUrl?: string;
+};
+
+type SessionTag = {
+ id: number;
+ groupId?: number;
+ name?: string;
+ isActive?: boolean;
+ isSelected?: boolean;
+ canDeleted?: boolean;
+};
+
+export type SessionsFilter = {
+ tags?: (number | string)[];
+ startDate?: string;
+ endDate?: string;
+};
+
+export type Session = {
+ id: number;
+ scheduledStartAtUtc?: string;
+ scheduledEndAtUtc?: string;
+ state?: string;
+ clientComment?: string;
+ secret?: string;
+ cost?: number;
+ themesTagName?: string;
+ totalDuration?: number;
+ type?: string;
+ maxClients: number;
+ title?: string;
+ description?: string;
+ isNeedSupervisor?: boolean;
+ supervisorComment?: string;
+ user?: User;
+ coach?: User;
+ supervisor?: User;
+ clients?: User[];
+ themesTags?: SessionTag[]
+};
+
+export enum SessionType {
+ UPCOMING = 'upcoming',
+ REQUESTED = 'requested',
+ RECENT = 'recent'
+}
+
+export type Sessions = {
+ [SessionType.UPCOMING]?: Session[];
+ [SessionType.REQUESTED]?: Session[];
+ [SessionType.RECENT]?: Session[];
+};
diff --git a/src/types/tags.ts b/src/types/tags.ts
index 20b0b2b..821ebec 100644
--- a/src/types/tags.ts
+++ b/src/types/tags.ts
@@ -19,3 +19,11 @@ export type SearchData = {
sessionDurationMin: number;
sessionDurationMax: number;
};
+
+export type Language = {
+ id: number;
+ code: string;
+ nativeSpelling: string;
+}
+
+export type Languages = Language[];
diff --git a/src/utils/filter.ts b/src/utils/filter.ts
index 3a2af00..3e7ae04 100644
--- a/src/utils/filter.ts
+++ b/src/utils/filter.ts
@@ -1,7 +1,8 @@
import { SearchData } from '../types/tags';
import { AdditionalFilter, Filter, GeneralFilter } from '../types/experts';
+import { DEFAULT_PAGE } from '../constants/common';
-export const getDefaultFilter = (searchData: SearchData | null): Filter => {
+export const getDefaultFilter = (searchData: SearchData | null, pageSize?: number): Filter => {
const themesTagIds = searchData?.themesGroups?.reduce((result, { tags }) => {
const t = tags?.map(({ id }) => id) || [];
@@ -16,15 +17,23 @@ export const getDefaultFilter = (searchData: SearchData | null): Filter => {
priceFrom: null,
priceTo: null,
durationFrom: null,
- durationTo: null
+ durationTo: null,
+ pageSize,
+ page: DEFAULT_PAGE
} as Filter;
};
-export const getFilter = (searchData: SearchData | null, searchParams?: { [key: string]: string | string[] | undefined }): Filter => {
- const filter = getDefaultFilter(searchData);
+type FilterProps = {
+ searchData: SearchData | null,
+ pageSize?: number,
+ searchParams?: { [key: string]: string | string[] | undefined }
+}
+
+export const getFilter = ({ searchData, searchParams, pageSize }: FilterProps): Filter => {
+ const filter = getDefaultFilter(searchData, pageSize);
if (searchParams) {
- const { themesTagIds, priceFrom, priceTo, durationFrom, durationTo } = searchParams;
+ const { themesTagIds, priceFrom, priceTo, durationFrom, durationTo, page, pageSize } = searchParams;
if (themesTagIds) {
if (Array.isArray(themesTagIds) && themesTagIds?.length > 0) {
@@ -51,20 +60,33 @@ export const getFilter = (searchData: SearchData | null, searchParams?: { [key:
if (durationTo) {
filter.durationTo = +durationTo;
}
+
+ if (page) {
+ filter.page = +page;
+ }
+
+ if (pageSize) {
+ filter.pageSize = +pageSize;
+ }
}
return filter;
};
-export const getObjectByFilter = (searchParams?: any): Filter | undefined => {
+export const getObjectByFilter = (searchParams?: any): Filter => {
const filter: Filter = {};
let tags = searchParams?.getAll('themesTagIds');
+ let languages = searchParams?.getAll('userLanguages');
if (tags && tags.length > 0) {
filter.themesTagIds = tags.map((id: string) => Number(id));
}
+ if (languages && languages.length > 0) {
+ filter.userLanguages = languages.map((code: string) => code);
+ }
+
if (searchParams?.has('priceFrom')) {
filter.priceFrom = Number(searchParams.get('priceFrom'));
}
@@ -81,6 +103,10 @@ export const getObjectByFilter = (searchParams?: any): Filter | undefined => {
filter.durationTo = Number(searchParams.get('durationTo'));
}
+ if (searchParams?.has('page')) {
+ filter.page = Number(searchParams.get('page'));
+ }
+
return filter;
};
@@ -91,12 +117,8 @@ export const getObjectByAdditionalFilter = (searchParams?: any): AdditionalFilte
additionalFilter.text = searchParams.get('text');
}
- if (searchParams?.has('sort')) {
- additionalFilter.sort = searchParams.get('sort');
- }
-
- if (searchParams?.has('language')) {
- additionalFilter.language = searchParams.getAll('language');
+ if (searchParams?.has('coachSort')) {
+ additionalFilter.coachSort = searchParams.get('coachSort');
}
return additionalFilter;