feat: add custom form fields, fix auth modal

This commit is contained in:
Сюткина Дарья Александровна (4047910) 2024-02-01 18:49:09 +04:00
parent c94c69202e
commit 9f225294c7
27 changed files with 587 additions and 222 deletions

43
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"antd": "^5.12.1", "antd": "^5.12.1",
"axios": "^1.6.5", "axios": "^1.6.5",
"lodash": "^4.17.21",
"next": "14.0.3", "next": "14.0.3",
"next-intl": "^3.3.1", "next-intl": "^3.3.1",
"react": "^18", "react": "^18",
@ -22,9 +23,11 @@
}, },
"devDependencies": { "devDependencies": {
"@next/eslint-plugin-next": "^14.0.4", "@next/eslint-plugin-next": "^14.0.4",
"@types/lodash": "^4.14.202",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/react-slick": "^0.23.13",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-config-next": "^14.0.3", "eslint-config-next": "^14.0.3",
@ -640,6 +643,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"node_modules/@types/lodash": {
"version": "4.14.202",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
"dev": true
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.9.4", "version": "20.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
@ -675,6 +684,15 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/react-slick": {
"version": "0.23.13",
"resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz",
"integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/scheduler": { "node_modules/@types/scheduler": {
"version": "0.16.8", "version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
@ -3209,6 +3227,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -5705,6 +5728,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"@types/lodash": {
"version": "4.14.202",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
"dev": true
},
"@types/node": { "@types/node": {
"version": "20.9.4", "version": "20.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
@ -5740,6 +5769,15 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-slick": {
"version": "0.23.13",
"resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz",
"integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/scheduler": { "@types/scheduler": {
"version": "0.16.8", "version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
@ -7606,6 +7644,11 @@
"p-locate": "^5.0.0" "p-locate": "^5.0.0"
} }
}, },
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.debounce": { "lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",

View File

@ -13,6 +13,7 @@
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"antd": "^5.12.1", "antd": "^5.12.1",
"axios": "^1.6.5", "axios": "^1.6.5",
"lodash": "^4.17.21",
"next": "14.0.3", "next": "14.0.3",
"next-intl": "^3.3.1", "next-intl": "^3.3.1",
"react": "^18", "react": "^18",
@ -23,9 +24,11 @@
}, },
"devDependencies": { "devDependencies": {
"@next/eslint-plugin-next": "^14.0.4", "@next/eslint-plugin-next": "^14.0.4",
"@types/lodash": "^4.14.202",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/react-slick": "^0.23.13",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-config-next": "^14.0.3", "eslint-config-next": "^14.0.3",

View File

@ -1,16 +1,9 @@
import React from 'react'; import React from 'react';
import { getTranslations } from 'next-intl/server'; import { useTranslations } from 'next-intl';
import { getFilter } from '../../../../utils/filter';
import { getExpertsList } from '../../../../actions/experts';
import { getTagList } from '../../../../actions/tags';
import { Experts } from '../../../../components/Experts/Experts'; import { Experts } from '../../../../components/Experts/Experts';
export default async function ExpertsPage({ params, searchParams }: { params: { locale: string }, searchParams: { [key: string]: string | string[] | undefined } }) { export default function ExpertsPage({ params }: { params: { locale: string } }) {
const searchData = await getTagList(params.locale); const t = useTranslations('Experts');
const filter = getFilter(searchData, searchParams);
console.log('filter main page', filter);
const experts = await getExpertsList(filter, params.locale);
const t = await getTranslations('Experts');
return ( return (
<div className="main-find"> <div className="main-find">
@ -21,10 +14,7 @@ export default async function ExpertsPage({ params, searchParams }: { params: {
<img src="/images/options-outline.svg" className="" alt=""/> <img src="/images/options-outline.svg" className="" alt=""/>
</div> </div>
</div> </div>
<Experts <Experts locale={params.locale} />
experts={experts}
searchData={searchData}
/>
</div> </div>
</div> </div>
); );

View File

@ -3,16 +3,16 @@ import type { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Bbuddy - News item', title: 'Bbuddy - Blog item',
description: 'Bbuddy desc news item' description: 'Bbuddy desc blog item'
}; };
export function generateStaticParams() { export function generateStaticParams() {
return [{ newsId: 'news-1' }, { newsId: 'news-2' }]; return [{ blogId: 'news-1' }, { blogId: 'news-2' }];
} }
export default function NewsItem({ params }: { params: { newsId: string } }) { export default function BlogItem({ params }: { params: { blogId: string } }) {
if (!params?.newsId) notFound(); if (!params?.blogId) notFound();
return ( return (
<div className="b-news-page"> <div className="b-news-page">
@ -20,7 +20,7 @@ export default function NewsItem({ params }: { params: { newsId: string } }) {
<h1 className="b-news-page__title">6 learnings from Shivpuri to Silicon Valley</h1> <h1 className="b-news-page__title">6 learnings from Shivpuri to Silicon Valley</h1>
<div className="news-item__badge">Leadership & Management</div> <div className="news-item__badge">Leadership & Management</div>
<div className="b-news-page__text"> <div className="b-news-page__text">
{`news id ${params.newsId}`}<br /> {`news id ${params.blogId}`}<br />
Im excited to kick off this series of newsletters where Ill be sharing my experiences, learnings, Im excited to kick off this series of newsletters where Ill be sharing my experiences, learnings,
and best practices which helped me to grow both in my personal and professional life. My hope is to and best practices which helped me to grow both in my personal and professional life. My hope is to
give back to the community and help anyone connect directly with me who may have got impacted with give back to the community and help anyone connect directly with me who may have got impacted with

View File

@ -2,11 +2,11 @@ import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Bbuddy - News', title: 'Bbuddy - Blog',
description: 'Bbuddy desc news' description: 'Bbuddy desc blog'
}; };
export default function News() { export default function Blog() {
return ( return (
<div className="b-news"> <div className="b-news">
<div className="b-news__header"> <div className="b-news__header">

View File

@ -1,9 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server'; import { useTranslations } from 'next-intl';
import { getFilter } from '../../../utils/filter';
import { getExpertsList } from '../../../actions/experts';
import { getTagList } from '../../../actions/tags';
import { Experts } from '../../../components/Experts/Experts'; import { Experts } from '../../../components/Experts/Experts';
export const metadata: Metadata = { export const metadata: Metadata = {
@ -11,12 +8,8 @@ export const metadata: Metadata = {
description: 'Bbuddy desc experts' description: 'Bbuddy desc experts'
}; };
export default async function ExpertsPage({ params, searchParams }: { params: { locale: string }, searchParams: { [key: string]: string | string[] | undefined } }) { export default function ExpertsPage({ params }: { params: { locale: string } }) {
const searchData = await getTagList(params.locale); const t = useTranslations('Experts');
const filter = getFilter(searchData, searchParams);
console.log('filter experts page', filter);
const experts = await getExpertsList(filter, params.locale);
const t = await getTranslations('Experts');
return ( return (
<div className="page-search"> <div className="page-search">
@ -29,8 +22,8 @@ export default async function ExpertsPage({ params, searchParams }: { params: {
</div> </div>
</div> </div>
<Experts <Experts
experts={experts} locale={params.locale}
searchData={searchData} basePath="/experts"
/> />
</div> </div>
</div> </div>

View File

@ -1,20 +1,20 @@
'use client'; 'use client';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import {Button, Select} from 'antd'; import { Button } from 'antd';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useRouter } from '../../navigation'; import { useRouter } from '../../navigation';
import { AdditionalFilter } from '../../types/experts'; import { AdditionalFilter } from '../../types/experts';
import { LOCALES } from '../../constants/locale'; import { LOCALES } from '../../constants/locale';
import { getObjectByFilter, getObjectByAdditionalFilter } from '../../utils/filter'; import { getObjectByFilter, getObjectByAdditionalFilter } from '../../utils/filter';
import { CustomInput } from '../view'; import { CustomInput, CustomSelect, CustomMultiSelect } from '../view';
type ExpertAdditionalFilterProps = { type ExpertAdditionalFilterProps = {
searchPlaceholder: string; searchPlaceholder: string;
sortLabel: string; sortLabel: string;
langLabel: string; langLabel: string;
buttonFind: string; buttonFind: string;
basePath?: string; basePath: string;
}; };
export const ExpertsAdditionalFilter = ({ export const ExpertsAdditionalFilter = ({
@ -22,28 +22,61 @@ export const ExpertsAdditionalFilter = ({
sortLabel, sortLabel,
langLabel, langLabel,
buttonFind, buttonFind,
basePath = '/', basePath,
}): ExpertAdditionalFilterProps => { }: ExpertAdditionalFilterProps) => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const [filter, setFilter] = useState<AdditionalFilter | undefined>(getObjectByAdditionalFilter(searchParams)); const [filter, setFilter] = useState<AdditionalFilter | undefined>(getObjectByAdditionalFilter(searchParams));
const onChangeFilter = useCallback((key: string, value: any) => { const onChangeInput = useCallback((e: any) => {
// setFilter({ if (e?.target?.value) {
// ...filter, setFilter({
// [key]: value ...filter,
// }) text: e.target.value
});
} else {
if (filter?.text) {
const newFilter = { ...filter };
delete newFilter.text;
setFilter(newFilter);
}
}
}, [filter]);
const onChangeSort = useCallback((value: string) => {
const newFilter: AdditionalFilter = { ...filter };
if (value) {
newFilter.sort = value;
} else {
delete newFilter?.sort;
}
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]); }, [filter]);
const goToFilterPage = useCallback(() => { const goToFilterPage = useCallback(() => {
router.push({ router.push({
pathname: basePath, pathname: basePath as any,
query: { query: {
...getObjectByFilter(searchParams), ...getObjectByFilter(searchParams),
...filter ...filter
} }
}) })
}, [filter, searchParams]); }, [filter, searchParams, router]);
return ( return (
<div className="main-find__search"> <div className="main-find__search">
@ -51,13 +84,14 @@ export const ExpertsAdditionalFilter = ({
<CustomInput <CustomInput
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
defaultValue={filter?.text} defaultValue={filter?.text}
onChange={(e: any) => onChangeFilter('text', e?.target?.value)} onChange={onChangeInput}
/> />
</div> </div>
<div className="main-find__search__sort"> <div className="main-find__search__sort">
<Select <CustomSelect
defaultValue={filter?.sort} label={sortLabel}
onChange={(val) => onChangeFilter('sort', val)} value={filter?.sort}
onChange={onChangeSort}
options={[ options={[
{ value: 'byTop', label: 'By top views' }, { value: 'byTop', label: 'By top views' },
{ value: 'byPriceAsc', label: 'By price ascending' }, { value: 'byPriceAsc', label: 'By price ascending' },
@ -67,10 +101,10 @@ export const ExpertsAdditionalFilter = ({
/> />
</div> </div>
<div className="main-find__search__language"> <div className="main-find__search__language">
<Select <CustomMultiSelect
mode="multiple" label={langLabel}
defaultValue={filter?.language} value={filter?.language}
onChange={(val) => onChangeFilter('language', val)} onChange={onChangeLang}
options={Object.entries(LOCALES).map(([ value, label ]) => ({ value, label }))} options={Object.entries(LOCALES).map(([ value, label ]) => ({ value, label }))}
/> />
</div> </div>

View File

@ -1,28 +1,31 @@
import React from 'react'; import React from 'react';
import { useTranslations } from 'next-intl'; import { getTranslations } from 'next-intl/server';
import { SearchData } from '../../types/tags'; import { getTagList } from '../../actions/tags';
import { ExpertsData } from '../../types/experts'; import { getFilter } from '../../utils/filter';
import { getExpertsList } from '../../actions/experts';
import { ExpertsFilter } from './Filter'; import { ExpertsFilter } from './Filter';
import { ExpertsAdditionalFilter } from './AdditionalFilter'; import { ExpertsAdditionalFilter } from './AdditionalFilter';
import { ExpertsList } from './ExpertsList'; import { ExpertsList } from './ExpertsList';
import { CustomPagination } from '../view/CustomPagination';
type ExpertsProps = { type ExpertsProps = {
searchData?: SearchData; basePath?: string;
experts?: ExpertsData; locale: string;
}; };
export const Experts = ({ searchData, experts }: ExpertsProps) => { export const Experts = async ({ basePath = '/', locale }: ExpertsProps) => {
const t = useTranslations('Experts'); const t = await getTranslations('Experts');
const searchData = await getTagList(locale);
const filter = getFilter(searchData);
const experts = await getExpertsList(filter, locale);
return ( return (
<div className="row"> <div className="row">
<div className="col-xl-3 col-lg-4 d-none d-lg-block"> <div className="col-xl-3 col-lg-4 d-none d-lg-block">
<ExpertsFilter <ExpertsFilter
searchData={searchData} searchData={searchData}
basePath="/experts" basePath={basePath}
priceTitle={t('filter.price', { from: searchData.sessionCostMin, to: searchData.sessionCostMax })} priceTitle={t('filter.price', { from: searchData?.sessionCostMin || 0, to: searchData?.sessionCostMax || 0 })}
durationTitle={t('filter.duration', { from: searchData.sessionDurationMin, to: searchData.sessionDurationMax })} durationTitle={t('filter.duration', { from: searchData?.sessionDurationMin || 0, to: searchData?.sessionDurationMax || 0 })}
buttonApply={t('filter.apply')} buttonApply={t('filter.apply')}
/> />
</div> </div>
@ -32,15 +35,16 @@ export const Experts = ({ searchData, experts }: ExpertsProps) => {
sortLabel={t('filter.sort')} sortLabel={t('filter.sort')}
langLabel={t('filter.language')} langLabel={t('filter.language')}
buttonFind={t('filter.find')} buttonFind={t('filter.find')}
basePath="/experts" basePath={basePath}
/> />
<ExpertsList <ExpertsList
locale={locale}
data={experts} data={experts}
baseFilter={filter}
priceTitle={t('list.price')} priceTitle={t('list.price')}
durationTitle={t('list.duration')} durationTitle={t('list.duration')}
detailButton={t('list.details')} detailButton={t('list.details')}
/> />
<CustomPagination total={20} />
</div> </div>
</div> </div>
) )

View File

@ -1,33 +1,61 @@
'use client'; 'use client';
import React from 'react'; import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { List, Tag } from 'antd'; import { List, Tag } from 'antd';
import { RightOutlined } from '@ant-design/icons'; import { RightOutlined } from '@ant-design/icons';
import isEqual from 'lodash/isEqual';
import Image from 'next/image'; import Image from 'next/image';
import { Link } from '../../navigation'; import { Link } from '../../navigation';
import { ExpertsData } from '../../types/experts'; import { ExpertsData, Filter } from '../../types/experts';
import { getObjectByFilter } from '../../utils/filter';
import { getExpertsList } from '../../actions/experts';
import { CustomPagination, CustomSpin } from '../view';
type ExpertListProps = { type ExpertListProps = {
data: ExpertsData; data?: ExpertsData;
priceTitle: string; priceTitle: string;
durationTitle: string; durationTitle: string;
detailButton: string; detailButton: string;
locale: string;
baseFilter: Filter;
}; };
export const ExpertsList = ({ export const ExpertsList = ({
data, data,
priceTitle, priceTitle,
durationTitle, durationTitle,
detailButton detailButton,
locale,
baseFilter
}: ExpertListProps) => { }: ExpertListProps) => {
const searchParams = useSearchParams();
const getTitle = (str: string, value?: any): string => (value ? str.replace('0', value) : str); const getTitle = (str: string, value?: any): string => (value ? str.replace('0', value) : str);
const [experts, setExperts] = useState<ExpertsData | undefined>();
return ( useEffect(() => {
const filter = {
...baseFilter,
...getObjectByFilter(searchParams)
};
if (!isEqual(baseFilter, filter)) {
getExpertsList(filter, locale)
.then((experts) => {
setExperts(experts);
});
} else {
setExperts(data);
}
}, []);
return experts ? (
<>
<List <List
itemLayout="vertical" itemLayout="vertical"
size="large" size="large"
className="search-result" className="search-result"
dataSource={data} dataSource={experts}
renderItem={(item) => ( renderItem={(item) => (
<List.Item key={item?.id} className="card-profile"> <List.Item key={item?.id} className="card-profile">
<List.Item.Meta <List.Item.Meta
@ -73,5 +101,7 @@ export const ExpertsList = ({
</List.Item> </List.Item>
)} )}
/> />
); <CustomPagination total={20} />
</>
) : <CustomSpin />;
}; };

View File

@ -10,8 +10,8 @@ import { getObjectByFilter, getObjectByAdditionalFilter } from '../../utils/filt
import { CustomSwitch, CustomSlider } from '../view'; import { CustomSwitch, CustomSlider } from '../view';
type ExpertsFilterProps = { type ExpertsFilterProps = {
searchData: SearchData; searchData?: SearchData;
basePath?: string; basePath: string;
priceTitle: string; priceTitle: string;
durationTitle: string; durationTitle: string;
buttonApply: string; buttonApply: string;
@ -19,7 +19,7 @@ type ExpertsFilterProps = {
export const ExpertsFilter = ({ export const ExpertsFilter = ({
searchData, searchData,
basePath = '/', basePath,
priceTitle, priceTitle,
durationTitle, durationTitle,
buttonApply buttonApply
@ -83,7 +83,7 @@ export const ExpertsFilter = ({
const goToFilterPage = useCallback(() => { const goToFilterPage = useCallback(() => {
router.push({ router.push({
pathname: basePath, pathname: basePath as any,
query: { query: {
...filter, ...filter,
...getObjectByAdditionalFilter(searchParams) ...getObjectByAdditionalFilter(searchParams)
@ -116,7 +116,7 @@ export const ExpertsFilter = ({
return ( return (
<div className="b-filter"> <div className="b-filter">
{searchData.themesGroups?.length && searchData.themesGroups.map(({ id, name, tags }) => ( {searchData?.themesGroups?.length && searchData.themesGroups.map(({ id, name, tags }) => (
<div key={id}> <div key={id}>
<h3 className="title-h3">{name}</h3> <h3 className="title-h3">{name}</h3>
{getList(tags)} {getList(tags)}
@ -127,9 +127,9 @@ export const ExpertsFilter = ({
<CustomSlider <CustomSlider
range range
step={1} step={1}
defaultValue={[filter?.priceFrom || searchData.sessionCostMin, filter?.priceTo || searchData.sessionCostMax]} defaultValue={[filter?.priceFrom || searchData?.sessionCostMin || 0, filter?.priceTo || searchData?.sessionCostMax || 0]}
min={searchData.sessionCostMin} min={searchData?.sessionCostMin || 0}
max={searchData.sessionCostMax} max={searchData?.sessionCostMax || 0}
onChange={onChangePrice} onChange={onChangePrice}
/> />
</div> </div>
@ -138,9 +138,9 @@ export const ExpertsFilter = ({
<CustomSlider <CustomSlider
range range
step={1} step={1}
defaultValue={[filter?.durationFrom || searchData.sessionDurationMin, filter?.durationTo || searchData.sessionDurationMax]} defaultValue={[filter?.durationFrom || searchData?.sessionDurationMin || 0, filter?.durationTo || searchData?.sessionDurationMax || 0]}
min={searchData.sessionDurationMin} min={searchData?.sessionDurationMin || 0}
max={searchData.sessionDurationMax} max={searchData?.sessionDurationMax || 0}
onChange={onChangeDuration} onChange={onChangeDuration}
/> />
</div> </div>

View File

@ -3,12 +3,13 @@
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link';
import {Button, Modal as AntdModal, Form, notification} from 'antd'; import {Button, Modal as AntdModal, Form, notification} from 'antd';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { styled } from 'styled-components'; import { styled } from 'styled-components';
import { CustomInput } from '../view'; import { CustomInput, CustomInputPassword } from '../view';
import { getAuth } from '../../actions/auth'; import { getAuth } from '../../actions/auth';
import {setAuthToken} from "../../utils/storage/auth"; import { setAuthToken } from '../../utils/storage/auth';
type AuthModalProps = { type AuthModalProps = {
open: boolean; open: boolean;
@ -74,7 +75,7 @@ export const AuthModal: FC<AuthModalProps> = ({
mode, mode,
updateMode updateMode
}) => { }) => {
const [form] = Form.useForm<{ login: string, password: string, name: string, surname: string, phone: string }>(); const [form] = Form.useForm<{ login: string, password: string, confirmPassword: string }>();
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const paths = usePathname().split('/'); const paths = usePathname().split('/');
@ -134,10 +135,9 @@ export const AuthModal: FC<AuthModalProps> = ({
/> />
</Form.Item> </Form.Item>
<Form.Item name="password" rules={[{ required: true }]} noStyle> <Form.Item name="password" rules={[{ required: true }]} noStyle>
<CustomInput <CustomInputPassword
size="small" size="small"
placeholder="Password" placeholder="Password"
type="password"
/> />
</Form.Item> </Form.Item>
</Form> </Form>
@ -176,25 +176,6 @@ export const AuthModal: FC<AuthModalProps> = ({
{mode === 'register' && ( {mode === 'register' && (
<> <>
<Form form={form} autoComplete="off" style={{ display: 'flex', gap: 16, flexDirection: 'column' }}> <Form form={form} autoComplete="off" style={{ display: 'flex', gap: 16, flexDirection: 'column' }}>
<Form.Item name="name" noStyle>
<CustomInput
size="small"
placeholder="Name"
/>
</Form.Item>
<Form.Item name="surname" noStyle>
<CustomInput
size="small"
placeholder="Surname"
/>
</Form.Item>
<Form.Item name="phone" noStyle>
<CustomInput
size="small"
placeholder="Phone"
type="phone"
/>
</Form.Item>
<Form.Item name="login" rules={[{ required: true }]} noStyle> <Form.Item name="login" rules={[{ required: true }]} noStyle>
<CustomInput <CustomInput
size="small" size="small"
@ -202,10 +183,15 @@ export const AuthModal: FC<AuthModalProps> = ({
/> />
</Form.Item> </Form.Item>
<Form.Item name="password" rules={[{ required: true }]} noStyle> <Form.Item name="password" rules={[{ required: true }]} noStyle>
<CustomInput <CustomInputPassword
size="small" size="small"
placeholder="Password" placeholder="Password"
type="password" />
</Form.Item>
<Form.Item name="confirmPassword" rules={[{ required: true }]} noStyle>
<CustomInputPassword
size="small"
placeholder="Confirm password"
/> />
</Form.Item> </Form.Item>
</Form> </Form>
@ -214,22 +200,7 @@ export const AuthModal: FC<AuthModalProps> = ({
> >
Register Register
</FilledButton> </FilledButton>
<span>or</span> <OutlinedButton onClick={() => updateMode('enter')}>Enter</OutlinedButton>
<OutlinedButton
icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />}
>
Facebook account
</OutlinedButton>
<OutlinedButton
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
>
Apple account
</OutlinedButton>
<OutlinedButton
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
>
Google account
</OutlinedButton>
</> </>
)} )}
{mode === 'reset' && ( {mode === 'reset' && (
@ -242,10 +213,9 @@ export const AuthModal: FC<AuthModalProps> = ({
/> />
</Form.Item> </Form.Item>
<Form.Item name="password" rules={[{ required: true }]} noStyle> <Form.Item name="password" rules={[{ required: true }]} noStyle>
<CustomInput <CustomInputPassword
size="small" size="small"
placeholder="Password" placeholder="Password"
type="password"
/> />
</Form.Item> </Form.Item>
</Form> </Form>
@ -277,7 +247,7 @@ export const AuthModal: FC<AuthModalProps> = ({
)} )}
<div className="b-modal__auth__agreement"> <div className="b-modal__auth__agreement">
I have read and agree with the terms of the I have read and agree with the terms of the
User Agreement, Privacy Policy User Agreement, <Link href={'/docs/BBUDDY_privacy_policy_fin.docx' as any}>Privacy Policy</Link>
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@ -2,10 +2,11 @@
import React, { FC, useState, useEffect } from 'react'; import React, { FC, useState, useEffect } from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import { useSelectedLayoutSegment } from 'next/navigation';
import { styled } from 'styled-components'; import { styled } from 'styled-components';
import { AuthModal } from '../../Modals/AuthModal'; import { AuthModal } from '../../Modals/AuthModal';
import {checkAuthToken} from "../../../utils/storage/auth"; import { checkAuthToken } from '../../../utils/storage/auth';
import {Link} from "../../../navigation"; import { Link } from '../../../navigation';
type HeaderAuthLinksProps = { type HeaderAuthLinksProps = {
enterTitle: string; enterTitle: string;
@ -32,6 +33,8 @@ export const HeaderAuthLinks: FC<HeaderAuthLinksProps> = ({
}) => { }) => {
const [isOpenModal, setIsOpenModal] = useState<boolean>(false); const [isOpenModal, setIsOpenModal] = useState<boolean>(false);
const [mode, setMode] = useState<'enter' | 'register' | 'reset' | 'finish'>('enter'); const [mode, setMode] = useState<'enter' | 'register' | 'reset' | 'finish'>('enter');
const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || '';
useEffect(() => { useEffect(() => {
if (!isOpenModal) { if (!isOpenModal) {
@ -47,7 +50,7 @@ export const HeaderAuthLinks: FC<HeaderAuthLinksProps> = ({
return checkAuthToken() return checkAuthToken()
? ( ? (
<li> <li>
<Link href={'/sessions' as any}>{accountTitle}</Link> <Link href={'/account' as any} className={pathname === 'account' ? 'active' : ''}>{accountTitle}</Link>
</li> </li>
) )
: ( : (

View File

@ -1,20 +1,38 @@
import React, { ReactNode } from 'react'; 'use client';
import React from 'react';
import { useSelectedLayoutSegment } from 'next/navigation';
import { Link } from '../../../navigation';
import { HeaderAuthLinks } from './HeaderAuthLinks'; import { HeaderAuthLinks } from './HeaderAuthLinks';
type HeaderMenuProps = { type HeaderMenuProps = {
children: ReactNode;
enterTitle: string; enterTitle: string;
registerTitle: string; registerTitle: string;
accountTitle: string; accountTitle: string;
linkConfig: { path: string, title: string }[];
}; };
export const HeaderMenu = ({ children, enterTitle, registerTitle, accountTitle }: HeaderMenuProps) => ( export const HeaderMenu = ({
enterTitle,
registerTitle,
accountTitle,
linkConfig
}: HeaderMenuProps) => {
const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || '';
return (
<div className="b-header__nav"> <div className="b-header__nav">
<nav> <nav>
<ul className="b-header__nav__list "> <ul className="b-header__nav__list ">
{children} {linkConfig.map(({ path, title }) => (
<li key={path}>
<Link href={`/${path}` as any} className={pathname === path ? 'active' : ''}>{title}</Link>
</li>
))}
<HeaderAuthLinks enterTitle={enterTitle} registerTitle={registerTitle} accountTitle={accountTitle} /> <HeaderAuthLinks enterTitle={enterTitle} registerTitle={registerTitle} accountTitle={accountTitle} />
</ul> </ul>
</nav> </nav>
</div> </div>
); );
}

View File

@ -1,17 +1,26 @@
'use client' 'use client'
import React, { FC, ReactNode, useState } from 'react'; import React, { FC, useState } from 'react';
import { useSelectedLayoutSegment } from 'next/navigation';
import { HeaderAuthLinks } from './HeaderAuthLinks'; import { HeaderAuthLinks } from './HeaderAuthLinks';
import { Link } from '../../../navigation';
type HeaderMenuMobileProps = { type HeaderMenuMobileProps = {
children: ReactNode; linkConfig: { path: string, title: string }[];
enterTitle: string; enterTitle: string;
registerTitle: string; registerTitle: string;
accountTitle: string; accountTitle: string;
}; };
export const HeaderMobileMenu: FC<HeaderMenuMobileProps> = ({ children, enterTitle, registerTitle, accountTitle }) => { export const HeaderMobileMenu: FC<HeaderMenuMobileProps> = ({
linkConfig,
enterTitle,
registerTitle,
accountTitle
}) => {
const [showMobileMenu, setShowMobileMenu] = useState<boolean>(false); const [showMobileMenu, setShowMobileMenu] = useState<boolean>(false);
const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || '';
return ( return (
<> <>
@ -40,7 +49,11 @@ export const HeaderMobileMenu: FC<HeaderMenuMobileProps> = ({ children, enterTit
</div> </div>
<div className="menu-mobile__body"> <div className="menu-mobile__body">
<ul className="menu-mobile__list"> <ul className="menu-mobile__list">
{children} {linkConfig.map(({ path, title }) => (
<li key={path}>
<Link href={`/${path}` as any} className={pathname === path ? 'active' : ''}>{title}</Link>
</li>
))}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -13,11 +13,10 @@ type HeaderProps = {
export const Header: FC<HeaderProps> = ({ locale }) => { export const Header: FC<HeaderProps> = ({ locale }) => {
const t = useTranslations('Header'); const t = useTranslations('Header');
const routes = HEAD_ROUTES.map((item) => ( const routes: { path: string, title: string }[] = HEAD_ROUTES.map((item) => ({
<li key={item}> path: item,
<Link href={`/${item}` as any}>{t(`menu.${item}`)}</Link> title: t(`menu.${item}`)
</li> }));
));
return ( return (
<> <>
@ -30,15 +29,21 @@ export const Header: FC<HeaderProps> = ({ locale }) => {
alt="" alt=""
/> />
</Link> </Link>
<HeaderMenu enterTitle={t('enter')} registerTitle={t('registration')} accountTitle={t('account')}> <HeaderMenu
{routes} enterTitle={t('enter')}
</HeaderMenu> registerTitle={t('registration')}
accountTitle={t('account')}
linkConfig={routes}
/>
<LanguageSwitcher locale={locale} /> <LanguageSwitcher locale={locale} />
</div> </div>
</header> </header>
<HeaderMobileMenu enterTitle={t('enter')} registerTitle={t('registration')} accountTitle={t('account')}> <HeaderMobileMenu
{routes} enterTitle={t('enter')}
</HeaderMobileMenu> registerTitle={t('registration')}
accountTitle={t('account')}
linkConfig={routes}
/>
</> </>
); );
}; };

View File

@ -0,0 +1,45 @@
import React from 'react';
import styled from 'styled-components';
import { Input as AntdInput } from 'antd';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
const Input = styled(AntdInput.Password)`
padding: 15px 16px !important;
background: #F8F8F7 !important;
border: 1px solid #F8F8F7 !important;
border-radius: 8px !important;
color: #000 !important;
box-shadow: none !important;
input {
background: transparent !important;
}
&:focus, &:hover, &.ant-input-affix-wrapper-focused {
border-color: #66A5AD !important;
box-shadow: none !important;
}
input::placeholder {
color: #000 !important;
opacity: .4 !important;
}
&.ant-input-status-error:not(.ant-input-disabled):not(.ant-input-borderless) {
border-color: #ff4d4f !important;
}
.ant-input-suffix {
opacity: .3;
}
`;
export const CustomInputPassword = (props: any) => (
<Input
iconRender={(visible) => (visible
? <EyeOutlined style={{ color: '#2C7873', fontSize: 20}} />
: <EyeInvisibleOutlined style={{ color: '#2C7873', fontSize: 20}} />
)}
{...props}
/>
);

View File

@ -0,0 +1,106 @@
import React, {useEffect, useState} from 'react';
import styled from 'styled-components';
import { Select as AntdSelect, Tag } from 'antd';
import type { SelectProps } from 'antd';
type TagRender = SelectProps['tagRender'];
const Select = styled(AntdSelect)`
width: 100% !important;
height: 54px !important;
.ant-select-selector {
background-color: #F8F8F7 !important;
border-color: #F8F8F7 !important;
border-radius: 8px !important;
padding: 22px 16px 8px !important;
box-shadow: none !important;
.ant-select-selection-item {
font-size: 15px !important;
font-weight: 400 !important;
line-height: 24px !important;
color: #313131 !important;
}
}
.ant-select-selection-overflow-item {
margin-right: 4px;
}
.ant-select-arrow {
color: #2c7873 !important;
}
&.ant-select-focused, &:hover {
.ant-select-selector {
border-color: #2c7873 !important;
box-shadow: none !important;
}
}
`;
const SelectWrap = styled.div`
position: relative;
width: 100%;
`;
const SelectLabel = styled.div`
font-size: 15px;
font-style: normal;
font-weight: 400;
line-height: 24px;
color: #000;
opacity: .3;
position: absolute;
left: 16px;
top: 15px;
z-index: 1;
transition: all .1s ease;
.b-multiselect__active & {
font-size: 12px;
font-weight: 300;
line-height: 14px;
top: 8px;
}
`;
const tagRender: TagRender = (props) => {
const { label } = props;
return (
<Tag className="skills__list__item">
{label}
</Tag>
);
};
export const CustomMultiSelect = (props: any) => {
const { label, value, ...other } = props;
const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false);
useEffect(() => {
if (label) {
setIsActiveLabel(!!value?.length);
} else {
setIsActiveLabel(false);
}
}, [value]);
return (
<SelectWrap className={isActiveLabel ? 'b-multiselect__active' : ''}>
<SelectLabel>{label}</SelectLabel>
<Select
mode="multiple"
value={value}
showSearch={false}
maxTagCount="responsive"
tagRender={tagRender}
onFocus={!isActiveLabel ? () => setIsActiveLabel(true) : undefined}
onBlur={() => setIsActiveLabel(!!value?.length)}
{...other}
/>
</SelectWrap>
);
};

View File

@ -0,0 +1,85 @@
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Select as AntdSelect} from 'antd';
const Select = styled(AntdSelect)`
width: 100% !important;
height: 54px !important;
.ant-select-selector {
background-color: #F8F8F7 !important;
border-color: #F8F8F7 !important;
border-radius: 8px !important;
padding: 22px 16px 8px !important;
box-shadow: none !important;
.ant-select-selection-item {
font-size: 15px !important;
font-weight: 400 !important;
line-height: 24px !important;
color: #313131 !important;
}
}
.ant-select-arrow {
color: #2c7873 !important;
}
&.ant-select-focused, &:hover {
.ant-select-selector {
border-color: #2c7873 !important;
box-shadow: none !important;
}
}
`;
const SelectWrap = styled.div`
position: relative;
width: 100%;
`;
const SelectLabel = styled.div`
font-size: 15px;
font-style: normal;
font-weight: 400;
line-height: 24px;
color: #000;
opacity: .3;
position: absolute;
left: 16px;
top: 15px;
z-index: 1;
transition: all .1s ease;
.b-select__active & {
font-size: 12px;
font-weight: 300;
line-height: 14px;
top: 8px;
}
`;
export const CustomSelect = (props: any) => {
const { label, value, ...other } = props;
const [isActiveLabel, setIsActiveLabel] = useState<boolean>(false);
useEffect(() => {
if (label) {
setIsActiveLabel(!!value);
} else {
setIsActiveLabel(false);
}
}, [value]);
return (
<SelectWrap className={isActiveLabel ? 'b-select__active' : ''}>
<SelectLabel>{label}</SelectLabel>
<Select
value={value}
onFocus={!isActiveLabel ? () => setIsActiveLabel(true) : undefined}
onBlur={() => setIsActiveLabel(!!value)}
{...other}
/>
</SelectWrap>
);
};

View File

@ -0,0 +1,12 @@
import React from 'react';
import { Spin as AntdSpin } from 'antd';
import { styled } from 'styled-components';
const Spin = styled(AntdSpin)`
width: 100%;
margin: 24px 0;
`;
export const CustomSpin = (props: any) => (
<Spin {...props} />
);

View File

@ -5,3 +5,7 @@ export * from './CustomSlider';
export * from './CustomPagination'; export * from './CustomPagination';
export * from './CustomRate'; export * from './CustomRate';
export * from './CustomInput'; export * from './CustomInput';
export * from './CustomInputPassword';
export * from './CustomSelect';
export * from './CustomMultiSelect';
export * from './CustomSpin';

View File

@ -9,5 +9,4 @@ export default createMiddleware({
export const config = { export const config = {
matcher: ['/', '/(en|ru|de|it|es|fr)/:path*'] matcher: ['/', '/(en|ru|de|it|es|fr)/:path*']
// matcher: ['/', '/(en|ru|de|it|es|fr).html']
}; };

View File

@ -15,6 +15,10 @@ body{
color: #fff; color: #fff;
} }
a {
color: #66A5AD !important;
}
.b-wrapper { .b-wrapper {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -426,21 +430,21 @@ body{
.btn-apply { .btn-apply {
user-select: none; user-select: none;
outline: none; outline: none !important;
border: none; border: none !important;
text-decoration: none; text-decoration: none;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 8px !important;
background: #FFBD00; background: #FFBD00 !important;
box-shadow: 0px 2px 4px 0px rgba(252, 214, 70, 0.16); box-shadow: 0px 2px 4px 0px rgba(252, 214, 70, 0.16) !important;
display: flex; display: flex;
gap: 10px; gap: 10px;
height: 54px; height: 54px !important;
padding: 8px 31px; padding: 8px 31px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: #003B46; color: #003B46 !important;
@include rem(15); @include rem(15);
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;

View File

@ -70,7 +70,7 @@
display: inline-flex; display: inline-flex;
gap: 8px; gap: 8px;
text-decoration: none; text-decoration: none;
color: #003B46; color: #003B46 !important;
@include rem(16); @include rem(16);
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;

View File

@ -66,6 +66,10 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
text-decoration: none; text-decoration: none;
&.active {
color: #003B46;
}
} }
.text { .text {

View File

@ -199,7 +199,7 @@
@include rem(14); @include rem(14);
border-radius: 24px; border-radius: 24px;
gap: 6px; gap: 6px;
color: $white; color: $white !important;
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
line-height: 133.333%; line-height: 133.333%;

View File

@ -62,7 +62,7 @@ export const getObjectByFilter = (searchParams?: any): Filter | undefined => {
let tags = searchParams?.getAll('themesTagIds'); let tags = searchParams?.getAll('themesTagIds');
if (tags && tags.length > 0) { if (tags && tags.length > 0) {
filter.themesTagIds = tags.map((id) => Number(id)); filter.themesTagIds = tags.map((id: string) => Number(id));
} }
if (searchParams?.has('priceFrom')) { if (searchParams?.has('priceFrom')) {

View File

@ -7,7 +7,7 @@ export function checkAuthToken() {
} }
export function getAuthToken() { export function getAuthToken() {
return 'tr'; return '';
} }
// export function getAuthToken() { // export function getAuthToken() {
@ -18,6 +18,6 @@ export function getAuthToken() {
// localStorage.removeItem(AUTH_TOKEN_KEY); // localStorage.removeItem(AUTH_TOKEN_KEY);
// } // }
// //
// export function setAuthToken(token: string) { export function setAuthToken(token: string) {
// localStorage.setItem(AUTH_TOKEN_KEY, token); // localStorage.setItem(AUTH_TOKEN_KEY, token);
// } }