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 ( + + + + + + + + + + + + {data.map(item => ( + + + + + + + + ))} + +
NameCardsLast UpdatedCreated by
{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) +``` + +Проверяем, при клике должна меняться иконка и в консоли должен появляться правильный объект. + +![sort](./images/table-with-basic-sort.png) + +## Рефакторинг + +Мы повторяем слишком много кода в обработчиках, из-за чего мы оставляем слишком много шансов для ошибок. + +Одним из возможных решений в данной ситуации будет создание массива с заголовками таблицы и использование его для отрисовки заголовков и обработчиков: + +```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 ( - - - - - - - - - - - - {data.map(item => ( - - - - - - - - ))} - -
NameCardsLast UpdatedCreated by
{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) -``` - -Проверяем, при клике должна меняться иконка и в консоли должен появляться правильный объект. - -![sort](./images/table-with-basic-sort.png) - -## Рефакторинг - -Мы повторяем слишком много кода в обработчиках, из-за чего мы оставляем слишком много шансов для ошибок. - -Одним из возможных решений в данной ситуации будет создание массива с заголовками таблицы и использование его для отрисовки заголовков и обработчиков: - -```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 +```