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",
"antd": "^5.12.1",
"axios": "^1.6.5",
"lodash": "^4.17.21",
"next": "14.0.3",
"next-intl": "^3.3.1",
"react": "^18",
@ -22,9 +23,11 @@
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.0.4",
"@types/lodash": "^4.14.202",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-slick": "^0.23.13",
"autoprefixer": "^10.0.1",
"eslint": "^8.55.0",
"eslint-config-next": "^14.0.3",
@ -640,6 +643,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"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": {
"version": "20.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
@ -675,6 +684,15 @@
"@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": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
@ -3209,6 +3227,11 @@
"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": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -5705,6 +5728,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"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": {
"version": "20.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
@ -5740,6 +5769,15 @@
"@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": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
@ -7606,6 +7644,11 @@
"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": {
"version": "4.0.8",
"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",
"antd": "^5.12.1",
"axios": "^1.6.5",
"lodash": "^4.17.21",
"next": "14.0.3",
"next-intl": "^3.3.1",
"react": "^18",
@ -23,9 +24,11 @@
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.0.4",
"@types/lodash": "^4.14.202",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-slick": "^0.23.13",
"autoprefixer": "^10.0.1",
"eslint": "^8.55.0",
"eslint-config-next": "^14.0.3",

View File

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

View File

@ -3,16 +3,16 @@ import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
export const metadata: Metadata = {
title: 'Bbuddy - News item',
description: 'Bbuddy desc news item'
title: 'Bbuddy - Blog item',
description: 'Bbuddy desc blog item'
};
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 } }) {
if (!params?.newsId) notFound();
export default function BlogItem({ params }: { params: { blogId: string } }) {
if (!params?.blogId) notFound();
return (
<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>
<div className="news-item__badge">Leadership & Management</div>
<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,
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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +1,61 @@
'use client';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
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 } 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 = {
data: ExpertsData;
data?: ExpertsData;
priceTitle: string;
durationTitle: string;
detailButton: string;
locale: string;
baseFilter: Filter;
};
export const ExpertsList = ({
data,
priceTitle,
durationTitle,
detailButton
detailButton,
locale,
baseFilter
}: ExpertListProps) => {
const searchParams = useSearchParams();
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
itemLayout="vertical"
size="large"
className="search-result"
dataSource={data}
dataSource={experts}
renderItem={(item) => (
<List.Item key={item?.id} className="card-profile">
<List.Item.Meta
@ -73,5 +101,7 @@ export const ExpertsList = ({
</List.Item>
)}
/>
);
<CustomPagination total={20} />
</>
) : <CustomSpin />;
};

View File

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

View File

@ -3,12 +3,13 @@
import React, { FC, useState } from 'react';
import { usePathname } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
import {Button, Modal as AntdModal, Form, notification} from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { styled } from 'styled-components';
import { CustomInput } from '../view';
import { CustomInput, CustomInputPassword } from '../view';
import { getAuth } from '../../actions/auth';
import {setAuthToken} from "../../utils/storage/auth";
import { setAuthToken } from '../../utils/storage/auth';
type AuthModalProps = {
open: boolean;
@ -74,7 +75,7 @@ export const AuthModal: FC<AuthModalProps> = ({
mode,
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 paths = usePathname().split('/');
@ -134,10 +135,9 @@ export const AuthModal: FC<AuthModalProps> = ({
/>
</Form.Item>
<Form.Item name="password" rules={[{ required: true }]} noStyle>
<CustomInput
<CustomInputPassword
size="small"
placeholder="Password"
type="password"
/>
</Form.Item>
</Form>
@ -176,25 +176,6 @@ export const AuthModal: FC<AuthModalProps> = ({
{mode === 'register' && (
<>
<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>
<CustomInput
size="small"
@ -202,10 +183,15 @@ export const AuthModal: FC<AuthModalProps> = ({
/>
</Form.Item>
<Form.Item name="password" rules={[{ required: true }]} noStyle>
<CustomInput
<CustomInputPassword
size="small"
placeholder="Password"
type="password"
/>
</Form.Item>
<Form.Item name="confirmPassword" rules={[{ required: true }]} noStyle>
<CustomInputPassword
size="small"
placeholder="Confirm password"
/>
</Form.Item>
</Form>
@ -214,22 +200,7 @@ export const AuthModal: FC<AuthModalProps> = ({
>
Register
</FilledButton>
<span>or</span>
<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>
<OutlinedButton onClick={() => updateMode('enter')}>Enter</OutlinedButton>
</>
)}
{mode === 'reset' && (
@ -242,10 +213,9 @@ export const AuthModal: FC<AuthModalProps> = ({
/>
</Form.Item>
<Form.Item name="password" rules={[{ required: true }]} noStyle>
<CustomInput
<CustomInputPassword
size="small"
placeholder="Password"
type="password"
/>
</Form.Item>
</Form>
@ -277,7 +247,7 @@ export const AuthModal: FC<AuthModalProps> = ({
)}
<div className="b-modal__auth__agreement">
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>
</Modal>

View File

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

View File

@ -1,17 +1,26 @@
'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 { Link } from '../../../navigation';
type HeaderMenuMobileProps = {
children: ReactNode;
linkConfig: { path: string, title: string }[];
enterTitle: string;
registerTitle: 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 selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || '';
return (
<>
@ -40,7 +49,11 @@ export const HeaderMobileMenu: FC<HeaderMenuMobileProps> = ({ children, enterTit
</div>
<div className="menu-mobile__body">
<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>
</div>
</div>

View File

@ -13,11 +13,10 @@ type HeaderProps = {
export const Header: FC<HeaderProps> = ({ locale }) => {
const t = useTranslations('Header');
const routes = HEAD_ROUTES.map((item) => (
<li key={item}>
<Link href={`/${item}` as any}>{t(`menu.${item}`)}</Link>
</li>
));
const routes: { path: string, title: string }[] = HEAD_ROUTES.map((item) => ({
path: item,
title: t(`menu.${item}`)
}));
return (
<>
@ -30,15 +29,21 @@ export const Header: FC<HeaderProps> = ({ locale }) => {
alt=""
/>
</Link>
<HeaderMenu enterTitle={t('enter')} registerTitle={t('registration')} accountTitle={t('account')}>
{routes}
</HeaderMenu>
<HeaderMenu
enterTitle={t('enter')}
registerTitle={t('registration')}
accountTitle={t('account')}
linkConfig={routes}
/>
<LanguageSwitcher locale={locale} />
</div>
</header>
<HeaderMobileMenu enterTitle={t('enter')} registerTitle={t('registration')} accountTitle={t('account')}>
{routes}
</HeaderMobileMenu>
<HeaderMobileMenu
enterTitle={t('enter')}
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 './CustomRate';
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 = {
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;
}
a {
color: #66A5AD !important;
}
.b-wrapper {
width: 100%;
height: 100%;
@ -426,21 +430,21 @@ body{
.btn-apply {
user-select: none;
outline: none;
border: none;
outline: none !important;
border: none !important;
text-decoration: none;
width: 100%;
cursor: pointer;
border-radius: 8px;
background: #FFBD00;
box-shadow: 0px 2px 4px 0px rgba(252, 214, 70, 0.16);
border-radius: 8px !important;
background: #FFBD00 !important;
box-shadow: 0px 2px 4px 0px rgba(252, 214, 70, 0.16) !important;
display: flex;
gap: 10px;
height: 54px;
height: 54px !important;
padding: 8px 31px;
justify-content: center;
align-items: center;
color: #003B46;
color: #003B46 !important;
@include rem(15);
font-style: normal;
font-weight: 400;

View File

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

View File

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

View File

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

View File

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

View File

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