Часть с карточками
Тут представлен компонент карточки, который может быть использован как под отзыв, так и под блок с HH.ru
components / Card.tsx
import { CardProps } from './Card.props';
import styles from './Card.module.css';
import cn from 'classnames';
export const Card = ({
color = 'white',
children,
className,
...props
}: CardProps): JSX.Element => {
return (
<div
className={cn(styles.card, className, {
[styles.white]: color == 'white',
[styles.blue]: color == 'blue',
})}
{...props}
>
{children}
</div>
);
};
Его стили:
.card {
background: var(--white);
border-radius: 5px;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.05);
}
.white {}
.blue {
background: #F9F8FF;
}
Его пропсы:
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
export interface CardProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
color?: 'white' | 'blue';
children: ReactNode;
}
Тут уже представлена сама вёрстка компонента блока HH.ru
components / HhData / HhData.tsx
import { HhDataProps } from './HhData.props';
import styles from './HhData.module.css';
import cn from 'classnames';
import { Card } from '../Card/Card';
import RateIcon from './rate.svg';
export const HhData = ({
count,
juniorSalary,
middleSalary,
seniorSalary,
}: HhDataProps): JSX.Element => {
return (
<div className={styles.hh}>
<Card className={styles.count}>
<div className={styles.title}>Всего вакансий</div>
<div className={styles.countValue}>{count}</div>
</Card>
<Card className={styles.salary}>
<div>
<div className={styles.title}>Начальный</div>
<div className={styles.salaryValue}>{juniorSalary}</div>
<div className={styles.rate}>
<RateIcon className={styles.filled} />
<RateIcon />
<RateIcon />
</div>
</div>
<div>
<div className={styles.title}>Средний</div>
<div className={styles.salaryValue}>{middleSalary}</div>
<div className={styles.rate}>
<RateIcon className={styles.filled} />
<RateIcon className={styles.filled} />
<RateIcon />
</div>
</div>
<div>
<div className={styles.title}>Профессионал</div>
<div className={styles.salaryValue}>{seniorSalary}</div>
<div className={styles.rate}>
<RateIcon className={styles.filled} />
<RateIcon className={styles.filled} />
<RateIcon className={styles.filled} />
</div>
</div>
</Card>
</div>
);
};
Его стили: тут уже представлен адаптив страницы
.hh {
display: grid;
grid-template-columns: 1fr 3fr;
gap: 30px;
}
.count {
padding: 20px;
text-align: center;
}
.title {
margin-bottom: 10px;
font-weight: 300;
font-size: 20px;
line-height: 27px;
}
.countValue {
color: var(--primary);
font-weight: bold;
font-size: 36px;
line-height: 49px;
}
.salaryValue {
margin-bottom: 10px;
font-weight: bold;
font-size: 26px;
line-height: 35px;
}
.salary {
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 20px;
text-align: center;
gap: 20px 0;
}
.salary > div:not(:last-child) {
border-right: 1px solid var(--gray-light);
}
.rate {
display: grid;
grid-template-columns: repeat(3, 20px);
gap: 10px;
justify-content: center;
}
/* покрасим кружок у svg-звёздочки */
.filled circle {
fill: var(--red)
}
@media (max-width: 1200px) {
.hh {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.salary {
grid-template-columns: 1fr;
}
.salary > div:not(:last-child) {
border-right: none;
border-bottom: 1px solid var(--gray-light);
padding-bottom: 20px;
}
}
Его пропсы: интерфейс пропсов будет просто расширяться от HhData
из page.interface.ts
(интерфейса страницы)
import { HhData } from '../../interfaces/page.interface';
export interface HhDataProps extends HhData {}
Тут были добавлены отступы от h2
заголовков
components / Htag / Htag.module.css
.h2 {
font-weight: 500;
font-size: 22px;
line-height: 30px;
margin-top: 0;
margin-bottom: 25px;
}
Экспортируем компоненты, чтобы до них было ближе добираться
components / index.ts
export * from './Htag/Htag';
export * from './Button/Button';
export * from './P/P';
export * from './Tag/Tag';
export * from './Rating/Rating';
export * from './Card/Card';
export * from './HhData/HhData';
Тут был реализован вывод компонента блока HH.ru
при условии, что пользователь находится на странице курсов
page-components / TopPageComponent / TopPageComponent.tsx
export const TopPageComponent = ({
page,
products,
firstCategory,
}: TopPageComponentProps): JSX.Element => {
return (
<div className={styles.wrapper}>
<div className={styles.title}>
<Htag tag={'h1'}>{page.title}</Htag>
{products && (
<Tag color='grey' size='m'>
{products.length}
</Tag>
)}
<span>Сортировка</span>
</div>
<div>{products && products.map(p => <div key={p._id}>{p.title}</div>)}</div>
<div className={styles.hhTitle}>
<Htag tag={'h2'}>Вакансии - {page.category}</Htag>
<Tag color={'red'} size={'m'}>
hh.ru
</Tag>
</div>
{/* Выведем блок с hh только если мы находимся на категории с курсами */}
{/* сюда можно не передавать отдельные параметры, а сразу передать все пропсы, которые имеются через спред */}
{firstCategory == TopLevelCategory.Courses && <HhData {...page.hh} />}
</div>
);
};
Его стили:
.wrapper {
margin-top: 40px;
}
.title {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: baseline;
justify-items: left;
gap: 20px;
}
.hhTitle {
display: grid;
grid-template-columns: auto 1fr;
align-items: baseline;
justify-items: left;
gap: 20px;
}
.hh {}
Так выглядит итоговая страница с адаптивом:
Часть с преймуществами
Делаем отступ для h2
тегов сверху
Htag.module.css
.h2 {
font-weight: 500;
font-size: 22px;
line-height: 30px;
margin-top: 50px;
margin-bottom: 25px;
}
Определяем необязательные параметры для модели основной страницы
page.interface.ts
export interface TopPageModel {
tags: string[];
_id: string;
secondCategory: string;
alias: string;
title: string;
category: string;
seoText?: string;
tagsTitle: string;
metaTitle: string;
metaDescription: string;
firstCategory: TopLevelCategory;
advantages?: TopPageAdvantage[];
createdAt: Date;
updatedAt: Date;
hh?: HhData;
}
Компонент преймуществ:
Advantages.tsx
export const Advantages = ({ advantages }: AdvantagesProps): JSX.Element => {
return (
<>
{advantages.map(a => (
<div key={a._id} className={styles.advantage}>
<CheckIcon />
<div className={styles.title}>{a.title}</div>
<hr className={styles.vline} />
<div>{a.description}</div>
</div>
))}
</>
);
};
Интерфейс:
Advantages.interface.ts
import { TopPageAdvantage } from '../../interfaces/page.interface';
export interface AdvantagesProps {
advantages: TopPageAdvantage[];
}
Стили:
Advantages.module.css
.advantage {
display: grid;
/* тег hr сам повернётся, когда укажем размер колонок */
grid-template-columns: 50px 1fr;
gap: 10px 40px;
margin-bottom: 30px;
}
.title {
align-self: center;
font-weight: bold;
}
.vline {
border-left: 1px solid var(--gray-light);
}
Далее тут добавляем вывод преимуществ, сео-текст (сгенерированный HTML-код) и теги получаемых навыков
TopPageComponent.tsx
export const TopPageComponent = ({
page,
products,
firstCategory,
}: TopPageComponentProps): JSX.Element => {
return (
<div className={styles.wrapper}>
<div className={styles.title}>
<Htag tag={'h1'}>{page.title}</Htag>
{products && (
<Tag color='grey' size='m'>
{products.length}
</Tag>
)}
<span>Сортировка</span>
</div>
<div>{products && products.map(p => <div key={p._id}>{p.title}</div>)}</div>
<div className={styles.hhTitle}>
<Htag tag={'h2'}>Вакансии - {page.category}</Htag>
<Tag color={'red'} size={'m'}>
hh.ru
</Tag>
</div>
{/* Выведем блок с hh только если мы находимся на категории с курсами */}
{/* сюда можно не передавать отдельные параметры, а сразу передать все пропсы, которые имеются через спред */}
{firstCategory == TopLevelCategory.Courses && page.hh && (
<HhData {...page.hh} />
)}
{/* тут мы уже выводим преимущества, если они у нас пришли */}
{page.advantages && page.advantages.length > 0 && (
<>
<Htag tag={'h2'}>Преимущества</Htag>
<Advantages advantages={page.advantages} />
</>
)}
{/* тут уже будем выводить сео-текст */}
{page.seoText && <P>{page.seoText}</P>}
<Htag tag={'h2'}>Получаемые навыки</Htag>
{/* выводим теги получаемых навыков */}
{page.tags.map(t => (
<Tag key={t} color={'primary'}>
{t}
</Tag>
))}
</div>
);
};
Экспортируем преймущества
index.ts
/// CODE ...
export * from './Advantages/Advantages';
Добавим отступ по всем сторонам в сайдбаре:
Sidebar.module.css
.sidebar {
display: grid;
align-content: flex-start;
gap: 20px;
}
При сжатии страницы до 765px
у body
появится padding
Layout.module.css
.wrapper {
display: grid;
grid-template-columns: auto 230px minmax(320px, 1200px) auto;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
gap: 40px 30px;
grid-template-areas:
". header header ."
". sidebar body ."
"footer footer footer footer";
}
.header {
grid-area: header;
display: none;
}
.sidebar {
grid-area: sidebar;
}
.body {
grid-area: body;
}
.footer {
grid-area: footer;
}
@media (max-width: 765px) {
.wrapper {
grid-template-columns: minmax(320px, 1fr);
grid-template-areas:
"header"
"body"
"footer";
}
.sidebar {
display: none !important;
}
.header {
display: block;
}
.body {
padding: 15px;
}
}
Преймущества - сео-текст:
И получаемые навыки внизу страницы: