001 Шрифты и цвета

Создаём какой-нибудь контент на странице

index.tsx

export default function Home(): JSX.Element {
	return (
		<div>
			<p> Какой-то текст </p>
		</div>
	);
}

Подключаем шрифты в наше приложение через основную страницу

_app.tsx

import '../styles/globals.css';
import type { AppProps } from 'next/app';
import React from 'react';
import Head from 'next/head';
 
export default function App({ Component, pageProps }: AppProps): JSX.Element {
	return (
		<>
			<Head>
				<title>Second Page</title>
				<link key={2} rel="icon" href="/favicon2.ico" />
 
				{/* подключем шрифты из гугла */}
 
				<link rel="preconnect" href="https://fonts.googleapis.com" />
				<link rel="preconnect" href="https://fonts.gstatic.com" />
				<link
					href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@300;400;500;700&display=swap"
					rel="stylesheet"
				/>
			</Head>
			<Component {...pageProps} />
		</>
	);
}

И тут уже создаём переменные в нашем базовом CSS и через var() эти переменные используем

global.css

/* тут мы будем хранить все переменные*/
:root {
	--black: #3b434e;
	--white: #ebebeb;
	--background: #f5f6f8;
	--primary: #7653fc;
	--red: #fc836d;
	--green: #1dc37e;
	--light-green: #c8f8e4;
	--font-family: "Noto Sans", sans-serif;
}
 
html,
body {
	padding: 0;
	margin: 0;
	/* используем кастомный цвет */
	color: var(--black);
	background: var(--background);
 
	/* меняем шрифт */
	font-family: var(--font-family);
}
 
a {
	color: inherit;
	text-decoration: none;
}
 
* {
	box-sizing: border-box;
}

002 Первый компонент

Компонент - это функция, которая на вход в себя принимает какие-либо параметры (пропсы, дефолтные пропсы или не принимает ничего вообще) и возвращает на выходе JSX-элемент

  • Компоненты кидаем в отдельную папку components в корне проекта
  • Под каждый компонент создаём отдельную папку
  • Стили стоит выносить внутри документа таким образом: имя_модуля.module.css
  • Так же стоит отдельно выносить пропсы: имя_модуля.props.ts

Заранее определим, какой компонент нам нужен:

  • Он должен генерировать в зависимости от значения пропса tag определённый тег от h1 до h3
  • Он должен выводить вложенное внутрь него значение

И первым делом, мы определим интерфейс передаваемых пропсов в наш компонент. Он на вход получает tag от h1 до h6 (с запасом). Для данных, которые вкладываются между тегами (то есть, в нашем случае, выводимый на страницу текст) в React предусмотрен тип ReactNode, который типизирует children элемент

components > Htag > Htag.props.ts

import { ReactNode } from 'react';
 
export interface IHtagProps {
	tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
	children: ReactNode;
}

Далее нужно заранее установить sass

npm i sass

Пропишем заранее стили для разных тегов текста на странице. Так как тут будет использоваться модульная система стилей, то нужно будет после названия стилей прописать .module, который определит файл стилей как модульные стили

components > Htag > Htag.module.scss

.h1 {
	margin: 0;
	font-weight: 500;
	font-size: 26px;
	line-height: 35px;
}
 
.h2 {
	margin: 0;
	font-weight: 500;
	font-size: 22px;
	line-height: 30px;
}
 
.h3 {
	margin: 0;
	font-weight: 600;
	font-size: 20px;
	line-height: 27px;
}

Далее мы пишем сам компонент оглавления Htag, который в себя принимает пропсы по интерфейсу. Первый вариант рендера нужного нам тега: прописать отдельно условия для каждого рендера

components > Htag > Htag.tsx

import { IHtagProps } from './Htag.props';
import styles from './Htag.module.scss';
 
export const Htag = ({ tag, children }: IHtagProps): JSX.Element => {
	return (
		<>
			{tag == 'h1' && <h1>{children}</h1>}
			{tag == 'h2' && <h2>{children}</h2>}
			{tag == 'h3' && <h3>{children}</h3>}
		</>
	);
};

Ну и второй вариант через switch-case. Его преймущество заключается в том, что он проще читается.

Так же для использования стилей из отдельного файла используется модульная система, что позволяет обратиться к стилям в удобном формате имя_импорта.имя_класса styles.h1

Htag.tsx

import { IHtagProps } from './Htag.props';
import styles from './Htag.module.scss';
 
export const Htag = ({ tag, children }: IHtagProps): JSX.Element => {
	switch (tag) {
		case 'h1':
			return <h1 className={styles.h1}>{children}</h1>;
		case 'h2':
			return <h2 className={styles.h2}>{children}</h2>;
		case 'h3':
			return <h3 className={styles.h3}>{children}</h3>;
		default:
			return <></>;
	}
};

Далее идёт очень важный трюк: мы можем экспортировать внутри index.ts компоненты, чтобы сократить путь для доступа к ним из других компонентов

components > index.ts

export * from './Htag/Htag';

И вот так выглядит сам импорт элемента на основной странице и его использование

pages > index.tsx

import { Htag } from '../components';
 
export default function Home(): JSX.Element {
	return (
		<>
			<Htag tag="h1">Какой-то текст</Htag>
		</>
	);
}

003 Update - Библиотека classnames

Установка

npm i classnames

Импорт

import cn from 'classnames';

Использование: Данный модуль позволяет по условию подключать классы к нужным нам объектам

cn('foo', 'bar'); // => 'foo bar'
cn('foo', { bar: true }); // => 'foo bar'
cn({ 'foo-bar': true }); // => 'foo-bar'
cn({ 'foo-bar': false }); // => ''
cn({ foo: true }, { bar: true }); // => 'foo bar'
cn({ foo: true, bar: true }); // => 'foo bar'
 
