diff --git a/bro.config.js b/bro.config.js index 4f8b651..5b00e49 100644 --- a/bro.config.js +++ b/bro.config.js @@ -11,7 +11,7 @@ module.exports = { /* use https://admin.bro-js.ru/ to create config, navigations and features */ navigations: { 'kfu-24-teacher.main': '/kfu-24-teacher', - 'kfu-24-teacher.detail': '/kfu-24-teacher/detail' + 'kfu-24-teacher.detail': '/kfu-24-teacher/:id' }, features: { 'kfu-24-teacher': { diff --git a/package-lock.json b/package-lock.json index 06b9da8..6c0a85b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,12 @@ "@brojs/cli": "^1.6.3", "@emotion/react": "^11.13.5", "@emotion/styled": "^11.13.5", + "@reduxjs/toolkit": "^2.5.0", + "axios": "^1.7.9", "express": "^4.19.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-redux": "^9.2.0", "react-router-dom": "^6.28.0" }, "devDependencies": { @@ -2420,6 +2423,40 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", + "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remix-run/router": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", @@ -2496,14 +2533,14 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2520,6 +2557,12 @@ "@types/react": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/webpack-env": { "version": "1.18.5", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.5.tgz", @@ -3266,9 +3309,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -8250,6 +8293,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.28.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", @@ -8340,6 +8406,21 @@ "recursive-watch": "bin.js" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -8458,6 +8539,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -9632,6 +9719,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 642e0db..e756cfc 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,12 @@ "@brojs/cli": "^1.6.3", "@emotion/react": "^11.13.5", "@emotion/styled": "^11.13.5", + "@reduxjs/toolkit": "^2.5.0", + "axios": "^1.7.9", "express": "^4.19.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-redux": "^9.2.0", "react-router-dom": "^6.28.0" }, "devDependencies": { diff --git a/src/container/detail/index.tsx b/src/container/detail/index.tsx index 807c1f5..59f628c 100644 --- a/src/container/detail/index.tsx +++ b/src/container/detail/index.tsx @@ -1,9 +1,11 @@ import React from 'react'; import Heading from '../../components/heading'; import { HeadingVariant } from '../../components/heading/types'; +import { useParams } from 'react-router-dom'; const DetailPage = (): React.ReactElement => { - return Detail Page; + const { id } = useParams(); + return Detail Page {id} ; }; export default DetailPage; diff --git a/src/container/list/index.tsx b/src/container/list/index.tsx index 1bfa56c..f00d247 100644 --- a/src/container/list/index.tsx +++ b/src/container/list/index.tsx @@ -1,30 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import Heading from '../../components/heading'; +import { useGetListQuery } from '../../store/api'; const ListPage = (): React.ReactElement => { - const [error, setError] = useState(null); - const [data, setData] = useState>(); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const handle = async () => { - setIsLoading(true); - try { - const res = await fetch('/api/list'); - const data = await res.json(); - if (res.ok) { - setData(data); - } else { - setError(data.message); - } - } catch (e) { - setError(e.message); - } finally { - setIsLoading(false); - } - }; - handle(); - }, []); + const { data, isLoading, error } = useGetListQuery(undefined); return ( <> diff --git a/src/container/main/index.tsx b/src/container/main/index.tsx index aa376b7..1adee23 100644 --- a/src/container/main/index.tsx +++ b/src/container/main/index.tsx @@ -1,9 +1,15 @@ import React from 'react'; import { RouterProvider } from 'react-router-dom'; import { router } from './router'; +import { store } from '../../store'; +import { Provider } from 'react-redux'; const Main = (): React.ReactElement => { - return ; + return ( + + + + ); }; export default Main; diff --git a/src/service/list/index.ts b/src/service/list/index.ts new file mode 100644 index 0000000..874a002 --- /dev/null +++ b/src/service/list/index.ts @@ -0,0 +1,11 @@ +import { network } from '../network'; +import { GetListResponse } from './types'; + +class ListService { + async getList() { + const res = await network.get('/list'); + return res.data; + } +} + +export const listService = new ListService(); diff --git a/src/service/list/types.ts b/src/service/list/types.ts new file mode 100644 index 0000000..d19b3cc --- /dev/null +++ b/src/service/list/types.ts @@ -0,0 +1,7 @@ +export interface ListItem { + id: number; + title: string; + description: string; +} + +export type GetListResponse = Array; diff --git a/src/service/network.ts b/src/service/network.ts new file mode 100644 index 0000000..a844967 --- /dev/null +++ b/src/service/network.ts @@ -0,0 +1,6 @@ +import axios from 'axios'; +import { getConfigValue } from '@brojs/cli'; + +const baseUrl = getConfigValue('kfu-24-teacher.api'); + +export const network = axios.create({ baseURL: baseUrl }); diff --git a/src/store/api.ts b/src/store/api.ts new file mode 100644 index 0000000..ee40b86 --- /dev/null +++ b/src/store/api.ts @@ -0,0 +1,29 @@ +// Need to use the React-specific entry point to import createApi +import { createApi, fetchBaseQuery, QueryReturnValue } from '@reduxjs/toolkit/query/react'; +import { GetListResponse } from '../service/list/types'; +import { listService } from '../service/list'; + +const createQueryFromPromise = + (fn: (...args: Array) => Promise) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (...args): Promise> => { + try { + const data = await fn(...args); + return { data }; + } catch (e: unknown) { + return { error: e }; + } + }; + +// Define a service using a base URL and expected endpoints +export const api = createApi({ + reducerPath: 'api', + baseQuery: fetchBaseQuery({ baseUrl: '' }), + endpoints: (builder) => ({ + getList: builder.query({ + queryFn: createQueryFromPromise(() => listService.getList()) + }) + }) +}); + +export const { useGetListQuery } = api; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..8a3c187 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,20 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { api } from './api'; +import { setupListeners } from '@reduxjs/toolkit/query'; + +export const store = configureStore({ + reducer: { + // Add the generated reducer as a specific top-level slice + [api.reducerPath]: api.reducer + }, + // Adding the api middleware enables caching, invalidation, polling, + // and other useful features of `rtk-query`. + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware) +}); + +setupListeners(store.dispatch); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch;