From 8cf89e8624648b47d4951883b81e787746254484 Mon Sep 17 00:00:00 2001 From: Ihor S Date: Tue, 9 Sep 2025 19:40:38 +0200 Subject: [PATCH 1/8] wip --- .env.example | 2 +- package-lock.json | 134 ++++++++++++++++++ package.json | 4 + .../customers/actionCreators/getCustomers.ts | 18 +++ .../reducers/customers/customersSlice.ts | 31 ++++ src/redux/reducers/customers/helpers.ts | 1 + src/redux/store/rootReducer.ts | 9 ++ src/redux/store/store.ts | 8 ++ 8 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/redux/reducers/customers/actionCreators/getCustomers.ts create mode 100644 src/redux/reducers/customers/customersSlice.ts create mode 100644 src/redux/reducers/customers/helpers.ts create mode 100644 src/redux/store/rootReducer.ts create mode 100644 src/redux/store/store.ts diff --git a/.env.example b/.env.example index 3123d7b..1a4612c 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ REACT_APP_GOOGLE_MAPS_API_KEY=--your-google-maps-api-key-- -VITE_API_SERVER_URL=http://localhost:3000/api \ No newline at end of file +VITE_API_SERVER_URL=http://192.168.0.17:3000/api \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f494b4a..a208664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@ionic/react": "^8.5.0", "@ionic/react-router": "^8.5.0", "@mui/material": "^7.3.1", + "@reduxjs/toolkit": "^2.9.0", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "class-transformer": "^0.5.1", @@ -29,8 +30,10 @@ "jwt-decode": "^4.0.0", "react": "19.0.0", "react-dom": "19.0.0", + "react-redux": "^9.2.0", "react-router": "^5.3.4", "react-router-dom": "^5.3.4", + "redux": "^5.0.1", "reflect-metadata": "^0.2.2" }, "devDependencies": { @@ -41,6 +44,7 @@ "@testing-library/user-event": "^14.4.3", "@types/react": "19.0.10", "@types/react-dom": "19.0.4", + "@types/react-redux": "^7.1.34", "@vitejs/plugin-legacy": "^5.0.0", "@vitejs/plugin-react": "^4.0.1", "cypress": "^13.5.0", @@ -3106,6 +3110,31 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "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/@rolldown/pluginutils": { "version": "1.0.0-beta.9", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", @@ -3378,6 +3407,16 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, "node_modules/@stencil/core": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.20.0.tgz", @@ -3580,6 +3619,18 @@ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "dev": true, + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3689,6 +3740,27 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-redux": { + "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "dev": true, + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-redux/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/@types/react-router": { "version": "5.1.20", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", @@ -3750,6 +3822,11 @@ "@types/jest": "*" } }, + "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==" + }, "node_modules/@types/validator": { "version": "13.15.3", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", @@ -6952,6 +7029,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -9055,6 +9141,28 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "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==", + "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-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -9148,6 +9256,19 @@ "node": ">=8" } }, + "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==" + }, + "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==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -9282,6 +9403,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "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==" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -10652,6 +10778,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "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 ecc2928..e1e3e48 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@ionic/react": "^8.5.0", "@ionic/react-router": "^8.5.0", "@mui/material": "^7.3.1", + "@reduxjs/toolkit": "^2.9.0", "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "class-transformer": "^0.5.1", @@ -35,8 +36,10 @@ "jwt-decode": "^4.0.0", "react": "19.0.0", "react-dom": "19.0.0", + "react-redux": "^9.2.0", "react-router": "^5.3.4", "react-router-dom": "^5.3.4", + "redux": "^5.0.1", "reflect-metadata": "^0.2.2" }, "devDependencies": { @@ -47,6 +50,7 @@ "@testing-library/user-event": "^14.4.3", "@types/react": "19.0.10", "@types/react-dom": "19.0.4", + "@types/react-redux": "^7.1.34", "@vitejs/plugin-legacy": "^5.0.0", "@vitejs/plugin-react": "^4.0.1", "cypress": "^13.5.0", diff --git a/src/redux/reducers/customers/actionCreators/getCustomers.ts b/src/redux/reducers/customers/actionCreators/getCustomers.ts new file mode 100644 index 0000000..0783412 --- /dev/null +++ b/src/redux/reducers/customers/actionCreators/getCustomers.ts @@ -0,0 +1,18 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' +import { getNameThunk } from '../helpers'; + +export const getCustomes = createAsyncThunk( + getNameThunk('getCustomers'), + async (filter, { rejectWithValue }) => { + try { + + // TODO: stopped here + // const customersService = new CustomersService(); + // const { data } = await customersService.getCustomers(createUrlQueryString(BASE_CUSTOMERS_URL, filter)); + + // return data.map((eventModelRes) => buildEvent(eventModelRes)); + } catch (error) { + // return rejectWithValue(error as TRejectValue); + } + }, +); diff --git a/src/redux/reducers/customers/customersSlice.ts b/src/redux/reducers/customers/customersSlice.ts new file mode 100644 index 0000000..2776d51 --- /dev/null +++ b/src/redux/reducers/customers/customersSlice.ts @@ -0,0 +1,31 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { getCustomes } from "./actionCreators/getCustomers"; + +type TCustomer = { + name: string; +} + +type TCustomerState = { + allCustomers: TCustomer[] + isLoading: boolean +} + + +const initialState: TCustomerState = { + allCustomers: [], + isLoading: false, +}; + + +const {actions, reducer: customersReducer} = createSlice({ + name: 'customers', + initialState: initialState, + reducers: {}, + extraReducers(builder) { + builder.addCase(getCustomes.fulfilled, (state, action) => { + state.allCustomers = action.payload; + }); + }, +}) + +export { customersReducer, actions } \ No newline at end of file diff --git a/src/redux/reducers/customers/helpers.ts b/src/redux/reducers/customers/helpers.ts new file mode 100644 index 0000000..cb42f8c --- /dev/null +++ b/src/redux/reducers/customers/helpers.ts @@ -0,0 +1 @@ +export const getNameThunk = (name: string) => `customers/${name}`; diff --git a/src/redux/store/rootReducer.ts b/src/redux/store/rootReducer.ts new file mode 100644 index 0000000..0eea6ea --- /dev/null +++ b/src/redux/store/rootReducer.ts @@ -0,0 +1,9 @@ +import { combineReducers } from "redux"; +import { customersReducer } from "../reducers/customers/customersSlice"; + +export const rootReducer = (state, action) => { + + return combineReducers({ + customers: customersReducer + }); +}; diff --git a/src/redux/store/store.ts b/src/redux/store/store.ts new file mode 100644 index 0000000..b99aeef --- /dev/null +++ b/src/redux/store/store.ts @@ -0,0 +1,8 @@ +import { configureStore } from '@reduxjs/toolkit'; + +import { rootReducer } from './rootReducer/rootReducer'; + +export const store = configureStore({ + reducer: rootReducer, + +}); From b433327dc060512a3612bf0d2e4262bdeb9e3843 Mon Sep 17 00:00:00 2001 From: Ihor S Date: Wed, 10 Sep 2025 19:02:29 +0200 Subject: [PATCH 2/8] add redux, before moving login to redux --- src/App.tsx | 54 ++++++++-------- src/api/services/users-service.ts | 2 +- src/interfaces/list-response.interface.ts | 4 ++ src/pages/Admin/Admin.tsx | 61 ++++++++++++------- src/pages/Admin/UsersList/UsersList.tsx | 20 ++++++ src/pages/Customers/Customers.tsx | 29 ++++++--- src/pages/Login/Login.tsx | 2 - .../reducers/auth/actionCreators/signIn.tsx | 11 ++++ .../reducers/auth/actionCreators/signOut.tsx | 0 src/redux/reducers/auth/authSlice.ts | 34 +++++++++++ src/redux/reducers/auth/helpers.ts | 1 + src/redux/reducers/auth/types.ts | 9 +++ .../customers/actionCreators/getCustomers.ts | 8 +-- .../reducers/customers/customersSlice.ts | 24 +++----- src/redux/reducers/customers/types.ts | 8 +++ .../reducers/users/actionCreators/getUsers.ts | 28 +++++++++ src/redux/reducers/users/helpers.ts | 1 + src/redux/reducers/users/types.ts | 15 +++++ src/redux/reducers/users/usersSlice.ts | 40 ++++++++++++ src/redux/selectors/customersSelectors.ts | 4 ++ src/redux/selectors/usersSelector.ts | 5 ++ src/redux/store/rootReducer.ts | 16 ++--- src/redux/store/store.ts | 10 ++- 23 files changed, 298 insertions(+), 88 deletions(-) create mode 100644 src/pages/Admin/UsersList/UsersList.tsx create mode 100644 src/redux/reducers/auth/actionCreators/signIn.tsx create mode 100644 src/redux/reducers/auth/actionCreators/signOut.tsx create mode 100644 src/redux/reducers/auth/authSlice.ts create mode 100644 src/redux/reducers/auth/helpers.ts create mode 100644 src/redux/reducers/auth/types.ts create mode 100644 src/redux/reducers/customers/types.ts create mode 100644 src/redux/reducers/users/actionCreators/getUsers.ts create mode 100644 src/redux/reducers/users/helpers.ts create mode 100644 src/redux/reducers/users/types.ts create mode 100644 src/redux/reducers/users/usersSlice.ts create mode 100644 src/redux/selectors/customersSelectors.ts create mode 100644 src/redux/selectors/usersSelector.ts diff --git a/src/App.tsx b/src/App.tsx index 33c06f8..fd2c264 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,37 +38,41 @@ import Supplyers from './pages/Supplyers/Supplyers'; import Customers from './pages/Customers/Customers'; import Login from './pages/Login/Login'; import AdminPage from './pages/Admin/Admin'; +import { Provider } from 'react-redux'; +import { store } from './redux/store/store'; setupIonicReact(); const App: React.FC = () => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/api/services/users-service.ts b/src/api/services/users-service.ts index 880472b..c38214f 100644 --- a/src/api/services/users-service.ts +++ b/src/api/services/users-service.ts @@ -4,7 +4,7 @@ import { UserDto } from '../dto/users/user.dto'; import { IListResponse } from '../../interfaces/list-response.interface'; export class UsersService { - async getAll() { + async getAll(): Promise> { const response = await transport.useEndpoint>(ENDPOINTS.users.getAll); return response; } diff --git a/src/interfaces/list-response.interface.ts b/src/interfaces/list-response.interface.ts index 9c9abd3..bf65f31 100644 --- a/src/interfaces/list-response.interface.ts +++ b/src/interfaces/list-response.interface.ts @@ -1,4 +1,8 @@ export interface IListResponse { data: Data[]; total: number; + offset?: number; + limit?: number; + filter?: string; + direction?: string; } diff --git a/src/pages/Admin/Admin.tsx b/src/pages/Admin/Admin.tsx index 7de73fa..ef6a730 100644 --- a/src/pages/Admin/Admin.tsx +++ b/src/pages/Admin/Admin.tsx @@ -1,22 +1,32 @@ import { IonButtons, IonContent, IonHeader, IonItem, IonLabel, IonList, IonMenuButton, IonPage, IonTitle, IonToolbar } from '@ionic/react'; import styled from '@emotion/styled'; -import React, { useEffect, useMemo } from 'react'; -import { UsersService } from '../../api/services/users-service'; -import { UserDto } from '../../api/dto/users/user.dto'; +import React, { useEffect } from 'react'; +import { UsersList } from './UsersList/UsersList'; +import { useAppDispatch } from '../../redux/store/store'; +import { getUsers } from '../../redux/reducers/users/actionCreators/getUsers'; +import { useSelector } from 'react-redux'; +import { allUsersSelector, totalUsersSelector } from '../../redux/selectors/usersSelector'; +import { Typography } from '@mui/material'; const AdminPage: React.FC = () => { - const name = 'Admin'; - - const [users, setUsers] = React.useState([]); - const usersService = useMemo(() => new UsersService(), []); // todo: move to context or redux - + // const [users, setUsers] = React.useState([]); + // const usersService = useMemo(() => new UsersService(), []); // todo: move to context or redux + + // useEffect(() => { + // const fetchUsers = async () => { + // const { data } = await usersService.getAll(); + // setUsers(data); + // }; + // fetchUsers(); + // }, [usersService]); + + const dispatch = useAppDispatch(); useEffect(() => { - const fetchUsers = async () => { - const {data} = await usersService.getAll(); - setUsers(data); - } - fetchUsers(); - }, [usersService]); + dispatch(getUsers()); + }, [dispatch]); + + const users = useSelector(allUsersSelector); + const total = useSelector(totalUsersSelector); return ( @@ -25,25 +35,30 @@ const AdminPage: React.FC = () => { - {name} + Admin panel - - {users.map((user, index) => ( - - {user.id} {user.name} {user.email} - - ))} - + + + + Total users: {total} ); -} +}; const AdminContentStyled = styled(IonContent)` --background: var(--ion-color-light); `; +const UsersListContainerStyled = styled.div` + padding: 16px; +`; + +const TotalUsersStyled = styled(Typography)` + margin-top: 16px; +`; + export default AdminPage; diff --git a/src/pages/Admin/UsersList/UsersList.tsx b/src/pages/Admin/UsersList/UsersList.tsx new file mode 100644 index 0000000..b62ae81 --- /dev/null +++ b/src/pages/Admin/UsersList/UsersList.tsx @@ -0,0 +1,20 @@ +import { IonItem, IonList } from '@ionic/react'; +import { Typography } from '@mui/material'; +import { UserDto } from 'src/api/dto/users/user.dto'; + +type TUsersListProps = { users: UserDto[] }; + +export const UsersList: React.FC = ({ users }) => { + return ( + <> + Users list + + {users.map((user, index) => ( + + {index + 1}. {user.name} ({user.id}) + + ))} + + + ); +}; diff --git a/src/pages/Customers/Customers.tsx b/src/pages/Customers/Customers.tsx index facefad..ea8c3aa 100644 --- a/src/pages/Customers/Customers.tsx +++ b/src/pages/Customers/Customers.tsx @@ -2,17 +2,27 @@ import { IonButtons, IonContent, IonHeader, IonItem, IonLabel, IonList, IonMenuB import styled from '@emotion/styled'; import { SelectChangeEvent } from '@mui/material'; import React from 'react'; -import { MuiMultiselect } from '../../components/MuiMultiselect/MuiMultiselect'; +import { MuiMultiselect } from 'src/components/MuiMultiselect/MuiMultiselect'; +import { getCustomes } from 'src/redux/reducers/customers/actionCreators/getCustomers'; +import { useSelector } from 'react-redux'; +import { allCustomersSelector } from '../../redux/selectors/customersSelectors'; +import { useAppDispatch } from '../../redux/store/store'; const Customers: React.FC = () => { - const customers = ['Customer1', 'Customer2', 'Customer3', 'Customer4', 'Customer5'] - const name = 'Customers' + const customers = ['Customer1', 'Customer2', 'Customer3', 'Customer4', 'Customer5']; + const name = 'Customers'; + const dispatch = useAppDispatch(); const [selectedCustomers, setSelectedCustomers] = React.useState([]); - + + React.useEffect(() => { + dispatch(getCustomes()); + }, [dispatch]); + + const allCustomers = useSelector(allCustomersSelector); + const handleChange = (event: SelectChangeEvent) => { setSelectedCustomers(typeof event.target.value === 'string' ? event.target.value.split(',') : event.target.value); - } - + }; return ( @@ -26,6 +36,11 @@ const Customers: React.FC = () => { + {allCustomers.map((customer, index) => ( + + {customer.name} + + ))} {customers.map((customer, index) => ( @@ -43,7 +58,7 @@ const Customers: React.FC = () => { ); -} +}; const CustomersContentStyled = styled(IonContent)` --background: var(--ion-color-light); diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 5d194c3..cc0e884 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -23,8 +23,6 @@ import { useLocalstorage } from '../../hooks/use-local-storage'; import { ResponseTokenDto } from '../../api/dto/auth/response-token.dto'; const Login: React.FC = () => { - // const [credentials, setCredentials] = useState({ email: '', password: '' }); - const [{ accessToken }, setTokens] = useLocalstorage('auth', { accessToken: '', refreshToken: '' }); const handleSignin = useCallback( diff --git a/src/redux/reducers/auth/actionCreators/signIn.tsx b/src/redux/reducers/auth/actionCreators/signIn.tsx new file mode 100644 index 0000000..434b2f2 --- /dev/null +++ b/src/redux/reducers/auth/actionCreators/signIn.tsx @@ -0,0 +1,11 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import { getNameThunk } from "../helpers"; + + +export const signIn = createAsyncThunk( + getNameThunk('signIn'), + async (credentials) => { + const { email, password } = credentials; + + } +) \ No newline at end of file diff --git a/src/redux/reducers/auth/actionCreators/signOut.tsx b/src/redux/reducers/auth/actionCreators/signOut.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/redux/reducers/auth/authSlice.ts b/src/redux/reducers/auth/authSlice.ts new file mode 100644 index 0000000..3d77ef7 --- /dev/null +++ b/src/redux/reducers/auth/authSlice.ts @@ -0,0 +1,34 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { signIn } from "./actionCreators/signIn"; +import { signOut } from "./actionCreators/signIn"; + +const initialState = { + user: null, + isLoading: false, +}; + +const { actions, reducer: authReducer } = createSlice({ + name: 'auth', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(signIn.fulfilled, (state, action) => { + state.user = action.payload; + state.isLoading = false; + }); + builder.addCase(signIn.pending, (state) => { + state.isLoading = true; + }); + builder.addCase(signIn.rejected, (state) => { + state.user = null; + state.isLoading = false; + }); + + builder.addCase(signOut.fulfilled, (state) => { + state.user = null; + state.isLoading = false; + }); + }, +}); + +export { authReducer, actions } \ No newline at end of file diff --git a/src/redux/reducers/auth/helpers.ts b/src/redux/reducers/auth/helpers.ts new file mode 100644 index 0000000..cfc4647 --- /dev/null +++ b/src/redux/reducers/auth/helpers.ts @@ -0,0 +1 @@ +export const getNameThunk = (name: string) => `auth/${name}`; diff --git a/src/redux/reducers/auth/types.ts b/src/redux/reducers/auth/types.ts new file mode 100644 index 0000000..e60f80e --- /dev/null +++ b/src/redux/reducers/auth/types.ts @@ -0,0 +1,9 @@ +export type TAuthedUser = { + id: string; + name: string; +} + +export type TUsersState = { + authedUser: TAuthedUser | null; + isLoading: boolean; +}; diff --git a/src/redux/reducers/customers/actionCreators/getCustomers.ts b/src/redux/reducers/customers/actionCreators/getCustomers.ts index 0783412..ac97783 100644 --- a/src/redux/reducers/customers/actionCreators/getCustomers.ts +++ b/src/redux/reducers/customers/actionCreators/getCustomers.ts @@ -1,18 +1,18 @@ import { createAsyncThunk } from '@reduxjs/toolkit' import { getNameThunk } from '../helpers'; +import { TCustomer } from '../types'; -export const getCustomes = createAsyncThunk( +export const getCustomes = createAsyncThunk( getNameThunk('getCustomers'), async (filter, { rejectWithValue }) => { try { - // TODO: stopped here // const customersService = new CustomersService(); // const { data } = await customersService.getCustomers(createUrlQueryString(BASE_CUSTOMERS_URL, filter)); - // return data.map((eventModelRes) => buildEvent(eventModelRes)); + return [{ name: '// todo: this is mock' }] as TCustomer[]; } catch (error) { - // return rejectWithValue(error as TRejectValue); + return rejectWithValue(error); } }, ); diff --git a/src/redux/reducers/customers/customersSlice.ts b/src/redux/reducers/customers/customersSlice.ts index 2776d51..f9f31a4 100644 --- a/src/redux/reducers/customers/customersSlice.ts +++ b/src/redux/reducers/customers/customersSlice.ts @@ -1,31 +1,25 @@ import { createSlice } from "@reduxjs/toolkit"; import { getCustomes } from "./actionCreators/getCustomers"; +import { TCustomersState } from './types'; -type TCustomer = { - name: string; -} - -type TCustomerState = { - allCustomers: TCustomer[] - isLoading: boolean -} - - -const initialState: TCustomerState = { +const initialState: TCustomersState = { allCustomers: [], isLoading: false, }; - -const {actions, reducer: customersReducer} = createSlice({ +const { actions, reducer: customersReducer } = createSlice({ name: 'customers', initialState: initialState, - reducers: {}, + reducers: { + setCustomers(state, action) { + state.allCustomers = action.payload; + }, + }, extraReducers(builder) { builder.addCase(getCustomes.fulfilled, (state, action) => { state.allCustomers = action.payload; }); }, -}) +}); export { customersReducer, actions } \ No newline at end of file diff --git a/src/redux/reducers/customers/types.ts b/src/redux/reducers/customers/types.ts new file mode 100644 index 0000000..c98c01a --- /dev/null +++ b/src/redux/reducers/customers/types.ts @@ -0,0 +1,8 @@ +export type TCustomer = { + name: string; +} + +export type TCustomersState = { + allCustomers: TCustomer[]; + isLoading: boolean; +}; diff --git a/src/redux/reducers/users/actionCreators/getUsers.ts b/src/redux/reducers/users/actionCreators/getUsers.ts new file mode 100644 index 0000000..e719d6f --- /dev/null +++ b/src/redux/reducers/users/actionCreators/getUsers.ts @@ -0,0 +1,28 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' +import { getNameThunk } from '../helpers'; +import { TUser } from '../types'; +import { UsersService } from 'src/api/services/users-service'; +import { IListResponse } from 'src/interfaces/list-response.interface'; + +export const getUsers = createAsyncThunk< IListResponse >( + getNameThunk('getUsers'), + async (filter, { rejectWithValue }) => { + try { + const usersService = new UsersService(); + const { data, total } = await usersService.getAll(); + + const users = data.map(user => ( + { + id: user.id, + name: user.name, + email: user.email + } as TUser + )); + + return { data: users, total }; + } catch (error) { + console.error(error); + return rejectWithValue(error); + } + }, +); diff --git a/src/redux/reducers/users/helpers.ts b/src/redux/reducers/users/helpers.ts new file mode 100644 index 0000000..0f0e96a --- /dev/null +++ b/src/redux/reducers/users/helpers.ts @@ -0,0 +1 @@ +export const getNameThunk = (name: string) => `users/${name}`; diff --git a/src/redux/reducers/users/types.ts b/src/redux/reducers/users/types.ts new file mode 100644 index 0000000..6a64b8b --- /dev/null +++ b/src/redux/reducers/users/types.ts @@ -0,0 +1,15 @@ +export type TUser = { + id: string; + name: string; + email: string; +} + +export type TUsersState = { + allUsers: TUser[]; + filter?: string; + direction?: string; + offset?: number; + limit?: number; + total: number; + isLoading: boolean; +}; diff --git a/src/redux/reducers/users/usersSlice.ts b/src/redux/reducers/users/usersSlice.ts new file mode 100644 index 0000000..1c108c7 --- /dev/null +++ b/src/redux/reducers/users/usersSlice.ts @@ -0,0 +1,40 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { TUsersState } from "./types"; +import { getUsers } from "./actionCreators/getUsers"; + +const initialState: TUsersState = { + allUsers: [], + isLoading: false, + filter: undefined, + direction: undefined, + offset: undefined, + limit: undefined, + total: 0, +}; + +const { actions, reducer: usersReducer } = createSlice({ + name: 'users', + initialState, + reducers: { + setFilter(state, action) { + state.filter = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(getUsers.fulfilled, (state, { payload }) => { + state.allUsers = payload.data; + state.total = payload.total; + state.isLoading = false; + }); + builder.addCase(getUsers.rejected, (state) => { + state.allUsers = []; + state.total = 0; + state.isLoading = false; + }); + builder.addCase(getUsers.pending, (state) => { + state.isLoading = true; + }); + }, +}); + +export { usersReducer, actions } diff --git a/src/redux/selectors/customersSelectors.ts b/src/redux/selectors/customersSelectors.ts new file mode 100644 index 0000000..87276a8 --- /dev/null +++ b/src/redux/selectors/customersSelectors.ts @@ -0,0 +1,4 @@ +import { TRootState } from "../store/rootReducer"; + +export const allCustomersSelector = (state: TRootState) => state.customers.allCustomers; +export const isCustomersLoading = (state: TRootState) => state.customers.isLoading; diff --git a/src/redux/selectors/usersSelector.ts b/src/redux/selectors/usersSelector.ts new file mode 100644 index 0000000..d80068b --- /dev/null +++ b/src/redux/selectors/usersSelector.ts @@ -0,0 +1,5 @@ +import { TRootState } from "../store/rootReducer"; + +export const allUsersSelector = (state: TRootState) => state.users.allUsers; +export const totalUsersSelector = (state: TRootState) => state.users.total; +export const isUsersLoading = (state: TRootState) => state.users.isLoading; diff --git a/src/redux/store/rootReducer.ts b/src/redux/store/rootReducer.ts index 0eea6ea..311dfa0 100644 --- a/src/redux/store/rootReducer.ts +++ b/src/redux/store/rootReducer.ts @@ -1,9 +1,9 @@ -import { combineReducers } from "redux"; -import { customersReducer } from "../reducers/customers/customersSlice"; +import { combineReducers } from 'redux'; +import { customersReducer } from '../reducers/customers/customersSlice'; +import { usersReducer } from '../reducers/users/usersSlice'; -export const rootReducer = (state, action) => { - - return combineReducers({ - customers: customersReducer - }); -}; +export const rootReducer = combineReducers({ + customers: customersReducer, + users: usersReducer, +}); +export type TRootState = ReturnType; diff --git a/src/redux/store/store.ts b/src/redux/store/store.ts index b99aeef..f4212e7 100644 --- a/src/redux/store/store.ts +++ b/src/redux/store/store.ts @@ -1,8 +1,12 @@ import { configureStore } from '@reduxjs/toolkit'; - -import { rootReducer } from './rootReducer/rootReducer'; +import { rootReducer } from './rootReducer'; +import { useDispatch } from 'react-redux'; export const store = configureStore({ reducer: rootReducer, - }); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +export const useAppDispatch = () => useDispatch(); \ No newline at end of file From 3e561165d55e3e6e847e44b1e9c4b1b8ff6e16bb Mon Sep 17 00:00:00 2001 From: Ihor S Date: Wed, 10 Sep 2025 20:27:01 +0200 Subject: [PATCH 3/8] use redux for auth --- src/api/constants.ts | 1 + src/api/services/auth-service.ts | 4 +- src/api/services/users-service.ts | 5 +++ src/api/types.ts | 2 +- src/constants/constants.ts | 5 +++ src/hooks/use-authed-user.ts | 6 ++- src/hooks/use-local-storage.ts | 3 ++ src/pages/Login/CurrentUser.tsx | 27 ++++++------- src/pages/Login/Login.tsx | 20 +++++----- .../reducers/auth/actionCreators/signIn.tsx | 38 +++++++++++++++---- .../reducers/auth/actionCreators/signOut.tsx | 11 ++++++ src/redux/reducers/auth/authSlice.ts | 17 +++++---- src/redux/reducers/auth/types.ts | 6 +-- src/redux/selectors/authSelectors.ts | 4 ++ src/redux/store/rootReducer.ts | 2 + src/redux/store/store.ts | 3 +- 16 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 src/constants/constants.ts create mode 100644 src/redux/selectors/authSelectors.ts diff --git a/src/api/constants.ts b/src/api/constants.ts index 856337d..5f870ba 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -10,5 +10,6 @@ export const ENDPOINTS: TEndpoints = { }, users: { getAll: { url: () => 'users', method: 'GET', options: { auth: true } }, + getById: { url: (params) => `users/${params?.id}`, method: 'GET', options: { auth: true } }, }, }; diff --git a/src/api/services/auth-service.ts b/src/api/services/auth-service.ts index fd42ba2..a15a564 100644 --- a/src/api/services/auth-service.ts +++ b/src/api/services/auth-service.ts @@ -4,8 +4,8 @@ import { ResponseTokenDto } from '../dto/auth/response-token.dto'; import { ENDPOINTS } from "../constants"; export class AuthService { - async login(credentials: AuthDto, setTokens: (tokens: ResponseTokenDto) => void) { + async signIn(credentials: AuthDto): Promise { const tokens = await transport.post(ENDPOINTS.auth.signin.url(), credentials); - setTokens(tokens); + return tokens; } } diff --git a/src/api/services/users-service.ts b/src/api/services/users-service.ts index c38214f..35c7ed0 100644 --- a/src/api/services/users-service.ts +++ b/src/api/services/users-service.ts @@ -8,4 +8,9 @@ export class UsersService { const response = await transport.useEndpoint>(ENDPOINTS.users.getAll); return response; } + + async getById(id: string): Promise { + const response = await transport.useEndpoint(ENDPOINTS.users.getById, null, { id }); + return response; + } } \ No newline at end of file diff --git a/src/api/types.ts b/src/api/types.ts index cdd01b2..7bcff70 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -4,7 +4,7 @@ export type TransportOptions = { }; export type TEndpoint = { - url: (params?: Record) => string; + url: (params?: { [key: string]: string }) => string; options: TransportOptions; method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; }; diff --git a/src/constants/constants.ts b/src/constants/constants.ts new file mode 100644 index 0000000..620fa3a --- /dev/null +++ b/src/constants/constants.ts @@ -0,0 +1,5 @@ +export const MESSAGES = { + signInFailed: 'Sign in failed', +}; + +export const LOCAL_STORAGE_AUTH_KEY = 'auth'; \ No newline at end of file diff --git a/src/hooks/use-authed-user.ts b/src/hooks/use-authed-user.ts index 9bb15d4..ca570f7 100644 --- a/src/hooks/use-authed-user.ts +++ b/src/hooks/use-authed-user.ts @@ -2,8 +2,12 @@ import { jwtDecode } from "jwt-decode"; import { ResponseTokenDto } from '../api/dto/auth/response-token.dto'; import { useLocalstorage } from "./use-local-storage"; import { JwtPayloadDto } from '../api/dto/auth/jwt-payload.dto'; + +/** + * @deprecated Use useAuthedUserSelector instead + */ export const useAuthedUser = ():JwtPayloadDto => { const [{ accessToken }] = useLocalstorage('auth', { accessToken: '', refreshToken: '' }); const userId = accessToken && jwtDecode(accessToken)?.sub; return { sub: userId }; -}; \ No newline at end of file +}; diff --git a/src/hooks/use-local-storage.ts b/src/hooks/use-local-storage.ts index d44a47d..5d37670 100644 --- a/src/hooks/use-local-storage.ts +++ b/src/hooks/use-local-storage.ts @@ -1,5 +1,8 @@ import { useState } from "react"; +/** + * @deprecated + */ export const useLocalstorage = (key: string, initialValue: T): [T, (newValue: T) => void] => { const [_value, _setValue] = useState(() => localStorage.getItem(key) || JSON.stringify(initialValue)); diff --git a/src/pages/Login/CurrentUser.tsx b/src/pages/Login/CurrentUser.tsx index 7b54b46..d138a9d 100644 --- a/src/pages/Login/CurrentUser.tsx +++ b/src/pages/Login/CurrentUser.tsx @@ -1,23 +1,24 @@ import { IonButton, IonTitle } from "@ionic/react"; import styled from "@emotion/styled"; -import { ResponseTokenDto } from '../../api/dto/auth/response-token.dto'; -import { useAuthedUser } from "../../hooks/use-authed-user"; +import { signOut } from 'src/redux/reducers/auth/actionCreators/signOut'; +import { useAppDispatch } from 'src/redux/store/store'; +import { TAuthedUser } from '../../redux/reducers/auth/types'; + +type CurrentUserProps = { authedUser: TAuthedUser }; + +export const CurrentUser: React.FC = ({ authedUser }) => { + const dispatch = useAppDispatch(); -export const CurrentUser: React.FC< - {setTokens: (tokens: ResponseTokenDto) => void} -> = ({setTokens}) => { - const handleLogout = () => { - setTokens({ accessToken: '', refreshToken: '' }); - window.location.reload(); + dispatch(signOut()); }; - - const { sub: userId } = useAuthedUser(); - + return ( - Hello, Current User {userId} - Logout + Hello, {authedUser.name} + + Logout + ); }; diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index cc0e884..751288c 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -16,26 +16,28 @@ import { IonButton, } from '@ionic/react'; import './Login.css'; -import { AuthService } from 'src/api/services/auth-service'; import { useCallback } from 'react'; import { CurrentUser } from './CurrentUser'; -import { useLocalstorage } from '../../hooks/use-local-storage'; -import { ResponseTokenDto } from '../../api/dto/auth/response-token.dto'; +import { signIn } from '../../redux/reducers/auth/actionCreators/signIn'; +import { useAppDispatch } from '../../redux/store/store'; +import { authedUserSelector } from '../../redux/selectors/authSelectors'; +import { useSelector } from 'react-redux'; const Login: React.FC = () => { - const [{ accessToken }, setTokens] = useLocalstorage('auth', { accessToken: '', refreshToken: '' }); + const dispatch = useAppDispatch(); const handleSignin = useCallback( (e: React.FormEvent) => { e.preventDefault(); const email = (e.currentTarget as HTMLFormElement).email?.value; const password = (e.currentTarget as HTMLFormElement).password?.value; - const authService = new AuthService(); - authService.login({ email, password }, setTokens); + dispatch(signIn({ email, password })); }, - [setTokens], + [dispatch], ); + const authedUser = useSelector(authedUserSelector); + return ( @@ -51,8 +53,8 @@ const Login: React.FC = () => { - {accessToken?.length > 0 ? ( - + {authedUser ? ( + ) : (
diff --git a/src/redux/reducers/auth/actionCreators/signIn.tsx b/src/redux/reducers/auth/actionCreators/signIn.tsx index 434b2f2..6e5fd71 100644 --- a/src/redux/reducers/auth/actionCreators/signIn.tsx +++ b/src/redux/reducers/auth/actionCreators/signIn.tsx @@ -1,11 +1,33 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { getNameThunk } from "../helpers"; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { getNameThunk } from '../helpers'; +import { AuthService } from 'src/api/services/auth-service'; +import { AuthDto } from 'src/api/dto/auth/auth.dto'; +import { UsersService } from 'src/api/services/users-service'; +import { jwtDecode } from 'jwt-decode'; +import { JwtPayloadDto } from 'src/api/dto/auth/jwt-payload.dto'; +import { MESSAGES } from 'src/constants/constants'; +import { LOCAL_STORAGE_AUTH_KEY } from 'src/constants/constants'; +import { TUser } from '../../users/types'; +export const signIn = createAsyncThunk(getNameThunk('signIn'), async (credentials: AuthDto, { rejectWithValue }) => { + const authService = new AuthService(); + const usersService = new UsersService(); + const tokens = await authService.signIn(credentials); -export const signIn = createAsyncThunk( - getNameThunk('signIn'), - async (credentials) => { - const { email, password } = credentials; - + if (!tokens.accessToken) { + console.error(MESSAGES.signInFailed); + return rejectWithValue(MESSAGES.signInFailed); } -) \ No newline at end of file + + localStorage.setItem(LOCAL_STORAGE_AUTH_KEY, JSON.stringify(tokens)); + + const id = tokens.accessToken && jwtDecode(tokens.accessToken)?.sub; + const data = await usersService.getById(id!); + const user: TUser = { + id: data.id, + name: data.name, + email: data.email, + }; + + return { user }; +}); diff --git a/src/redux/reducers/auth/actionCreators/signOut.tsx b/src/redux/reducers/auth/actionCreators/signOut.tsx index e69de29..3e0e853 100644 --- a/src/redux/reducers/auth/actionCreators/signOut.tsx +++ b/src/redux/reducers/auth/actionCreators/signOut.tsx @@ -0,0 +1,11 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { getNameThunk } from '../helpers'; +import { LOCAL_STORAGE_AUTH_KEY } from 'src/constants/constants'; +import { TUser } from '../../users/types'; + +export const signOut = createAsyncThunk(getNameThunk('signOut'), async () => { + localStorage.removeItem(LOCAL_STORAGE_AUTH_KEY); + const user: TUser | null = null; + + return { user }; +}); diff --git a/src/redux/reducers/auth/authSlice.ts b/src/redux/reducers/auth/authSlice.ts index 3d77ef7..a70565f 100644 --- a/src/redux/reducers/auth/authSlice.ts +++ b/src/redux/reducers/auth/authSlice.ts @@ -1,9 +1,10 @@ import { createSlice } from "@reduxjs/toolkit"; -import { signIn } from "./actionCreators/signIn"; -import { signOut } from "./actionCreators/signIn"; +import { signIn } from './actionCreators/signIn'; +import { TAuthedUserState } from './types'; +import { signOut } from './actionCreators/signOut'; -const initialState = { - user: null, +const initialState: TAuthedUserState = { + authedUser: null, isLoading: false, }; @@ -13,19 +14,19 @@ const { actions, reducer: authReducer } = createSlice({ reducers: {}, extraReducers: (builder) => { builder.addCase(signIn.fulfilled, (state, action) => { - state.user = action.payload; + state.authedUser = action.payload.user; state.isLoading = false; }); builder.addCase(signIn.pending, (state) => { state.isLoading = true; }); builder.addCase(signIn.rejected, (state) => { - state.user = null; + state.authedUser = null; state.isLoading = false; }); - + builder.addCase(signOut.fulfilled, (state) => { - state.user = null; + state.authedUser = null; state.isLoading = false; }); }, diff --git a/src/redux/reducers/auth/types.ts b/src/redux/reducers/auth/types.ts index e60f80e..48ecb98 100644 --- a/src/redux/reducers/auth/types.ts +++ b/src/redux/reducers/auth/types.ts @@ -1,9 +1,9 @@ export type TAuthedUser = { id: string; name: string; -} +}; -export type TUsersState = { +export type TAuthedUserState = { authedUser: TAuthedUser | null; isLoading: boolean; -}; +}; \ No newline at end of file diff --git a/src/redux/selectors/authSelectors.ts b/src/redux/selectors/authSelectors.ts new file mode 100644 index 0000000..df94269 --- /dev/null +++ b/src/redux/selectors/authSelectors.ts @@ -0,0 +1,4 @@ +import { TRootState } from "../store/rootReducer"; + +export const authedUserSelector = (state: TRootState) => state.auth.authedUser; +export const isAuthLoading = (state: TRootState) => state.auth.isLoading; diff --git a/src/redux/store/rootReducer.ts b/src/redux/store/rootReducer.ts index 311dfa0..b282b0b 100644 --- a/src/redux/store/rootReducer.ts +++ b/src/redux/store/rootReducer.ts @@ -1,9 +1,11 @@ import { combineReducers } from 'redux'; import { customersReducer } from '../reducers/customers/customersSlice'; import { usersReducer } from '../reducers/users/usersSlice'; +import { authReducer } from '../reducers/auth/authSlice'; export const rootReducer = combineReducers({ customers: customersReducer, users: usersReducer, + auth: authReducer, }); export type TRootState = ReturnType; diff --git a/src/redux/store/store.ts b/src/redux/store/store.ts index f4212e7..d857bee 100644 --- a/src/redux/store/store.ts +++ b/src/redux/store/store.ts @@ -9,4 +9,5 @@ export const store = configureStore({ export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; -export const useAppDispatch = () => useDispatch(); \ No newline at end of file +export const useAppDispatch = () => useDispatch(); +// export const useAppSelector = (selector: (state: RootState) => unknown) => useSelector(selector); \ No newline at end of file From fb3c06a0ec0ed4dd71b2c24fab87c3f348695ba0 Mon Sep 17 00:00:00 2001 From: Ihor S Date: Wed, 10 Sep 2025 20:29:22 +0200 Subject: [PATCH 4/8] remove redundant files --- src/hooks/use-authed-user.ts | 13 ------------- src/hooks/use-local-storage.ts | 17 ----------------- 2 files changed, 30 deletions(-) delete mode 100644 src/hooks/use-authed-user.ts delete mode 100644 src/hooks/use-local-storage.ts diff --git a/src/hooks/use-authed-user.ts b/src/hooks/use-authed-user.ts deleted file mode 100644 index ca570f7..0000000 --- a/src/hooks/use-authed-user.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { jwtDecode } from "jwt-decode"; -import { ResponseTokenDto } from '../api/dto/auth/response-token.dto'; -import { useLocalstorage } from "./use-local-storage"; -import { JwtPayloadDto } from '../api/dto/auth/jwt-payload.dto'; - -/** - * @deprecated Use useAuthedUserSelector instead - */ -export const useAuthedUser = ():JwtPayloadDto => { - const [{ accessToken }] = useLocalstorage('auth', { accessToken: '', refreshToken: '' }); - const userId = accessToken && jwtDecode(accessToken)?.sub; - return { sub: userId }; -}; diff --git a/src/hooks/use-local-storage.ts b/src/hooks/use-local-storage.ts deleted file mode 100644 index 5d37670..0000000 --- a/src/hooks/use-local-storage.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useState } from "react"; - -/** - * @deprecated - */ -export const useLocalstorage = (key: string, initialValue: T): [T, (newValue: T) => void] => { - const [_value, _setValue] = useState(() => localStorage.getItem(key) || JSON.stringify(initialValue)); - - const setValue = (newValue: T) => { - _setValue(JSON.stringify(newValue)); - localStorage.setItem(key, JSON.stringify(newValue)); - }; - - const value = JSON.parse(_value) as T; - - return [value, setValue]; -}; From 9352eb1a84486fc8c5cd82f1bbf28bfd4dbedd40 Mon Sep 17 00:00:00 2001 From: Ihor S Date: Thu, 11 Sep 2025 16:59:01 +0200 Subject: [PATCH 5/8] fix --- src/App.tsx | 46 ++++++++--------- src/components/Menu/Menu.tsx | 43 +++++++++++----- .../PrermissionsGuard/PermissionsGuard.tsx | 15 ++++++ src/pages/Admin/Admin.tsx | 17 ++----- src/routes/routes.ts | 51 +++++++++++++++++++ 5 files changed, 120 insertions(+), 52 deletions(-) create mode 100644 src/components/PrermissionsGuard/PermissionsGuard.tsx create mode 100644 src/routes/routes.ts diff --git a/src/App.tsx b/src/App.tsx index fd2c264..adc070a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ -import { IonApp, IonRouterOutlet, IonSplitPane, setupIonicReact } from '@ionic/react'; +import { IonApp, IonPage, IonRouterOutlet, IonSplitPane, setupIonicReact } from '@ionic/react'; import { IonReactRouter } from '@ionic/react-router'; -import { Redirect, Route } from 'react-router-dom'; +import { Route } from 'react-router-dom'; import Menu from './components/Menu/Menu'; /* Core CSS required for Ionic components to work properly */ @@ -33,13 +33,9 @@ import '@ionic/react/css/palettes/dark.system.css'; /* Theme variables */ import './theme/variables.css'; import './App.css'; -import Services from './pages/Services/Services'; -import Supplyers from './pages/Supplyers/Supplyers'; -import Customers from './pages/Customers/Customers'; -import Login from './pages/Login/Login'; -import AdminPage from './pages/Admin/Admin'; import { Provider } from 'react-redux'; import { store } from './redux/store/store'; +import { routes } from './routes/routes'; setupIonicReact(); @@ -51,24 +47,24 @@ const App: React.FC = () => { - - - - - - - - - - - - - - - - - - + {routes.map((route) => ( + ( + + {route.guard ? ( + + + + ) : ( + + )} + + )} + /> + ))} diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 98f3b2d..d76caae 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -13,49 +13,61 @@ import { import { useLocation } from 'react-router-dom'; import { archiveOutline, archiveSharp, logInOutline, mapOutline, paperPlaneOutline, paperPlaneSharp, warningOutline, warningSharp } from 'ionicons/icons'; import './Menu.css'; +import { TAuthedUser } from '../../redux/reducers/auth/types'; +import { useSelector } from 'react-redux'; +import { authedUserSelector } from '../../redux/selectors/authSelectors'; -interface AppPage { +interface MenuItem { url: string; iosIcon: string; mdIcon: string; title: string; + isShown: (authedUser: TAuthedUser | null) => boolean; } -const appPages: AppPage[] = [ +const isUserAuthed = (authedUser: TAuthedUser | null) => !!authedUser?.id; + +const menuItems: MenuItem[] = [ { title: 'Login', url: '/Login', iosIcon: logInOutline, - mdIcon: logInOutline + mdIcon: logInOutline, + isShown: () => true, }, { title: 'Services', url: '/Services', iosIcon: mapOutline, - mdIcon: mapOutline + mdIcon: mapOutline, + isShown: () => true, }, { title: 'Supplyers', url: '/Supplyers', iosIcon: paperPlaneOutline, - mdIcon: paperPlaneSharp + mdIcon: paperPlaneSharp, + isShown: () => true, }, { title: 'Customers', url: '/Customers', iosIcon: archiveOutline, - mdIcon: archiveSharp + mdIcon: archiveSharp, + isShown: () => true, }, { title: 'Admin', url: '/Admin', iosIcon: warningOutline, - mdIcon: warningSharp - } + mdIcon: warningSharp, + isShown: isUserAuthed, + }, ]; const Menu: React.FC = () => { const location = useLocation(); + const authedUser = useSelector(authedUserSelector); return ( @@ -63,18 +75,23 @@ const Menu: React.FC = () => { Hello Dear guest, login or register to get access to all features - {appPages.map((appPage, index) => { - return ( + {menuItems.map((appPage, index) => { + return appPage.isShown(authedUser) ? ( - + - ); + ) : null; })} - ); diff --git a/src/components/PrermissionsGuard/PermissionsGuard.tsx b/src/components/PrermissionsGuard/PermissionsGuard.tsx new file mode 100644 index 0000000..23828d3 --- /dev/null +++ b/src/components/PrermissionsGuard/PermissionsGuard.tsx @@ -0,0 +1,15 @@ +import { useSelector } from "react-redux"; +import { authedUserSelector } from "../../redux/selectors/authSelectors"; +import { Redirect } from "react-router-dom"; +import { TRouteGuardProps } from "../../routes/routes"; + + +export const PermissionsGuard: React.FC = ({ children, options }) => { + const authedUser = useSelector(authedUserSelector); + + if (options.onlyForAuthedUser && !authedUser?.id) { + return ; + } + + return children; +}; \ No newline at end of file diff --git a/src/pages/Admin/Admin.tsx b/src/pages/Admin/Admin.tsx index ef6a730..e27128e 100644 --- a/src/pages/Admin/Admin.tsx +++ b/src/pages/Admin/Admin.tsx @@ -1,4 +1,4 @@ -import { IonButtons, IonContent, IonHeader, IonItem, IonLabel, IonList, IonMenuButton, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import { IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar } from '@ionic/react'; import styled from '@emotion/styled'; import React, { useEffect } from 'react'; import { UsersList } from './UsersList/UsersList'; @@ -9,17 +9,6 @@ import { allUsersSelector, totalUsersSelector } from '../../redux/selectors/user import { Typography } from '@mui/material'; const AdminPage: React.FC = () => { - // const [users, setUsers] = React.useState([]); - // const usersService = useMemo(() => new UsersService(), []); // todo: move to context or redux - - // useEffect(() => { - // const fetchUsers = async () => { - // const { data } = await usersService.getAll(); - // setUsers(data); - // }; - // fetchUsers(); - // }, [usersService]); - const dispatch = useAppDispatch(); useEffect(() => { dispatch(getUsers()); @@ -29,7 +18,7 @@ const AdminPage: React.FC = () => { const total = useSelector(totalUsersSelector); return ( - + <> @@ -45,7 +34,7 @@ const AdminPage: React.FC = () => { Total users: {total} - + ); }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts new file mode 100644 index 0000000..4f72c5f --- /dev/null +++ b/src/routes/routes.ts @@ -0,0 +1,51 @@ +import { PermissionsGuard } from "../components/PrermissionsGuard/PermissionsGuard"; +import AdminPage from "../pages/Admin/Admin"; +import Customers from "../pages/Customers/Customers"; +import Login from "../pages/Login/Login"; +import Services from "../pages/Services/Services"; +import Supplyers from "../pages/Supplyers/Supplyers"; + + +export type TRoute = { + path: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: React.ComponentType; + guard?: React.FC; + guardOptions?: TRouteGuardOptions; +}; + +export type TRouteGuardOptions = { + onlyForAuthedUser?: boolean; +}; + +export type TRouteGuardProps = { + children: React.ReactNode; + options: TRouteGuardOptions; +} + +export const routes: TRoute[] = [ + { + path: '/Services', + component: Services, + }, + { + path: '/Supplyers', + component: Supplyers, + }, + { + path: '/Customers', + component: Customers, + }, + { + path: '/Login', + component: Login, + }, + { + path: '/Admin', + component: AdminPage, + guard: PermissionsGuard, + guardOptions: { + onlyForAuthedUser: true, + }, + }, +]; \ No newline at end of file From 860c97508f1c7054706a69f1d3519dad83353266 Mon Sep 17 00:00:00 2001 From: Ihor S Date: Thu, 11 Sep 2025 17:44:17 +0200 Subject: [PATCH 6/8] fix --- src/App.tsx | 8 ++- src/components/Menu/Menu.tsx | 8 ++- src/layouts/basicLayout.tsx | 19 +++++++ src/pages/Admin/Admin.tsx | 25 +++------ src/pages/Customers/Customers.tsx | 49 +++++++---------- src/pages/Login/Login.css | 9 ---- src/pages/Login/Login.tsx | 79 ++++++++++++---------------- src/pages/Services/Services.tsx | 59 +++++++++++---------- src/pages/Supplyers/Supplyers.tsx | 24 +++------ src/routes/{routes.ts => routes.tsx} | 23 ++++++++ 10 files changed, 149 insertions(+), 154 deletions(-) create mode 100644 src/layouts/basicLayout.tsx rename src/routes/{routes.ts => routes.tsx} (67%) diff --git a/src/App.tsx b/src/App.tsx index adc070a..8db4b85 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,10 +56,14 @@ const App: React.FC = () => { {route.guard ? ( - + + + ) : ( - + + + )} )} diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index d76caae..92bc8bb 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -25,8 +25,6 @@ interface MenuItem { isShown: (authedUser: TAuthedUser | null) => boolean; } -const isUserAuthed = (authedUser: TAuthedUser | null) => !!authedUser?.id; - const menuItems: MenuItem[] = [ { title: 'Login', @@ -61,7 +59,7 @@ const menuItems: MenuItem[] = [ url: '/Admin', iosIcon: warningOutline, mdIcon: warningSharp, - isShown: isUserAuthed, + isShown: (authedUser: TAuthedUser | null) => !!authedUser?.id, }, ]; @@ -73,8 +71,8 @@ const Menu: React.FC = () => { - Hello - Dear guest, login or register to get access to all features + Hello{authedUser?.name ? `, ${authedUser.name}` : ''} + {!authedUser?.name ? Dear guest, login or register to get access to all features : null} {menuItems.map((appPage, index) => { return appPage.isShown(authedUser) ? ( diff --git a/src/layouts/basicLayout.tsx b/src/layouts/basicLayout.tsx new file mode 100644 index 0000000..c4362c1 --- /dev/null +++ b/src/layouts/basicLayout.tsx @@ -0,0 +1,19 @@ +import { IonButtons, IonContent, IonHeader, IonMenuButton, IonTitle, IonToolbar } from "@ionic/react"; + +export const BasicLayout: React.FC<{ children: React.ReactNode; title: string }> = ({ children, title }) => { + return ( + <> + + + + + + {title} + + + + {children} + + + ); +}; \ No newline at end of file diff --git a/src/pages/Admin/Admin.tsx b/src/pages/Admin/Admin.tsx index e27128e..b97ef05 100644 --- a/src/pages/Admin/Admin.tsx +++ b/src/pages/Admin/Admin.tsx @@ -18,27 +18,16 @@ const AdminPage: React.FC = () => { const total = useSelector(totalUsersSelector); return ( - <> - - - - - - Admin panel - - - - - - - - Total users: {total} - - + + + + + Total users: {total} + ); }; -const AdminContentStyled = styled(IonContent)` +const AdminContentStyled = styled.div` --background: var(--ion-color-light); `; diff --git a/src/pages/Customers/Customers.tsx b/src/pages/Customers/Customers.tsx index ea8c3aa..865e28e 100644 --- a/src/pages/Customers/Customers.tsx +++ b/src/pages/Customers/Customers.tsx @@ -25,42 +25,31 @@ const Customers: React.FC = () => { }; return ( - - - - - - - {name} - - - - - {allCustomers.map((customer, index) => ( + + {allCustomers.map((customer, index) => ( + + {customer.name} + + ))} + + {customers.map((customer, index) => ( - {customer.name} + {customer} ))} - - {customers.map((customer, index) => ( - - {customer} - - ))} - - - - - + + + + ); }; -const CustomersContentStyled = styled(IonContent)` +const CustomersContentStyled = styled.div` --background: var(--ion-color-light); `; diff --git a/src/pages/Login/Login.css b/src/pages/Login/Login.css index 8f151aa..fac243b 100644 --- a/src/pages/Login/Login.css +++ b/src/pages/Login/Login.css @@ -1,12 +1,3 @@ -/* Center the login container vertically and horizontally */ -.login-content { - --background: var(--ion-color-light); - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} - .login-container { width: 100%; padding: 16px; diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 751288c..ab9b90d 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -39,51 +39,40 @@ const Login: React.FC = () => { const authedUser = useSelector(authedUserSelector); return ( - - - - - - - Login - - - - - - - - {authedUser ? ( - - ) : ( -
- - - - Enter your email - - - - Enter your password - - - - - Sign in - - -
- )} -
-
-
-
-
+
+ + + + {authedUser ? ( + + ) : ( +
+
+ + + Enter your email + + + + Enter your password + + + + + Sign in + +
+
+ )} +
+
+
+
); }; diff --git a/src/pages/Services/Services.tsx b/src/pages/Services/Services.tsx index a4b902a..1db3e5e 100644 --- a/src/pages/Services/Services.tsx +++ b/src/pages/Services/Services.tsx @@ -1,43 +1,48 @@ -import { IonButtons, IonContent, IonHeader, IonItem, IonLabel, IonList, IonMenuButton, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import { + IonButtons, + IonContent, + IonHeader, + IonItem, + IonLabel, + IonList, + IonMenuButton, + IonTitle, + IonToolbar, +} from '@ionic/react'; import { ReactElement } from 'react'; import styled from '@emotion/styled'; const Services: React.FC = () => { + const name = 'Services'; - const name = 'Services' - const services: Array = ['Service1', 'Service2', 'Service3', 'Service4', 'Service5']; - services.push(

Explore UI Components

) + services.push( +

+ Explore{' '} + + UI Components + +

, + ); return ( - - + + - - - - {name} + {name} - - - - - {name} - - - - {services.map((service, index) => ( - - {service} - - ))} - - - + + {services.map((service, index) => ( + + {service} + + ))} + + ); }; -const ServicesContent = styled(IonContent)` +const ServicesContent = styled.div` background-color: var(--ion-color-light); `; diff --git a/src/pages/Supplyers/Supplyers.tsx b/src/pages/Supplyers/Supplyers.tsx index 9f7640f..8c48aa9 100644 --- a/src/pages/Supplyers/Supplyers.tsx +++ b/src/pages/Supplyers/Supplyers.tsx @@ -1,30 +1,18 @@ -import { IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import { IonHeader, IonTitle, IonToolbar } from '@ionic/react'; import MyMap from '../../components/Map/Map'; const Supplyers: React.FC = () => { - const name = 'Supplyers'; return ( - - +
+ - - - - {name} + {name} - - - - - {name} - - - - - + +
); }; diff --git a/src/routes/routes.ts b/src/routes/routes.tsx similarity index 67% rename from src/routes/routes.ts rename to src/routes/routes.tsx index 4f72c5f..2aa0c1b 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.tsx @@ -1,4 +1,5 @@ import { PermissionsGuard } from "../components/PrermissionsGuard/PermissionsGuard"; +import { BasicLayout } from '../layouts/basicLayout'; import AdminPage from "../pages/Admin/Admin"; import Customers from "../pages/Customers/Customers"; import Login from "../pages/Login/Login"; @@ -8,6 +9,8 @@ import Supplyers from "../pages/Supplyers/Supplyers"; export type TRoute = { path: string; + layout: React.ComponentType<{ children: React.ReactNode, title: string }>; + layoutProps: { title: string }; // eslint-disable-next-line @typescript-eslint/no-explicit-any component: React.ComponentType; guard?: React.FC; @@ -27,22 +30,42 @@ export const routes: TRoute[] = [ { path: '/Services', component: Services, + layout: BasicLayout, + layoutProps: { + title: 'Services', + }, }, { path: '/Supplyers', component: Supplyers, + layout: BasicLayout, + layoutProps: { + title: 'Supplyers', + }, }, { path: '/Customers', component: Customers, + layout: BasicLayout, + layoutProps: { + title: 'Customers', + }, }, { path: '/Login', component: Login, + layout: BasicLayout, + layoutProps: { + title: 'Login', + }, }, { path: '/Admin', component: AdminPage, + layout: BasicLayout, + layoutProps: { + title: 'Admin', + }, guard: PermissionsGuard, guardOptions: { onlyForAuthedUser: true, From aa9b1e2ac0f37057beb7888ac06a9d2e6e227446 Mon Sep 17 00:00:00 2001 From: Ihor S Date: Thu, 11 Sep 2025 18:08:58 +0200 Subject: [PATCH 7/8] fix android --- src/components/Menu/Menu.tsx | 14 +++++++------- src/pages/Admin/Admin.tsx | 6 +++--- src/pages/Customers/Customers.tsx | 3 +-- src/pages/Login/Login.css | 9 +++++++++ src/pages/Login/Login.tsx | 22 +++------------------- src/pages/Services/Services.tsx | 2 +- src/pages/Supplyers/Supplyers.tsx | 6 +++--- src/routes/routes.tsx | 8 ++++++++ 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 92bc8bb..f55096e 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -26,13 +26,6 @@ interface MenuItem { } const menuItems: MenuItem[] = [ - { - title: 'Login', - url: '/Login', - iosIcon: logInOutline, - mdIcon: logInOutline, - isShown: () => true, - }, { title: 'Services', url: '/Services', @@ -54,6 +47,13 @@ const menuItems: MenuItem[] = [ mdIcon: archiveSharp, isShown: () => true, }, + { + title: 'Login', + url: '/Login', + iosIcon: logInOutline, + mdIcon: logInOutline, + isShown: () => true, + }, { title: 'Admin', url: '/Admin', diff --git a/src/pages/Admin/Admin.tsx b/src/pages/Admin/Admin.tsx index b97ef05..56f3926 100644 --- a/src/pages/Admin/Admin.tsx +++ b/src/pages/Admin/Admin.tsx @@ -1,4 +1,4 @@ -import { IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import { IonContent } from '@ionic/react'; import styled from '@emotion/styled'; import React, { useEffect } from 'react'; import { UsersList } from './UsersList/UsersList'; @@ -18,7 +18,7 @@ const AdminPage: React.FC = () => { const total = useSelector(totalUsersSelector); return ( - + @@ -27,7 +27,7 @@ const AdminPage: React.FC = () => { ); }; -const AdminContentStyled = styled.div` +const AdminContentStyled = styled(IonContent)` --background: var(--ion-color-light); `; diff --git a/src/pages/Customers/Customers.tsx b/src/pages/Customers/Customers.tsx index 865e28e..a46409f 100644 --- a/src/pages/Customers/Customers.tsx +++ b/src/pages/Customers/Customers.tsx @@ -10,7 +10,6 @@ import { useAppDispatch } from '../../redux/store/store'; const Customers: React.FC = () => { const customers = ['Customer1', 'Customer2', 'Customer3', 'Customer4', 'Customer5']; - const name = 'Customers'; const dispatch = useAppDispatch(); const [selectedCustomers, setSelectedCustomers] = React.useState([]); @@ -49,7 +48,7 @@ const Customers: React.FC = () => { ); }; -const CustomersContentStyled = styled.div` +const CustomersContentStyled = styled(IonContent)` --background: var(--ion-color-light); `; diff --git a/src/pages/Login/Login.css b/src/pages/Login/Login.css index fac243b..8f151aa 100644 --- a/src/pages/Login/Login.css +++ b/src/pages/Login/Login.css @@ -1,3 +1,12 @@ +/* Center the login container vertically and horizontally */ +.login-content { + --background: var(--ion-color-light); + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + .login-container { width: 100%; padding: 16px; diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index ab9b90d..6270eba 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -1,20 +1,4 @@ -import { - IonButtons, - IonContent, - IonGrid, - IonHeader, - IonInput, - IonItem, - IonLabel, - IonList, - IonMenuButton, - IonPage, - IonRow, - IonCol, - IonTitle, - IonToolbar, - IonButton, -} from '@ionic/react'; +import { IonContent, IonGrid, IonInput, IonItem, IonLabel, IonList, IonCol, IonRow, IonButton } from '@ionic/react'; import './Login.css'; import { useCallback } from 'react'; import { CurrentUser } from './CurrentUser'; @@ -39,7 +23,7 @@ const Login: React.FC = () => { const authedUser = useSelector(authedUserSelector); return ( -
+ @@ -72,7 +56,7 @@ const Login: React.FC = () => { -
+
); }; diff --git a/src/pages/Services/Services.tsx b/src/pages/Services/Services.tsx index 1db3e5e..e564a51 100644 --- a/src/pages/Services/Services.tsx +++ b/src/pages/Services/Services.tsx @@ -42,7 +42,7 @@ const Services: React.FC = () => { ); }; -const ServicesContent = styled.div` +const ServicesContent = styled(IonContent)` background-color: var(--ion-color-light); `; diff --git a/src/pages/Supplyers/Supplyers.tsx b/src/pages/Supplyers/Supplyers.tsx index 8c48aa9..3c86d63 100644 --- a/src/pages/Supplyers/Supplyers.tsx +++ b/src/pages/Supplyers/Supplyers.tsx @@ -1,18 +1,18 @@ -import { IonHeader, IonTitle, IonToolbar } from '@ionic/react'; +import { IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/react'; import MyMap from '../../components/Map/Map'; const Supplyers: React.FC = () => { const name = 'Supplyers'; return ( -
+ {name} -
+ ); }; diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index 2aa0c1b..26f6889 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -27,6 +27,14 @@ export type TRouteGuardProps = { } export const routes: TRoute[] = [ + { + path: '/', + component: Services, + layout: BasicLayout, + layoutProps: { + title: 'Services', + }, + }, { path: '/Services', component: Services, From 0f992e3d294b5f652017c5604350dc2f1f2e1ea3 Mon Sep 17 00:00:00 2001 From: Ihor S Date: Thu, 11 Sep 2025 18:35:46 +0200 Subject: [PATCH 8/8] fix --- src/layouts/basicLayout.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/layouts/basicLayout.tsx b/src/layouts/basicLayout.tsx index c4362c1..b0790b0 100644 --- a/src/layouts/basicLayout.tsx +++ b/src/layouts/basicLayout.tsx @@ -1,4 +1,4 @@ -import { IonButtons, IonContent, IonHeader, IonMenuButton, IonTitle, IonToolbar } from "@ionic/react"; +import { IonButtons, IonHeader, IonMenuButton, IonTitle, IonToolbar } from '@ionic/react'; export const BasicLayout: React.FC<{ children: React.ReactNode; title: string }> = ({ children, title }) => { return ( @@ -10,10 +10,8 @@ export const BasicLayout: React.FC<{ children: React.ReactNode; title: string }> {title} - - - {children} - + + {children} ); -}; \ No newline at end of file +};