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;