001 Компонент Link
Главная особеность тега <Link>
, которую предоставляет нам NextJS является то, что этот тег заставляет перезагружать только отдельную часть документа, где меняется контент, а не весь документ сразу. Это обеспечивает работу со страницей как с клиентским приложением, когда у нас перезагружается на вся форма, а только отдельная его часть
href
as
- он уже позволит указать то, что будет отображаться в браузере, если ссылки не совпадаютreplace
- еслиfalse
, то при возвращении обратно на эту страницу, будет совершён возврат на предыдущую за нейshallow
- еслиtrue
, то эта настройка запретит запускать получение пропсов и путей с сервера (функции некста)
Чтобы указать наш линк, нужно записать этот тег внутри написать блок с ссылкой <a>
.
Начиная с NextJS 13 тег <a>
для указания ссылки использовать нельзя.
Так выглядит пробрасывание ссылки из Link
во вложенный внутрь него компонент
Так же мы имеем возможность прокинуть не просто ссылку, а полноценный объект, в котором укажем все части ссылки. Это предоставит более понятную и читабельную ссылку.
Чтобы тег Link
заработал в нашем проекте, нужно просто заменить <a>
на <Link>
.
Конкретно вот так сейчас выглядят функции с ссылками:
layout / menu / Menu.tsx
// Тут мы строим первый уровень меню
const buildFirstLevel = () => {
return (
<>
{/* Проходимся по всем элементам меню */}
{firstLevelMenu.map((m) => (
// Будем создавать элементы по роуту
<div key={m.route}>
<Link href={`/${m.route}`}>
<div
className={cn(styles.firstLevel, {
[styles.firstLevelActive]: m.id == firstCategory,
})}
>
{m.icon}
<span>{m.name}</span>
</div>
</Link>
{/* Далее мы строим второй уровень, если id меню совпадает с id выбранной категории */}
{m.id == firstCategory && buildSecondLevel(m)}
</div>
))}
</>
);
};
// функция для построения третьего уровня меню
const buildThirdLevel = (pages: PageItem[], route: string) => {
// на третьем уровне нужно будет просто вывести ссылки под вторым уровнем
return pages.map((p) => (
<Link
// тут мы указываем главный роут и алиас страницы
href={`/${route}/${p.alias}`}
className={cn(styles.thirdLevel, {
[styles.thirdLevelActive]: false,
})}
>
{p.category}
</Link>
));
};
Теперь наша страница не перезагружается при переходе по роутам - обновляются только те данные, которые изменились на странице
002 useRouter
useRouter
- это хук доступный исключительно в NextJS
Так роутер используется
const router = useRouter();
Роутер состоит из:
route
- это текущий роут, на котором мы находимсяpathname
- текущий путь, по которому мы находимсяquery
- параметры путиasPath
- если задалиas
при переходе наLink
basePath
- базовый путь, который был задан И свойства, которые относятся к языку:locale
locales
defaultLocale
domainLocales
isLocaleDomen
Так же у него имеются методы и события, которые позволяют скорректировать роуты:
Pick<Router, 'push' | 'replace' | 'reload' | 'back' | ... >
План работ:
- Нужно скрывать все остальные уровни, когда у нас выбран определённый раздел
- Нужно выделить активным то меню, в котором мы сейчас находимся
Для того, чтобы отобразить выбранный элемент, нужно в buildSecondLevel
указывать стиль Opened
только указанному элементу
layout / menu / Menu.tsx
// функция построения второго уровня меню
const buildSecondLevel = (menuItem: FirstLevelMenuItem) => {
return (
<div className={styles.secondBlock}>
{menu.map((m) => {
// будет отображать только тот блок с курсами, на котором сейчас находится пользователь
// проверка происходит по наличию в пути страницы значения алиаса страницы
if (m.pages.map((p) => p.alias).includes(router.asPath.split('/')[2])) m.isOpened = true;
return (
<div key={m._id.secondCategory}>
<div className={styles.secondLevel}>{m._id.secondCategory}</div>
<div
className={cn(styles.secondLevelBlock, {
[styles.secondLevelBlockOpened]: m.isOpened,
})}
>
{buildThirdLevel(m.pages, menuItem.route)}
</div>
</div>
);
})}
</div>
);
};
Скроем изначально все блоки и будем показывать их содержимое через secondLevelBlockOpened
layout / menu / Menu.module.css
.secondLevelBlock {
display: none;
}
.secondLevelBlockOpened {
display: block;
}
И сейчас мы видим, что все остальные меню скрылись
Тут мы укажем, что заголовок третьего уровня будет активен, если имя его роута (путь в браузере) будет совпадать с тем, где сейчас находится роутер (в браузере)
layout / menu / Menu.tsx
// функция для построения третьего уровня меню
const buildThirdLevel = (pages: PageItem[], route: string) => {
// на третьем уровне нужно будет просто вывести ссылки под вторым уровнем
return pages.map((p) => (
<Link
// тут мы указываем главный роут и алиас страницы
href={`/${route}/${p.alias}`}
className={cn(styles.thirdLevel, {
[styles.thirdLevelActive]: `/${route}/${p.alias}` == router.asPath,
})}
>
{p.category}
</Link>
));
};
Сейчас мы можем нажать на определённую страницу и она будет выделена в меню
Далее нужно добавить метод openSecondLevel
, который будет свитчить состояние открытости или закрытости блока (менять его между друг другом). Далее нужно будет поместить его на клик в заголовок второго уровня меню.
layout / menu / Menu.tsx
// данная функция будет скрывать или показывать блок второго уровня меню по клику
const openSecondLevel = (secondCategory: string) => {
// так как setMenu может и не быть (= null), то нужно обязательно прописать проверку через &&, что нужно выполнять функцию при условии её наличия
setMenu &&
setMenu(
menu.map((m) => {
if (m._id.secondCategory == secondCategory) {
m.isOpened = !m.isOpened;
}
return m;
}),
);
};
// функция построения второго уровня меню
const buildSecondLevel = (menuItem: FirstLevelMenuItem) => {
return (
<div className={styles.secondBlock}>
{menu.map((m) => {
// будет отображать только тот блок с курсами, на котором сейчас находится пользователь
// проверка происходит по наличию в пути страницы значения алиаса страницы
if (m.pages.map((p) => p.alias).includes(router.asPath.split('/')[2])) {
m.isOpened = true;
}
return (
<div key={m._id.secondCategory}>
<div
className={styles.secondLevel}
// при нажатии на уровень будет происходить изменение видимости блока
onClick={() => openSecondLevel(m._id.secondCategory)}
>
{m._id.secondCategory}
</div>
<div
className={cn(styles.secondLevelBlock, {
[styles.secondLevelBlockOpened]: m.isOpened,
})}
>
{buildThirdLevel(m.pages, menuItem.route)}
</div>
</div>
);
})}
</div>
);
};
На данном этапе мы уже можем открывать и скрывать определённые разделы с курсами
004 Структура роутинга
Вынесем на главную отдельный модуль хелперов
Из [alias].tsx
перенесём в хелпер информацию по заголовкам первого уровня. Делаем это для того, чтобы можно было воспользоваться этими данными сразу на нескольких страницах.
helpers / helpers.tsx
import { FirstLevelMenuItem } from '../interfaces/menu.interface';
import { TopLevelCategory } from '../interfaces/page.interface';
import CoursesIcon from './icons/courses.svg';
import ServicesIcon from './icons/services.svg';
import BooksIcon from './icons/books.svg';
import ProductsIcon from './icons/products.svg';
export const firstLevelMenu: FirstLevelMenuItem[] = [
{ route: 'courses', name: 'Курсы', icon: <CoursesIcon />, id: TopLevelCategory.Courses },
{ route: 'services', name: 'Сервисы', icon: <ServicesIcon />, id: TopLevelCategory.Services },
{ route: 'books', name: 'Книги', icon: <BooksIcon />, id: TopLevelCategory.Books },
{ route: 'products', name: 'Продукты', icon: <ProductsIcon />, id: TopLevelCategory.Products },
];
Далее в страницах нужно будет поменять courses
на [type]
, для того, чтобы генерировать вместо тайпа отдельную страницу под каждый заголовок первого уровня меню
Тут уже добавляем функционал, который позволит при неправильно выбранном роуте производить вывод страницы 404
. Так же связываем данные firstLevelMenu
с теми, что находятся в хелпере.
pages / [type] / [alias].tsx
function Course({ menu, page, products }: CourseProps): JSX.Element {
return <>{products && products.length}</>;
}
export default withLayout(Course);
export const getStaticPaths: GetStaticPaths = async () => {
let paths: string[] = []; // тут будут храниться доступные пути
// тут мы перебираем пути, получаем их и складываем в массив путей for (const m of firstLevelMenu) {
// тут происходит получение пути по отправке запроса с именем
const { data: menu } = await axios.post<MenuItem[]>(
process.env.NEXT_PUBLIC_DOMAIN + '/api/top-page/find',
{ firstCategory: m.id,
}, );
// тут происходит конкатенация путей
paths = paths.concat(menu.flatMap((s) => s.pages.map((p) => `/${m.route}/${p.alias}`)));
}
return {
paths,
fallback: true,
};
};
export const getStaticProps: GetStaticProps<CourseProps> = async ({
params,
}: GetStaticPropsContext<ParsedUrlQuery>) => {
if (!params) {
return {
notFound: true,
}; }
// мы ищем категорию, которая совпадает с типом, который передаётся в параметрах
const firstCategoryItem = firstLevelMenu.find((m) => m.route == params.type);
// если нет объекта первой категории, то мы возвращаем 404
if (!firstCategoryItem) {
return {
notFound: true,
}; }
// если роут будет введён неправильно, то чтобы не получить ошибку, нужно обернуть его в try-catch
try {
const { data: menu } = await axios.post<MenuItem[]>(
process.env.NEXT_PUBLIC_DOMAIN + '/api/top-page/find',
{
firstCategory: firstCategoryItem.id,
},
);
// если нам придёт пустое меню, то нам нужно будет его
if (menu.length == 0) {
return {
notFound: true,
};
}
const { data: page } = await axios.get<TopPageModel>(
process.env.NEXT_PUBLIC_DOMAIN + '/api/top-page/byAlias/' + params.alias,
);
const { data: products } = await axios.post<ProductModel[]>(
process.env.NEXT_PUBLIC_DOMAIN + '/api/product/find',
{
category: page.category,
limit: 10,
},
);
return {
props: {
menu,
// тут тоже возвращаем id первой категории
firstCategory: firstCategoryItem.id,
page,
products,
},
}; } catch (error: unknown) {
return {
notFound: true,
}; }
};
interface CourseProps extends Record<string, unknown> {
menu: MenuItem[];
firstCategory: TopLevelCategory; // меняем тип на тот, что в интерфейсе
page: TopPageModel;
products: ProductModel[];
}
Тут мы уже реализовали вывод страниц заголовков первого порядка меню. Они будут генерироваться по роуту названия этой страницы. Реализация данной страницы будет схожа с [alias].tsx
.
pages / [type] / index.tsx
function Type({ firstCategory }: TypeProps): JSX.Element {
return <>Type: {firstCategory}</>;
}
export default withLayout(Type);
// чтобы эта страница заработала, нужно сюда добавить статичное получение путей
export const getStaticPaths: GetStaticPaths = async () => {
return {
// каждая страница будет называться по названию роута из массива первоуровневого меню
paths: firstLevelMenu.map((m) => '/' + m.route),
fallback: true,
};
};
export const getStaticProps: GetStaticProps<TypeProps> = async ({
params,
}: GetStaticPropsContext<ParsedUrlQuery>) => {
if (!params) {
return {
notFound: true,
};
}
const firstCategoryItem = firstLevelMenu.find((m) => m.route == params.type);
if (!firstCategoryItem) {
return {
notFound: true,
};
}
const { data: menu } = await axios.post<MenuItem[]>(
process.env.NEXT_PUBLIC_DOMAIN + '/api/top-page/find',
{
firstCategory: firstCategoryItem.id,
},
);
return {
props: {
menu,
firstCategory: firstCategoryItem.id,
},
};
};
interface TypeProps extends Record<string, unknown> {
menu: MenuItem[];
firstCategory: number;
}
А тут уже была добавлена страница /search
, которая будет появляться при поиске определённого курса
pages / search.tsx
function Search(): JSX.Element {
return (
<>
Search
</>
);
}
export default withLayout(Search);
export const getStaticProps: GetStaticProps<HomeProps> = async () => {
const firstCategory = 0;
const { data: menu } = await axios.post<MenuItem[]>(process.env.NEXT_PUBLIC_DOMAIN + '/api/top-page/find', {
firstCategory
});
return {
props: {
menu,
firstCategory
}
};
};
interface HomeProps extends Record<string, unknown> {
menu: MenuItem[];
firstCategory: number;
}
Сейчас у нас генерируется страница 404
под каждый неправильный запрос (если пользователь попадёт не туда) благодаря try-catch
, который находится в [alias].tsx
Так же при переходе на главную страницу, которую мы описали через [type]
, мы не получаем ошибку, а видим главную страницу по курсам (и по остальным заголовкам первого уровня)
Если попытаться вывести массив paths
, то на выходе мы получим огромное количество готовых ссылок
При попытке сбилдить проект, мы получим полную структуру роутов нашего сайта (количество страниц и как к ним попасть)
005 Вёрстка Sidebar
Добавим в компонент сайдбара логотип с заглушку под будущий поиск
layout / Sidebar / Sidebar.tsx
import { SidebarProps } from './Sidebar.props';
import styles from './Sidebar.module.css';
import cn from 'classnames';
import { Menu } from '../Menu/Menu';
import Logo from '../logo.svg';
export const Sidebar = ({ className, ...props }: SidebarProps): JSX.Element => {
return (
<div className={cn(className, styles.sidebar)} {...props}>
<Logo className={styles.logo} />
<div>Поиск</div>
<Menu />
</div>
);
};
Стили для расположения элементов сбоку страницы:
layout / Sidebar / Sidebar.module.css
.sidebar {
display: grid;
align-content: flex-start;
gap: 20px;
}
.logo {
margin-top: 34px;
}
И примерно так будет выглядеть сайдбар: