diff --git a/pages/_meta.ru.json b/pages/_meta.ru.json
index 4d52b7e..9cad5d7 100644
--- a/pages/_meta.ru.json
+++ b/pages/_meta.ru.json
@@ -8,5 +8,6 @@
},
"lesson-1": "Урок 1",
"lesson-2": "Урок 2: Формы",
- "lesson-3": "Урок 3: Таблицы"
+ "lesson-3": "Урок 3: Роутинг, Авторизация",
+ "extras": "Дополнительно"
}
diff --git a/pages/extras/_meta.en.json b/pages/extras/_meta.en.json
new file mode 100644
index 0000000..2bc4800
--- /dev/null
+++ b/pages/extras/_meta.en.json
@@ -0,0 +1,3 @@
+{
+ "tables": "Tables"
+}
diff --git a/pages/extras/_meta.ru.json b/pages/extras/_meta.ru.json
new file mode 100644
index 0000000..7750ffe
--- /dev/null
+++ b/pages/extras/_meta.ru.json
@@ -0,0 +1,3 @@
+{
+ "tables": "Таблицы"
+}
diff --git a/pages/lesson-3/images/table-with-basic-sort.png b/pages/extras/images/table-with-basic-sort.png
similarity index 100%
rename from pages/lesson-3/images/table-with-basic-sort.png
rename to pages/extras/images/table-with-basic-sort.png
diff --git a/pages/extras/tables.en.mdx b/pages/extras/tables.en.mdx
new file mode 100644
index 0000000..922266a
--- /dev/null
+++ b/pages/extras/tables.en.mdx
@@ -0,0 +1 @@
+# Under construction
diff --git a/pages/extras/tables.ru.mdx b/pages/extras/tables.ru.mdx
new file mode 100644
index 0000000..ca87b80
--- /dev/null
+++ b/pages/extras/tables.ru.mdx
@@ -0,0 +1,254 @@
+# Таблицы
+
+## Сортировка
+
+Наш бэкэнд будет принимать параметр sort формата `name-asc` где `name` -
+название поля, а `asc` - направление сортировки. Возможные направления сортировки:
+`asc` и `desc`.
+
+Добавим возможность сортировки для таблиц, для этого:
+
+- Создадим историю в сторибуке:
+
+```tsx
+const data = [
+ {
+ title: 'Project A',
+ cardsCount: 10,
+ updated: '2023-07-07',
+ createdBy: 'John Doe',
+ },
+ {
+ title: 'Project B',
+ cardsCount: 5,
+ updated: '2023-07-06',
+ createdBy: 'Jane Smith',
+ },
+ {
+ title: 'Project C',
+ cardsCount: 8,
+ updated: '2023-07-05',
+ createdBy: 'Alice Johnson',
+ },
+ {
+ title: 'Project D',
+ cardsCount: 3,
+ updated: '2023-07-07',
+ createdBy: 'Bob Anderson',
+ },
+ {
+ title: 'Project E',
+ cardsCount: 12,
+ updated: '2023-07-04',
+ createdBy: 'Emma Davis',
+ },
+]
+export const WithSort = {
+ render: () => {
+ return (
+
+
+
+ | Name |
+ Cards |
+ Last Updated |
+ Created by |
+ |
+
+
+
+ {data.map(item => (
+
+ | {item.title} |
+ {item.cardsCount} |
+ {item.updated} |
+ {item.createdBy} |
+ icons... |
+
+ ))}
+
+
+ )
+ },
+}
+```
+
+Получим просто таблицу, которая пока не сортируемая.
+
+- Создадим стейт для сортировки:
+
+```tsx
+type Sort = {
+ key: string
+ direction: 'asc' | 'desc'
+} | null
+
+const [sort, setSort] = useState(null)
+```
+
+- Добавим обработчик клика на заголовок таблицы:
+
+```tsx
+const handleSort = (key: string) => {
+ if (sort && sort.key === key) {
+ setSort({
+ key,
+ direction: sort.direction === 'asc' ? 'desc' : 'asc',
+ })
+ } else {
+ setSort({
+ key,
+ direction: 'asc',
+ })
+ }
+}
+```
+
+и используем его в таблице:
+
+```tsx
+
+ | handleSort('name')}>Name |
+ handleSort('cardsCount')}>Cards |
+ handleSort('updated')}>Last Updated |
+ handleSort('createdBy')}>Created by |
+ |
+
+```
+
+- Добавим иконки в ячейки заголовка:
+
+```tsx
+ handleSort('name')}>
+ Name
+ {sort && sort.key === 'name' && {sort.direction === 'asc' ? '▲' : '▼'}}
+ |
+```
+
+- Добавим `console.log()` для проверки стейта:
+
+```tsx
+console.log(sort)
+```
+
+Проверяем, при клике должна меняться иконка и в консоли должен появляться правильный объект.
+
+
+
+## Рефакторинг
+
+Мы повторяем слишком много кода в обработчиках, из-за чего мы оставляем слишком много шансов для ошибок.
+
+Одним из возможных решений в данной ситуации будет создание массива с заголовками таблицы и использование его для отрисовки заголовков и обработчиков:
+
+```tsx
+const columns = [
+ {
+ key: 'name',
+ title: 'Name',
+ },
+ {
+ key: 'cardsCount',
+ title: 'Cards',
+ },
+ {
+ key: 'updated',
+ title: 'Last Updated',
+ },
+ {
+ key: 'createdBy',
+ title: 'Created by',
+ },
+]
+
+// ...
+
+{
+ columns.map(column => (
+ handleSort(column.key)}>
+ {column.title}
+ {sort && sort.key === column.key && {sort.direction === 'asc' ? '▲' : '▼'}}
+ |
+ ))
+}
+
+// ...
+```
+
+Протипизируем columns:
+
+```tsx
+type Column = {
+ key: string
+ title: string
+}
+const columns: Array = ...
+```
+
+У нас будет несколько таблиц, поэтому имеет смысл вынести часть логики в отдельный компонент:
+
+```tsx
+export const Header: FC<
+ Omit<
+ ComponentPropsWithoutRef<'thead'> & {
+ columns: Column[]
+ sort?: Sort
+ onSort?: (sort: Sort) => void
+ },
+ 'children'
+ >
+> = ({ columns, sort, onSort, ...restProps }) => {
+ const handleSort = (key: string, sortable?: boolean) => () => {
+ if (!onSort || !sortable) return
+
+ if (sort?.key !== key) return onSort({ key, direction: 'asc' })
+
+ if (sort.direction === 'desc') return onSort(null)
+
+ return onSort({
+ key,
+ direction: sort?.direction === 'asc' ? 'desc' : 'asc',
+ })
+ }
+
+ return (
+
+
+ {columns.map(({ title, key, sortable }) => (
+ |
+ {title}
+ {sort && sort.key === key && {sort.direction === 'asc' ? '▲' : '▼'}}
+ |
+ ))}
+
+
+ )
+}
+```
+
+Обратите внимание, что состоянием мы будем управлять снаружи, а не внутри компонента.
+
+Используем новый компонент:
+
+```tsx
+
+```
+
+И, наконец, создадим нужную нам строку для бэкэнда:
+
+```tsx
+const sortedString = useMemo(() => {
+ if (!sort) return null
+
+ return `${sort.key}-${sort.direction}`
+}, [sort])
+
+console.log(sortedString)
+```
+
+## Самостоятельная работа:
+
+- Добавить сортировку по умолчанию (при третьем клике по заголовку таблицы сортировка должна сбрасываться (null))
diff --git a/pages/lesson-3/_meta.en.json b/pages/lesson-3/_meta.en.json
index 5bbe580..2bc4800 100644
--- a/pages/lesson-3/_meta.en.json
+++ b/pages/lesson-3/_meta.en.json
@@ -1,3 +1,3 @@
{
- "chapter-1": "Chapter 1"
+ "tables": "Tables"
}
diff --git a/pages/lesson-3/_meta.ru.json b/pages/lesson-3/_meta.ru.json
index 894d4e8..d5c6ce3 100644
--- a/pages/lesson-3/_meta.ru.json
+++ b/pages/lesson-3/_meta.ru.json
@@ -1,3 +1,3 @@
{
- "chapter-1": "Глава 1.Таблицы"
+ "chapter-1": "Роутинг"
}
diff --git a/pages/lesson-3/chapter-1.en.mdx b/pages/lesson-3/chapter-1.en.mdx
index 922266a..621f70a 100644
--- a/pages/lesson-3/chapter-1.en.mdx
+++ b/pages/lesson-3/chapter-1.en.mdx
@@ -1 +1 @@
-# Under construction
+# Under Construction
diff --git a/pages/lesson-3/chapter-1.ru.mdx b/pages/lesson-3/chapter-1.ru.mdx
index 5c41905..a712f37 100644
--- a/pages/lesson-3/chapter-1.ru.mdx
+++ b/pages/lesson-3/chapter-1.ru.mdx
@@ -1,254 +1,85 @@
-# Таблицы
+# Роутинг и защита страниц авторизацией
-## Сортировка
+## Роутинг
-Наш бэкэнд будет принимать параметр sort формата `name-asc` где `name` -
-название поля, а `asc` - направление сортировки. Возможные направления сортировки:
-`asc` и `desc`.
+Установим `react-router-dom`:
-Добавим возможность сортировки для таблиц, для этого:
+```bash filename="Terminal"
+pnpm i react-router-dom
+```
-- Создадим историю в сторибуке:
+Создадим файл `src/router.tsx`:
-```tsx
-const data = [
+```tsx filename="src/router.tsx"
+import { createBrowserRouter, RouterProvider } from 'react-router-dom'
+
+const router = createBrowserRouter([
{
- title: 'Project A',
- cardsCount: 10,
- updated: '2023-07-07',
- createdBy: 'John Doe',
- },
- {
- title: 'Project B',
- cardsCount: 5,
- updated: '2023-07-06',
- createdBy: 'Jane Smith',
- },
- {
- title: 'Project C',
- cardsCount: 8,
- updated: '2023-07-05',
- createdBy: 'Alice Johnson',
- },
- {
- title: 'Project D',
- cardsCount: 3,
- updated: '2023-07-07',
- createdBy: 'Bob Anderson',
- },
- {
- title: 'Project E',
- cardsCount: 12,
- updated: '2023-07-04',
- createdBy: 'Emma Davis',
- },
-]
-export const WithSort = {
- render: () => {
- return (
-
-
-
- | Name |
- Cards |
- Last Updated |
- Created by |
- |
-
-
-
- {data.map(item => (
-
- | {item.title} |
- {item.cardsCount} |
- {item.updated} |
- {item.createdBy} |
- icons... |
-
- ))}
-
-
- )
+ path: '/',
+ element: hello
,
},
+])
+
+export const Router = () => {
+ return
}
```
-Получим просто таблицу, которая пока не сортируемая.
+Отрендерим его в `App`:
-- Создадим стейт для сортировки:
+```tsx filename="src/App.tsx"
+import { Router } from '@/router'
-```tsx
-type Sort = {
- key: string
- direction: 'asc' | 'desc'
-} | null
-
-const [sort, setSort] = useState(null)
-```
-
-- Добавим обработчик клика на заголовок таблицы:
-
-```tsx
-const handleSort = (key: string) => {
- if (sort && sort.key === key) {
- setSort({
- key,
- direction: sort.direction === 'asc' ? 'desc' : 'asc',
- })
- } else {
- setSort({
- key,
- direction: 'asc',
- })
- }
+export function App() {
+ return
}
```
-и используем его в таблице:
+## Защита страниц авторизацией
-```tsx
-
- | handleSort('name')}>Name |
- handleSort('cardsCount')}>Cards |
- handleSort('updated')}>Last Updated |
- handleSort('createdBy')}>Created by |
- |
-
-```
+Разделим наши роуты на две переменные - `publicRoutes` и `privateRoutes`:
-- Добавим иконки в ячейки заголовка:
+```tsx filename="src/router.tsx"
+import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router-dom'
-```tsx
- handleSort('name')}>
- Name
- {sort && sort.key === 'name' && {sort.direction === 'asc' ? '▲' : '▼'}}
- |
-```
-
-- Добавим `console.log()` для проверки стейта:
-
-```tsx
-console.log(sort)
-```
-
-Проверяем, при клике должна меняться иконка и в консоли должен появляться правильный объект.
-
-
-
-## Рефакторинг
-
-Мы повторяем слишком много кода в обработчиках, из-за чего мы оставляем слишком много шансов для ошибок.
-
-Одним из возможных решений в данной ситуации будет создание массива с заголовками таблицы и использование его для отрисовки заголовков и обработчиков:
-
-```tsx
-const columns = [
+const publicRoutes: RouteObject[] = [
{
- key: 'name',
- title: 'Name',
- },
- {
- key: 'cardsCount',
- title: 'Cards',
- },
- {
- key: 'updated',
- title: 'Last Updated',
- },
- {
- key: 'createdBy',
- title: 'Created by',
+ path: '/login',
+ element: login
,
},
]
-// ...
+const privateRoutes: RouteObject[] = [
+ {
+ path: '/',
+ element: hello
,
+ },
+]
-{
- columns.map(column => (
- handleSort(column.key)}>
- {column.title}
- {sort && sort.key === column.key && {sort.direction === 'asc' ? '▲' : '▼'}}
- |
- ))
-}
-
-// ...
+const router = createBrowserRouter([...privateRoutes, ...publicRoutes])
```
-Протипизируем columns:
+Создадим компонент `PrivateRoutes`:
-```tsx
-type Column = {
- key: string
- title: string
-}
-const columns: Array = ...
-```
+```tsx filename="src/router.tsx"
+function PrivateRoutes() {
+ const isAuthenticated = false
-У нас будет несколько таблиц, поэтому имеет смысл вынести часть логики в отдельный компонент:
-
-```tsx
-export const Header: FC<
- Omit<
- ComponentPropsWithoutRef & {
- columns: Column[]
- sort?: Sort
- onSort?: (sort: Sort) => void
- },
- 'children'
- >
-> = ({ columns, sort, onSort, ...restProps }) => {
- const handleSort = (key: string, sortable?: boolean) => () => {
- if (!onSort || !sortable) return
-
- if (sort?.key !== key) return onSort({ key, direction: 'asc' })
-
- if (sort.direction === 'desc') return onSort(null)
-
- return onSort({
- key,
- direction: sort?.direction === 'asc' ? 'desc' : 'asc',
- })
- }
-
- return (
-
-
- {columns.map(({ title, key, sortable }) => (
- |
- {title}
- {sort && sort.key === key && {sort.direction === 'asc' ? '▲' : '▼'}}
- |
- ))}
-
-
- )
+ return isAuthenticated ? :
}
```
-Обратите внимание, что состоянием мы будем управлять снаружи, а не внутри компонента.
+И обернем им наши privateRoutes:
-Используем новый компонент:
-
-```tsx
-
+```tsx filename="src/router.tsx"
+const router = createBrowserRouter([
+ {
+ element: ,
+ children: privateRoutes,
+ },
+ ...publicRoutes,
+])
```
-И, наконец, создадим нужную нам строку для бэкэнда:
-
-```tsx
-const sortedString = useMemo(() => {
- if (!sort) return null
-
- return `${sort.key}-${sort.direction}`
-}, [sort])
-
-console.log(sortedString)
-```
-
-## Самостоятельная работа:
-
-- Добавить сортировку по умолчанию (при третьем клике по заголовку таблицы сортировка должна сбрасываться (null))
+Теперь когда `isAuthenticated` будет `false`,
+при переходе на `/` мы будем перенаправлены на `/login`.
diff --git a/pages/lesson-3/chapter-2.ru.mdx b/pages/lesson-3/chapter-2.ru.mdx
new file mode 100644
index 0000000..fc2eaa6
--- /dev/null
+++ b/pages/lesson-3/chapter-2.ru.mdx
@@ -0,0 +1,61 @@
+# RTK Query
+
+## Установка
+
+```bash filename="Terminal"
+pnpm i @reduxjs/toolkit react-redux
+```
+
+## Создание стора
+
+```ts filename="src/services/store.ts"
+import { configureStore } from '@reduxjs/toolkit'
+
+export const store = configureStore({
+ reducer: {},
+ middleware: getDefaultMiddleware => getDefaultMiddleware(),
+})
+
+export type AppDispatch = typeof store.dispatch
+export type RootState = ReturnType
+```
+
+## Подключение стора к приложению
+
+```tsx filename="src/App.tsx"
+import { Provider } from 'react-redux'
+
+import { Router } from '@/router'
+import { store } from '@/services/store'
+
+export function App() {
+ return (
+
+
+
+ )
+}
+```
+
+## Создание Api
+
+```ts filename="src/services/auth/auth.ts"
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
+
+export const authApi = createApi({
+ reducerPath: 'authApi',
+ baseQuery: fetchBaseQuery({
+ baseUrl: 'https://api.flashcards.andrii.es',
+ credentials: 'include',
+ }),
+ endpoints: builder => {
+ return {
+ getMe: builder.query({
+ query: () => `auth/me`,
+ }),
+ }
+ },
+})
+
+export const { useGetMeQuery } = authApi
+```