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')}
  • -
    -
    -
    - -
    -
    {t('photo-desc')}
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - - {t('change-password')} - -
    - -
    + + + ); }; 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 ( +
    +
    +
    + +
    +
    {photoDesc}
    +
    + + + + + +
    + + + +
    +
    + + + +
    + {/*
    + + + +
    */} +
    + + + +
    +
    + + {changePasswordLink} + +
    + +
    + ); +}; 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}

    -
    - +
    {priceTitle}
    -

    {durationTitle}

    -
    - +
    +

    Duration

    +
    + +
    +
    {durationTitle}
    + ); +}; diff --git a/src/components/view/CustomInput.tsx b/src/components/view/CustomInput.tsx index 3cf4cf7..c06a98a 100644 --- a/src/components/view/CustomInput.tsx +++ b/src/components/view/CustomInput.tsx @@ -8,8 +8,13 @@ const Input = styled(AntdInput)` border: 1px solid #F8F8F7 !important; border-radius: 8px !important; color: #000 !important; + align-items: center; - &:focus, &:hover { + input { + background-color: transparent !important; + } + + &:focus, &:hover, &:focus-within { border-color: #66A5AD !important; box-shadow: none !important; } diff --git a/src/components/view/CustomPagination.tsx b/src/components/view/CustomPagination.tsx index 0f807af..9b0361f 100644 --- a/src/components/view/CustomPagination.tsx +++ b/src/components/view/CustomPagination.tsx @@ -4,8 +4,6 @@ import React from 'react'; import styled from 'styled-components'; import { Pagination as AntdPagination, PaginationProps } from 'antd'; -const DEFAULT_SIZE = 5; - const Pagination = styled(AntdPagination)` display: flex; gap: 8px; @@ -52,7 +50,6 @@ export const CustomPagination = (props: PaginationProps) => ( ); diff --git a/src/components/view/CustomSlider.tsx b/src/components/view/CustomSlider.tsx index a9eacbc..2326fc5 100644 --- a/src/components/view/CustomSlider.tsx +++ b/src/components/view/CustomSlider.tsx @@ -38,7 +38,7 @@ const Slider = styled(AntdSlider)` &:focus, &:hover { &::after { - box-shadow: 0 0 0 10px rgba(102, 165, 173, .2) !important; + box-shadow: 0 0 0 12px rgba(102, 165, 173, .2) !important; } } } diff --git a/src/components/view/Loader.tsx b/src/components/view/Loader.tsx new file mode 100644 index 0000000..b62fa58 --- /dev/null +++ b/src/components/view/Loader.tsx @@ -0,0 +1,30 @@ +import React, { FC, ReactNode } from 'react'; +import { CustomSpin } from './CustomSpin'; +import { WithError } from './WithError'; + +type LoaderProps = { + children?: ReactNode; + isLoading?: boolean; + errorData?: any; + refresh?: () => void; +}; + +export const Loader: FC = ({ + children, + isLoading, + errorData, + refresh +}) => { + if (isLoading) { + return + } + + return ( + + {children} + + ); +}; diff --git a/src/components/view/WithError.tsx b/src/components/view/WithError.tsx new file mode 100644 index 0000000..454a45f --- /dev/null +++ b/src/components/view/WithError.tsx @@ -0,0 +1,31 @@ +import React, { FC, ReactNode } from 'react'; +import { Button, Result } from 'antd'; + +type WithErrorProps = { + children?: ReactNode; + errorData?: any; + refresh?: () => void; +}; + +export const WithError: FC = ({ + children, + errorData, + refresh +}) => { + if (errorData) { + return ( + + Refresh page + + ) : 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;