// lots of arguments of various types
cn('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
 
// other falsy values are just ignored
cn(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

004 Classnames

Структура:

Первым делом добавим несколько переменных стилей внутрь глобального CSS

global.css

:root {
	--black: #3b434e;
	--white: #ebebeb;
	--gray-light: #ebebeb;
	--gray-light-hover: #c5c5c5;
	--gray-light-active: #8f8f8f;
	--background: #f5f6f8;
	--primary: #7653fc;
	--primary-hover: #5c37ee;
	--primary-active: #3d24a1;
	--red: #fc836d;
	--green: #1dc37e;
	--light-green: #c8f8e4;
	--font-family: 'Noto Sans', sans-serif;
}

Далее реализуем модульные стили для отдельных видов кнопок

components > Button > Button.module.scss

.button {
	display: inline-block;
 
	box-sizing: border-box;
	padding: 10px;
 
	cursor: pointer;
	outline: none;
	text-align: center;
	border: none;
	border-radius: 5px;
 
	font-size: 14px;
	transition: all 0.1s;
}
 
.primary {
	color: var(--white);
	background-color: var(--primary);
 
	&:hover {
		background-color: var(--primary-hover);
	}
 
	&:active {
		background-color: var(--primary-active);
	}
}
 
.ghost {
	color: var(--black);
	background-color: none;
	border: 1px solid var(--gray-light);
 
	&:hover {
		background-color: var(--gray-light-hover);
	}
 
	&:active {
		background-color: var(--gray-light-active);
	}
}

Далее укажем какие пропсы должны попадать в наш компонент

components > Button > Button.props.ts

import { ReactNode } from 'react';
 
export interface IButtonProps {
	children: ReactNode;
	appearance: 'primary' | 'ghost';
}

Сейчас уже реализуем кнопку с использованием модульных стилей и динамического добавления класса в зависимости от переданного пропса (с использованием модуля classnames)

components > Button > Button.tsx

import React from 'react';
import { IButtonProps } from './Button.props';
import styles from './Button.module.scss';
import cn from 'classnames';
 
export const Button = ({ appearance, children }: IButtonProps) => {
	return (
		<button
			className={cn(styles.button, {
				[styles.primary]: appearance == 'primary',
				[styles.ghost]: appearance == 'ghost',
			})}
		>
			{children}
		</button>
	);
};

Добавляем модуль на экспорт

components > index.ts

export * from './Htag/Htag';
export * from './Button/Button';

И выводим кнопку на страницу

pages > index.tsx

import { Button, Htag } from '../components';
 
export default function Home(): JSX.Element {
	return (
		<>
			<Htag tag="h1">Какой-то текст</Htag>
			<Button appearance="primary">Основная кнопка</Button>
			<Button appearance="ghost">Призрачная кнопка</Button>
		</>
	);
}

005 HTMLProps

Однако после наших действий с добавлением определённых пропсов под компонент кнопки, наша кнопка не имеет возможности получать другие пропсы (по типу onClick для привязки функции)

Первым делом, чтобы исправить данную ситуацию, нужно заэкстендить интерфейс кнопки стандартными атрибутами, которые принимает в себя кнопка внутри HTML

components > Button > Button.props.ts

import { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from 'react';
 
export interface IButtonProps
	extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
	children: ReactNode;
	appearance: 'primary' | 'ghost';
}

Далее нам нужно прокинуть актуальные для нас HTML-пропсы явно, а остальные прокинуть через деструктуризацию

components > Button > Button.tsx

export const Button = ({ appearance, children, className, ...props }: IButtonProps) => {
	return (
		<button
			className={cn(styles.button, className, {
				[styles.primary]: appearance == 'primary',
				[styles.ghost]: appearance == 'ghost',
			})}
			{...props}
		>
			{children}
		</button>
	);
};

006 Updated - Детали HTMLprops

Первым делом, основные типы под все наши атрибуты, которые могут нам пригодиться при типизации React, мы можем найти в тайпах к реакту

А тут уже представлены интерфейсы всех основных атрибутов HTML, которые мы можем прописать. Все эти интерфейсы заполняются благодря подключаемой в tsconfig библиотеке lib, которая в нём уже присутствует при использовании фреймворка nextjs

Для типизации можно просто вставлять интерфейсы под каждый элемент, который мы используем. Если у данного элемента нет специализированных под него атрибутов, то нам достаточно будет просто добавить для него HTMLAttributes, а не искать под него его специализированный интерфейс

008 Работа с svg

Структура:

Нужно добавить свойство arrow, которое будет определять положение стрелки относительно самой себя в пространстве

components > Button > Button.props.ts

import { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from 'react';
 
export interface IButtonProps
	extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
	children: ReactNode;
	appearance: 'primary' | 'ghost';
	arrow?: 'right' | 'down' | 'none';
}

Так же нам предварительно стоит удалить свойство fill в конце svg иконки и заменить его тем, что находится вначале, чтобы иметь возможность иметь цвета из css

Дальше нужно добавить стили для трансформации и цвета иконки

components > Button > Button.module.scss

.primary svg,
.ghost:active svg {
	fill: var(--white);
}
 
.arrow {
	display: inline-block;
 
	margin-left: 10px;
}
 
.right {
}
 
.down {
	transition: all 0.2s;
	transform: rotate(90deg);
}

Далее определяем логику наличия иконки на странице. Если мы передали arrow отличное от дефолтного значения (none), то будет появляться стрелка в нужном нам направлении, которое мы определили через стили. Нужно ещё отдельно сказать, что в пути изображения указан путь /arrow.svg - тут иконка располагается в папке public, доступ к которой можно получить так из всего проекта

components > Button > Button.tsx

import React from 'react';
import { IButtonProps } from './Button.props';
import styles from './Button.module.scss';
import cn from 'classnames';
 
export const Button = ({
	appearance,
	children,
	arrow = 'none',
	className,
	...props
}: IButtonProps) => {
	return (
		<button
			className={cn(styles.button, className, {
				[styles.primary]: appearance == 'primary',
				[styles.ghost]: appearance == 'ghost',
			})}
			{...props}
		>
			{children}
			{arrow != 'none' && (
				<span
					className={cn(styles.arrow, {
						[styles.down]: arrow == 'down',
					})}
				>
					<img src="/arrow.svg" alt="" />
				</span>
			)}
		</button>
	);
};
 

Далее остаётся только прописать на странице поворот стрелки для кнопки

pages > index.tsx

export default function Home(): JSX.Element {
	return (
		<>
			<Htag tag="h1">Какой-то текст</Htag>
			<Button appearance="primary" arrow="right">
				Основная кнопка
			</Button>
			<Button appearance="ghost" arrow="down">
				Призрачная кнопка
			</Button>
		</>
	);
}

|600

011 Упражнение - Компонент p

Тут опишем интерфейс принимаемых пропсов

Paragraph.props.ts

import { HTMLAttributes, DetailedHTMLProps, ReactNode } from 'react';
 
export interface IParagraphProps
	extends DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement> {
	children: ReactNode;
	size?: 's' | 'm' | 'l';
}

Компонент параграфа: в зависимости от полученного значения размера, подставляется разный класс со своими значениями размера шрифта

Paragraph.tsx

import { IParagraphProps } from './Paragraph.props';
import cn from 'classnames';
import styles from './Paragraph.module.scss';
 
export const Paragraph = ({ size = 'm', children, className, ...props }: IParagraphProps) => {
	return (
		<p
			className={cn(styles.p, className, {
				[styles.s]: size == 's',
				[styles.m]: size == 'm',
				[styles.l]: size == 'l',
			})}
			{...props}
		>
			{children}
		</p>
	);
};

Тут уже описаны стили для разных размеров шрифтов

Paragraph.module.scss

.p {
	margin: 0;
}
 
.s {
	font-size: 14px;
	line-height: 24px;
}
 
.m {
	font-size: 16px;
	line-height: 24px;
}
 
.l {
	font-size: 18px;
	line-height: 29px;
}

Передаём компонент дальше

index.ts

export * from './Htag/Htag';
export * from './Button/Button';
export * from './Paragraph/Paragraph';

И используем компонент кнопки

index.tsx

import { IParagraphProps } from './Paragraph.props';
import cn from 'classnames';
import styles from './Paragraph.module.scss';
 
export default function Home(): JSX.Element {
	return (
		<>
			<Htag tag="h1">Какой-то текст</Htag>
			<Button appearance="primary" arrow="right">
				Основная кнопка
			</Button>
			<Button appearance="ghost" arrow="down">
				Призрачная кнопка
			</Button>
			<Paragraph size="l">БОЛЬШОЙ: Текста очень много</Paragraph>
			<Paragraph size="m">СРЕДНИЙ: Текста очень много</Paragraph>
			<Paragraph size="s">МАЛЕНЬКИЙ: Текста очень много</Paragraph>
		</>
	);
}

012 Компонент тэга

Далее нам нужно реализовать теги нашего сайта:

Tag.module.scss

.tag {
	display: inline-block;
 
	box-sizing: border-box;
	margin-right: 5px;
	border-radius: 20px;
}
 
.s {
	padding: 5px 10px;
 
	font-size: 12px;
	line-height: 12px;
}
 
.m {
	padding: 5px 10px;
 
	font-size: 14px;
	line-height: 14px;
}
 
.ghost {
	border: 1px solid var(--gray-light);
}
 
.primary {
	color: var(--primary);
	border: 1px solid var(--primary);
	background: none;
}
 
.grey {
	color: var(--white);
	background: #b3c0d9;
 
	font-weight: bold;
}
 
.red {
	color: var(--white);
	background: #de0000;
 
	font-weight: bold;
}
 
.green {
	color: var(--green);
	background: var(--light-green);
 
	font-weight: bold;
}

Tag.props.ts

import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
 
export interface ITagProps
	extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
	children: ReactNode;
	size?: 's' | 'm';
	color?: 'ghost' | 'primary' | 'grey' | 'red' | 'green';
	href?: string;
}

Особенность реализации даного компонет состоит в том, что для вывода текста тега используется тернарный оператор. Если ссылка имеется (мы её передали в компонент), то мы выводим ссылку - если не передали, то выводим обычный текст.

Tag.tsx

import React from 'react';
import { ITagProps } from './Tag.props';
import styles from './Tag.module.scss';
import cn from 'classnames';
import Link from 'next/link';
 
export const Tag = ({
	children,
	className,
	href,
	color = 'ghost',
	size = 's',
	...props
}: ITagProps) => {
	return (
		<div
			className={cn(styles.tag, className, {
				[styles.s]: size == 's',
				[styles.m]: size == 'm',
				[styles.ghost]: color == 'ghost',
				[styles.primary]: color == 'primary',
				[styles.grey]: color == 'grey',
				[styles.green]: color == 'green',
				[styles.red]: color == 'red',
			})}
			{...props}
		>
			{
				href
					? <Link href={`${href}`}>{children}</Link>
					: <>{children}</>
			}
		</div>
	);
};

index.ts

export * from './Htag/Htag';
export * from './Button/Button';
export * from './Paragraph/Paragraph';
export * from './Tag/Tag';

index.tsx

import { Button, Htag, Paragraph, Tag } from '../components';
 
export default function Home(): JSX.Element {
	return (
		<>
			<Htag tag="h1">Какой-то текст</Htag>
			<Button appearance="primary" arrow="right">
				Основная кнопка
			</Button>
			<Button appearance="ghost" arrow="down">
				Призрачная кнопка
			</Button>
			<Paragraph size="l">БОЛЬШОЙ: Текста очень много</Paragraph>
			<Paragraph size="m">СРЕДНИЙ: Текста очень много</Paragraph>
			<Paragraph size="s">МАЛЕНЬКИЙ: Текста очень много</Paragraph>
			<Tag color="green" size="m">
				-10000
			</Tag>
			<Tag size="m" color="primary" href="www.google.com">
				Google
			</Tag>
		</>
	);
}