From e1f51f023400b9ed43c0670e714ad2b4a8f8e8c7 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Mon, 23 Dec 2024 23:30:09 +0500 Subject: [PATCH 01/19] fixed exmpted urls from refresh token retry --- src/lib/axios.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/axios.config.ts b/src/lib/axios.config.ts index 3b56446..68e1614 100644 --- a/src/lib/axios.config.ts +++ b/src/lib/axios.config.ts @@ -46,7 +46,7 @@ axios.interceptors.response.use(undefined, async (error) => { error.message = msg; if (error.response?.status === 401) { - if (!["/auth/login", "/auth/refresh-token"].includes(error.config.url!)) { + if (!["auth/login", "auth/refresh"].includes(error.config.url!)) { // retry request after refreshing token const { accessToken, refreshToken } = await authService.refreshToken(); localStorage.setItem("accessToken", accessToken); From a9c2835c31e282752944c2c1778401f2a50f36ce Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Fri, 4 Apr 2025 18:38:15 +0500 Subject: [PATCH 02/19] updated gcp project id and fierbase hosting --- .firebaserc | 8 ++++---- .../workflows/firebase-hosting-merge-dev.yml | 4 +++- .../workflows/firebase-hosting-merge-main.yml | 4 +++- .../firebase-hosting-pull-request-dev.yml | 19 +++++++++++++++++++ .../firebase-hosting-pull-request-main.yml | 19 +++++++++++++++++++ README.md | 4 ++-- package.json | 3 ++- 7 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/firebase-hosting-pull-request-dev.yml create mode 100644 .github/workflows/firebase-hosting-pull-request-main.yml diff --git a/.firebaserc b/.firebaserc index c67b671..d01250d 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,16 +1,16 @@ { "targets": { - "applied-abbey-378009": { + "rayon-gcp-starter": { "hosting": { "prod": [ - "rayon-react-starter" + "rayon-gcp-starter" ] } }, - "physikomatics-be": { + "rayon-gcp-starter": { "hosting": { "dev": [ - "rayon-react-starter-dev" + "rayon-gcp-starter" ] } } diff --git a/.github/workflows/firebase-hosting-merge-dev.yml b/.github/workflows/firebase-hosting-merge-dev.yml index f06361e..13b22d1 100644 --- a/.github/workflows/firebase-hosting-merge-dev.yml +++ b/.github/workflows/firebase-hosting-merge-dev.yml @@ -3,6 +3,8 @@ name: Deploy to Firebase Hosting on merge (dev) push: branches: - dev + workflow_dispatch: + jobs: build_and_deploy: permissions: write-all @@ -30,5 +32,5 @@ jobs: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_DEV }}" channelId: live - projectId: physikomatics-be + projectId: rayon-gcp-starter target: dev diff --git a/.github/workflows/firebase-hosting-merge-main.yml b/.github/workflows/firebase-hosting-merge-main.yml index 25c1934..cbb1716 100755 --- a/.github/workflows/firebase-hosting-merge-main.yml +++ b/.github/workflows/firebase-hosting-merge-main.yml @@ -3,6 +3,8 @@ name: Deploy to Firebase Hosting on merge (main) push: branches: - main + workflow_dispatch: + jobs: build_and_deploy: permissions: write-all @@ -30,5 +32,5 @@ jobs: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }}" channelId: live - projectId: applied-abbey-378009 + projectId: rayon-gcp-starter target: prod diff --git a/.github/workflows/firebase-hosting-pull-request-dev.yml b/.github/workflows/firebase-hosting-pull-request-dev.yml new file mode 100644 index 0000000..45892eb --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request-dev.yml @@ -0,0 +1,19 @@ +name: Deploy to Firebase Hosting on PR (dev) +on: + pull_request: + branches: + - dev + +jobs: + build_and_preview: + permissions: write-all + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: yarn install --frozen-lockfile && npm run build:dev + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_DEV }}" + projectId: rayon-gcp-starter + target: dev diff --git a/.github/workflows/firebase-hosting-pull-request-main.yml b/.github/workflows/firebase-hosting-pull-request-main.yml new file mode 100644 index 0000000..442d8e2 --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request-main.yml @@ -0,0 +1,19 @@ +name: Deploy to Firebase Hosting on PR (main) +on: + pull_request: + branches: + - main + +jobs: + build_and_preview: + permissions: write-all + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: yarn install --frozen-lockfile && npm run build:prod + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }}" + projectId: rayon-gcp-starter + target: prod diff --git a/README.md b/README.md index 53b753d..4d91638 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Rayon React Starter is an opinionated starter kit designed to scaffold React projects quickly with a comprehensive and well-structured environment. Built with a modern tech stack and batteries included, it helps you start building your project in no time. -[Live Demo](https://rayon-react-starter.web.app/) +[Live Demo](https://rayon-gcp-starter.web.app/) ## Tech Stack @@ -44,7 +44,7 @@ Ensure you have the following installed: 1. Clone the repository: ```bash git clone https://github.com/rayonstudios/rayon_react_starter - cd rayon-react-starter + cd rayon-gcp-starter ``` 2. Install dependencies: ```bash diff --git a/package.json b/package.json index 282b095..42b525c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "rayon_react_starter", "private": true, - "version": "0.0.3", + "version": "1.4", + "description": "Rayon React Starter is an opinionated starter kit designed to scaffold React projects quickly with a comprehensive and well-structured environment. Built with a modern tech stack and batteries included, it helps you start building your project in no time.", "type": "module", "scripts": { "dev": "vite", From 392bbc5ab4921188fd1de58df5978ae8b16c3d44 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Fri, 4 Apr 2025 18:41:05 +0500 Subject: [PATCH 03/19] update firebase service account secrets in workflow files --- .github/workflows/firebase-hosting-merge-dev.yml | 2 +- .github/workflows/firebase-hosting-merge-main.yml | 2 +- .github/workflows/firebase-hosting-pull-request-dev.yml | 2 +- .github/workflows/firebase-hosting-pull-request-main.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/firebase-hosting-merge-dev.yml b/.github/workflows/firebase-hosting-merge-dev.yml index 13b22d1..fee4a19 100644 --- a/.github/workflows/firebase-hosting-merge-dev.yml +++ b/.github/workflows/firebase-hosting-merge-dev.yml @@ -30,7 +30,7 @@ jobs: - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" - firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_DEV }}" + firebaseServiceAccount: "${{ secrets.SECRETJSON_DEV }}" channelId: live projectId: rayon-gcp-starter target: dev diff --git a/.github/workflows/firebase-hosting-merge-main.yml b/.github/workflows/firebase-hosting-merge-main.yml index cbb1716..e5769fb 100755 --- a/.github/workflows/firebase-hosting-merge-main.yml +++ b/.github/workflows/firebase-hosting-merge-main.yml @@ -30,7 +30,7 @@ jobs: - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" - firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }}" + firebaseServiceAccount: "${{ secrets.SECRETJSON_PROD }}" channelId: live projectId: rayon-gcp-starter target: prod diff --git a/.github/workflows/firebase-hosting-pull-request-dev.yml b/.github/workflows/firebase-hosting-pull-request-dev.yml index 45892eb..612f011 100644 --- a/.github/workflows/firebase-hosting-pull-request-dev.yml +++ b/.github/workflows/firebase-hosting-pull-request-dev.yml @@ -14,6 +14,6 @@ jobs: - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" - firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_DEV }}" + firebaseServiceAccount: "${{ secrets.SECRETJSON_DEV }}" projectId: rayon-gcp-starter target: dev diff --git a/.github/workflows/firebase-hosting-pull-request-main.yml b/.github/workflows/firebase-hosting-pull-request-main.yml index 442d8e2..6b505bc 100644 --- a/.github/workflows/firebase-hosting-pull-request-main.yml +++ b/.github/workflows/firebase-hosting-pull-request-main.yml @@ -14,6 +14,6 @@ jobs: - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" - firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }}" + firebaseServiceAccount: "${{ secrets.SECRETJSON_PROD }}" projectId: rayon-gcp-starter target: prod From 51cc7cd88302b8f9bf515c35b9a4100d8a5a8383 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Fri, 4 Apr 2025 20:05:33 +0500 Subject: [PATCH 04/19] update live demo link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d91638..84da5df 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Rayon React Starter is an opinionated starter kit designed to scaffold React projects quickly with a comprehensive and well-structured environment. Built with a modern tech stack and batteries included, it helps you start building your project in no time. -[Live Demo](https://rayon-gcp-starter.web.app/) +[Live Demo](https://fe.starters.rayonstudios.com/) ## Tech Stack From 08a1800fc974821e7ff32e03b6021ebb9f52b3a5 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Fri, 4 Apr 2025 20:07:10 +0500 Subject: [PATCH 05/19] refactor workflow names for clarity in Firebase Hosting deployments --- .github/workflows/firebase-hosting-merge-dev.yml | 2 +- .github/workflows/firebase-hosting-merge-main.yml | 2 +- .github/workflows/firebase-hosting-pull-request-dev.yml | 2 +- .github/workflows/firebase-hosting-pull-request-main.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/firebase-hosting-merge-dev.yml b/.github/workflows/firebase-hosting-merge-dev.yml index fee4a19..29bd95e 100644 --- a/.github/workflows/firebase-hosting-merge-dev.yml +++ b/.github/workflows/firebase-hosting-merge-dev.yml @@ -1,4 +1,4 @@ -name: Deploy to Firebase Hosting on merge (dev) +name: Deploy to Firebase Hosting - Dev "on": push: branches: diff --git a/.github/workflows/firebase-hosting-merge-main.yml b/.github/workflows/firebase-hosting-merge-main.yml index e5769fb..62fa129 100755 --- a/.github/workflows/firebase-hosting-merge-main.yml +++ b/.github/workflows/firebase-hosting-merge-main.yml @@ -1,4 +1,4 @@ -name: Deploy to Firebase Hosting on merge (main) +name: Deploy to Firebase Hosting - Prod "on": push: branches: diff --git a/.github/workflows/firebase-hosting-pull-request-dev.yml b/.github/workflows/firebase-hosting-pull-request-dev.yml index 612f011..2ac6539 100644 --- a/.github/workflows/firebase-hosting-pull-request-dev.yml +++ b/.github/workflows/firebase-hosting-pull-request-dev.yml @@ -1,4 +1,4 @@ -name: Deploy to Firebase Hosting on PR (dev) +name: Preview deploy to Firebase Hosting on PR (dev) on: pull_request: branches: diff --git a/.github/workflows/firebase-hosting-pull-request-main.yml b/.github/workflows/firebase-hosting-pull-request-main.yml index 6b505bc..4c17ba7 100644 --- a/.github/workflows/firebase-hosting-pull-request-main.yml +++ b/.github/workflows/firebase-hosting-pull-request-main.yml @@ -1,4 +1,4 @@ -name: Deploy to Firebase Hosting on PR (main) +name: Preview deploy to Firebase Hosting on PR (main) on: pull_request: branches: From 1a98e7abc1ddc54e0f4d7be68c9aaf843010a46b Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Fri, 4 Apr 2025 20:50:09 +0500 Subject: [PATCH 06/19] fix: optimized gh workflows for firebase deployments --- ...irebase-hosting-merge-dev.yml => dev-deploy.yml} | 11 ----------- ...ting-pull-request-dev.yml => pr-preview-dev.yml} | 6 +++++- ...ng-pull-request-main.yml => pr-preview-prod.yml} | 6 +++++- ...ebase-hosting-merge-main.yml => prod-deploy.yml} | 13 +------------ 4 files changed, 11 insertions(+), 25 deletions(-) rename .github/workflows/{firebase-hosting-merge-dev.yml => dev-deploy.yml} (66%) rename .github/workflows/{firebase-hosting-pull-request-dev.yml => pr-preview-dev.yml} (87%) rename .github/workflows/{firebase-hosting-pull-request-main.yml => pr-preview-prod.yml} (88%) rename .github/workflows/{firebase-hosting-merge-main.yml => prod-deploy.yml} (59%) diff --git a/.github/workflows/firebase-hosting-merge-dev.yml b/.github/workflows/dev-deploy.yml similarity index 66% rename from .github/workflows/firebase-hosting-merge-dev.yml rename to .github/workflows/dev-deploy.yml index 29bd95e..6250fc9 100644 --- a/.github/workflows/firebase-hosting-merge-dev.yml +++ b/.github/workflows/dev-deploy.yml @@ -11,17 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - with: - persist-credentials: false - - - name: Reconfigure git to use HTTP authentication - run: > - git config --global url."https://github.com/".insteadOf - ssh://git@github.com/ - - - uses: actions/setup-node@v4 - with: - node-version: 18 - run: yarn install --frozen-lockfile && npm run build:dev env: diff --git a/.github/workflows/firebase-hosting-pull-request-dev.yml b/.github/workflows/pr-preview-dev.yml similarity index 87% rename from .github/workflows/firebase-hosting-pull-request-dev.yml rename to .github/workflows/pr-preview-dev.yml index 2ac6539..548c71d 100644 --- a/.github/workflows/firebase-hosting-pull-request-dev.yml +++ b/.github/workflows/pr-preview-dev.yml @@ -9,8 +9,12 @@ jobs: permissions: write-all runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + - run: yarn install --frozen-lockfile && npm run build:dev + env: + CI: false + - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/firebase-hosting-pull-request-main.yml b/.github/workflows/pr-preview-prod.yml similarity index 88% rename from .github/workflows/firebase-hosting-pull-request-main.yml rename to .github/workflows/pr-preview-prod.yml index 4c17ba7..2c6c521 100644 --- a/.github/workflows/firebase-hosting-pull-request-main.yml +++ b/.github/workflows/pr-preview-prod.yml @@ -9,8 +9,12 @@ jobs: permissions: write-all runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + - run: yarn install --frozen-lockfile && npm run build:prod + env: + CI: false + - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/firebase-hosting-merge-main.yml b/.github/workflows/prod-deploy.yml similarity index 59% rename from .github/workflows/firebase-hosting-merge-main.yml rename to .github/workflows/prod-deploy.yml index 62fa129..5693f22 100755 --- a/.github/workflows/firebase-hosting-merge-main.yml +++ b/.github/workflows/prod-deploy.yml @@ -11,19 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - with: - persist-credentials: false - - - name: Reconfigure git to use HTTP authentication - run: > - git config --global url."https://github.com/".insteadOf - ssh://git@github.com/ - - - uses: actions/setup-node@v4 - with: - node-version: 18 - - run: yarn install --frozen-lockfile && npm run build:prod + - run: yarn install --frozen-lockfile && npm run build:dev env: CI: false From 76396f3f10a9b28c26a5f3fc4a9fa2da35bfe4f1 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Tue, 8 Apr 2025 16:37:30 +0500 Subject: [PATCH 07/19] openapi fetch integration with auth and profile services --- .env.development | 2 +- .env.production | 2 +- .eslintrc.cjs | 1 + package.json | 12 +- scripts/gen-openapi-types.ts | 112 ++ src/lib/axios.config.ts | 69 - .../server-paginated-table.tsx | 77 +- src/lib/layouts/dashboard-layout/header.tsx | 2 +- src/lib/openapi-fetch.config.ts | 120 ++ src/lib/router/router-config.tsx | 6 +- src/lib/router/router.tsx | 11 +- src/lib/types/api.ts | 26 + src/lib/types/misc.ts | 3 + src/lib/types/openapi-fetch.d.ts | 1435 +++++++++++++++++ src/modules/auth/hooks/role.hooks.ts | 6 + src/modules/auth/services/auth.service.ts | 32 +- src/modules/auth/services/profile.service.ts | 16 +- src/modules/auth/slices/auth.slice.ts | 1 - src/modules/auth/slices/profile.slice.ts | 2 +- src/modules/auth/types/profile.type.ts | 12 - tsconfig.json | 1 + yarn.lock | 369 ++++- 22 files changed, 2167 insertions(+), 150 deletions(-) create mode 100644 scripts/gen-openapi-types.ts delete mode 100644 src/lib/axios.config.ts create mode 100644 src/lib/openapi-fetch.config.ts create mode 100644 src/lib/types/api.ts create mode 100644 src/lib/types/openapi-fetch.d.ts delete mode 100644 src/modules/auth/types/profile.type.ts diff --git a/.env.development b/.env.development index 3b9e7b2..e145da1 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,2 @@ -VITE_API_BASE_URL=http://localhost:8000/dev/api +VITE_API_BASE_URL=http://localhost:3000/api/v1 VITE_ENV=dev \ No newline at end of file diff --git a/.env.production b/.env.production index 57b59aa..0204a81 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,2 @@ -VITE_API_BASE_URL=http://localhost:8000/api +VITE_API_BASE_URL=https://be.starters.rayonstudios.com/api VITE_ENV=production \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 227d154..f1a7f53 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -15,5 +15,6 @@ module.exports = { "warn", { allowConstantExport: true }, ], + "@typescript-eslint/no-explicit-any": "off", }, }; diff --git a/package.json b/package.json index 42b525c..0315ef5 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,12 @@ "description": "Rayon React Starter is an opinionated starter kit designed to scaffold React projects quickly with a comprehensive and well-structured environment. Built with a modern tech stack and batteries included, it helps you start building your project in no time.", "type": "module", "scripts": { - "dev": "vite", - "prod": "vite --mode production", + "dev": "npm run gen-types:dev && vite", + "prod": "npm run gen-types:prod && vite --mode production", "build:prod": "tsc && vite build", "build:dev": "tsc && vite build --mode development", + "gen-types:dev": "tsx scripts/gen-openapi-types.ts development", + "gen-types:prod": "tsx scripts/gen-openapi-types.ts production", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "watch:types": "tsc -w", @@ -29,6 +31,7 @@ "dayjs": "^1.11.10", "i18next": "^23.10.1", "lucide": "^0.447.0", + "openapi-fetch": "^0.13.5", "qs": "^6.13.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -41,6 +44,7 @@ }, "devDependencies": { "@types/color": "^3.0.6", + "@types/node": "^22.14.0", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.14.0", @@ -53,10 +57,12 @@ "eslint-plugin-react-refresh": "^0.4.5", "husky": ">=6", "lint-staged": ">=10", + "openapi-typescript": "^7.6.1", "postcss": "^8.4.33", "prettier": "^3.3.3", "tailwindcss": "^3.4.1", - "typescript": "^5.2.2", + "tsx": "^4.19.3", + "typescript": "^5.8.3", "vite": "^5.0.8", "vite-tsconfig-paths": "^4.3.2" }, diff --git a/scripts/gen-openapi-types.ts b/scripts/gen-openapi-types.ts new file mode 100644 index 0000000..620936f --- /dev/null +++ b/scripts/gen-openapi-types.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env tsx + +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as http from "http"; +import * as https from "https"; +import * as path from "path"; +import { URL } from "url"; + +// Get environment type from command line arguments +const envType = process.argv[2]; + +if (!envType) { + console.error( + "Usage: tsx gen-openapi-types.ts (development|production)" + ); + process.exit(1); +} + +const envFile = path.join(process.cwd(), `.env.${envType}`); + +// Check if the environment file exists +if (!fs.existsSync(envFile)) { + console.error(`Error: Environment file .env.${envType} does not exist`); + process.exit(1); +} + +// Read the environment file +const envContent = fs.readFileSync(envFile, "utf8"); + +// Extract API base URL from environment file +const apiBaseUrlMatch = envContent.match(/VITE_API_BASE_URL=(.+)/); +const apiBaseUrl = apiBaseUrlMatch ? apiBaseUrlMatch[1].trim() : null; + +if (!apiBaseUrl) { + console.error(`Error: VITE_API_BASE_URL not found in .env.${envType}`); + process.exit(1); +} + +console.log(`Using API base URL: ${apiBaseUrl}`); + +// Fetch OpenAPI schema +const openApiUrl = `${apiBaseUrl}/openapi.json`; +console.log(`Fetching OpenAPI schema from: ${openApiUrl}`); + +const tempFilePath = path.join(process.cwd(), "openapi.json"); +const outputDir = path.join(process.cwd(), "src", "lib", "types"); +const outputPath = path.join(outputDir, "openapi-fetch.d.ts"); + +// Create output directory if it doesn't exist +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +// Function to download the OpenAPI schema +function downloadSchema(): Promise { + return new Promise((resolve, reject) => { + const urlObj = new URL(openApiUrl); + const client = urlObj.protocol === "https:" ? https : http; + + const req = client.get(openApiUrl, (res) => { + if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) { + reject(new Error(`Failed to fetch OpenAPI schema: ${res.statusCode}`)); + return; + } + + const fileStream = fs.createWriteStream(tempFilePath); + res.pipe(fileStream); + + fileStream.on("finish", () => { + fileStream.close(); + resolve(); + }); + }); + + req.on("error", (error) => { + reject(error); + }); + }); +} + +// Main execution +async function main(): Promise { + try { + // Download schema + await downloadSchema(); + + // Generate TypeScript types + console.log("Generating TypeScript types"); + execSync(`npx openapi-typescript ${tempFilePath} -o ${outputPath}`, { + stdio: "inherit", + }); + + // Clean up + fs.unlinkSync(tempFilePath); + + console.log( + `TypeScript types successfully generated at src/lib/types/openapi-fetch.d.ts` + ); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + + // Clean up temp file if it exists + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } + + process.exit(1); + } +} + +main(); diff --git a/src/lib/axios.config.ts b/src/lib/axios.config.ts deleted file mode 100644 index 68e1614..0000000 --- a/src/lib/axios.config.ts +++ /dev/null @@ -1,69 +0,0 @@ -import AlertPopup from "@/lib/components/alert-popup/alert-popup"; -import { getErrorMessage, globalErrorHandler } from "@/lib/utils/error.utils"; -import authService from "@/modules/auth/services/auth.service"; -import { authActions } from "@/modules/auth/slices/auth.slice"; -import axiosApi from "axios"; -import { store } from "./redux/store"; - -export const PERMISSION_ERR_MSG = - "You don't have permission to perform this action. Please contact your organization admin."; - -const axios = axiosApi.create({ - baseURL: import.meta.env.VITE_API_BASE_URL, - headers: { - "Content-Type": "application/json", - }, -}); - -axios.interceptors.request.use((reqConfig) => { - const token = localStorage.getItem("accessToken"); - if (token && !reqConfig.headers.Authorization) { - reqConfig.headers.Authorization = `Bearer ${token}`; - } - return reqConfig; -}); - -axios.interceptors.response.use(undefined, async (error) => { - if (error.response?.status === 403 && error.config.method !== "get") { - AlertPopup({ - title: "Permission Denied", - message: PERMISSION_ERR_MSG, - cancelText: null, - okText: "Ok", - }); - error.message = PERMISSION_ERR_MSG; - throw error; - } - - let msg = getErrorMessage( - error.response?.data?.error || error.response?.data, - "" - ); - if (!msg) - msg = `${ - error.response?.statusText ? error.response.statusText + "! " : "" - }${error.message}`; - error.message = msg; - - if (error.response?.status === 401) { - if (!["auth/login", "auth/refresh"].includes(error.config.url!)) { - // retry request after refreshing token - const { accessToken, refreshToken } = await authService.refreshToken(); - localStorage.setItem("accessToken", accessToken); - localStorage.setItem("refreshToken", refreshToken); - error.config.headers.Authorization = `Bearer ${accessToken}`; - const data = await axios(error.config); - return data; - } else { - // logout - localStorage.removeItem("accessToken"); - localStorage.removeItem("refreshToken"); - store.dispatch(authActions.setStatus("unauthenticated")); - } - } - - globalErrorHandler(error); - throw error; -}); - -export default axios; diff --git a/src/lib/components/server-paginated-table/server-paginated-table.tsx b/src/lib/components/server-paginated-table/server-paginated-table.tsx index 3107a48..8ab15bc 100644 --- a/src/lib/components/server-paginated-table/server-paginated-table.tsx +++ b/src/lib/components/server-paginated-table/server-paginated-table.tsx @@ -5,9 +5,10 @@ import useUrlState from "@ahooksjs/use-url-state"; import { useDeepCompareEffect, useUpdateEffect } from "ahooks"; import { Table, TableProps } from "antd"; import { AnyObject } from "antd/es/_util/type"; +import { ColumnsType } from "antd/es/table"; import { capitalize, pickBy } from "lodash"; import qs from "qs"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import FiltersButton from "./filters-button"; function generateColsFromData(data: T[] = []) { @@ -26,16 +27,16 @@ function generateColsFromData(data: T[] = []) { typeof item === "string" ? item : JSON.stringify(item), }); } - return cols; + return cols as ColumnsType; } export type GetHelpers = { data: T[]; - fetchData: Function; - setData: Function; - setLoading: Function; - setTableParams: Function; - setTotal: Function; + fetchData: () => void; + setData: React.Dispatch>; + setLoading: React.Dispatch>; + setTableParams: (params: GenericObject) => void; + setTotal: React.Dispatch>; }; export type ServerPaginatedTableProps = TableProps & { @@ -67,6 +68,10 @@ export default function ServerPaginatedTable({ showTotal = (total, range) => `${range[0]} - ${range[1]} of ${total} items`, ...props }: ServerPaginatedTableProps) { + const defaultSortCol = useMemo( + () => columns?.find((col) => col.defaultSortOrder), + [columns] + ); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [tableParams, setTableParams] = useUrlState( @@ -77,18 +82,30 @@ export default function ServerPaginatedTable({ (acc, filter) => ({ ...acc, [`filter.${filter.key}`]: "" }), {} ), + ["sort.field"]: + (defaultSortCol as any)?.dataIndex || defaultSortCol?.key || "", + ["sort.order"]: defaultSortCol?.defaultSortOrder || "", }, - { parseOptions: { parseNumbers: true } } + { + parseOptions: { parseNumbers: true }, + } ); const [total, setTotal] = useState(0); - const handleTableChange: ServerPaginatedTableProps["onChange"] = (val) => { + const handleTableChange: ServerPaginatedTableProps["onChange"] = ( + pagination, + _, + sorters + ) => { + const sortObj = (Array.isArray(sorters) ? sorters[0] : sorters) || {}; setTableParams({ ...tableParams, - current: val.current, - pageSize: val.pageSize, + ["sort.field"]: sortObj?.field || "", + ["sort.order"]: sortObj?.order || "", + current: pagination.current, + pageSize: pagination.pageSize, }); - if (tableParams.pageSize !== val?.pageSize) { + if (tableParams.pageSize !== pagination?.pageSize) { setData([]); } }; @@ -111,7 +128,9 @@ export default function ServerPaginatedTable({ pickBy( tableParams, (_, key) => - ["current", "pageSize"].includes(key) || key.startsWith("filter.") + ["current", "pageSize"].includes(key) || + key.startsWith("filter.") || + key.startsWith("sort.") ), ]); @@ -126,14 +145,21 @@ export default function ServerPaginatedTable({ const [_url, _query = ""] = url.split("?"); const query: GenericObject = qs.parse(_query); + // pagination if (pageSize) { - query.per_page = pageSize; - query.page = current - 1; + query.limit = pageSize; + query.page = current; } + // filters Object.entries(restParams).forEach(([key, value]) => { if (!key.startsWith("filter.")) return; if (!isNullish(value)) query[key.split("filter.")[1]] = value; }); + // sorting + if (restParams["sort.field"] && !isNullish(restParams["sort.order"])) { + query.sortField = restParams["sort.field"]; + query.sortOrder = restParams["sort.order"] === "ascend" ? "asc" : "desc"; + } axios .get(`${_url}?${qs.stringify(query)}`) @@ -146,9 +172,7 @@ export default function ServerPaginatedTable({ setData( list.map((item, ix: number) => { - // @ts-ignore - item.key = item.id ?? ix.toString(); - return item; + return { ...item, key: item.id ?? ix.toString() }; }) ); @@ -170,6 +194,20 @@ export default function ServerPaginatedTable({ }); }, [data, fetchData, setData, setLoading, setTableParams, setTotal]); + const cols = useMemo(() => { + const colsList = Array.isArray(columns) + ? columns + : generateColsFromData(data); + return colsList.map((col) => ({ + ...col, + sortOrder: + col.sorter && + tableParams["sort.field"] === ((col as any).dataIndex || col.key) + ? tableParams["sort.order"] || undefined + : col.defaultSortOrder, + })); + }, [columns]); + return (
@@ -192,8 +230,7 @@ export default function ServerPaginatedTable({ onChange={handleTableChange} dataSource={typeof dataSource === "function" ? dataSource(data) : data} loading={loading} - //@ts-ignore - columns={Array.isArray(columns) ? columns : generateColsFromData(data)} + columns={cols} style={style} pagination={{ total, diff --git a/src/lib/layouts/dashboard-layout/header.tsx b/src/lib/layouts/dashboard-layout/header.tsx index a03e62c..d509d2a 100644 --- a/src/lib/layouts/dashboard-layout/header.tsx +++ b/src/lib/layouts/dashboard-layout/header.tsx @@ -127,7 +127,7 @@ function Header() { trigger={["click"]} > - + {profile.name} diff --git a/src/lib/openapi-fetch.config.ts b/src/lib/openapi-fetch.config.ts new file mode 100644 index 0000000..3fd5cd5 --- /dev/null +++ b/src/lib/openapi-fetch.config.ts @@ -0,0 +1,120 @@ +import AlertPopup from "@/lib/components/alert-popup/alert-popup"; +import { globalErrorHandler } from "@/lib/utils/error.utils"; +import authService from "@/modules/auth/services/auth.service"; +import { authActions } from "@/modules/auth/slices/auth.slice"; +import createClient from "openapi-fetch"; +import { store } from "./redux/store"; +import { paths } from "./types/openapi-fetch"; + +export const PERMISSION_ERR_MSG = + "You don't have permission to perform this action. Please contact your organization admin."; + +// Create enhanced client by wrapping the fetch implementation +const apiClient = createClient({ + baseUrl: import.meta.env.VITE_API_BASE_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +apiClient.use({ + async onRequest({ request }) { + const token = localStorage.getItem("accessToken"); + if (token && !request.headers.has("Authorization")) { + request.headers.set("Authorization", `Bearer ${token}`); + } + + return request; + }, + async onResponse({ request, response }) { + if (response.ok) { + return response; + } + + if (response.status === 403 && request.method !== "GET") { + AlertPopup({ + title: "Permission Denied", + message: PERMISSION_ERR_MSG, + cancelText: null, + okText: "Ok", + }); + return response; + } + + if (response.status === 401) { + let shouldLogout = false; + + if (!["auth/login", "auth/refresh"].includes(request.url)) { + try { + // Retry request after refreshing token + const { accessToken, refreshToken } = await authService.refreshToken( + localStorage.getItem("refreshToken")! + ); + localStorage.setItem("accessToken", accessToken); + localStorage.setItem("refreshToken", refreshToken); + + // Create a new request with the fresh token + const newRequest = new Request(request, { + headers: new Headers(request.headers), + }); + newRequest.headers.set("Authorization", `Bearer ${accessToken}`); + + // Retry the original request with the new token + return fetch(newRequest); + } catch (e) { + shouldLogout = true; + } + } else { + shouldLogout = true; + } + + if (shouldLogout) { + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + store.dispatch(authActions.setStatus("unauthenticated")); + return response; + } + } + + // Try to parse error message from response + let errorData; + try { + errorData = await response.clone().json(); + } catch { + errorData = null; + } + + globalErrorHandler(errorData?.error || errorData); + return response; + }, + + async onError({ error }) { + globalErrorHandler(error); + }, +}); + +export async function withApiResponseHandling( + request: Promise<{ + data?: { data: T; error: string | null }; + error?: E; + response: Response; + }> +): Promise<{ + data: NonNullable; + response: Response; +}> { + const { data, error, response } = await request; + + if (error || data?.error || !data?.data) { + throw error ?? data?.error; + } + + const result = { + data: data.data, + response, + }; + + return result; +} + +export default apiClient; diff --git a/src/lib/router/router-config.tsx b/src/lib/router/router-config.tsx index 2bda16f..554251e 100644 --- a/src/lib/router/router-config.tsx +++ b/src/lib/router/router-config.tsx @@ -1,4 +1,4 @@ -import { Role } from "@/modules/auth/types/profile.type"; +import { Role } from "@/modules/auth/hooks/role.hooks"; import NotFound from "@/pages/404/404"; import Login from "@/pages/auth/login"; import Home from "@/pages/home/home"; @@ -99,11 +99,11 @@ export const useRouterConfig = (): RouterConfig[] => { component: , allowedRoles: [Role.ADMIN], menuItem: { - title: "Admin Only Page", + title: "Users", icon: , }, route: { - path: "/admin-only", + path: "/users", }, }, { diff --git a/src/lib/router/router.tsx b/src/lib/router/router.tsx index 4a18a1a..881d8e8 100644 --- a/src/lib/router/router.tsx +++ b/src/lib/router/router.tsx @@ -5,7 +5,7 @@ import DashboardLayout from "@/lib/layouts/dashboard-layout/dashboard-layout"; import EmptyLayout from "@/lib/layouts/empty-layout"; import { useAppDispatch } from "@/lib/redux/store"; import { useAuth } from "@/modules/auth/hooks/auth.hooks"; -import { useRole } from "@/modules/auth/hooks/role.hooks"; +import { Role, useRole } from "@/modules/auth/hooks/role.hooks"; import { authActions } from "@/modules/auth/slices/auth.slice"; import { profileActions } from "@/modules/auth/slices/profile.slice"; import _ from "lodash"; @@ -87,7 +87,7 @@ export default function Router() { const dispatch = useAppDispatch(); const [ar, cr] = routeRenderer(routerConfig); dispatch( - authActions.setComputedRoutes(cr.map((r) => _.pick(r, ["route", "key"]))) + authActions.setComputedRoutes(cr?.map((r) => _.pick(r, ["route", "key"]))) ); return ( @@ -172,12 +172,15 @@ const RoleCheckWrapper: React.FC< if (authStatus === "authenticated") { dispatch(profileActions.fetch()); } - }, [authStatus]); + }, []); useLayoutEffect(() => { if (authStatus === "authenticated" && !role) { setIsRendered(false); - } else if (Array.isArray(allowedRoles) && !allowedRoles.includes(role!)) { + } else if ( + Array.isArray(allowedRoles) && + !allowedRoles.includes(role as Role) + ) { navigate("/not-found"); setIsRendered(false); } else { diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts new file mode 100644 index 0000000..42b51f9 --- /dev/null +++ b/src/lib/types/api.ts @@ -0,0 +1,26 @@ +import { components, operations } from "./openapi-fetch"; + +export type ApiSchemas = components["schemas"]; + +export type ApiQueryParams = + operations[T]["parameters"]["query" extends keyof operations[T]["parameters"] + ? "query" + : never]; + +export type ApiBody = + operations[T]["requestBody"] extends { content: { "application/json": any } } + ? operations[T]["requestBody"]["content"]["application/json"] + : operations[T]["requestBody"] extends { + content: { "multipart/form-data": any }; + } + ? operations[T]["requestBody"]["content"]["multipart/form-data"] + : never; + +export type ApiResponse = + operations[T]["responses"] extends { + "200": { content: { "application/json": { data: any } } }; + } + ? operations[T]["responses"]["200"]["content"]["application/json"]["data"] + : never; + +export type Profile = ApiResponse<"ProfileFetch">; diff --git a/src/lib/types/misc.ts b/src/lib/types/misc.ts index c2579a7..c0e5765 100644 --- a/src/lib/types/misc.ts +++ b/src/lib/types/misc.ts @@ -9,4 +9,7 @@ export enum ThunkStatus { } export type GenericObject = Record; + export type KeyValuePair = Record; + +export type Modify = Omit & R; diff --git a/src/lib/types/openapi-fetch.d.ts b/src/lib/types/openapi-fetch.d.ts new file mode 100644 index 0000000..1280568 --- /dev/null +++ b/src/lib/types/openapi-fetch.d.ts @@ -0,0 +1,1435 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/users/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["UserFetch"]; + put?: never; + post?: never; + delete: operations["UserRemove"]; + options?: never; + head?: never; + patch: operations["UserUpdate"]; + trace?: never; + }; + "/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["UserFetchList"]; + put?: never; + post: operations["UserCreate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/profile": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["ProfileFetch"]; + put?: never; + post?: never; + delete: operations["ProfileDelete"]; + options?: never; + head?: never; + patch: operations["ProfileUpdate"]; + trace?: never; + }; + "/posts/{postId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["PostFetch"]; + put?: never; + post?: never; + delete: operations["PostRemove"]; + options?: never; + head?: never; + patch: operations["PostUpdate"]; + trace?: never; + }; + "/posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["PostFetchList"]; + put?: never; + post: operations["PostCreate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/notifications/webhooks/handle-trigger": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["NotificationHandleTrigger"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/notifications/general": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["NotificationSendGeneral"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/notifications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["NotificationFetchList"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/notifications/mark-read": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["NotificationMarkRead"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["MiscGetStatus"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/openapi.json": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["MiscGetOpenApiSpec"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/reload-secrets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["MiscReloadSecrets"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/files": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["FileCreate"]; + delete: operations["FileRemove"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/files/webhooks/handle-img-resize": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["FileHandleImageResize"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthLogin"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/signup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthSignup"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/verify-email": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthVerifyEmail"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/signout-all": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthSignout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/forgot-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthForgotPassword"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/reset-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthResetPassword"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/change-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthChangePassword"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/resend-verification": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthResendVerification"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthRefresh"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** @description From https://github.com/sindresorhus/type-fest/ + * Matches any valid JSON value. */ + JsonValue: (string | number | boolean | components["schemas"]["JsonObject"] | components["schemas"]["JsonArray"]) | null; + /** @description From https://github.com/sindresorhus/type-fest/ + * Matches a JSON object. + * This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. */ + JsonObject: { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** @description From https://github.com/sindresorhus/type-fest/ + * Matches a JSON array. */ + JsonArray: Record; + /** @description From T, pick a set of properties whose keys are in the union K */ + "Pick_User.Exclude_keyofUser.password_hash-or-refresh_token_version__": { + name: string; + created_at?: string; + updated_at?: string; + id?: string; + role?: string; + email_verified?: boolean; + email: string; + photo?: string; + fcm_tokens?: string[]; + bio?: string; + /** Format: double */ + unread_noti_count?: number; + photo_sizes?: components["schemas"]["JsonValue"]; + }; + /** @description Construct a type with the properties of T except for those in type K. */ + "Omit_User.password_hash-or-refresh_token_version_": components["schemas"]["Pick_User.Exclude_keyofUser.password_hash-or-refresh_token_version__"]; + SanitizedUser: components["schemas"]["Omit_User.password_hash-or-refresh_token_version_"]; + APIResponse_SanitizedUser_: { + data: components["schemas"]["SanitizedUser"] | null; + error: string | null; + }; + PaginationResponse_SanitizedUser_: { + /** Format: double */ + total?: number; + list: components["schemas"]["SanitizedUser"][]; + }; + APIResponse_PaginationResponse_SanitizedUser__: { + data: components["schemas"]["PaginationResponse_SanitizedUser_"] | null; + error: string | null; + }; + UserFetchList: { + /** Format: double */ + limit?: number; + /** Format: double */ + page?: number; + search?: string; + }; + "Expand_Optional_UserMutable.bio__": { + bio?: string; + name: string; + role: string; + email: string; + photo: string; + }; + UserCreate: components["schemas"]["Expand_Optional_UserMutable.bio__"]; + /** @description Make all properties in T optional */ + "Partial_Omit_UserMutable.email__": { + name?: string; + role?: string; + photo?: string; + bio?: string; + }; + UserUpdate: components["schemas"]["Partial_Omit_UserMutable.email__"]; + "Prisma.postsCreatelabelsInput": { + set: string[]; + }; + "Expand_PostUnlinked-and-_author-SanitizedUser__": { + title: string; + labels?: string[] | components["schemas"]["Prisma.postsCreatelabelsInput"]; + slug: string; + text: string; + author_id: string; + id?: string; + created_at?: string; + updated_at?: string; + /** Format: double */ + views?: number; + author: components["schemas"]["SanitizedUser"]; + }; + PostType: components["schemas"]["Expand_PostUnlinked-and-_author-SanitizedUser__"]; + APIResponse_PostType_: { + data: components["schemas"]["PostType"] | null; + error: string | null; + }; + PaginationResponse_PostType_: { + /** Format: double */ + total?: number; + list: components["schemas"]["PostType"][]; + }; + APIResponse_PaginationResponse_PostType__: { + data: components["schemas"]["PaginationResponse_PostType_"] | null; + error: string | null; + }; + PostFetchList: { + /** Format: double */ + limit?: number; + /** Format: double */ + page?: number; + search?: string; + author_id?: string; + labels?: string[]; + populate?: boolean; + }; + "Expand_Omit_PostMutable.author_id__": { + title: string; + labels?: string[] | components["schemas"]["Prisma.postsCreatelabelsInput"]; + text: string; + }; + PostCreate: components["schemas"]["Expand_Omit_PostMutable.author_id__"]; + Expand_Partial_PostMutable__: { + title?: string; + labels?: string[] | components["schemas"]["Prisma.postsCreatelabelsInput"]; + text?: string; + author_id?: string; + }; + PostUpdate: components["schemas"]["Expand_Partial_PostMutable__"]; + Message: { + message: string; + }; + APIResponse_Message_: { + data: components["schemas"]["Message"] | null; + error: string | null; + }; + /** @enum {string} */ + "NotificationEvent.GENERAL": "general"; + /** @description Type of `Prisma.DbNull`. + * + * You cannot use other instances of this class. Please use the `Prisma.DbNull` value. */ + "Prisma.NullTypes.DbNull": Record; + /** @description Type of `Prisma.JsonNull`. + * + * You cannot use other instances of this class. Please use the `Prisma.JsonNull` value. */ + "Prisma.NullTypes.JsonNull": Record; + "Prisma.NullableJsonNullValueInput": components["schemas"]["Prisma.NullTypes.DbNull"] | components["schemas"]["Prisma.NullTypes.JsonNull"]; + /** @description Matches any valid value that can be used as an input for operations like + * create and update as the value of a JSON field. Unlike \`JsonValue\`, this + * type allows read-only arrays and read-only object properties and disallows + * \`null\` at the top level. + * + * \`null\` cannot be used as the value of a JSON field because its meaning + * would be ambiguous. Use \`Prisma.JsonNull\` to store the JSON null value or + * \`Prisma.DbNull\` to clear the JSON value and set the field to the database + * NULL value instead. */ + InputJsonValue: string | number | boolean | components["schemas"]["InputJsonObject"] | components["schemas"]["InputJsonArray"] | Record; + /** @description Matches a JSON object. + * Unlike \`JsonObject\`, this type allows undefined and read-only properties. */ + InputJsonObject: { + [key: string]: components["schemas"]["InputJsonValue"]; + }; + /** @description Matches a JSON array. + * Unlike \`JsonArray\`, readonly arrays are assignable to this type. */ + InputJsonArray: Record; + /** @description Construct a type with a set of properties K of type T */ + "Record_string.any_": { + [key: string]: unknown; + }; + GenericObject: components["schemas"]["Record_string.any_"]; + /** @enum {string} */ + Role: "user" | "admin" | "super-admin"; + "Expand_Omit_NotificationMutable.event_-and-_roles_63_-Role-Array--userIds_63_-string-Array--metadata_63_-GenericObject__": { + title: string; + body: string; + image?: string; + link?: string; + metadata?: ((components["schemas"]["Prisma.NullableJsonNullValueInput"] | components["schemas"]["InputJsonValue"]) & components["schemas"]["GenericObject"]) & components["schemas"]["GenericObject"]; + roles?: components["schemas"]["Role"][]; + userIds?: string[]; + }; + NotificationSendGeneral: components["schemas"]["Expand_Omit_NotificationMutable.event_-and-_roles_63_-Role-Array--userIds_63_-string-Array--metadata_63_-GenericObject__"]; + /** @enum {string} */ + "NotificationEvent.SIGN_UP": "sign-up"; + /** @enum {string} */ + "NotificationEvent.NEW_POST": "new-post"; + NotificationPayload: { + /** Format: double */ + timestamp?: number; + } & ({ + data: components["schemas"]["NotificationSendGeneral"]; + event: components["schemas"]["NotificationEvent.GENERAL"]; + } | { + data: { + email: string; + name: string; + }; + event: components["schemas"]["NotificationEvent.SIGN_UP"]; + } | { + data: { + title: string; + author: string; + }; + event: components["schemas"]["NotificationEvent.NEW_POST"]; + }); + "Expand_Prisma.notificationsCreateManyInput-and-_metadata_63_-GenericObject__": { + id?: string; + created_at?: string; + updated_at?: string; + title: string; + body: string; + image?: string; + link?: string; + metadata?: (components["schemas"]["Prisma.NullableJsonNullValueInput"] | components["schemas"]["InputJsonValue"]) & components["schemas"]["GenericObject"]; + event: string; + }; + Notification: components["schemas"]["Expand_Prisma.notificationsCreateManyInput-and-_metadata_63_-GenericObject__"]; + PaginationResponse_Notification_: { + /** Format: double */ + total?: number; + list: components["schemas"]["Notification"][]; + }; + APIResponse_PaginationResponse_Notification__: { + data: components["schemas"]["PaginationResponse_Notification_"] | null; + error: string | null; + }; + NotificationFetchList: { + /** Format: double */ + limit?: number; + /** Format: double */ + page?: number; + }; + /** @description Construct a type with a set of properties K of type T */ + "Record_string.string_": { + [key: string]: string; + }; + FileWithImgVariants: { + img_sizes?: components["schemas"]["Record_string.string_"]; + url: string; + }; + APIResponse_FileWithImgVariants_: { + data: components["schemas"]["FileWithImgVariants"] | null; + error: string | null; + }; + FileDelete: { + url: string; + }; + /** @enum {string} */ + "Prisma.ModelName": "otps" | "posts" | "users" | "notifications" | "userNotifications"; + Resizeconfig: { + sizes: ("small" | "medium" | "large")[]; + img_field: string; + record_id: string; + model: components["schemas"]["Prisma.ModelName"]; + }; + FileWebhookHandleResize: { + resize_config: components["schemas"]["Resizeconfig"]; + url: string; + }; + AuthLoginResponse: { + refreshToken: string; + accessToken: string; + user: components["schemas"]["SanitizedUser"]; + }; + APIResponse_AuthLoginResponse_: { + data: components["schemas"]["AuthLoginResponse"] | null; + error: string | null; + }; + AuthLogin: { + password: string; + email: string; + }; + AuthVerifyEmail: { + email: string; + otp: string; + }; + AuthForgotPass: { + hcaptcha_token?: string; + email: string; + }; + AuthResetPass: { + password: string; + otp: string; + email: string; + }; + AuthChangePass: { + password: string; + }; + /** @description From T, pick a set of properties whose keys are in the union K */ + "Pick_AuthLoginResponse.Exclude_keyofAuthLoginResponse.user__": { + accessToken: string; + refreshToken: string; + }; + /** @description Construct a type with the properties of T except for those in type K. */ + "Omit_AuthLoginResponse.user_": components["schemas"]["Pick_AuthLoginResponse.Exclude_keyofAuthLoginResponse.user__"]; + "APIResponse_Omit_AuthLoginResponse.user__": { + data: components["schemas"]["Omit_AuthLoginResponse.user_"] | null; + error: string | null; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + UserFetch: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_SanitizedUser_"]; + }; + }; + }; + }; + UserRemove: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_SanitizedUser_"]; + }; + }; + }; + }; + UserUpdate: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UserUpdate"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_SanitizedUser_"]; + }; + }; + }; + }; + UserFetchList: { + parameters: { + query?: { + limit?: number; + page?: number; + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_PaginationResponse_SanitizedUser__"]; + }; + }; + }; + }; + UserCreate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UserCreate"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_SanitizedUser_"]; + }; + }; + }; + }; + ProfileFetch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_SanitizedUser_"]; + }; + }; + }; + }; + ProfileDelete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_SanitizedUser_"]; + }; + }; + }; + }; + ProfileUpdate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "multipart/form-data": { + name?: string; + bio?: string; + added_fcm_token?: string; + removed_fcm_token?: string; + /** Format: binary */ + photo?: string; + }; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_SanitizedUser_"]; + }; + }; + }; + }; + PostFetch: { + parameters: { + query?: never; + header?: never; + path: { + postId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_PostType_"]; + }; + }; + }; + }; + PostRemove: { + parameters: { + query?: never; + header?: never; + path: { + postId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_PostType_"]; + }; + }; + }; + }; + PostUpdate: { + parameters: { + query?: never; + header?: never; + path: { + postId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PostUpdate"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_PostType_"]; + }; + }; + }; + }; + PostFetchList: { + parameters: { + query?: { + limit?: number; + page?: number; + search?: string; + author_id?: string; + labels?: string[]; + populate?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_PaginationResponse_PostType__"]; + }; + }; + }; + }; + PostCreate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PostCreate"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_PostType_"]; + }; + }; + }; + }; + NotificationHandleTrigger: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NotificationPayload"] & { + taskMetadata: components["schemas"]["GenericObject"]; + }; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + NotificationSendGeneral: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NotificationSendGeneral"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + NotificationFetchList: { + parameters: { + query?: { + limit?: number; + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_PaginationResponse_Notification__"]; + }; + }; + }; + }; + NotificationMarkRead: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + MiscGetStatus: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + MiscGetOpenApiSpec: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GenericObject"]; + }; + }; + }; + }; + MiscReloadSecrets: { + parameters: { + query: { + api_key: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + FileCreate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": { + /** Format: binary */ + file: string; + img_sizes?: string; + }; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_FileWithImgVariants_"]; + }; + }; + }; + }; + FileRemove: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FileDelete"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + FileHandleImageResize: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FileWebhookHandleResize"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + AuthLogin: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AuthLogin"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_AuthLoginResponse_"]; + }; + }; + }; + }; + AuthSignup: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": { + name: string; + email: string; + password: string; + bio?: string; + hcaptcha_token?: string; + /** Format: binary */ + photo?: string; + }; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_SanitizedUser_"]; + }; + }; + }; + }; + AuthVerifyEmail: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AuthVerifyEmail"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_AuthLoginResponse_"]; + }; + }; + }; + }; + AuthSignout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + AuthForgotPassword: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AuthForgotPass"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + AuthResetPassword: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AuthResetPass"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + AuthChangePassword: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AuthChangePass"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + AuthResendVerification: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AuthForgotPass"]; + }; + }; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Message_"]; + }; + }; + }; + }; + AuthRefresh: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIResponse_Omit_AuthLoginResponse.user__"]; + }; + }; + }; + }; +} diff --git a/src/modules/auth/hooks/role.hooks.ts b/src/modules/auth/hooks/role.hooks.ts index d07c25b..02bc4cc 100644 --- a/src/modules/auth/hooks/role.hooks.ts +++ b/src/modules/auth/hooks/role.hooks.ts @@ -1,5 +1,11 @@ import { useAppSelector } from "@/lib/redux/store"; +export enum Role { + USER = "user", + ADMIN = "admin", + SUPER_ADMIN = "super-admin", +} + export const useRole = () => { const role = useAppSelector((state) => state.profile.data?.role); return role; diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 06588e1..901738a 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,26 +1,26 @@ -import { fakeApi } from "@/lib/utils/misc.utils"; +import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; +import { ApiBody } from "@/lib/types/api"; -async function login({ email }: { email: string; password: string }) { - return fakeApi(() => ({ - accessToken: email.toLowerCase(), - refreshToken: "refreshToken", - })) as Promise<{ accessToken: string; refreshToken: string }>; +async function login(payload: ApiBody<"AuthLogin">) { + const { data } = await withApiResponseHandling( + apiClient.POST("/auth/login", { body: payload }) + ); + return data; } -async function logout() { - return fakeApi(() => true) as Promise; -} - -async function refreshToken() { - return fakeApi(() => ({ - accessToken: "accessToken", - refreshToken: "refreshToken", - })) as Promise<{ accessToken: string; refreshToken: string }>; +async function refreshToken(refreshToken: string) { + const { data } = await withApiResponseHandling( + apiClient.POST("/auth/refresh", { + headers: { + Authorization: `Bearer ${refreshToken}`, + }, + }) + ); + return data; } const authService = { login, - logout, refreshToken, }; diff --git a/src/modules/auth/services/profile.service.ts b/src/modules/auth/services/profile.service.ts index e534a7e..e14488b 100644 --- a/src/modules/auth/services/profile.service.ts +++ b/src/modules/auth/services/profile.service.ts @@ -1,18 +1,8 @@ -import { fakeApi } from "@/lib/utils/misc.utils"; -import { Profile } from "../types/profile.type"; +import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; async function fetch() { - const email = localStorage.getItem("accessToken"); - return fakeApi( - () => ({ - id: "1", - name: "John Doe", - email, - role: email?.includes("admin") ? "admin" : "user", - picture: "https://via.placeholder.com/150", - }), - { errorRate: 0 } - ) as Promise; + const { data } = await withApiResponseHandling(apiClient.GET("/profile")); + return data; } const profileService = { diff --git a/src/modules/auth/slices/auth.slice.ts b/src/modules/auth/slices/auth.slice.ts index 0d1a772..1296dea 100644 --- a/src/modules/auth/slices/auth.slice.ts +++ b/src/modules/auth/slices/auth.slice.ts @@ -30,7 +30,6 @@ const login = createAsyncThunk( } ); const logout = createAsyncThunk(`${name}/logout`, async () => { - await authService.logout(); localStorage.removeItem("accessToken"); localStorage.removeItem("refreshToken"); }); diff --git a/src/modules/auth/slices/profile.slice.ts b/src/modules/auth/slices/profile.slice.ts index d586564..439bb19 100644 --- a/src/modules/auth/slices/profile.slice.ts +++ b/src/modules/auth/slices/profile.slice.ts @@ -1,7 +1,7 @@ +import { Profile } from "@/lib/types/api"; import { ThunkStatus } from "@/lib/types/misc"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import profileService from "../services/profile.service"; -import { Profile } from "../types/profile.type"; export const name = "profile"; diff --git a/src/modules/auth/types/profile.type.ts b/src/modules/auth/types/profile.type.ts deleted file mode 100644 index 73517ff..0000000 --- a/src/modules/auth/types/profile.type.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum Role { - ADMIN = "admin", - USER = "user", -} - -export type Profile = { - id: string; - name: string; - email: string; - role: Role; - picture?: string; -}; diff --git a/tsconfig.json b/tsconfig.json index a73c288..dc83233 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, + "noUncheckedIndexedAccess": true, "noFallthroughCasesInSwitch": true, "baseUrl": "./", diff --git a/yarn.lock b/yarn.lock index 2a308d3..67f3818 100644 --- a/yarn.lock +++ b/yarn.lock @@ -107,6 +107,20 @@ resize-observer-polyfill "^1.5.1" throttle-debounce "^5.0.0" +"@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.6", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.6": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" @@ -160,116 +174,241 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3" integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g== +"@esbuild/aix-ppc64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz#b87036f644f572efb2b3c75746c97d1d2d87ace8" + integrity sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag== + "@esbuild/android-arm64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220" integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q== +"@esbuild/android-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz#5ca7dc20a18f18960ad8d5e6ef5cf7b0a256e196" + integrity sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w== + "@esbuild/android-arm@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c" integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw== +"@esbuild/android-arm@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.2.tgz#3c49f607b7082cde70c6ce0c011c362c57a194ee" + integrity sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA== + "@esbuild/android-x64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2" integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg== +"@esbuild/android-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.2.tgz#8a00147780016aff59e04f1036e7cb1b683859e2" + integrity sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg== + "@esbuild/darwin-arm64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf" integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ== +"@esbuild/darwin-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz#486efe7599a8d90a27780f2bb0318d9a85c6c423" + integrity sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA== + "@esbuild/darwin-x64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e" integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g== +"@esbuild/darwin-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz#95ee222aacf668c7a4f3d7ee87b3240a51baf374" + integrity sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA== + "@esbuild/freebsd-arm64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a" integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA== +"@esbuild/freebsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz#67efceda8554b6fc6a43476feba068fb37fa2ef6" + integrity sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w== + "@esbuild/freebsd-x64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2" integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw== +"@esbuild/freebsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz#88a9d7ecdd3adadbfe5227c2122d24816959b809" + integrity sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ== + "@esbuild/linux-arm64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545" integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg== +"@esbuild/linux-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz#87be1099b2bbe61282333b084737d46bc8308058" + integrity sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g== + "@esbuild/linux-arm@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3" integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q== +"@esbuild/linux-arm@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz#72a285b0fe64496e191fcad222185d7bf9f816f6" + integrity sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g== + "@esbuild/linux-ia32@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4" integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA== +"@esbuild/linux-ia32@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz#337a87a4c4dd48a832baed5cbb022be20809d737" + integrity sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ== + "@esbuild/linux-loong64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121" integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg== +"@esbuild/linux-loong64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz#1b81aa77103d6b8a8cfa7c094ed3d25c7579ba2a" + integrity sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w== + "@esbuild/linux-mips64el@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9" integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg== +"@esbuild/linux-mips64el@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz#afbe380b6992e7459bf7c2c3b9556633b2e47f30" + integrity sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q== + "@esbuild/linux-ppc64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912" integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA== +"@esbuild/linux-ppc64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz#6bf8695cab8a2b135cca1aa555226dc932d52067" + integrity sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g== + "@esbuild/linux-riscv64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916" integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ== +"@esbuild/linux-riscv64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz#43c2d67a1a39199fb06ba978aebb44992d7becc3" + integrity sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw== + "@esbuild/linux-s390x@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8" integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q== +"@esbuild/linux-s390x@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz#419e25737ec815c6dce2cd20d026e347cbb7a602" + integrity sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q== + "@esbuild/linux-x64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766" integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA== +"@esbuild/linux-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz#22451f6edbba84abe754a8cbd8528ff6e28d9bcb" + integrity sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg== + +"@esbuild/netbsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz#744affd3b8d8236b08c5210d828b0698a62c58ac" + integrity sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw== + "@esbuild/netbsd-x64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d" integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ== +"@esbuild/netbsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz#dbbe7521fd6d7352f34328d676af923fc0f8a78f" + integrity sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg== + +"@esbuild/openbsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz#f9caf987e3e0570500832b487ce3039ca648ce9f" + integrity sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg== + "@esbuild/openbsd-x64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2" integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw== +"@esbuild/openbsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz#d2bb6a0f8ffea7b394bb43dfccbb07cabd89f768" + integrity sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw== + "@esbuild/sunos-x64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767" integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ== +"@esbuild/sunos-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz#49b437ed63fe333b92137b7a0c65a65852031afb" + integrity sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA== + "@esbuild/win32-arm64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee" integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ== +"@esbuild/win32-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz#081424168463c7d6c7fb78f631aede0c104373cf" + integrity sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q== + "@esbuild/win32-ia32@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c" integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg== +"@esbuild/win32-ia32@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz#3f9e87143ddd003133d21384944a6c6cadf9693f" + integrity sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg== + "@esbuild/win32-x64@0.19.11": version "0.19.11" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04" integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw== +"@esbuild/win32-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz#839f72c2decd378f86b8f525e1979a97b920c67d" + integrity sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -473,6 +612,36 @@ rc-resize-observer "^1.3.1" rc-util "^5.38.0" +"@redocly/ajv@^8.11.2": + version "8.11.2" + resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.11.2.tgz#46e1bf321ec0ac1e0fd31dea41a3d1fcbdcda0b5" + integrity sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js-replace "^1.0.1" + +"@redocly/config@^0.22.0": + version "0.22.2" + resolved "https://registry.yarnpkg.com/@redocly/config/-/config-0.22.2.tgz#9a05e694816d53a5236cf8768d3cad0e49d8b116" + integrity sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ== + +"@redocly/openapi-core@^1.28.0": + version "1.34.1" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.34.1.tgz#d303c0f129c9166e293e7e6ee88de77e29fd6e16" + integrity sha512-KI1QOGvDk6oREbTu0JORxZX1NBxraXUbXczv0LYDs9EPp06coq874hQORqSHGEUV/DX2A6gjv4Ax33g/LFJBww== + dependencies: + "@redocly/ajv" "^8.11.2" + "@redocly/config" "^0.22.0" + colorette "^1.2.0" + https-proxy-agent "^7.0.5" + js-levenshtein "^1.1.6" + js-yaml "^4.1.0" + minimatch "^5.0.1" + pluralize "^8.0.0" + yaml-ast-parser "0.0.43" + "@reduxjs/toolkit@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.1.0.tgz#b613226669557080d5d683f3dbbd95462f94b965" @@ -666,6 +835,13 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3" integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA== +"@types/node@^22.14.0": + version "22.14.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.0.tgz#d3bfa3936fef0dbacd79ea3eb17d521c628bb47e" + integrity sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA== + dependencies: + undici-types "~6.21.0" + "@types/prop-types@*": version "15.7.11" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" @@ -815,6 +991,11 @@ acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + ahooks@^3.4.1, ahooks@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/ahooks/-/ahooks-3.8.1.tgz#3a19d0e4085618a7a38a22a34b568c8d3fd974c0" @@ -840,6 +1021,11 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + ansi-escapes@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" @@ -1071,6 +1257,11 @@ chalk@~5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== +change-case@^5.4.4: + version "5.4.4" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" + integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== + chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -1139,6 +1330,11 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" +colorette@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + colorette@^2.0.20: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" @@ -1224,6 +1420,13 @@ dayjs@^1.11.11: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== +debug@4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -1357,6 +1560,37 @@ esbuild@^0.19.3: "@esbuild/win32-ia32" "0.19.11" "@esbuild/win32-x64" "0.19.11" +esbuild@~0.25.0: + version "0.25.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.2.tgz#55a1d9ebcb3aa2f95e8bba9e900c1a5061bc168b" + integrity sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.2" + "@esbuild/android-arm" "0.25.2" + "@esbuild/android-arm64" "0.25.2" + "@esbuild/android-x64" "0.25.2" + "@esbuild/darwin-arm64" "0.25.2" + "@esbuild/darwin-x64" "0.25.2" + "@esbuild/freebsd-arm64" "0.25.2" + "@esbuild/freebsd-x64" "0.25.2" + "@esbuild/linux-arm" "0.25.2" + "@esbuild/linux-arm64" "0.25.2" + "@esbuild/linux-ia32" "0.25.2" + "@esbuild/linux-loong64" "0.25.2" + "@esbuild/linux-mips64el" "0.25.2" + "@esbuild/linux-ppc64" "0.25.2" + "@esbuild/linux-riscv64" "0.25.2" + "@esbuild/linux-s390x" "0.25.2" + "@esbuild/linux-x64" "0.25.2" + "@esbuild/netbsd-arm64" "0.25.2" + "@esbuild/netbsd-x64" "0.25.2" + "@esbuild/openbsd-arm64" "0.25.2" + "@esbuild/openbsd-x64" "0.25.2" + "@esbuild/sunos-x64" "0.25.2" + "@esbuild/win32-arm64" "0.25.2" + "@esbuild/win32-ia32" "0.25.2" + "@esbuild/win32-x64" "0.25.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -1636,6 +1870,13 @@ get-stream@^8.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== +get-tsconfig@^4.7.5: + version "4.10.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz#403a682b373a823612475a4c2928c7326fc0f6bb" + integrity sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A== + dependencies: + resolve-pkg-maps "^1.0.0" + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -1752,6 +1993,14 @@ html-parse-stringify@^3.0.1: dependencies: void-elements "3.1.0" +https-proxy-agent@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + human-signals@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" @@ -1797,6 +2046,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +index-to-position@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/index-to-position/-/index-to-position-1.0.0.tgz#baca236eb6e8c2b750b9225313c31751f84ef357" + integrity sha512-sCO7uaLVhRJ25vz1o8s9IFM3nVS4DkuQnyjMwiQPKvQuBYBDmb8H7zx8ki7nVh4HJQOdVWebyvLE0qt+clruxA== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1907,7 +2161,12 @@ js-cookie@^3.0.5: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== -"js-tokens@^3.0.0 || ^4.0.0": +js-levenshtein@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -1929,6 +2188,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -2252,6 +2516,13 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + "minipass@^5.0.0 || ^6.0.2 || ^7.0.0": version "7.0.4" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" @@ -2344,6 +2615,30 @@ onetime@^7.0.0: dependencies: mimic-function "^5.0.0" +openapi-fetch@^0.13.5: + version "0.13.5" + resolved "https://registry.yarnpkg.com/openapi-fetch/-/openapi-fetch-0.13.5.tgz#805606860d85b8ba8c2e7cb36ea30b473d8065d9" + integrity sha512-AQK8T9GSKFREFlN1DBXTYsLjs7YV2tZcJ7zUWxbjMoQmj8dDSFRrzhLCbHPZWA1TMV3vACqfCxLEZcwf2wxV6Q== + dependencies: + openapi-typescript-helpers "^0.0.15" + +openapi-typescript-helpers@^0.0.15: + version "0.0.15" + resolved "https://registry.yarnpkg.com/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz#96ffa762a5e01ef66a661b163d5f1109ed1967ed" + integrity sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw== + +openapi-typescript@^7.6.1: + version "7.6.1" + resolved "https://registry.yarnpkg.com/openapi-typescript/-/openapi-typescript-7.6.1.tgz#e39d1e21ebf43f91712703f7063118246d099d19" + integrity sha512-F7RXEeo/heF3O9lOXo2bNjCOtfp7u+D6W3a3VNEH2xE6v+fxLtn5nq0uvUcA1F5aT+CMhNeC5Uqtg5tlXFX/ag== + dependencies: + "@redocly/openapi-core" "^1.28.0" + ansi-colors "^4.1.3" + change-case "^5.4.4" + parse-json "^8.1.0" + supports-color "^9.4.0" + yargs-parser "^21.1.1" + optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -2377,6 +2672,15 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-json@^8.1.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-8.2.0.tgz#794a590dcf54588ec2282ce6065f15121fa348a0" + integrity sha512-eONBZy4hm2AgxjNFd8a4nyDJnzUAH0g34xSQAwWEVGCjdZ4ZL7dKZBfq267GWP/JaS9zW62Xs2FeAdDvpHHJGQ== + dependencies: + "@babel/code-frame" "^7.26.2" + index-to-position "^1.0.0" + type-fest "^4.37.0" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -2445,6 +2749,11 @@ pirates@^4.0.1: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + postcss-import@^15.1.0: version "15.1.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" @@ -3028,6 +3337,11 @@ regenerator-runtime@^0.14.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + reselect@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.0.tgz#c479139ab9dd91be4d9c764a7f3868210ef8cd21" @@ -3043,6 +3357,11 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve@^1.1.7, resolve@^1.22.2: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" @@ -3321,6 +3640,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" + integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -3437,6 +3761,16 @@ tslib@^2.4.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tsx@^4.19.3: + version "4.19.3" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.19.3.tgz#2bdbcb87089374d933596f8645615142ed727666" + integrity sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ== + dependencies: + esbuild "~0.25.0" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -3449,10 +3783,20 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^5.2.2: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +type-fest@^4.37.0: + version "4.39.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.39.1.tgz#7521f6944e279abaf79cf60cfbc4823f4858083e" + integrity sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w== + +typescript@^5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== update-browserslist-db@^1.0.13: version "1.0.13" @@ -3462,6 +3806,11 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +uri-js-replace@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uri-js-replace/-/uri-js-replace-1.0.1.tgz#c285bb352b701c9dfdaeffc4da5be77f936c9048" + integrity sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -3548,6 +3897,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml-ast-parser@0.0.43: + version "0.0.43" + resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" + integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== + yaml@^2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" @@ -3558,6 +3912,11 @@ yaml@~2.5.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From c2edbc0a3f6ae37a621b45fd45a97533b83af2fe Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Tue, 8 Apr 2025 19:23:12 +0500 Subject: [PATCH 08/19] use axios as request client for openapi-fetch --- src/lib/axios.config.ts | 75 ++++++++++ .../server-paginated-table.tsx | 2 +- src/lib/openapi-fetch.config.ts | 130 ++++++++---------- src/modules/auth/services/auth.service.ts | 2 +- 4 files changed, 131 insertions(+), 78 deletions(-) create mode 100644 src/lib/axios.config.ts diff --git a/src/lib/axios.config.ts b/src/lib/axios.config.ts new file mode 100644 index 0000000..900878f --- /dev/null +++ b/src/lib/axios.config.ts @@ -0,0 +1,75 @@ +import AlertPopup from "@/lib/components/alert-popup/alert-popup"; +import { getErrorMessage, globalErrorHandler } from "@/lib/utils/error.utils"; +import authService from "@/modules/auth/services/auth.service"; +import { authActions } from "@/modules/auth/slices/auth.slice"; +import axiosApi from "axios"; +import { store } from "./redux/store"; + +export const PERMISSION_ERR_MSG = + "You don't have permission to perform this action. Please contact your organization admin."; + +const axios = axiosApi.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +axios.interceptors.request.use((reqConfig) => { + const token = localStorage.getItem("accessToken"); + if (token && !reqConfig.headers.toJSON().authorization) { + reqConfig.headers.authorization = `Bearer ${token}`; + } + return reqConfig; +}); + +axios.interceptors.response.use(undefined, async (error) => { + if (error.response?.status === 403 && error.config.method !== "get") { + AlertPopup({ + title: "Permission Denied", + message: PERMISSION_ERR_MSG, + cancelText: null, + okText: "Ok", + }); + error.message = PERMISSION_ERR_MSG; + throw error; + } + + let msg = getErrorMessage( + error.response?.data?.error || error.response?.data, + "" + ); + if (!msg) + msg = `${ + error.response?.statusText ? error.response.statusText + "! " : "" + }${error.message}`; + error.message = msg; + + if (error.response?.status === 401) { + if ( + !["auth/login", "auth/refresh"].some((url) => + error.config.url.includes(url) + ) + ) { + // retry request after refreshing token + const { accessToken, refreshToken } = await authService.refreshToken( + localStorage.getItem("refreshToken")! + ); + localStorage.setItem("accessToken", accessToken); + localStorage.setItem("refreshToken", refreshToken); + error.config.headers.Authorization = `Bearer ${accessToken}`; + const data = await axios(error.config); + return data; + } else { + // logout + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + store.dispatch(authActions.setStatus("unauthenticated")); + } + } + + globalErrorHandler(error); + throw error; +}); + +export default axios; diff --git a/src/lib/components/server-paginated-table/server-paginated-table.tsx b/src/lib/components/server-paginated-table/server-paginated-table.tsx index 8ab15bc..58e8ece 100644 --- a/src/lib/components/server-paginated-table/server-paginated-table.tsx +++ b/src/lib/components/server-paginated-table/server-paginated-table.tsx @@ -153,7 +153,7 @@ export default function ServerPaginatedTable({ // filters Object.entries(restParams).forEach(([key, value]) => { if (!key.startsWith("filter.")) return; - if (!isNullish(value)) query[key.split("filter.")[1]] = value; + if (!isNullish(value)) query[key.split("filter.")[1]!] = value; }); // sorting if (restParams["sort.field"] && !isNullish(restParams["sort.order"])) { diff --git a/src/lib/openapi-fetch.config.ts b/src/lib/openapi-fetch.config.ts index 3fd5cd5..32e5d75 100644 --- a/src/lib/openapi-fetch.config.ts +++ b/src/lib/openapi-fetch.config.ts @@ -1,93 +1,68 @@ -import AlertPopup from "@/lib/components/alert-popup/alert-popup"; import { globalErrorHandler } from "@/lib/utils/error.utils"; -import authService from "@/modules/auth/services/auth.service"; -import { authActions } from "@/modules/auth/slices/auth.slice"; +import { AxiosError } from "axios"; import createClient from "openapi-fetch"; -import { store } from "./redux/store"; +import axios from "./axios.config"; import { paths } from "./types/openapi-fetch"; export const PERMISSION_ERR_MSG = "You don't have permission to perform this action. Please contact your organization admin."; -// Create enhanced client by wrapping the fetch implementation +// Create enhanced client by wrapping Axios as the fetch implementation const apiClient = createClient({ baseUrl: import.meta.env.VITE_API_BASE_URL, - headers: { - "Content-Type": "application/json", - }, -}); - -apiClient.use({ - async onRequest({ request }) { - const token = localStorage.getItem("accessToken"); - if (token && !request.headers.has("Authorization")) { - request.headers.set("Authorization", `Bearer ${token}`); - } - - return request; - }, - async onResponse({ request, response }) { - if (response.ok) { - return response; - } - - if (response.status === 403 && request.method !== "GET") { - AlertPopup({ - title: "Permission Denied", - message: PERMISSION_ERR_MSG, - cancelText: null, - okText: "Ok", - }); - return response; - } - - if (response.status === 401) { - let shouldLogout = false; - - if (!["auth/login", "auth/refresh"].includes(request.url)) { - try { - // Retry request after refreshing token - const { accessToken, refreshToken } = await authService.refreshToken( - localStorage.getItem("refreshToken")! - ); - localStorage.setItem("accessToken", accessToken); - localStorage.setItem("refreshToken", refreshToken); - - // Create a new request with the fresh token - const newRequest = new Request(request, { - headers: new Headers(request.headers), - }); - newRequest.headers.set("Authorization", `Bearer ${accessToken}`); - - // Retry the original request with the new token - return fetch(newRequest); - } catch (e) { - shouldLogout = true; + // Use Axios as the fetch implementation with correct signature + fetch: async (input: Request) => { + try { + // Extract request details from the Request object + const url = input.url; + const method = input.method.toLowerCase(); + const headers = Object.fromEntries(input.headers.entries()); + + // Handle request body based on content type + let data = undefined; + if (input.body) { + const contentType = input.headers.get("Content-Type"); + if (contentType?.includes("application/json")) { + data = await input.json(); + } else { + data = await input.text(); } - } else { - shouldLogout = true; } - if (shouldLogout) { - localStorage.removeItem("accessToken"); - localStorage.removeItem("refreshToken"); - store.dispatch(authActions.setStatus("unauthenticated")); - return response; - } - } - - // Try to parse error message from response - let errorData; - try { - errorData = await response.clone().json(); - } catch { - errorData = null; + // Convert fetch API request to Axios format + const axiosConfig = { + url: url.replace(import.meta.env.VITE_API_BASE_URL, ""), // Remove base URL as axios config already has it + method, + headers, + data, + }; + + // Use our configured axios instance to make the request + const response = await axios.request(axiosConfig); + + // Convert Axios response format back to fetch Response format + return new Response(JSON.stringify(response.data), { + status: response.status || 200, + statusText: response.statusText || "", + }); + } catch (error) { + // Axios error handling is already done in axios.config.ts + // Here we just convert it to a Response object for openapi-fetch + const axiosError = error as AxiosError; + return new Response( + JSON.stringify( + axiosError?.response?.data ?? { error: "Network Error" } + ), + { + status: axiosError?.response?.status || 500, + statusText: axiosError?.response?.statusText || "Error", + } + ); } - - globalErrorHandler(errorData?.error || errorData); - return response; }, +}); +apiClient.use({ async onError({ error }) { globalErrorHandler(error); }, @@ -106,7 +81,10 @@ export async function withApiResponseHandling( const { data, error, response } = await request; if (error || data?.error || !data?.data) { - throw error ?? data?.error; + throw { + message: (error as any)?.error ?? error ?? data?.error, + name: "AxiosError", + }; } const result = { diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 901738a..22938dd 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -12,7 +12,7 @@ async function refreshToken(refreshToken: string) { const { data } = await withApiResponseHandling( apiClient.POST("/auth/refresh", { headers: { - Authorization: `Bearer ${refreshToken}`, + authorization: `Bearer ${refreshToken}`, }, }) ); From b9e79b99bcf4278174665ed88d5e669f6415c7f5 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Wed, 9 Apr 2025 16:20:14 +0500 Subject: [PATCH 09/19] feat: users and posts tables --- index.html | 4 +- .../server-paginated-table.tsx | 5 +- .../layouts/dashboard-layout/main-menu.tsx | 7 +- src/lib/router/router-config.tsx | 28 ++++++-- src/lib/router/router.tsx | 20 ++++-- src/lib/translations/en-US.json | 4 +- src/lib/types/api.ts | 17 +++-- src/lib/types/openapi-fetch.d.ts | 15 ++-- src/lib/utils/colors.ts | 45 ++++++++++++ src/lib/utils/dateTime.utils.ts | 16 +++++ src/lib/utils/number.utils.ts | 3 + src/lib/utils/string.utils.ts | 28 ++++++++ src/main.tsx | 9 +-- src/pages/posts/posts.tsx | 65 +++++++++++++++++ src/pages/users/components/user-avatar.tsx | 23 ++++++ src/pages/users/users.tsx | 72 +++++++++++++++++++ tailwind.config.js | 1 + 17 files changed, 325 insertions(+), 37 deletions(-) create mode 100644 src/lib/utils/colors.ts create mode 100644 src/lib/utils/dateTime.utils.ts create mode 100644 src/lib/utils/number.utils.ts create mode 100644 src/pages/posts/posts.tsx create mode 100644 src/pages/users/components/user-avatar.tsx create mode 100644 src/pages/users/users.tsx diff --git a/index.html b/index.html index c00a48b..47e70af 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + @@ -15,7 +15,7 @@ Rayon React Starter diff --git a/src/lib/components/server-paginated-table/server-paginated-table.tsx b/src/lib/components/server-paginated-table/server-paginated-table.tsx index 58e8ece..2a50196 100644 --- a/src/lib/components/server-paginated-table/server-paginated-table.tsx +++ b/src/lib/components/server-paginated-table/server-paginated-table.tsx @@ -163,7 +163,7 @@ export default function ServerPaginatedTable({ axios .get(`${_url}?${qs.stringify(query)}`) - .then(({ data }) => { + .then(({ data: { data } }) => { const list = ( Array.isArray(data) ? data @@ -206,7 +206,7 @@ export default function ServerPaginatedTable({ ? tableParams["sort.order"] || undefined : col.defaultSortOrder, })); - }, [columns]); + }, [columns, data]); return (
@@ -225,6 +225,7 @@ export default function ServerPaginatedTable({ /> ) : null}
+ { return routes.map((route, i) => { if (!route.menuItem) return null; + + if (route.allowedRoles?.includes(Role.ADMIN)) + route.allowedRoles.push(Role.SUPER_ADMIN); if ( Array.isArray(route.allowedRoles) && - !route.allowedRoles.includes(role!) + !route.allowedRoles.includes(role as Role) ) return null; diff --git a/src/lib/router/router-config.tsx b/src/lib/router/router-config.tsx index 554251e..5f86a27 100644 --- a/src/lib/router/router-config.tsx +++ b/src/lib/router/router-config.tsx @@ -2,7 +2,13 @@ import { Role } from "@/modules/auth/hooks/role.hooks"; import NotFound from "@/pages/404/404"; import Login from "@/pages/auth/login"; import Home from "@/pages/home/home"; -import { DashboardOutlined } from "@ant-design/icons"; +import Posts from "@/pages/posts/posts"; +import Users from "@/pages/users/users"; +import { + BookOutlined, + DashboardOutlined, + UserOutlined, +} from "@ant-design/icons"; import { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { useAppSelector } from "../redux/store"; @@ -96,15 +102,27 @@ export const useRouterConfig = (): RouterConfig[] => { { layoutType: "dashboard", authType: "private", - component: , - allowedRoles: [Role.ADMIN], + component: , menuItem: { - title: "Users", - icon: , + title: t("sidebar:posts"), + icon: , + }, + route: { + path: "/posts", + }, + }, + { + layoutType: "dashboard", + authType: "private", + component: , + menuItem: { + title: t("sidebar:users"), + icon: , }, route: { path: "/users", }, + allowedRoles: [Role.SUPER_ADMIN], }, { layoutType: "auth", diff --git a/src/lib/router/router.tsx b/src/lib/router/router.tsx index 881d8e8..dbd7a80 100644 --- a/src/lib/router/router.tsx +++ b/src/lib/router/router.tsx @@ -8,12 +8,14 @@ import { useAuth } from "@/modules/auth/hooks/auth.hooks"; import { Role, useRole } from "@/modules/auth/hooks/role.hooks"; import { authActions } from "@/modules/auth/slices/auth.slice"; import { profileActions } from "@/modules/auth/slices/profile.slice"; +import NotFound from "@/pages/404/404"; import _ from "lodash"; import React, { PropsWithChildren, ReactNode, useEffect, useLayoutEffect, + useRef, useState, } from "react"; import { @@ -163,10 +165,12 @@ const RoleCheckWrapper: React.FC< PropsWithChildren & { allowedRoles: RouterConfig["allowedRoles"] } > = ({ children, allowedRoles }) => { const [isRendered, setIsRendered] = useState(false); + const isAllowed = useRef(true); const role = useRole(); const dispatch = useAppDispatch(); const { status: authStatus } = useAuth(); - const navigate = useNavigate(); + + if (allowedRoles?.includes(Role.ADMIN)) allowedRoles.push(Role.SUPER_ADMIN); useEffect(() => { if (authStatus === "authenticated") { @@ -181,12 +185,20 @@ const RoleCheckWrapper: React.FC< Array.isArray(allowedRoles) && !allowedRoles.includes(role as Role) ) { - navigate("/not-found"); - setIsRendered(false); + isAllowed.current = false; + setIsRendered(true); } else { setIsRendered(true); } }, [allowedRoles, role, authStatus]); - return isRendered ? children : ; + return isRendered ? ( + isAllowed.current ? ( + children + ) : ( + + ) + ) : ( + + ); }; diff --git a/src/lib/translations/en-US.json b/src/lib/translations/en-US.json index 7b3bdfe..c82c391 100644 --- a/src/lib/translations/en-US.json +++ b/src/lib/translations/en-US.json @@ -9,7 +9,9 @@ }, "sidebar": { - "home": "Home" + "home": "Home", + "users": "Users", + "posts": "Posts" }, "logout": { diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index 42b51f9..24a17e6 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -2,25 +2,30 @@ import { components, operations } from "./openapi-fetch"; export type ApiSchemas = components["schemas"]; -export type ApiQueryParams = +export type ApiQueryParams = NonNullable< operations[T]["parameters"]["query" extends keyof operations[T]["parameters"] ? "query" - : never]; + : never] +>; -export type ApiBody = +export type ApiBody = NonNullable< operations[T]["requestBody"] extends { content: { "application/json": any } } ? operations[T]["requestBody"]["content"]["application/json"] : operations[T]["requestBody"] extends { content: { "multipart/form-data": any }; } ? operations[T]["requestBody"]["content"]["multipart/form-data"] - : never; + : never +>; -export type ApiResponse = +export type ApiResponse = NonNullable< operations[T]["responses"] extends { "200": { content: { "application/json": { data: any } } }; } ? operations[T]["responses"]["200"]["content"]["application/json"]["data"] - : never; + : never +>; export type Profile = ApiResponse<"ProfileFetch">; +export type Post = ApiResponse<"PostFetch">; +export type User = ApiResponse<"UserFetch">; diff --git a/src/lib/types/openapi-fetch.d.ts b/src/lib/types/openapi-fetch.d.ts index 1280568..33f0d1b 100644 --- a/src/lib/types/openapi-fetch.d.ts +++ b/src/lib/types/openapi-fetch.d.ts @@ -443,20 +443,17 @@ export interface components { bio?: string; }; UserUpdate: components["schemas"]["Partial_Omit_UserMutable.email__"]; - "Prisma.postsCreatelabelsInput": { - set: string[]; - }; "Expand_PostUnlinked-and-_author-SanitizedUser__": { + created_at?: string; + updated_at?: string; + id?: string; title: string; - labels?: string[] | components["schemas"]["Prisma.postsCreatelabelsInput"]; slug: string; text: string; author_id: string; - id?: string; - created_at?: string; - updated_at?: string; /** Format: double */ views?: number; + labels: string[]; author: components["schemas"]["SanitizedUser"]; }; PostType: components["schemas"]["Expand_PostUnlinked-and-_author-SanitizedUser__"]; @@ -485,13 +482,13 @@ export interface components { }; "Expand_Omit_PostMutable.author_id__": { title: string; - labels?: string[] | components["schemas"]["Prisma.postsCreatelabelsInput"]; + labels: string[]; text: string; }; PostCreate: components["schemas"]["Expand_Omit_PostMutable.author_id__"]; Expand_Partial_PostMutable__: { title?: string; - labels?: string[] | components["schemas"]["Prisma.postsCreatelabelsInput"]; + labels?: string[]; text?: string; author_id?: string; }; diff --git a/src/lib/utils/colors.ts b/src/lib/utils/colors.ts new file mode 100644 index 0000000..1a9a415 --- /dev/null +++ b/src/lib/utils/colors.ts @@ -0,0 +1,45 @@ +import { stringToNumberInRange } from "./string.utils"; + +export const colors = [ + "#FF5733", + "#00AABB", + "#FFC300", + "#FF0066", + "#22DD55", + "#FF3399", + "#FF9900", + "#44FFAA", + "#FF3366", + "#0088FF", + "#FFCC00", + "#DD33FF", + "#33FF99", + "#FF0033", + "#55AAFF", + "#FF6600", + "#FF00CC", + "#11FF44", + "#FF9933", + "#0099FF", +]; + +export const getColorFromStr = (value: string) => { + return colors[stringToNumberInRange(value, 0, colors.length - 1)]; +}; + +export const fadeColor = (hex: string, opacity: number) => { + // Remove the '#' character if present + hex = hex.replace(/^#/, ""); + + // Parse the hex value into individual RGB components + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + + // Create the faded color in RGBA format + return `rgba(${r}, ${g}, ${b}, ${opacity})`; +}; + +export const getRandomColor = () => + colors[Math.floor(Math.random() * colors.length)]; diff --git a/src/lib/utils/dateTime.utils.ts b/src/lib/utils/dateTime.utils.ts new file mode 100644 index 0000000..145d88e --- /dev/null +++ b/src/lib/utils/dateTime.utils.ts @@ -0,0 +1,16 @@ +import dayjs from "dayjs"; +import { getRootContextValues } from "../contexts/root.context"; + +export const formattedDate = (date: dayjs.ConfigType) => { + return dayjs(date).toDate().toLocaleDateString(getRootContextValues().lang); +}; + +export const formattedTime = (date: dayjs.ConfigType) => { + return dayjs(date).toDate().toLocaleTimeString(getRootContextValues().lang); +}; + +export const formattedDateTime = (date: dayjs.ConfigType) => { + return date + ? dayjs(date).toDate().toLocaleString(getRootContextValues().lang) + : "-"; +}; diff --git a/src/lib/utils/number.utils.ts b/src/lib/utils/number.utils.ts new file mode 100644 index 0000000..7f50aa0 --- /dev/null +++ b/src/lib/utils/number.utils.ts @@ -0,0 +1,3 @@ +export const formattedNumber = (value: number) => { + return new Intl.NumberFormat("en-US").format(value); +}; diff --git a/src/lib/utils/string.utils.ts b/src/lib/utils/string.utils.ts index 38effd1..9057822 100644 --- a/src/lib/utils/string.utils.ts +++ b/src/lib/utils/string.utils.ts @@ -6,6 +6,13 @@ export const capitalize = (s: string) => { .join(" "); }; +export const kebabCaseToWords = (str: string) => { + return str + .split("-") + .map((word) => capitalize(word)) + .join(" "); +}; + export const randString = (len: number = 8) => window .btoa( @@ -15,3 +22,24 @@ export const randString = (len: number = 8) => ) .replace(/[+/]/g, "") .substring(0, len); + +export const stringToNumberInRange = ( + text: string, + min: number, + max: number +) => { + // Calculate a hash value from the input string + let hash = 0; + for (let i = 0; i < text.length; i++) { + hash += text.charCodeAt(i); + } + + // Map the hash value to the desired range + const range = max - min + 1; + const mappedValue = ((hash % range) + range) % range; + + // Add the minimum value to get the final result within the range + const result = min + mappedValue; + + return result; +}; diff --git a/src/main.tsx b/src/main.tsx index 85b14b2..5cddea1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,11 @@ import "@/lib/styles/global.css"; -import React from "react"; import ReactDOM from "react-dom/client"; import { Provider } from "react-redux"; import { store } from "./lib/redux/store"; import Router from "./lib/router/router"; ReactDOM.createRoot(document.getElementById("root")!).render( - - - - - + + + ); diff --git a/src/pages/posts/posts.tsx b/src/pages/posts/posts.tsx new file mode 100644 index 0000000..922441c --- /dev/null +++ b/src/pages/posts/posts.tsx @@ -0,0 +1,65 @@ +import PageHeading from "@/lib/components/page-heading/page-heading"; +import ServerPaginatedTable from "@/lib/components/server-paginated-table/server-paginated-table"; +import { useLang } from "@/lib/contexts/root.context"; +import { Post } from "@/lib/types/api"; +import { getColorFromStr } from "@/lib/utils/colors"; +import { formattedDateTime } from "@/lib/utils/dateTime.utils"; +import { Tag, Typography } from "antd"; +import React from "react"; +import UserAvatar from "../users/components/user-avatar"; + +interface Props {} + +const Posts: React.FC = () => { + const { t } = useLang(); + + return ( +
+ {t("sidebar:posts")} + + url="posts?populate=true" + columns={[ + { + title: "Date", + dataIndex: "created_at", + render: (_, record) => ( + + {formattedDateTime(record.created_at)} + + ), + }, + { + title: "Author", + dataIndex: "author", + render: (_, record) => , + }, + { title: "Title", dataIndex: "title", ellipsis: true }, + { + title: "Labels", + dataIndex: "labels", + render: (_, record) => + record.labels.map((label, ix) => ( + + {label} + + )), + }, + { title: "Views", dataIndex: "views" }, + { + title: "Content", + dataIndex: "text", + width: 400, + render: (_, record) => ( + + {record.text.slice(0, 110)} + {record.text.length > 110 ? "..." : ""} + + ), + }, + ]} + /> +
+ ); +}; + +export default Posts; diff --git a/src/pages/users/components/user-avatar.tsx b/src/pages/users/components/user-avatar.tsx new file mode 100644 index 0000000..df7ef29 --- /dev/null +++ b/src/pages/users/components/user-avatar.tsx @@ -0,0 +1,23 @@ +import { User } from "@/lib/types/api"; +import { Avatar, Space, Typography } from "antd"; +import React from "react"; + +interface Props { + user: Pick; +} + +const UserAvatar: React.FC = ({ user }) => { + return ( + + +
+ {user.name} + + {user.email} + +
+
+ ); +}; + +export default UserAvatar; diff --git a/src/pages/users/users.tsx b/src/pages/users/users.tsx new file mode 100644 index 0000000..6c04077 --- /dev/null +++ b/src/pages/users/users.tsx @@ -0,0 +1,72 @@ +import PageHeading from "@/lib/components/page-heading/page-heading"; +import ServerPaginatedTable from "@/lib/components/server-paginated-table/server-paginated-table"; +import { useLang } from "@/lib/contexts/root.context"; +import { User } from "@/lib/types/api"; +import { formattedDateTime } from "@/lib/utils/dateTime.utils"; +import { kebabCaseToWords } from "@/lib/utils/string.utils"; +import { CheckCircleFilled } from "@ant-design/icons"; +import { Tag, Typography } from "antd"; +import React from "react"; +import UserAvatar from "./components/user-avatar"; + +interface Props {} + +const Users: React.FC = () => { + const { t } = useLang(); + + return ( +
+ {t("sidebar:users")} + + url="users" + columns={[ + { + title: "Creation Date", + dataIndex: "created_at", + render: (_, record) => ( + + {formattedDateTime(record.created_at)} + + ), + }, + { + title: "User", + dataIndex: "name", + render: (_, record) => , + }, + { + title: "Role", + dataIndex: "role", + render: (_, record) => ( + {kebabCaseToWords(record.role ?? "")} + ), + }, + { + title: "Bio", + dataIndex: "bio", + width: 300, + render: (_, record) => + record.bio ? ( + + {record.bio.slice(0, 110)} + {record.bio.length > 110 ? "..." : ""} + + ) : ( + "" + ), + }, + { + title: "Verified", + dataIndex: "email_verified", + render: (_, record) => + record.email_verified ? ( + + ) : null, + }, + ]} + /> +
+ ); +}; + +export default Users; diff --git a/tailwind.config.js b/tailwind.config.js index cb608ee..57c5d44 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -7,6 +7,7 @@ export default { colors: { primary: "#DAA520", text: "#333", + textSecondary: "#777", }, }, }, From 96bf46430e85abe0d8c7764018121911cd786a77 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Wed, 9 Apr 2025 16:37:18 +0500 Subject: [PATCH 10/19] fix: typescript errors --- .eslintrc.cjs | 2 ++ .../components/server-paginated-select/lazy-select.tsx | 10 ++-------- .../server-paginated-select.tsx | 7 +++---- src/lib/contexts/root.context.tsx | 6 +++--- src/lib/redux/createSubscriptions.ts | 6 +++--- src/lib/redux/enhancers/status.enhancer.ts | 4 ++-- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f1a7f53..d16048e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -16,5 +16,7 @@ module.exports = { { allowConstantExport: true }, ], "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/ban-ts-comment": "off", }, }; diff --git a/src/lib/components/server-paginated-select/lazy-select.tsx b/src/lib/components/server-paginated-select/lazy-select.tsx index a49c380..0dc9f65 100644 --- a/src/lib/components/server-paginated-select/lazy-select.tsx +++ b/src/lib/components/server-paginated-select/lazy-select.tsx @@ -1,6 +1,6 @@ import { Select, Skeleton } from "antd"; -import { useCallback, useEffect, useRef } from "react"; import { debounce } from "lodash"; +import { useCallback, useEffect, useRef } from "react"; type LazySelectProps = { style?: React.CSSProperties; @@ -12,11 +12,8 @@ type LazySelectProps = { debouncing?: number; loading?: boolean; skeletonProps?: any; - optionContainerStyle?: React.CSSProperties; - containerStyle?: React.CSSProperties; valueResolver?: (item: any) => any; idResolver?: (value: any) => any; - optionProps?: any; onChange?: (value: any) => void; value?: any; defaultValue?: any; @@ -32,11 +29,8 @@ function LazySelect({ debouncing = 800, loading, skeletonProps, - optionContainerStyle, - containerStyle, valueResolver, //how to get value from item when onChange is called idResolver, //how to get unique id from above value (valueResolver) - optionProps, onChange, value, defaultValue, @@ -116,7 +110,7 @@ function LazySelect({ style={style} value={_idResolver(value)} defaultValue={_idResolver(defaultValue)} - // @ts-ignore + // @ts-expect-error options={data.map((item, ix) => { const value = item._loading ? ix + "loading" : item.id; const children: any = diff --git a/src/lib/components/server-paginated-select/server-paginated-select.tsx b/src/lib/components/server-paginated-select/server-paginated-select.tsx index fe7287c..a21da19 100644 --- a/src/lib/components/server-paginated-select/server-paginated-select.tsx +++ b/src/lib/components/server-paginated-select/server-paginated-select.tsx @@ -65,14 +65,13 @@ export default function ServerPaginatedSelect({ const list = Array.isArray(data) ? data : Array.isArray(data.docs) - ? data.docs - : []; + ? data.docs + : []; if (!pageSize || list.length < pageSize) setNoMore(true); setData((prev) => uniqBy(prev.concat(list), "id") as any[]); }) - //@ts-ignore .catch(console.error) .finally(() => setLoading(false)); }; @@ -138,7 +137,7 @@ export default function ServerPaginatedSelect({ }} loading={loading} dataSource={_data} - // @ts-ignore + // @ts-expect-error renderItem={renderItem} valueResolver={valueResolver} style={style} diff --git a/src/lib/contexts/root.context.tsx b/src/lib/contexts/root.context.tsx index a187b1a..07fda3a 100644 --- a/src/lib/contexts/root.context.tsx +++ b/src/lib/contexts/root.context.tsx @@ -62,7 +62,7 @@ const translations = { }, }; -const fallbackLng = Object.values(Lang)[0]; +const fallbackLng = Object.values(Lang)[0]!; const initialLang = (localStorage.getItem(langKey) || fallbackLng) as Lang; i18n.use(initReactI18next).init({ @@ -203,12 +203,12 @@ const RootContextProvider: React.FC = ({ children }) => { if (accept) ip.setAttribute("accept", accept); if (multiple) ip.setAttribute("multiple", multiple.toString()); if (onChange) { - //@ts-ignore + //@ts-expect-error ip.onchange = (e: { target: { files: File[] } }) => { const files = []; for (const file of e.target.files) files.push(file); typeof onChange === "function" && files.length && onChange(files); - //@ts-ignore + //@ts-expect-error e.target.value = ""; }; } diff --git a/src/lib/redux/createSubscriptions.ts b/src/lib/redux/createSubscriptions.ts index 41e97e2..355c997 100644 --- a/src/lib/redux/createSubscriptions.ts +++ b/src/lib/redux/createSubscriptions.ts @@ -16,7 +16,7 @@ const createSubscriptons = ( ) => { const { name, reducer } = slice; slice.reducer = function (state, action) { - for (let key in subObj) { + for (const key in subObj) { if (action.type === `${name}/_register_${key}_unsubscriber_`) { return { ...state, [`__unsubscriber_${key}_`]: action.payload }; } @@ -25,7 +25,7 @@ const createSubscriptons = ( }; const res: GenericObject = {}; - for (let key in subObj) { + for (const key in subObj) { res[`${key}Unsub`] = createAsyncThunk( `${name}/unsub_${key}`, (payload, thunksOptions) => { @@ -41,7 +41,7 @@ const createSubscriptons = ( `${name}/sub_${key}`, (payload, thunkOptions) => { store.dispatch(res[`${key}Unsub`]()).then(() => { - const unsubscriber = subObj[key](payload, thunkOptions as any); + const unsubscriber = subObj[key]?.(payload, thunkOptions as any); if (typeof unsubscriber === "function") store.dispatch({ type: `${name}/_register_${key}_unsubscriber_`, diff --git a/src/lib/redux/enhancers/status.enhancer.ts b/src/lib/redux/enhancers/status.enhancer.ts index 2eaf3b5..ef4002e 100644 --- a/src/lib/redux/enhancers/status.enhancer.ts +++ b/src/lib/redux/enhancers/status.enhancer.ts @@ -10,7 +10,7 @@ import { import { Reducer } from "react"; import { ThunkStatus } from "../../types/misc"; -//@ts-ignore +//@ts-expect-error export const statusHandlerEnahncer: StoreEnhancer<{}, {}> = (cs: typeof createStore) => ( @@ -23,7 +23,7 @@ export const statusHandlerEnahncer: StoreEnhancer<{}, {}> = //get slicename and type value from action.type const split = action.type.split("/"); - const sliceName = split[0]; + const sliceName = split[0]!; const type = split[1]; let status: ThunkStatus | undefined; From a1f88891d62f2bd439a6b7e0f3639b78425a1be8 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Wed, 16 Apr 2025 15:18:28 +0500 Subject: [PATCH 11/19] file-picker, server-img, hover, server-paginated-select components --- .../ellipsis-text/ellipsis-text.tsx | 16 +++ .../components/file-picker/file-picker.tsx | 78 +++++++++++++ src/lib/components/hover/hover.tsx | 49 ++++++++ src/lib/components/server-img/server-img.tsx | 107 ++++++++++++++++++ .../server-paginated-select.tsx | 3 +- src/lib/utils/misc.utils.ts | 4 + src/pages/users/components/user-avatar.tsx | 10 +- 7 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 src/lib/components/ellipsis-text/ellipsis-text.tsx create mode 100644 src/lib/components/file-picker/file-picker.tsx create mode 100644 src/lib/components/hover/hover.tsx create mode 100644 src/lib/components/server-img/server-img.tsx diff --git a/src/lib/components/ellipsis-text/ellipsis-text.tsx b/src/lib/components/ellipsis-text/ellipsis-text.tsx new file mode 100644 index 0000000..619ee0e --- /dev/null +++ b/src/lib/components/ellipsis-text/ellipsis-text.tsx @@ -0,0 +1,16 @@ +import { Tooltip, Typography } from "antd"; +import React from "react"; + +interface Props { + showTooltip?: boolean; +} + +const EllipsisText: React.FC< + Props & React.ComponentProps +> = ({ showTooltip = true, ...props }) => { + const node = ; + + return showTooltip ? {node} : node; +}; + +export default EllipsisText; diff --git a/src/lib/components/file-picker/file-picker.tsx b/src/lib/components/file-picker/file-picker.tsx new file mode 100644 index 0000000..071f8a9 --- /dev/null +++ b/src/lib/components/file-picker/file-picker.tsx @@ -0,0 +1,78 @@ +import { useRootContextValues } from "@/lib/contexts/root.context"; +import { fileNameFromUrl } from "@/lib/utils/misc.utils"; +import { CloseCircleOutlined, UploadOutlined } from "@ant-design/icons"; +import { Button, Space, Typography } from "antd"; +import React, { useMemo } from "react"; + +type ConsistentFile = { + src: string; + name: string; +}; + +type AnyFile = File | string; + +type Props = { + value?: AnyFile[]; + count?: number; + onChange?: (files: AnyFile[]) => void; + accept?: string; + loading?: boolean; +}; + +function toConsistentFile(file: AnyFile): ConsistentFile { + return { + src: typeof file === "string" ? file : URL.createObjectURL(file), + name: typeof file === "string" ? fileNameFromUrl(file)! : file.name, + }; +} + +const FilePicker: React.FC = ({ + value, + count, + onChange, + accept, + loading, +}) => { + const { openFile } = useRootContextValues(); + const _value = useMemo(() => value?.map(toConsistentFile) || [], [value]); + + return ( +
+ + {_value.map((file, ix) => ( + + {file && ( + file.src && window.open(file.src, "_blank")} + className="text-xs" + > + {file.name || "Unnamed File"} + + )} + + onChange?.(_value.filter((_, _ix) => ix !== _ix) as any) + } + /> + + ))} +
+ ); +}; + +export default FilePicker; diff --git a/src/lib/components/hover/hover.tsx b/src/lib/components/hover/hover.tsx new file mode 100644 index 0000000..4ba2f67 --- /dev/null +++ b/src/lib/components/hover/hover.tsx @@ -0,0 +1,49 @@ +import React, { + CSSProperties, + ReactElement, + useCallback, + useState, +} from "react"; + +interface HoverProps { + element?: string | React.ElementType; + style?: CSSProperties; + hoverStyle?: CSSProperties; + children?: ReactElement; + onHover?: (isHovered: boolean) => void; +} + +export default function Hover({ + element, + style = {}, + hoverStyle = {}, + children, + onHover, +}: HoverProps) { + const [appliedStyle, setAppliedStyle] = useState(style); + + const onMouseOver = useCallback(() => { + setAppliedStyle({ ...style, ...hoverStyle }); + typeof onHover === "function" && onHover(true); + }, [style, hoverStyle, onHover]); + + const onMouseOut = useCallback(() => { + setAppliedStyle(style); + typeof onHover === "function" && onHover(false); + }, [style, onHover]); + + const elementType = element || "div"; + const props = { + style: { + ...(children?.props?.style || {}), + ...appliedStyle, + }, + onMouseOver, + onMouseOut, + }; + const childContent = element ? children : children?.props?.children; + + return typeof element === "string" + ? React.createElement(elementType, props, childContent) + : React.cloneElement(children as ReactElement, props); +} diff --git a/src/lib/components/server-img/server-img.tsx b/src/lib/components/server-img/server-img.tsx new file mode 100644 index 0000000..bbd93e7 --- /dev/null +++ b/src/lib/components/server-img/server-img.tsx @@ -0,0 +1,107 @@ +import { randString } from "@/lib/utils/string.utils"; +import { cn } from "@/lib/utils/styles.utils"; +import { AvatarProps, Image, Skeleton, Spin } from "antd"; +import { useRef, useState } from "react"; + +export const placeholderImg = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg=="; + +type ServerImgProps = { + loader?: { + type: "skeleton" | "spin" | React.ReactNode; + shape: AvatarProps["shape"]; + }; + defaultWidth?: number; + defaultHeight?: number; + fallback?: string; + id?: string; + onLoad?: () => void; + preview?: boolean; + style?: React.CSSProperties; + className?: string; + src?: string; +}; + +export default function ServerImg({ + loader = { type: "skeleton", shape: "square" }, + defaultWidth = 400, + defaultHeight = 250, + fallback = placeholderImg, + preview = true, + ...props +}: ServerImgProps) { + const [loading, setLoading] = useState(true); + const id = useRef(props.id || randString(8)).current; + + const onLoad = () => { + setLoading(false); + if (typeof props.onLoad === "function") props.onLoad(); + }; + + return ( + <> + {loading ? ( + loader.type === "skeleton" ? ( + + ) : loader.type === "spin" ? ( +
+ +
+ ) : ( + loader + ) + ) : null} +
+ +
+ + ); +} diff --git a/src/lib/components/server-paginated-select/server-paginated-select.tsx b/src/lib/components/server-paginated-select/server-paginated-select.tsx index a21da19..1dcbca7 100644 --- a/src/lib/components/server-paginated-select/server-paginated-select.tsx +++ b/src/lib/components/server-paginated-select/server-paginated-select.tsx @@ -18,7 +18,7 @@ type ServerPaginatedSelectProps = React.ComponentProps & { value?: any; dataSource?: (data: any[]) => any[]; searchDebouncing?: number; - fetchDefaultValue: (value: any) => Promise; + fetchDefaultValue?: (value: any) => Promise; }; export default function ServerPaginatedSelect({ @@ -72,6 +72,7 @@ export default function ServerPaginatedSelect({ setData((prev) => uniqBy(prev.concat(list), "id") as any[]); }) + //@ts-ignore .catch(console.error) .finally(() => setLoading(false)); }; diff --git a/src/lib/utils/misc.utils.ts b/src/lib/utils/misc.utils.ts index db35c8b..0247f99 100644 --- a/src/lib/utils/misc.utils.ts +++ b/src/lib/utils/misc.utils.ts @@ -18,3 +18,7 @@ export function isDev() { export function isNullish(value: any) { return [null, undefined, ""].includes(value); } + +export const fileNameFromUrl = (url: string) => { + return decodeURIComponent(url.split("?")?.[0]?.split("/").pop() || ""); +}; diff --git a/src/pages/users/components/user-avatar.tsx b/src/pages/users/components/user-avatar.tsx index df7ef29..d3803f4 100644 --- a/src/pages/users/components/user-avatar.tsx +++ b/src/pages/users/components/user-avatar.tsx @@ -1,5 +1,6 @@ +import ServerImg from "@/lib/components/server-img/server-img"; import { User } from "@/lib/types/api"; -import { Avatar, Space, Typography } from "antd"; +import { Space, Typography } from "antd"; import React from "react"; interface Props { @@ -9,7 +10,12 @@ interface Props { const UserAvatar: React.FC = ({ user }) => { return ( - +
{user.name} From 344d7e899239f50b989b2cf60f3f80be16328ce8 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Mon, 21 Apr 2025 15:49:33 +0500 Subject: [PATCH 12/19] feat: add settings page and update profile service with new update method feat: enhance user avatar component to display user role feat: implement image utility functions and error suppression utility fix: update validation rules for required fields chore: add danger color to tailwind configuration --- .eslintrc.cjs | 2 + scripts/gen-openapi-types.ts | 39 ++++- .../components/file-picker/file-picker.tsx | 2 +- .../components/image-picker/image-picker.tsx | 141 ++++++++++++++++++ src/lib/layouts/dashboard-layout/header.tsx | 14 +- src/lib/openapi-fetch.config.ts | 4 +- src/lib/router/router-config.tsx | 9 ++ src/lib/translations/en-US.json | 4 + src/lib/types/api.ts | 24 ++- src/lib/types/openapi-fetch.d.ts | 30 ++-- src/lib/utils/error.utils.ts | 2 + src/lib/utils/image.utils.ts | 11 ++ src/lib/utils/misc.utils.ts | 32 +++- src/lib/utils/validations.ts | 14 +- src/modules/auth/services/profile.service.ts | 13 ++ src/modules/auth/slices/profile.slice.ts | 14 +- src/modules/auth/types/auth.types.ts | 3 + src/pages/auth/login.tsx | 6 +- src/pages/settings/settings.tsx | 113 ++++++++++++++ src/pages/users/components/user-avatar.tsx | 15 +- tailwind.config.js | 1 + 21 files changed, 429 insertions(+), 64 deletions(-) create mode 100644 src/lib/components/image-picker/image-picker.tsx create mode 100644 src/lib/utils/image.utils.ts create mode 100644 src/modules/auth/types/auth.types.ts create mode 100644 src/pages/settings/settings.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d16048e..eddf588 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -18,5 +18,7 @@ module.exports = { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/ban-types": "off", "@typescript-eslint/ban-ts-comment": "off", + "no-useless-escape": "off", + "no-empty": "off", }, }; diff --git a/scripts/gen-openapi-types.ts b/scripts/gen-openapi-types.ts index 620936f..3de87d9 100644 --- a/scripts/gen-openapi-types.ts +++ b/scripts/gen-openapi-types.ts @@ -1,12 +1,20 @@ #!/usr/bin/env tsx - -import { execSync } from "child_process"; import * as fs from "fs"; import * as http from "http"; import * as https from "https"; +import openapiTS, { astToString } from "openapi-typescript"; import * as path from "path"; +import ts from "typescript"; import { URL } from "url"; +const DATE = ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("Date") +); // `Date` +const FILE = ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("File") +); // `Blob +const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null` + // Get environment type from command line arguments const envType = process.argv[2]; @@ -87,9 +95,32 @@ async function main(): Promise { // Generate TypeScript types console.log("Generating TypeScript types"); - execSync(`npx openapi-typescript ${tempFilePath} -o ${outputPath}`, { - stdio: "inherit", + const openApiSchema = fs.readFileSync(tempFilePath, "utf8"); + const ast = await openapiTS(openApiSchema, { + transform(schemaObject) { + // handle date-time type + if (schemaObject.format === "date-time") { + return { + schema: schemaObject.nullable + ? ts.factory.createUnionTypeNode([DATE, NULL]) + : DATE, + questionToken: true, + }; + } + + // handle File type + if (schemaObject.format === "binary") { + return { + schema: schemaObject.nullable + ? ts.factory.createUnionTypeNode([FILE, NULL]) + : FILE, + questionToken: true, + }; + } + }, }); + const contents = astToString(ast); + fs.writeFileSync(outputPath, contents); // Clean up fs.unlinkSync(tempFilePath); diff --git a/src/lib/components/file-picker/file-picker.tsx b/src/lib/components/file-picker/file-picker.tsx index 071f8a9..fac9cbd 100644 --- a/src/lib/components/file-picker/file-picker.tsx +++ b/src/lib/components/file-picker/file-picker.tsx @@ -64,7 +64,7 @@ const FilePicker: React.FC = ({ )} onChange?.(_value.filter((_, _ix) => ix !== _ix) as any) } diff --git a/src/lib/components/image-picker/image-picker.tsx b/src/lib/components/image-picker/image-picker.tsx new file mode 100644 index 0000000..f3c975b --- /dev/null +++ b/src/lib/components/image-picker/image-picker.tsx @@ -0,0 +1,141 @@ +import { useRootContextValues } from "@/lib/contexts/root.context"; +import { isImage } from "@/lib/utils/image.utils"; +import { arrayExtend } from "@/lib/utils/misc.utils"; +import { cn } from "@/lib/utils/styles.utils"; +import { CloseCircleFilled } from "@ant-design/icons"; +import { useUpdateEffect } from "ahooks"; +import { List } from "antd"; +import { useEffect, useMemo, useState } from "react"; +import Hover from "../hover/hover"; +import ServerImg, { placeholderImg } from "../server-img/server-img"; + +function urlToImg(url: string) { + return { + src: url, + type: "image/", + }; +} + +type ImagePickerProps = { + count: number; + width?: number; + height?: number; + gutter?: number; + listProps?: React.ComponentProps; + imgProps?: React.ComponentProps; + onChange?: (images: any[]) => void; + value?: string[]; + editable?: boolean; +}; + +export default function ImagePicker({ + count, + width = 120, + height = 120, + gutter = 16, + listProps = {}, + imgProps = {}, + onChange = () => {}, + value, + editable = true, +}: ImagePickerProps) { + const _value = useMemo( + () => + value?.map((item) => (typeof item === "string" ? urlToImg(item) : item)), + [value] + ); + + const { openFile } = useRootContextValues(); + const [images, setImages] = useState( + _value ? arrayExtend(_value, count) : Array(count).fill({}) + ); + + useUpdateEffect(() => { + onChange && onChange(images.filter(isImage)); + }, [images]); + + useEffect(() => { + if ( + Array.isArray(_value) && + JSON.stringify(arrayExtend(_value, count)) !== JSON.stringify(images) + ) + setImages(arrayExtend(_value, count)); + }, [_value]); + + const fillImagesArr = (files: File[], start: number) => { + const newImages = [...images]; + let count = 0, + i = 0; + while (count < newImages.length && i < files.length) { + const ix = (start + count) % newImages.length; + if (!isImage(newImages[ix])) { + newImages[ix] = files[i]; + newImages[ix].src = URL.createObjectURL(files[i]!); + i++; + } + count++; + } + setImages(newImages); + }; + + const onClose = (i: number) => { + const newImages = images.map((img, ix) => (ix === i ? {} : img)); + setImages(newImages); + }; + + return ( + { + const isImageValid = isImage(img as any); + return ( + + !isImageValid && + editable && + openFile({ + onChange: (files) => fillImagesArr(files, i), + accept: "image/*", + }) + } + > + + {isImageValid && editable && ( + + onClose(i)} + className={cn( + "text-danger absolute text-base transition-transform duration-300", + imgProps.loader?.shape === "circle" + ? "right-[24px] top-[1px]" + : "right-0 top-[-8px]" + )} + /> + + )} + + ); + }} + {...listProps} + /> + ); +} diff --git a/src/lib/layouts/dashboard-layout/header.tsx b/src/lib/layouts/dashboard-layout/header.tsx index d509d2a..d27072e 100644 --- a/src/lib/layouts/dashboard-layout/header.tsx +++ b/src/lib/layouts/dashboard-layout/header.tsx @@ -3,17 +3,10 @@ import { useLang, useThemeMode } from "@/lib/contexts/root.context"; import { useAppDispatch, useAppSelector } from "@/lib/redux/store"; import { useAuth } from "@/modules/auth/hooks/auth.hooks"; import { authActions } from "@/modules/auth/slices/auth.slice"; +import UserAvatar from "@/pages/users/components/user-avatar"; import { DownOutlined, MenuOutlined } from "@ant-design/icons"; import { useResponsive } from "ahooks"; -import { - Avatar, - Dropdown, - Layout, - Select, - Space, - Tooltip, - Typography, -} from "antd"; +import { Dropdown, Layout, Select, Space, Tooltip } from "antd"; import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -127,8 +120,7 @@ function Header() { trigger={["click"]} > - - {profile.name} + diff --git a/src/lib/openapi-fetch.config.ts b/src/lib/openapi-fetch.config.ts index 32e5d75..c65cfa4 100644 --- a/src/lib/openapi-fetch.config.ts +++ b/src/lib/openapi-fetch.config.ts @@ -24,8 +24,10 @@ const apiClient = createClient({ const contentType = input.headers.get("Content-Type"); if (contentType?.includes("application/json")) { data = await input.json(); + } else if (contentType?.includes("multipart/form-data")) { + data = await input.formData(); } else { - data = await input.text(); + data = input.body; } } diff --git a/src/lib/router/router-config.tsx b/src/lib/router/router-config.tsx index 5f86a27..e308bcf 100644 --- a/src/lib/router/router-config.tsx +++ b/src/lib/router/router-config.tsx @@ -3,6 +3,7 @@ import NotFound from "@/pages/404/404"; import Login from "@/pages/auth/login"; import Home from "@/pages/home/home"; import Posts from "@/pages/posts/posts"; +import Settings from "@/pages/settings/settings"; import Users from "@/pages/users/users"; import { BookOutlined, @@ -124,6 +125,14 @@ export const useRouterConfig = (): RouterConfig[] => { }, allowedRoles: [Role.SUPER_ADMIN], }, + { + layoutType: "dashboard", + authType: "private", + component: , + route: { + path: "/settings", + }, + }, { layoutType: "auth", authType: "public", diff --git a/src/lib/translations/en-US.json b/src/lib/translations/en-US.json index c82c391..0775462 100644 --- a/src/lib/translations/en-US.json +++ b/src/lib/translations/en-US.json @@ -17,5 +17,9 @@ "logout": { "title": "Logout", "message": "Are you sure you want to logout?" + }, + + "settings": { + "title": "Settings" } } diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index 24a17e6..75705eb 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -1,28 +1,38 @@ import { components, operations } from "./openapi-fetch"; +type OperationParameters = NonNullable< + operations[T]["parameters"] +>; +type OperationRequestBody = NonNullable< + operations[T]["requestBody"] +>; +type OperationResponse = NonNullable< + operations[T]["responses"] +>; + export type ApiSchemas = components["schemas"]; export type ApiQueryParams = NonNullable< - operations[T]["parameters"]["query" extends keyof operations[T]["parameters"] + OperationParameters["query" extends keyof OperationParameters ? "query" : never] >; export type ApiBody = NonNullable< - operations[T]["requestBody"] extends { content: { "application/json": any } } - ? operations[T]["requestBody"]["content"]["application/json"] - : operations[T]["requestBody"] extends { + OperationRequestBody extends { content: { "application/json": any } } + ? OperationRequestBody["content"]["application/json"] + : OperationRequestBody extends { content: { "multipart/form-data": any }; } - ? operations[T]["requestBody"]["content"]["multipart/form-data"] + ? NonNullable>["content"]["multipart/form-data"] : never >; export type ApiResponse = NonNullable< - operations[T]["responses"] extends { + OperationResponse extends { "200": { content: { "application/json": { data: any } } }; } - ? operations[T]["responses"]["200"]["content"]["application/json"]["data"] + ? OperationResponse["200"]["content"]["application/json"]["data"] : never >; diff --git a/src/lib/types/openapi-fetch.d.ts b/src/lib/types/openapi-fetch.d.ts index 33f0d1b..859e683 100644 --- a/src/lib/types/openapi-fetch.d.ts +++ b/src/lib/types/openapi-fetch.d.ts @@ -1,8 +1,3 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - export interface paths { "/users/{userId}": { parameters: { @@ -391,8 +386,8 @@ export interface components { /** @description From T, pick a set of properties whose keys are in the union K */ "Pick_User.Exclude_keyofUser.password_hash-or-refresh_token_version__": { name: string; - created_at?: string; - updated_at?: string; + created_at?: string | (Date | undefined); + updated_at?: string | (Date | undefined); id?: string; role?: string; email_verified?: boolean; @@ -444,8 +439,8 @@ export interface components { }; UserUpdate: components["schemas"]["Partial_Omit_UserMutable.email__"]; "Expand_PostUnlinked-and-_author-SanitizedUser__": { - created_at?: string; - updated_at?: string; + created_at?: string | (Date | undefined); + updated_at?: string | (Date | undefined); id?: string; title: string; slug: string; @@ -571,8 +566,8 @@ export interface components { }); "Expand_Prisma.notificationsCreateManyInput-and-_metadata_63_-GenericObject__": { id?: string; - created_at?: string; - updated_at?: string; + created_at?: string | (Date | undefined); + updated_at?: string | (Date | undefined); title: string; body: string; image?: string; @@ -613,8 +608,10 @@ export interface components { }; /** @enum {string} */ "Prisma.ModelName": "otps" | "posts" | "users" | "notifications" | "userNotifications"; + /** @enum {string} */ + IMAGE_SIZE: "small" | "medium" | "large"; Resizeconfig: { - sizes: ("small" | "medium" | "large")[]; + sizes: components["schemas"]["IMAGE_SIZE"][]; img_field: string; record_id: string; model: components["schemas"]["Prisma.ModelName"]; @@ -650,7 +647,8 @@ export interface components { email: string; }; AuthChangePass: { - password: string; + newPassword: string; + oldPassword: string; }; /** @description From T, pick a set of properties whose keys are in the union K */ "Pick_AuthLoginResponse.Exclude_keyofAuthLoginResponse.user__": { @@ -845,7 +843,7 @@ export interface operations { added_fcm_token?: string; removed_fcm_token?: string; /** Format: binary */ - photo?: string; + photo?: File; }; }; }; @@ -1148,7 +1146,7 @@ export interface operations { content: { "multipart/form-data": { /** Format: binary */ - file: string; + file?: File; img_sizes?: string; }; }; @@ -1253,7 +1251,7 @@ export interface operations { bio?: string; hcaptcha_token?: string; /** Format: binary */ - photo?: string; + photo?: File; }; }; }; diff --git a/src/lib/utils/error.utils.ts b/src/lib/utils/error.utils.ts index 76f67a9..40ffbbd 100644 --- a/src/lib/utils/error.utils.ts +++ b/src/lib/utils/error.utils.ts @@ -22,3 +22,5 @@ export const globalErrorHandler = (error: any) => { console.error(errorMsg, error); message.error(errorMsg); }; + +export const suppressError = () => {}; diff --git a/src/lib/utils/image.utils.ts b/src/lib/utils/image.utils.ts new file mode 100644 index 0000000..d305a10 --- /dev/null +++ b/src/lib/utils/image.utils.ts @@ -0,0 +1,11 @@ +export type IMAGE_SIZE = "small" | "medium" | "large"; + +export const isImage = (file?: File) => { + return file?.type?.startsWith("image/"); +}; + +export const sizedImg = (obj: T, field: keyof T, size: IMAGE_SIZE) => { + if (!obj) return ""; + + return (obj as any)[`${field.toString()}_sizes`]?.[size] || obj[field]; +}; diff --git a/src/lib/utils/misc.utils.ts b/src/lib/utils/misc.utils.ts index 0247f99..134f679 100644 --- a/src/lib/utils/misc.utils.ts +++ b/src/lib/utils/misc.utils.ts @@ -11,14 +11,38 @@ export const fakeApi = async ( return res; }; -export function isDev() { +export const isDev = () => { return import.meta.env.VITE_ENV === "dev"; -} +}; -export function isNullish(value: any) { +export const isNullish = (value: any) => { return [null, undefined, ""].includes(value); -} +}; export const fileNameFromUrl = (url: string) => { return decodeURIComponent(url.split("?")?.[0]?.split("/").pop() || ""); }; + +export const arrayExtend = (arr: Array, count: number) => { + if (count <= arr.length) return [...arr]; + return [...arr, ...Array(count - arr.length).fill({})]; +}; + +export const objectToFormData = ( + obj?: Record, + formData = new FormData(), + parentKey = "" +) => { + if (!obj) return formData; + + for (const key in obj) { + const propName = parentKey ? `${parentKey}[${key}]` : key; + if (obj[key] === undefined) continue; + if (typeof obj[key] === "object" && !(obj[key] instanceof File)) { + objectToFormData(obj[key], formData, propName); + } else { + formData.append(propName, obj[key]); + } + } + return formData; +}; diff --git a/src/lib/utils/validations.ts b/src/lib/utils/validations.ts index 89ce242..70dcc9c 100755 --- a/src/lib/utils/validations.ts +++ b/src/lib/utils/validations.ts @@ -33,13 +33,13 @@ function getValidator( const Validations = { email: getValidator(isValidEmail, "Email is invalid"), - min_len: (len: number) => (_: any, str: string) => + minLen: (len: number) => (_: any, str: string) => getValidator(isValidMinLength, `Minimum ${len} characters are required`)( undefined, str, len ), - max_len: (len: number) => (_: any, str: string) => + maxLen: (len: number) => (_: any, str: string) => getValidator(isValidMaxLength, `Maximum ${len} characters are allowed`)( undefined, str, @@ -47,14 +47,10 @@ const Validations = { ), name: getValidator(isValidName, "Please enter a valid name"), phone: getValidator(isValidPhone, "Phone number is invalid"), - reqd_msg: (field: string) => `${field} is required`, -}; - -export const requiredRule = (name: string) => { - return { + requiredField: (name: string = "This field") => ({ required: true, - message: Validations.reqd_msg(name), - }; + message: `${name} is required`, + }), }; export default Validations; diff --git a/src/modules/auth/services/profile.service.ts b/src/modules/auth/services/profile.service.ts index e14488b..ffdf860 100644 --- a/src/modules/auth/services/profile.service.ts +++ b/src/modules/auth/services/profile.service.ts @@ -1,12 +1,25 @@ import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; +import { ApiBody } from "@/lib/types/api"; +import { objectToFormData } from "@/lib/utils/misc.utils"; async function fetch() { const { data } = await withApiResponseHandling(apiClient.GET("/profile")); return data; } +async function update(payload: ApiBody<"ProfileUpdate">) { + const { data } = await withApiResponseHandling( + apiClient.PATCH("/profile", { + body: payload, + bodySerializer: objectToFormData, + }) + ); + return data; +} + const profileService = { fetch, + update, }; export default profileService; diff --git a/src/modules/auth/slices/profile.slice.ts b/src/modules/auth/slices/profile.slice.ts index 439bb19..0aa1ac8 100644 --- a/src/modules/auth/slices/profile.slice.ts +++ b/src/modules/auth/slices/profile.slice.ts @@ -16,17 +16,23 @@ const initialState: { const fetch = createAsyncThunk(`${name}/fetch`, profileService.fetch); +const update = createAsyncThunk(`${name}/update`, profileService.update); + //slice export const profileSlice = createSlice({ name, initialState, reducers: {}, extraReducers: (builder) => { - builder.addCase(fetch.fulfilled, (state, action) => { - state.data = action.payload; - }); + builder + .addCase(fetch.fulfilled, (state, action) => { + state.data = action.payload; + }) + .addCase(update.fulfilled, (state, action) => { + state.data = action.payload; + }); }, }); //action creators -export const profileActions = { ...profileSlice.actions, fetch }; +export const profileActions = { ...profileSlice.actions, fetch, update }; diff --git a/src/modules/auth/types/auth.types.ts b/src/modules/auth/types/auth.types.ts new file mode 100644 index 0000000..3e6cbad --- /dev/null +++ b/src/modules/auth/types/auth.types.ts @@ -0,0 +1,3 @@ +import { ApiBody } from "@/lib/types/api"; + +export type AuthChangePasswordBody = ApiBody<"AuthChangePassword">; diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index f1c02b0..35056cd 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -22,7 +22,7 @@ const Login: React.FC = () => { = () => { } diff --git a/src/pages/settings/settings.tsx b/src/pages/settings/settings.tsx new file mode 100644 index 0000000..eef90db --- /dev/null +++ b/src/pages/settings/settings.tsx @@ -0,0 +1,113 @@ +import ImagePicker from "@/lib/components/image-picker/image-picker"; +import PageSpinner from "@/lib/components/spinner/page-spinner"; +import { useAppDispatch, useAppSelector } from "@/lib/redux/store"; +import { Profile } from "@/lib/types/api"; +import { Modify } from "@/lib/types/misc"; +import { suppressError } from "@/lib/utils/error.utils"; +import { sizedImg } from "@/lib/utils/image.utils"; +import Validations from "@/lib/utils/validations"; +import { profileActions } from "@/modules/auth/slices/profile.slice"; +import { AuthChangePasswordBody } from "@/modules/auth/types/auth.types"; +import { Button, Form, Input, message, Typography } from "antd"; +import { omit } from "lodash"; +import React from "react"; + +type ProfileForm = Modify; + +interface Props {} + +const Settings: React.FC = () => { + const user = useAppSelector((state) => state.profile.data); + const dispatch = useAppDispatch(); + + const onUpdateProfile = (values: ProfileForm) => { + const data = omit(values, ["photo"]); + dispatch( + profileActions.update({ + ...data, + photo: values.photo[0] instanceof File ? values.photo[0] : undefined, + }) + ) + .unwrap() + .then(() => message.success("Profile updated successfully")) + .catch(suppressError); + }; + + if (!user) return ; + + return ( +
+ + Update Profile + + + onFinish={onUpdateProfile} + layout="vertical" + initialValues={{ ...user, photo: [sizedImg(user, "photo", "small")] }} + className="w-96" + > + name="photo"> + + + name="name"> + + + name="bio"> + + + + + + + + + Update Password + + layout="vertical" className="w-96"> + + name="oldPassword" + rules={[Validations.requiredField()]} + > + + + + name="newPassword" + rules={[Validations.requiredField()]} + > + + + ({ + validator(_, value) { + if (!value || getFieldValue("newPassword") === value) { + return Promise.resolve(); + } + return Promise.reject("Passwords do not match"); + }, + }), + ]} + > + + + + + + +
+ ); +}; + +export default Settings; diff --git a/src/pages/users/components/user-avatar.tsx b/src/pages/users/components/user-avatar.tsx index d3803f4..5917a5b 100644 --- a/src/pages/users/components/user-avatar.tsx +++ b/src/pages/users/components/user-avatar.tsx @@ -1,13 +1,16 @@ import ServerImg from "@/lib/components/server-img/server-img"; import { User } from "@/lib/types/api"; +import { kebabCaseToWords } from "@/lib/utils/string.utils"; import { Space, Typography } from "antd"; import React from "react"; interface Props { - user: Pick; + user: Pick; + imgPreview?: boolean; + showRole?: boolean; } -const UserAvatar: React.FC = ({ user }) => { +const UserAvatar: React.FC = ({ user, imgPreview, showRole }) => { return ( = ({ user }) => { loader={{ shape: "circle", type: "skeleton" }} defaultHeight={36} defaultWidth={36} + preview={imgPreview} />
- {user.name} +
+ + {user.name} + {showRole ? ` (${kebabCaseToWords(user.role ?? "")})` : ""} + +
{user.email} diff --git a/tailwind.config.js b/tailwind.config.js index 57c5d44..1a2c567 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,6 +8,7 @@ export default { primary: "#DAA520", text: "#333", textSecondary: "#777", + danger: "#EC4949", }, }, }, From e0c41dac68c064957cf9940dbca02c99bc437259 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Mon, 21 Apr 2025 18:16:23 +0500 Subject: [PATCH 13/19] feat: add user and profile types, update services and slices for improved state management --- src/lib/axios.config.ts | 5 +- .../components/custom-modal/custom-modal.tsx | 2 +- .../server-paginated-table.tsx | 2 +- src/lib/layouts/dashboard-layout/header.tsx | 11 ++- src/lib/redux/enhancers/status.enhancer.ts | 11 +++ src/lib/router/router-config.tsx | 88 ++++++++---------- src/modules/auth/services/auth.service.ts | 12 ++- src/modules/auth/services/profile.service.ts | 4 +- src/modules/auth/slices/auth.slice.ts | 15 ++- src/modules/auth/slices/profile.slice.ts | 2 + src/modules/auth/types/auth.types.ts | 2 + src/modules/auth/types/profile.types.ts | 3 + src/modules/users/services/user.service.ts | 0 src/modules/users/slices/user.slice.ts | 0 src/modules/users/types/user.types.ts | 0 src/pages/auth/login.tsx | 12 +-- src/pages/home/home.tsx | 56 ------------ src/pages/posts/posts.tsx | 2 +- src/pages/sample-page/sample-page.tsx | 19 ++++ src/pages/settings/settings.tsx | 51 ++++++++--- src/pages/users/users.tsx | 91 +++++++++++++++++-- 21 files changed, 246 insertions(+), 142 deletions(-) create mode 100644 src/modules/auth/types/profile.types.ts create mode 100644 src/modules/users/services/user.service.ts create mode 100644 src/modules/users/slices/user.slice.ts create mode 100644 src/modules/users/types/user.types.ts delete mode 100644 src/pages/home/home.tsx create mode 100644 src/pages/sample-page/sample-page.tsx diff --git a/src/lib/axios.config.ts b/src/lib/axios.config.ts index 900878f..fd6c2be 100644 --- a/src/lib/axios.config.ts +++ b/src/lib/axios.config.ts @@ -45,7 +45,10 @@ axios.interceptors.response.use(undefined, async (error) => { }${error.message}`; error.message = msg; - if (error.response?.status === 401) { + if ( + store.getState().auth.status !== "unauthenticated" && + error.response?.status === 401 + ) { if ( !["auth/login", "auth/refresh"].some((url) => error.config.url.includes(url) diff --git a/src/lib/components/custom-modal/custom-modal.tsx b/src/lib/components/custom-modal/custom-modal.tsx index 301aff5..04af1ab 100644 --- a/src/lib/components/custom-modal/custom-modal.tsx +++ b/src/lib/components/custom-modal/custom-modal.tsx @@ -7,7 +7,7 @@ interface Props { } const CustomModal: React.FC> = ({ - loading, + loading = false, submitLoading, ...props }) => { diff --git a/src/lib/components/server-paginated-table/server-paginated-table.tsx b/src/lib/components/server-paginated-table/server-paginated-table.tsx index 2a50196..dc37db7 100644 --- a/src/lib/components/server-paginated-table/server-paginated-table.tsx +++ b/src/lib/components/server-paginated-table/server-paginated-table.tsx @@ -55,7 +55,7 @@ export type ServerPaginatedTableProps = TableProps & { export default function ServerPaginatedTable({ url, - pageSize = 20, + pageSize = 10, dataSource, onDocsChange, columns, diff --git a/src/lib/layouts/dashboard-layout/header.tsx b/src/lib/layouts/dashboard-layout/header.tsx index d27072e..39ec8a2 100644 --- a/src/lib/layouts/dashboard-layout/header.tsx +++ b/src/lib/layouts/dashboard-layout/header.tsx @@ -4,7 +4,12 @@ import { useAppDispatch, useAppSelector } from "@/lib/redux/store"; import { useAuth } from "@/modules/auth/hooks/auth.hooks"; import { authActions } from "@/modules/auth/slices/auth.slice"; import UserAvatar from "@/pages/users/components/user-avatar"; -import { DownOutlined, MenuOutlined } from "@ant-design/icons"; +import { + DownOutlined, + LogoutOutlined, + MenuOutlined, + SettingOutlined, +} from "@ant-design/icons"; import { useResponsive } from "ahooks"; import { Dropdown, Layout, Select, Space, Tooltip } from "antd"; import { useCallback } from "react"; @@ -35,6 +40,7 @@ function Header() { title: t("logout:title"), message: t("logout:message"), onOk: () => logout().unwrap().catch(console.error), + okType: "danger", }); }, []); @@ -107,11 +113,14 @@ function Header() { { key: "settings", label: "Settings", + icon: , onClick: onSettingsClicked, }, { key: "logout", label: "Logout", + danger: true, + icon: , disabled: logoutLoading, onClick: onLogout, }, diff --git a/src/lib/redux/enhancers/status.enhancer.ts b/src/lib/redux/enhancers/status.enhancer.ts index ef4002e..306c9e1 100644 --- a/src/lib/redux/enhancers/status.enhancer.ts +++ b/src/lib/redux/enhancers/status.enhancer.ts @@ -9,6 +9,7 @@ import { } from "@reduxjs/toolkit"; import { Reducer } from "react"; import { ThunkStatus } from "../../types/misc"; +import { RootState, useAppSelector } from "../store"; //@ts-expect-error export const statusHandlerEnahncer: StoreEnhancer<{}, {}> = @@ -44,3 +45,13 @@ export const statusHandlerEnahncer: StoreEnhancer<{}, {}> = return cs(statusHandlerReducer, initialState, enhancer); }; + +export const useIsLoading = < + S extends keyof RootState, + K extends keyof RootState[S], +>( + slice: S, + key: K +): boolean => { + return useAppSelector((state) => state[slice][key] === ThunkStatus.LOADING); +}; diff --git a/src/lib/router/router-config.tsx b/src/lib/router/router-config.tsx index e308bcf..382fe24 100644 --- a/src/lib/router/router-config.tsx +++ b/src/lib/router/router-config.tsx @@ -1,8 +1,8 @@ import { Role } from "@/modules/auth/hooks/role.hooks"; import NotFound from "@/pages/404/404"; import Login from "@/pages/auth/login"; -import Home from "@/pages/home/home"; import Posts from "@/pages/posts/posts"; +import SamplePage from "@/pages/sample-page/sample-page"; import Settings from "@/pages/settings/settings"; import Users from "@/pages/users/users"; import { @@ -39,92 +39,80 @@ export const useRouterConfig = (): RouterConfig[] => { { layoutType: "dashboard", authType: "private", - component: , + component: , menuItem: { - title: t("sidebar:home"), - icon: , + title: t("sidebar:posts"), + icon: , }, route: { path: "/", }, + }, + { + layoutType: "dashboard", + authType: "private", + component: , + menuItem: { + title: t("sidebar:users"), + icon: , + }, + route: { + path: "/users", + }, + allowedRoles: [Role.SUPER_ADMIN], + }, + { + layoutType: "dashboard", + authType: "private", + component: , + menuItem: { + title: "Parent", + icon: , + }, + route: { + path: "/parent", + }, subRoutes: [ { layoutType: "dashboard", authType: "private", - component: , + component: , menuItem: { - title: "Home 2", + title: "Child 1", icon: , }, route: { - path: "home-2", + path: "child-1", }, }, { layoutType: "dashboard", authType: "private", - component: , + component: , menuItem: { - title: "Home 3", + title: "Child 2", icon: , }, route: { - path: "home-3", + path: "child-2", }, subRoutes: [ { layoutType: "dashboard", authType: "private", - component: , + component: , menuItem: { - title: "Home 3.1", + title: "Grand Child", icon: , }, route: { - path: "home-3.1", + path: "grand-child", }, }, ], }, ], }, - { - layoutType: "dashboard", - authType: "private", - component: , - menuItem: { - title: "Page 2", - icon: , - }, - route: { - path: "/page-2", - }, - }, - { - layoutType: "dashboard", - authType: "private", - component: , - menuItem: { - title: t("sidebar:posts"), - icon: , - }, - route: { - path: "/posts", - }, - }, - { - layoutType: "dashboard", - authType: "private", - component: , - menuItem: { - title: t("sidebar:users"), - icon: , - }, - route: { - path: "/users", - }, - allowedRoles: [Role.SUPER_ADMIN], - }, { layoutType: "dashboard", authType: "private", diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 22938dd..6136080 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,7 +1,7 @@ import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; -import { ApiBody } from "@/lib/types/api"; +import { AuthChangePasswordBody, AuthLoginBody } from "../types/auth.types"; -async function login(payload: ApiBody<"AuthLogin">) { +async function login(payload: AuthLoginBody) { const { data } = await withApiResponseHandling( apiClient.POST("/auth/login", { body: payload }) ); @@ -19,9 +19,17 @@ async function refreshToken(refreshToken: string) { return data; } +async function changePassword(payload: AuthChangePasswordBody) { + const { data: response } = await withApiResponseHandling( + apiClient.POST("/auth/change-password", { body: payload }) + ); + return response; +} + const authService = { login, refreshToken, + changePassword, }; export default authService; diff --git a/src/modules/auth/services/profile.service.ts b/src/modules/auth/services/profile.service.ts index ffdf860..d51e3d8 100644 --- a/src/modules/auth/services/profile.service.ts +++ b/src/modules/auth/services/profile.service.ts @@ -1,13 +1,13 @@ import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; -import { ApiBody } from "@/lib/types/api"; import { objectToFormData } from "@/lib/utils/misc.utils"; +import { ProfileUpdateBody } from "../types/profile.types"; async function fetch() { const { data } = await withApiResponseHandling(apiClient.GET("/profile")); return data; } -async function update(payload: ApiBody<"ProfileUpdate">) { +async function update(payload: ProfileUpdateBody) { const { data } = await withApiResponseHandling( apiClient.PATCH("/profile", { body: payload, diff --git a/src/modules/auth/slices/auth.slice.ts b/src/modules/auth/slices/auth.slice.ts index 1296dea..8ab3d79 100644 --- a/src/modules/auth/slices/auth.slice.ts +++ b/src/modules/auth/slices/auth.slice.ts @@ -12,12 +12,14 @@ const initialState: { status: "processing" | "authenticated" | "unauthenticated"; loginStatus: ThunkStatus; logoutStatus: ThunkStatus; + changePasswordStatus: ThunkStatus; } = { computedRoutes: undefined, sidebarCollapsed: false, status: "processing", loginStatus: ThunkStatus.IDLE, logoutStatus: ThunkStatus.IDLE, + changePasswordStatus: ThunkStatus.IDLE, }; const login = createAsyncThunk( @@ -29,11 +31,17 @@ const login = createAsyncThunk( return res; } ); + const logout = createAsyncThunk(`${name}/logout`, async () => { localStorage.removeItem("accessToken"); localStorage.removeItem("refreshToken"); }); +const changePassword = createAsyncThunk( + `${name}/changePassword`, + authService.changePassword +); + //slice export const authSlice = createSlice({ name, @@ -60,4 +68,9 @@ export const authSlice = createSlice({ }); //action creators -export const authActions = { ...authSlice.actions, login, logout }; +export const authActions = { + ...authSlice.actions, + login, + logout, + changePassword, +}; diff --git a/src/modules/auth/slices/profile.slice.ts b/src/modules/auth/slices/profile.slice.ts index 0aa1ac8..3c2d86b 100644 --- a/src/modules/auth/slices/profile.slice.ts +++ b/src/modules/auth/slices/profile.slice.ts @@ -9,9 +9,11 @@ export const name = "profile"; const initialState: { data?: Profile; fetchStatus: ThunkStatus; + updateStatus: ThunkStatus; } = { data: undefined, fetchStatus: ThunkStatus.IDLE, + updateStatus: ThunkStatus.IDLE, }; const fetch = createAsyncThunk(`${name}/fetch`, profileService.fetch); diff --git a/src/modules/auth/types/auth.types.ts b/src/modules/auth/types/auth.types.ts index 3e6cbad..7360a50 100644 --- a/src/modules/auth/types/auth.types.ts +++ b/src/modules/auth/types/auth.types.ts @@ -1,3 +1,5 @@ import { ApiBody } from "@/lib/types/api"; +export type AuthLoginBody = ApiBody<"AuthLogin">; + export type AuthChangePasswordBody = ApiBody<"AuthChangePassword">; diff --git a/src/modules/auth/types/profile.types.ts b/src/modules/auth/types/profile.types.ts new file mode 100644 index 0000000..e698431 --- /dev/null +++ b/src/modules/auth/types/profile.types.ts @@ -0,0 +1,3 @@ +import { ApiBody } from "@/lib/types/api"; + +export type ProfileUpdateBody = ApiBody<"ProfileUpdate">; diff --git a/src/modules/users/services/user.service.ts b/src/modules/users/services/user.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/users/slices/user.slice.ts b/src/modules/users/slices/user.slice.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/users/types/user.types.ts b/src/modules/users/types/user.types.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index 35056cd..f2edf7d 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -1,25 +1,23 @@ import Validations from "@/lib/utils/validations"; import { useAuth } from "@/modules/auth/hooks/auth.hooks"; -import { authActions } from "@/modules/auth/slices/auth.slice"; +import { AuthLoginBody } from "@/modules/auth/types/auth.types"; import { LockOutlined, MailOutlined } from "@ant-design/icons"; import { Button, Form, Input } from "antd"; import React from "react"; -type FormValues = Parameters[0]; - interface Props {} const Login: React.FC = () => { const { login, loginLoading } = useAuth(); - const onFinish = (values: FormValues) => { + const onFinish = (values: AuthLoginBody) => { login(values); }; return (
- onFinish={onFinish}> - onFinish={onFinish}> + name="email" rules={[ Validations.requiredField("Email"), @@ -30,7 +28,7 @@ const Login: React.FC = () => { } placeholder="Email" size="large" /> - name="password" rules={[Validations.requiredField("Password")]} > diff --git a/src/pages/home/home.tsx b/src/pages/home/home.tsx deleted file mode 100644 index aaac60d..0000000 --- a/src/pages/home/home.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import PageHeading from "@/lib/components/page-heading/page-heading"; -import { useLang } from "@/lib/contexts/root.context"; -import React from "react"; - -interface Props {} - -const Home: React.FC = () => { - const { t } = useLang(); - - return ( -
- {t("sidebar:home")} - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla quam velit, - vulputate eu pharetra nec, mattis ac neque. Duis vulputate commodo lectus, - ac blandit elit tincidunt id. Sed rhoncus, tortor sed eleifend tristique, - tortor mauris molestie elit, et luctus enim justo sit amet diam. Vivamus - at dui a mi lobortis pretium et eu quam. Suspendisse nec erat eu nunc - porttitor luctus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Proin nec purus magna. Aenean non mauris in velit mollis posuere. - Phasellus a tristique ligula. Quisque non urna ut sapien semper dictum. - Nulla facilisi. Aenean non pharetra nisl. Proin accumsan, nulla sed - feugiat vestibulum, nulla dui iaculis justo, a egestas nisi turpis vel - metus. Phasellus viverra nisl vitae risus lacinia eget aliquam ipsum - rhoncus. Suspendisse et urna. Integer semper, sem venenatis ultrices - vulputate, purus dui egestas massa, a commodo dui dolor non purus. Nam ut - est vel lacus tincidunt elementum. Nulla facilisi. Sed tincidunt, lorem in - lacinia congue, magna sem congue dui, et convallis libero dui a tortor. - Nulla facilisi. Sed tincidunt, lorem in lacinia congue, magna sem congue - dui, et convallis libero dui a tortor. Nulla facilisi. Sed tincidunt, - lorem in lacinia congue, magna sem congue dui, et convallis libero dui a - tortor. Nulla facilisi. Sed tincidunt, lorem in lacinia congue, magna sem - congue dui, et convallis libero dui a tortor. Nulla facilisi. Sed - tincidunt, lorem in lacinia congue, magna sem congue dui, et convallis - libero dui a tortor. Nulla facilisi. Sed tincidunt, lorem in lacinia - congue, magna sem congue dui, et convallis libero dui a tortor. Nulla - facilisi. -
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla quam velit, - vulputate eu pharetra nec, mattis ac neque. Duis vulputate commodo lectus, - ac blandit elit tincidunt id. Sed rhoncus, tortor sed eleifend tristique, - tortor mauris molestie elit, et luctus enim justo sit amet diam. Vivamus - at dui a mi lobortis pretium et eu quam. Suspendisse nec erat eu nunc - porttitor luctus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Proin nec purus magna. Aenean non mauris in velit mollis posuere. - Phasellus a tristique ligula. Quisque non urna ut sapien semper dictum. - Nulla facilisi. Aenean non pharetra nisl. Proin accumsan, nulla sed - feugiat vestibulum, nulla dui iaculis justo, a egestas nisi turpis vel - metus. Phasellus viverra nisl vitae risus lacinia eget aliquam ipsum - rhoncus. Suspendisse et urna. Integer semper, sem venenatis ultrices - vulputate, purus dui egestas massa, a commodo dui dolor non purus. Nam ut -
- ); -}; - -export default Home; diff --git a/src/pages/posts/posts.tsx b/src/pages/posts/posts.tsx index 922441c..3f7570d 100644 --- a/src/pages/posts/posts.tsx +++ b/src/pages/posts/posts.tsx @@ -19,6 +19,7 @@ const Posts: React.FC = () => { url="posts?populate=true" columns={[ + { title: "Title", dataIndex: "title", ellipsis: true, fixed: "left" }, { title: "Date", dataIndex: "created_at", @@ -33,7 +34,6 @@ const Posts: React.FC = () => { dataIndex: "author", render: (_, record) => , }, - { title: "Title", dataIndex: "title", ellipsis: true }, { title: "Labels", dataIndex: "labels", diff --git a/src/pages/sample-page/sample-page.tsx b/src/pages/sample-page/sample-page.tsx new file mode 100644 index 0000000..3f608b6 --- /dev/null +++ b/src/pages/sample-page/sample-page.tsx @@ -0,0 +1,19 @@ +import PageHeading from "@/lib/components/page-heading/page-heading"; +import { Typography } from "antd"; +import React from "react"; + +interface Props { + title: string; +} + +const SamplePage: React.FC = ({ title }) => { + return ( +
+ {title} + + This is just a sample page +
+ ); +}; + +export default SamplePage; diff --git a/src/pages/settings/settings.tsx b/src/pages/settings/settings.tsx index eef90db..2a72e1b 100644 --- a/src/pages/settings/settings.tsx +++ b/src/pages/settings/settings.tsx @@ -1,11 +1,13 @@ import ImagePicker from "@/lib/components/image-picker/image-picker"; import PageSpinner from "@/lib/components/spinner/page-spinner"; +import { useIsLoading } from "@/lib/redux/enhancers/status.enhancer"; import { useAppDispatch, useAppSelector } from "@/lib/redux/store"; import { Profile } from "@/lib/types/api"; import { Modify } from "@/lib/types/misc"; import { suppressError } from "@/lib/utils/error.utils"; import { sizedImg } from "@/lib/utils/image.utils"; import Validations from "@/lib/utils/validations"; +import { authActions } from "@/modules/auth/slices/auth.slice"; import { profileActions } from "@/modules/auth/slices/profile.slice"; import { AuthChangePasswordBody } from "@/modules/auth/types/auth.types"; import { Button, Form, Input, message, Typography } from "antd"; @@ -14,11 +16,17 @@ import React from "react"; type ProfileForm = Modify; +type ChangePasswordForm = AuthChangePasswordBody & { + confirmPassword: string; +}; + interface Props {} const Settings: React.FC = () => { const user = useAppSelector((state) => state.profile.data); const dispatch = useAppDispatch(); + const updateProfileLoading = useIsLoading("profile", "updateStatus"); + const changePasswordLoading = useIsLoading("auth", "changePasswordStatus"); const onUpdateProfile = (values: ProfileForm) => { const data = omit(values, ["photo"]); @@ -33,6 +41,13 @@ const Settings: React.FC = () => { .catch(suppressError); }; + const onChangePassword = (values: ChangePasswordForm) => { + dispatch(authActions.changePassword(omit(values, ["confirmPassword"]))) + .unwrap() + .then(() => message.success("Password updated successfully")) + .catch(suppressError); + }; + if (!user) return ; return ( @@ -41,28 +56,33 @@ const Settings: React.FC = () => { Update Profile - onFinish={onUpdateProfile} layout="vertical" - initialValues={{ ...user, photo: [sizedImg(user, "photo", "small")] }} className="w-96" + initialValues={{ ...user, photo: [sizedImg(user, "photo", "small")] }} + onFinish={onUpdateProfile} > - name="photo"> + name="photo">
- name="name"> + name="name"> - name="bio"> + name="bio"> - @@ -71,20 +91,24 @@ const Settings: React.FC = () => { Update Password - layout="vertical" className="w-96"> - + + layout="vertical" + className="w-96" + onFinish={onChangePassword} + > + name="oldPassword" rules={[Validations.requiredField()]} > - + name="newPassword" rules={[Validations.requiredField()]} > - name="confirmPassword" rules={[ Validations.requiredField(), @@ -101,7 +125,12 @@ const Settings: React.FC = () => { - diff --git a/src/pages/users/users.tsx b/src/pages/users/users.tsx index 6c04077..015bcaa 100644 --- a/src/pages/users/users.tsx +++ b/src/pages/users/users.tsx @@ -1,11 +1,18 @@ +import AlertPopup from "@/lib/components/alert-popup/alert-popup"; import PageHeading from "@/lib/components/page-heading/page-heading"; import ServerPaginatedTable from "@/lib/components/server-paginated-table/server-paginated-table"; import { useLang } from "@/lib/contexts/root.context"; import { User } from "@/lib/types/api"; import { formattedDateTime } from "@/lib/utils/dateTime.utils"; import { kebabCaseToWords } from "@/lib/utils/string.utils"; -import { CheckCircleFilled } from "@ant-design/icons"; -import { Tag, Typography } from "antd"; +import { Role } from "@/modules/auth/hooks/role.hooks"; +import { + CheckCircleFilled, + DeleteOutlined, + EditOutlined, + PlusOutlined, +} from "@ant-design/icons"; +import { Button, Space, Tag, Tooltip, Typography } from "antd"; import React from "react"; import UserAvatar from "./components/user-avatar"; @@ -14,12 +21,60 @@ interface Props {} const Users: React.FC = () => { const { t } = useLang(); + const onEdit = (user: User) => { + console.log(user); + }; + + const onDelete = (user: User) => { + AlertPopup({ + title: "Delete User", + message: ( + <> + Are you sure you want to delete user {user.name}? + + ), + okType: "danger", + // onOk: () => + // dispatch(userActions.remove(user.id)) + // .unwrap() + // .then(() => { + // onDeleteSucces(user); + // }) + // .catch(console.error), + }); + }; + return ( -
+
{t("sidebar:users")} url="users" + filters={[ + { + label: "Search", + type: "search", + key: "search", + filterProps: { placeholder: "Search by name, email or bio" }, + }, + { + label: "Role", + type: "select", + key: "role", + filterProps: { + options: Object.values(Role).map((item) => ({ + label: kebabCaseToWords(item), + value: item, + })), + }, + }, + ]} columns={[ + { + title: "User", + dataIndex: "name", + render: (_, record) => , + fixed: "left", + }, { title: "Creation Date", dataIndex: "created_at", @@ -29,11 +84,6 @@ const Users: React.FC = () => { ), }, - { - title: "User", - dataIndex: "name", - render: (_, record) => , - }, { title: "Role", dataIndex: "role", @@ -63,8 +113,33 @@ const Users: React.FC = () => { ) : null, }, + { + title: "Actions", + key: "actions", + fixed: "right", + render: (_, user) => { + return ( + + + onEdit(user)} /> + + + onDelete(user)} + /> + + + ); + }, + }, ]} /> +
+ +
); }; From 5a98553382bef1ebf9b6ac099a905c4c96a8c2fe Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Wed, 23 Apr 2025 19:43:33 +0500 Subject: [PATCH 14/19] feat: refactor user and profile types, implement user service and slice, and update components for improved state management --- src/lib/types/api.ts | 4 --- src/modules/auth/slices/profile.slice.ts | 2 +- src/modules/auth/types/profile.types.ts | 4 ++- src/modules/posts/post.types.ts | 3 ++ src/modules/user/services/user.service.ts | 28 ++++++++++++++++ src/modules/user/slices/user.slice.ts | 33 +++++++++++++++++++ src/modules/user/types/user.types.ts | 5 +++ src/modules/users/services/user.service.ts | 0 src/modules/users/slices/user.slice.ts | 0 src/modules/users/types/user.types.ts | 0 src/pages/posts/posts.tsx | 2 +- src/pages/settings/settings.tsx | 2 +- src/pages/users/components/user-avatar.tsx | 2 +- .../users/components/user-edit-modal.tsx | 9 +++++ src/pages/users/users.tsx | 32 +++++++++++------- 15 files changed, 106 insertions(+), 20 deletions(-) create mode 100644 src/modules/posts/post.types.ts create mode 100644 src/modules/user/services/user.service.ts create mode 100644 src/modules/user/slices/user.slice.ts create mode 100644 src/modules/user/types/user.types.ts delete mode 100644 src/modules/users/services/user.service.ts delete mode 100644 src/modules/users/slices/user.slice.ts delete mode 100644 src/modules/users/types/user.types.ts create mode 100644 src/pages/users/components/user-edit-modal.tsx diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index 75705eb..7fc0937 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -35,7 +35,3 @@ export type ApiResponse = NonNullable< ? OperationResponse["200"]["content"]["application/json"]["data"] : never >; - -export type Profile = ApiResponse<"ProfileFetch">; -export type Post = ApiResponse<"PostFetch">; -export type User = ApiResponse<"UserFetch">; diff --git a/src/modules/auth/slices/profile.slice.ts b/src/modules/auth/slices/profile.slice.ts index 3c2d86b..44b0b10 100644 --- a/src/modules/auth/slices/profile.slice.ts +++ b/src/modules/auth/slices/profile.slice.ts @@ -1,7 +1,7 @@ -import { Profile } from "@/lib/types/api"; import { ThunkStatus } from "@/lib/types/misc"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import profileService from "../services/profile.service"; +import { Profile } from "../types/profile.types"; export const name = "profile"; diff --git a/src/modules/auth/types/profile.types.ts b/src/modules/auth/types/profile.types.ts index e698431..1a5bbef 100644 --- a/src/modules/auth/types/profile.types.ts +++ b/src/modules/auth/types/profile.types.ts @@ -1,3 +1,5 @@ -import { ApiBody } from "@/lib/types/api"; +import { ApiBody, ApiResponse } from "@/lib/types/api"; + +export type Profile = ApiResponse<"ProfileFetch">; export type ProfileUpdateBody = ApiBody<"ProfileUpdate">; diff --git a/src/modules/posts/post.types.ts b/src/modules/posts/post.types.ts new file mode 100644 index 0000000..23047b1 --- /dev/null +++ b/src/modules/posts/post.types.ts @@ -0,0 +1,3 @@ +import { ApiResponse } from "@/lib/types/api"; + +export type Post = ApiResponse<"PostFetch">; diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts new file mode 100644 index 0000000..45a331d --- /dev/null +++ b/src/modules/user/services/user.service.ts @@ -0,0 +1,28 @@ +import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; +import { UserUpdateBody } from "../types/user.types"; + +async function update(id: string, payload: UserUpdateBody) { + const { data } = await withApiResponseHandling( + apiClient.PATCH("/users/{userId}", { + params: { path: { userId: id } }, + body: payload, + }) + ); + return data; +} + +async function remove(id: string) { + const { data } = await withApiResponseHandling( + apiClient.DELETE("/users/{userId}", { + params: { path: { userId: id } }, + }) + ); + return data; +} + +const userService = { + update, + remove, +}; + +export default userService; diff --git a/src/modules/user/slices/user.slice.ts b/src/modules/user/slices/user.slice.ts new file mode 100644 index 0000000..8729647 --- /dev/null +++ b/src/modules/user/slices/user.slice.ts @@ -0,0 +1,33 @@ +import { ThunkStatus } from "@/lib/types/misc"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import userService from "../services/user.service"; +import { UserUpdateBody } from "../types/user.types"; + +export const name = "user"; + +//initial state +const initialState: { + updateStatus: ThunkStatus; + removeStatus: ThunkStatus; +} = { + updateStatus: ThunkStatus.IDLE, + removeStatus: ThunkStatus.IDLE, +}; + +const update = createAsyncThunk( + `${name}/update`, + ({ id, ...payload }: UserUpdateBody & { id: string }) => + userService.update(id, payload) +); + +const remove = createAsyncThunk(`${name}/remove`, userService.remove); + +//slice +export const userSlice = createSlice({ + name, + initialState, + reducers: {}, +}); + +//action creators +export const userActions = { ...userSlice.actions, update, remove }; diff --git a/src/modules/user/types/user.types.ts b/src/modules/user/types/user.types.ts new file mode 100644 index 0000000..b3bc1e3 --- /dev/null +++ b/src/modules/user/types/user.types.ts @@ -0,0 +1,5 @@ +import { ApiBody, ApiResponse } from "@/lib/types/api"; + +export type User = ApiResponse<"UserFetch">; + +export type UserUpdateBody = ApiBody<"UserUpdate">; diff --git a/src/modules/users/services/user.service.ts b/src/modules/users/services/user.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/users/slices/user.slice.ts b/src/modules/users/slices/user.slice.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/users/types/user.types.ts b/src/modules/users/types/user.types.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/posts/posts.tsx b/src/pages/posts/posts.tsx index 3f7570d..a4382d3 100644 --- a/src/pages/posts/posts.tsx +++ b/src/pages/posts/posts.tsx @@ -1,9 +1,9 @@ import PageHeading from "@/lib/components/page-heading/page-heading"; import ServerPaginatedTable from "@/lib/components/server-paginated-table/server-paginated-table"; import { useLang } from "@/lib/contexts/root.context"; -import { Post } from "@/lib/types/api"; import { getColorFromStr } from "@/lib/utils/colors"; import { formattedDateTime } from "@/lib/utils/dateTime.utils"; +import { Post } from "@/modules/posts/post.types"; import { Tag, Typography } from "antd"; import React from "react"; import UserAvatar from "../users/components/user-avatar"; diff --git a/src/pages/settings/settings.tsx b/src/pages/settings/settings.tsx index 2a72e1b..0c7535d 100644 --- a/src/pages/settings/settings.tsx +++ b/src/pages/settings/settings.tsx @@ -2,7 +2,6 @@ import ImagePicker from "@/lib/components/image-picker/image-picker"; import PageSpinner from "@/lib/components/spinner/page-spinner"; import { useIsLoading } from "@/lib/redux/enhancers/status.enhancer"; import { useAppDispatch, useAppSelector } from "@/lib/redux/store"; -import { Profile } from "@/lib/types/api"; import { Modify } from "@/lib/types/misc"; import { suppressError } from "@/lib/utils/error.utils"; import { sizedImg } from "@/lib/utils/image.utils"; @@ -10,6 +9,7 @@ import Validations from "@/lib/utils/validations"; import { authActions } from "@/modules/auth/slices/auth.slice"; import { profileActions } from "@/modules/auth/slices/profile.slice"; import { AuthChangePasswordBody } from "@/modules/auth/types/auth.types"; +import { Profile } from "@/modules/auth/types/profile.types"; import { Button, Form, Input, message, Typography } from "antd"; import { omit } from "lodash"; import React from "react"; diff --git a/src/pages/users/components/user-avatar.tsx b/src/pages/users/components/user-avatar.tsx index 5917a5b..51aa4c9 100644 --- a/src/pages/users/components/user-avatar.tsx +++ b/src/pages/users/components/user-avatar.tsx @@ -1,6 +1,6 @@ import ServerImg from "@/lib/components/server-img/server-img"; -import { User } from "@/lib/types/api"; import { kebabCaseToWords } from "@/lib/utils/string.utils"; +import { User } from "@/modules/user/types/user.types"; import { Space, Typography } from "antd"; import React from "react"; diff --git a/src/pages/users/components/user-edit-modal.tsx b/src/pages/users/components/user-edit-modal.tsx new file mode 100644 index 0000000..efd1041 --- /dev/null +++ b/src/pages/users/components/user-edit-modal.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +interface Props {} + +const UserEditModal: React.FC = () => { + return
; +}; + +export default UserEditModal; diff --git a/src/pages/users/users.tsx b/src/pages/users/users.tsx index 015bcaa..e915ec8 100644 --- a/src/pages/users/users.tsx +++ b/src/pages/users/users.tsx @@ -1,11 +1,16 @@ import AlertPopup from "@/lib/components/alert-popup/alert-popup"; import PageHeading from "@/lib/components/page-heading/page-heading"; -import ServerPaginatedTable from "@/lib/components/server-paginated-table/server-paginated-table"; +import ServerPaginatedTable, { + GetHelpers, +} from "@/lib/components/server-paginated-table/server-paginated-table"; import { useLang } from "@/lib/contexts/root.context"; -import { User } from "@/lib/types/api"; +import { useAppDispatch } from "@/lib/redux/store"; import { formattedDateTime } from "@/lib/utils/dateTime.utils"; +import { suppressError } from "@/lib/utils/error.utils"; import { kebabCaseToWords } from "@/lib/utils/string.utils"; import { Role } from "@/modules/auth/hooks/role.hooks"; +import { userActions } from "@/modules/user/slices/user.slice"; +import { User } from "@/modules/user/types/user.types"; import { CheckCircleFilled, DeleteOutlined, @@ -13,16 +18,18 @@ import { PlusOutlined, } from "@ant-design/icons"; import { Button, Space, Tag, Tooltip, Typography } from "antd"; -import React from "react"; +import React, { useRef } from "react"; import UserAvatar from "./components/user-avatar"; interface Props {} const Users: React.FC = () => { const { t } = useLang(); + const dispatch = useAppDispatch(); + const tableHelpers = useRef>(); const onEdit = (user: User) => { - console.log(user); + console.log("user: ", user); }; const onDelete = (user: User) => { @@ -34,13 +41,15 @@ const Users: React.FC = () => { ), okType: "danger", - // onOk: () => - // dispatch(userActions.remove(user.id)) - // .unwrap() - // .then(() => { - // onDeleteSucces(user); - // }) - // .catch(console.error), + onOk: () => + dispatch(userActions.remove(user.id!)) + .unwrap() + .then(() => { + tableHelpers.current?.setData((data) => + data.filter((item) => item.id !== user.id) + ); + }) + .catch(suppressError), }); }; @@ -49,6 +58,7 @@ const Users: React.FC = () => { {t("sidebar:users")} url="users" + getHelpers={(helpers) => (tableHelpers.current = helpers)} filters={[ { label: "Search", From 3aae36a821f24922711b27c0aa951d9edf10dbfc Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Wed, 7 May 2025 00:18:01 +0500 Subject: [PATCH 15/19] chore: update API base URL and replace ts-expect-error with ts-ignore in multiple components --- .env.development | 2 +- src/lib/components/server-paginated-select/lazy-select.tsx | 2 +- .../server-paginated-select/server-paginated-select.tsx | 2 +- src/lib/contexts/root.context.tsx | 4 ++-- src/lib/redux/enhancers/status.enhancer.ts | 2 +- src/lib/types/openapi-fetch.d.ts | 6 ++++-- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.env.development b/.env.development index e145da1..8461442 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,2 @@ -VITE_API_BASE_URL=http://localhost:3000/api/v1 +VITE_API_BASE_URL=https://be.starters.rayonstudios.com/api/v1 VITE_ENV=dev \ No newline at end of file diff --git a/src/lib/components/server-paginated-select/lazy-select.tsx b/src/lib/components/server-paginated-select/lazy-select.tsx index 0dc9f65..bfcdd30 100644 --- a/src/lib/components/server-paginated-select/lazy-select.tsx +++ b/src/lib/components/server-paginated-select/lazy-select.tsx @@ -110,7 +110,7 @@ function LazySelect({ style={style} value={_idResolver(value)} defaultValue={_idResolver(defaultValue)} - // @ts-expect-error + // @ts-ignore options={data.map((item, ix) => { const value = item._loading ? ix + "loading" : item.id; const children: any = diff --git a/src/lib/components/server-paginated-select/server-paginated-select.tsx b/src/lib/components/server-paginated-select/server-paginated-select.tsx index 1dcbca7..aee19a4 100644 --- a/src/lib/components/server-paginated-select/server-paginated-select.tsx +++ b/src/lib/components/server-paginated-select/server-paginated-select.tsx @@ -138,7 +138,7 @@ export default function ServerPaginatedSelect({ }} loading={loading} dataSource={_data} - // @ts-expect-error + // @ts-ignore renderItem={renderItem} valueResolver={valueResolver} style={style} diff --git a/src/lib/contexts/root.context.tsx b/src/lib/contexts/root.context.tsx index 07fda3a..cee3e71 100644 --- a/src/lib/contexts/root.context.tsx +++ b/src/lib/contexts/root.context.tsx @@ -203,12 +203,12 @@ const RootContextProvider: React.FC = ({ children }) => { if (accept) ip.setAttribute("accept", accept); if (multiple) ip.setAttribute("multiple", multiple.toString()); if (onChange) { - //@ts-expect-error + //@ts-ignore ip.onchange = (e: { target: { files: File[] } }) => { const files = []; for (const file of e.target.files) files.push(file); typeof onChange === "function" && files.length && onChange(files); - //@ts-expect-error + //@ts-ignore e.target.value = ""; }; } diff --git a/src/lib/redux/enhancers/status.enhancer.ts b/src/lib/redux/enhancers/status.enhancer.ts index 306c9e1..c9867f7 100644 --- a/src/lib/redux/enhancers/status.enhancer.ts +++ b/src/lib/redux/enhancers/status.enhancer.ts @@ -11,7 +11,7 @@ import { Reducer } from "react"; import { ThunkStatus } from "../../types/misc"; import { RootState, useAppSelector } from "../store"; -//@ts-expect-error +//@ts-ignore export const statusHandlerEnahncer: StoreEnhancer<{}, {}> = (cs: typeof createStore) => ( diff --git a/src/lib/types/openapi-fetch.d.ts b/src/lib/types/openapi-fetch.d.ts index 859e683..580ab0e 100644 --- a/src/lib/types/openapi-fetch.d.ts +++ b/src/lib/types/openapi-fetch.d.ts @@ -415,12 +415,15 @@ export interface components { data: components["schemas"]["PaginationResponse_SanitizedUser_"] | null; error: string | null; }; + /** @enum {string} */ + Role: "user" | "admin" | "super-admin"; UserFetchList: { /** Format: double */ limit?: number; /** Format: double */ page?: number; search?: string; + role?: components["schemas"]["Role"]; }; "Expand_Optional_UserMutable.bio__": { bio?: string; @@ -529,8 +532,6 @@ export interface components { [key: string]: unknown; }; GenericObject: components["schemas"]["Record_string.any_"]; - /** @enum {string} */ - Role: "user" | "admin" | "super-admin"; "Expand_Omit_NotificationMutable.event_-and-_roles_63_-Role-Array--userIds_63_-string-Array--metadata_63_-GenericObject__": { title: string; body: string; @@ -746,6 +747,7 @@ export interface operations { limit?: number; page?: number; search?: string; + role?: components["schemas"]["Role"]; }; header?: never; path?: never; From 7127d9e37d7d84fa36ec3cc17ca88f81ead66615 Mon Sep 17 00:00:00 2001 From: MohammedMaaz Date: Wed, 7 May 2025 02:07:23 +0500 Subject: [PATCH 16/19] feat: add forgot password and reset password functionality, update types and services accordingly --- src/lib/router/router-config.tsx | 18 ++++ src/modules/auth/services/auth.service.ts | 23 +++- src/modules/auth/slices/auth.slice.ts | 16 +++ src/modules/auth/types/auth.types.ts | 4 +- src/modules/auth/types/profile.types.ts | 2 + src/modules/posts/post.types.ts | 1 + src/modules/user/types/user.types.ts | 2 + src/pages/auth/forgot-password.tsx | 86 +++++++++++++++ src/pages/auth/login.tsx | 18 +++- src/pages/auth/reset-password.tsx | 122 ++++++++++++++++++++++ src/pages/settings/settings.tsx | 7 +- 11 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 src/pages/auth/forgot-password.tsx create mode 100644 src/pages/auth/reset-password.tsx diff --git a/src/lib/router/router-config.tsx b/src/lib/router/router-config.tsx index 382fe24..18477f3 100644 --- a/src/lib/router/router-config.tsx +++ b/src/lib/router/router-config.tsx @@ -1,6 +1,8 @@ import { Role } from "@/modules/auth/hooks/role.hooks"; import NotFound from "@/pages/404/404"; +import ForgotPassword from "@/pages/auth/forgot-password"; import Login from "@/pages/auth/login"; +import ResetPassword from "@/pages/auth/reset-password"; import Posts from "@/pages/posts/posts"; import SamplePage from "@/pages/sample-page/sample-page"; import Settings from "@/pages/settings/settings"; @@ -129,6 +131,22 @@ export const useRouterConfig = (): RouterConfig[] => { path: "/login", }, }, + { + layoutType: "auth", + authType: "public", + component: , + route: { + path: "/forgot-password", + }, + }, + { + layoutType: "auth", + authType: "public", + component: , + route: { + path: "/reset-password", + }, + }, { layoutType: authStatus === "authenticated" ? "dashboard" : "empty", authType: "none", diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 6136080..b917568 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,5 +1,10 @@ import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; -import { AuthChangePasswordBody, AuthLoginBody } from "../types/auth.types"; +import { + AuthChangePasswordBody, + AuthForgotPasswordBody, + AuthLoginBody, + AuthResetPasswordBody, +} from "../types/auth.types"; async function login(payload: AuthLoginBody) { const { data } = await withApiResponseHandling( @@ -26,10 +31,26 @@ async function changePassword(payload: AuthChangePasswordBody) { return response; } +async function forgotPassword(payload: AuthForgotPasswordBody) { + const { data: response } = await withApiResponseHandling( + apiClient.POST("/auth/forgot-password", { body: payload }) + ); + return response; +} + +async function resetPassword(payload: AuthResetPasswordBody) { + const { data: response } = await withApiResponseHandling( + apiClient.POST("/auth/reset-password", { body: payload }) + ); + return response; +} + const authService = { login, refreshToken, changePassword, + forgotPassword, + resetPassword, }; export default authService; diff --git a/src/modules/auth/slices/auth.slice.ts b/src/modules/auth/slices/auth.slice.ts index 8ab3d79..0102ac3 100644 --- a/src/modules/auth/slices/auth.slice.ts +++ b/src/modules/auth/slices/auth.slice.ts @@ -13,6 +13,8 @@ const initialState: { loginStatus: ThunkStatus; logoutStatus: ThunkStatus; changePasswordStatus: ThunkStatus; + forgotPasswordStatus: ThunkStatus; + resetPasswordStatus: ThunkStatus; } = { computedRoutes: undefined, sidebarCollapsed: false, @@ -20,6 +22,8 @@ const initialState: { loginStatus: ThunkStatus.IDLE, logoutStatus: ThunkStatus.IDLE, changePasswordStatus: ThunkStatus.IDLE, + forgotPasswordStatus: ThunkStatus.IDLE, + resetPasswordStatus: ThunkStatus.IDLE, }; const login = createAsyncThunk( @@ -42,6 +46,16 @@ const changePassword = createAsyncThunk( authService.changePassword ); +const forgotPassword = createAsyncThunk( + `${name}/forgotPassword`, + authService.forgotPassword +); + +const resetPassword = createAsyncThunk( + `${name}/resetPassword`, + authService.resetPassword +); + //slice export const authSlice = createSlice({ name, @@ -73,4 +87,6 @@ export const authActions = { login, logout, changePassword, + forgotPassword, + resetPassword, }; diff --git a/src/modules/auth/types/auth.types.ts b/src/modules/auth/types/auth.types.ts index 7360a50..f16ec5d 100644 --- a/src/modules/auth/types/auth.types.ts +++ b/src/modules/auth/types/auth.types.ts @@ -1,5 +1,7 @@ import { ApiBody } from "@/lib/types/api"; +// Request types export type AuthLoginBody = ApiBody<"AuthLogin">; - +export type AuthForgotPasswordBody = ApiBody<"AuthForgotPassword">; +export type AuthResetPasswordBody = ApiBody<"AuthResetPassword">; export type AuthChangePasswordBody = ApiBody<"AuthChangePassword">; diff --git a/src/modules/auth/types/profile.types.ts b/src/modules/auth/types/profile.types.ts index 1a5bbef..d402973 100644 --- a/src/modules/auth/types/profile.types.ts +++ b/src/modules/auth/types/profile.types.ts @@ -1,5 +1,7 @@ import { ApiBody, ApiResponse } from "@/lib/types/api"; +// Response types export type Profile = ApiResponse<"ProfileFetch">; +// Request types export type ProfileUpdateBody = ApiBody<"ProfileUpdate">; diff --git a/src/modules/posts/post.types.ts b/src/modules/posts/post.types.ts index 23047b1..988c807 100644 --- a/src/modules/posts/post.types.ts +++ b/src/modules/posts/post.types.ts @@ -1,3 +1,4 @@ import { ApiResponse } from "@/lib/types/api"; +// Response types export type Post = ApiResponse<"PostFetch">; diff --git a/src/modules/user/types/user.types.ts b/src/modules/user/types/user.types.ts index b3bc1e3..d4077c1 100644 --- a/src/modules/user/types/user.types.ts +++ b/src/modules/user/types/user.types.ts @@ -1,5 +1,7 @@ import { ApiBody, ApiResponse } from "@/lib/types/api"; +// Response types export type User = ApiResponse<"UserFetch">; +// Request types export type UserUpdateBody = ApiBody<"UserUpdate">; diff --git a/src/pages/auth/forgot-password.tsx b/src/pages/auth/forgot-password.tsx new file mode 100644 index 0000000..697a69d --- /dev/null +++ b/src/pages/auth/forgot-password.tsx @@ -0,0 +1,86 @@ +import { useIsLoading } from "@/lib/redux/enhancers/status.enhancer"; +import { useAppDispatch } from "@/lib/redux/store"; +import { suppressError } from "@/lib/utils/error.utils"; +import Validations from "@/lib/utils/validations"; +import { authActions } from "@/modules/auth/slices/auth.slice"; +import { AuthForgotPasswordBody } from "@/modules/auth/types/auth.types"; +import { MailOutlined } from "@ant-design/icons"; +import { Button, Form, Input, Typography } from "antd"; +import React, { useRef, useState } from "react"; + +interface Props {} + +const ForgotPassword: React.FC = () => { + const dispatch = useAppDispatch(); + const loading = useIsLoading("auth", "forgotPasswordStatus"); + const [isSuccess, setIsSuccess] = useState(false); + const email = useRef(); + + const onFinish = (values: AuthForgotPasswordBody) => { + dispatch(authActions.forgotPassword(values)) + .unwrap() + .then(() => { + email.current = values.email; + setIsSuccess(true); + }) + .catch(suppressError); + }; + + if (isSuccess) + return ( +
+ + Password reset instructions sent! + + + Check your inbox or spam folder. You will receive an email at{" "} + {email.current} with instructions on how to reset + your password. + +
+ ); + + return ( +
+ onFinish={onFinish}> +
+ + Enter your email to start the password reset process. You will + receive an email with instructions. + +
+ + + className="mb-1" + name="email" + rules={[ + Validations.requiredField("Email"), + { validator: Validations.email }, + ]} + validateFirst + > + } placeholder="Email" size="large" /> + + +
+ + Back to login + +
+ + + + + +
+ ); +}; + +export default ForgotPassword; diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index f2edf7d..1be59da 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -2,7 +2,7 @@ import Validations from "@/lib/utils/validations"; import { useAuth } from "@/modules/auth/hooks/auth.hooks"; import { AuthLoginBody } from "@/modules/auth/types/auth.types"; import { LockOutlined, MailOutlined } from "@ant-design/icons"; -import { Button, Form, Input } from "antd"; +import { Button, Form, Input, Typography } from "antd"; import React from "react"; interface Props {} @@ -31,6 +31,7 @@ const Login: React.FC = () => { name="password" rules={[Validations.requiredField("Password")]} + className="mb-1" > } @@ -39,8 +40,19 @@ const Login: React.FC = () => { /> - - diff --git a/src/pages/auth/reset-password.tsx b/src/pages/auth/reset-password.tsx new file mode 100644 index 0000000..4b48913 --- /dev/null +++ b/src/pages/auth/reset-password.tsx @@ -0,0 +1,122 @@ +import { useIsLoading } from "@/lib/redux/enhancers/status.enhancer"; +import { useAppDispatch } from "@/lib/redux/store"; +import { suppressError } from "@/lib/utils/error.utils"; +import Validations from "@/lib/utils/validations"; +import { authActions } from "@/modules/auth/slices/auth.slice"; +import { AuthResetPasswordBody } from "@/modules/auth/types/auth.types"; +import useUrlState from "@ahooksjs/use-url-state"; +import { LockOutlined } from "@ant-design/icons"; +import { Button, Form, Input, message, Typography } from "antd"; +import { omit } from "lodash"; +import React from "react"; +import { useNavigate } from "react-router-dom"; + +type ResetPasswordForm = Omit & { + confirmPassword: string; +}; + +interface Props {} + +const ResetPassword: React.FC = () => { + const dispatch = useAppDispatch(); + const loading = useIsLoading("auth", "resetPasswordStatus"); + const [{ email, otp }] = useUrlState({ email: "", otp: "" }); + const navigate = useNavigate(); + + const onFinish = (values: ResetPasswordForm) => { + dispatch( + authActions.resetPassword({ ...omit(values, ["confirmPassword"]), email }) + ) + .unwrap() + .then(() => { + message.success("Your password has been updated successfully!"); + navigate("/login"); + }) + .catch(suppressError); + }; + + if (!email) { + return ( +
+ Invalid link + + The link you used to reset your password is invalid or has expired. + +
+ ); + } + + return ( +
+
+ + Enter your new password below for email {email}. Make + sure to remember it for future logins. + +
+ + + onFinish={onFinish} + layout="vertical" + requiredMark={false} + > + + name="otp" + rules={[Validations.requiredField("Recovery code")]} + initialValue={otp} + label={otp ? "Recovery code" : undefined} + > + + + + + name="password" + rules={[ + Validations.requiredField("Password"), + { validator: Validations.minLen(6) }, + ]} + > + } + placeholder="New password" + size="large" + /> + + + + name="confirmPassword" + rules={[ + Validations.requiredField(), + ({ getFieldValue }) => ({ + validator(_, value) { + if (!value || getFieldValue("password") === value) { + return Promise.resolve(); + } + return Promise.reject("Passwords do not match"); + }, + }), + ]} + > + } + placeholder="Confirm new password" + size="large" + /> + + + + + + +
+ ); +}; + +export default ResetPassword; diff --git a/src/pages/settings/settings.tsx b/src/pages/settings/settings.tsx index 0c7535d..ef4bd6e 100644 --- a/src/pages/settings/settings.tsx +++ b/src/pages/settings/settings.tsx @@ -104,7 +104,10 @@ const Settings: React.FC = () => { name="newPassword" - rules={[Validations.requiredField()]} + rules={[ + Validations.requiredField(), + { validator: Validations.minLen(6) }, + ]} > @@ -122,7 +125,7 @@ const Settings: React.FC = () => { }), ]} > - + + + )}
getDefaultMiddleware({ serializableCheck: false }).concat( diff --git a/src/lib/types/openapi-fetch.d.ts b/src/lib/types/openapi-fetch.d.ts index 580ab0e..a316745 100644 --- a/src/lib/types/openapi-fetch.d.ts +++ b/src/lib/types/openapi-fetch.d.ts @@ -200,7 +200,7 @@ export interface paths { }; get?: never; put?: never; - post: operations["FileCreate"]; + post: operations["FileSave"]; delete: operations["FileRemove"]; options?: never; head?: never; @@ -406,18 +406,25 @@ export interface components { data: components["schemas"]["SanitizedUser"] | null; error: string | null; }; - PaginationResponse_SanitizedUser_: { + PaginationSortResponse_SanitizedUser_: { /** Format: double */ total?: number; list: components["schemas"]["SanitizedUser"][]; }; - APIResponse_PaginationResponse_SanitizedUser__: { - data: components["schemas"]["PaginationResponse_SanitizedUser_"] | null; + APIResponse_PaginationSortResponse_SanitizedUser__: { + data: components["schemas"]["PaginationSortResponse_SanitizedUser_"] | null; error: string | null; }; /** @enum {string} */ Role: "user" | "admin" | "super-admin"; + /** @enum {string} */ + "SortFields_SanitizedUser.created_at-or-updated_at-or-name_": "created_at" | "updated_at" | "name"; + UserSortFields: components["schemas"]["SortFields_SanitizedUser.created_at-or-updated_at-or-name_"]; + /** @enum {string} */ + SortOrder: "asc" | "desc"; UserFetchList: { + sortOrder?: components["schemas"]["SortOrder"]; + sortField?: components["schemas"]["UserSortFields"]; /** Format: double */ limit?: number; /** Format: double */ @@ -425,14 +432,14 @@ export interface components { search?: string; role?: components["schemas"]["Role"]; }; - "Expand_Optional_UserMutable.bio__": { + "Expand_Optional_UserMutable.bio-or-photo__": { + photo?: string; bio?: string; name: string; role: string; email: string; - photo: string; }; - UserCreate: components["schemas"]["Expand_Optional_UserMutable.bio__"]; + UserCreate: components["schemas"]["Expand_Optional_UserMutable.bio-or-photo__"]; /** @description Make all properties in T optional */ "Partial_Omit_UserMutable.email__": { name?: string; @@ -459,16 +466,21 @@ export interface components { data: components["schemas"]["PostType"] | null; error: string | null; }; - PaginationResponse_PostType_: { + PaginationSortResponse_PostType_: { /** Format: double */ total?: number; list: components["schemas"]["PostType"][]; }; - APIResponse_PaginationResponse_PostType__: { - data: components["schemas"]["PaginationResponse_PostType_"] | null; + APIResponse_PaginationSortResponse_PostType__: { + data: components["schemas"]["PaginationSortResponse_PostType_"] | null; error: string | null; }; + /** @enum {string} */ + "SortFields_Post.created_at-or-updated_at-or-title-or-views_": "created_at" | "updated_at" | "title" | "views"; + PostSortFields: components["schemas"]["SortFields_Post.created_at-or-updated_at-or-title-or-views_"]; PostFetchList: { + sortOrder?: components["schemas"]["SortOrder"]; + sortField?: components["schemas"]["PostSortFields"]; /** Format: double */ limit?: number; /** Format: double */ @@ -577,16 +589,21 @@ export interface components { event: string; }; Notification: components["schemas"]["Expand_Prisma.notificationsCreateManyInput-and-_metadata_63_-GenericObject__"]; - PaginationResponse_Notification_: { + PaginationSortResponse_Notification_: { /** Format: double */ total?: number; list: components["schemas"]["Notification"][]; }; - APIResponse_PaginationResponse_Notification__: { - data: components["schemas"]["PaginationResponse_Notification_"] | null; + APIResponse_PaginationSortResponse_Notification__: { + data: components["schemas"]["PaginationSortResponse_Notification_"] | null; error: string | null; }; + /** @enum {string} */ + "SortFields_Notification.created_at_": "created_at"; + NotificationSortFields: components["schemas"]["SortFields_Notification.created_at_"]; NotificationFetchList: { + sortOrder?: components["schemas"]["SortOrder"]; + sortField?: components["schemas"]["NotificationSortFields"]; /** Format: double */ limit?: number; /** Format: double */ @@ -744,6 +761,8 @@ export interface operations { UserFetchList: { parameters: { query?: { + sortOrder?: components["schemas"]["SortOrder"]; + sortField?: components["schemas"]["UserSortFields"]; limit?: number; page?: number; search?: string; @@ -761,7 +780,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["APIResponse_PaginationResponse_SanitizedUser__"]; + "application/json": components["schemas"]["APIResponse_PaginationSortResponse_SanitizedUser__"]; }; }; }; @@ -934,6 +953,8 @@ export interface operations { PostFetchList: { parameters: { query?: { + sortOrder?: components["schemas"]["SortOrder"]; + sortField?: components["schemas"]["PostSortFields"]; limit?: number; page?: number; search?: string; @@ -953,7 +974,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["APIResponse_PaginationResponse_PostType__"]; + "application/json": components["schemas"]["APIResponse_PaginationSortResponse_PostType__"]; }; }; }; @@ -1035,6 +1056,8 @@ export interface operations { NotificationFetchList: { parameters: { query?: { + sortOrder?: components["schemas"]["SortOrder"]; + sortField?: components["schemas"]["NotificationSortFields"]; limit?: number; page?: number; }; @@ -1050,7 +1073,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["APIResponse_PaginationResponse_Notification__"]; + "application/json": components["schemas"]["APIResponse_PaginationSortResponse_Notification__"]; }; }; }; @@ -1137,7 +1160,7 @@ export interface operations { }; }; }; - FileCreate: { + FileSave: { parameters: { query?: never; header?: never; diff --git a/src/modules/file/services/file.service.ts b/src/modules/file/services/file.service.ts new file mode 100644 index 0000000..b5f90e1 --- /dev/null +++ b/src/modules/file/services/file.service.ts @@ -0,0 +1,29 @@ +import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; +import { objectToFormData } from "@/lib/utils/misc.utils"; +import { FileRemoveBody, FileSaveBody } from "../types/file.types"; + +const save = async (payload: FileSaveBody) => { + const { data } = await withApiResponseHandling( + apiClient.POST("/files", { + body: payload, + bodySerializer: objectToFormData, + }) + ); + return data; +}; + +const remove = async (payload: FileRemoveBody) => { + const { data } = await withApiResponseHandling( + apiClient.DELETE("/files", { + body: payload, + }) + ); + return data; +}; + +const fileService = { + save, + remove, +}; + +export default fileService; diff --git a/src/modules/file/slices/file.slice.ts b/src/modules/file/slices/file.slice.ts new file mode 100644 index 0000000..e0d052a --- /dev/null +++ b/src/modules/file/slices/file.slice.ts @@ -0,0 +1,32 @@ +import { ThunkStatus } from "@/lib/types/misc"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import fileService from "../services/file.service"; + +export const name = "file"; + +//initial state +const initialState: { + saveStatus: ThunkStatus; + removeStatus: ThunkStatus; +} = { + saveStatus: ThunkStatus.IDLE, + removeStatus: ThunkStatus.IDLE, +}; + +const save = createAsyncThunk(`${name}/save`, fileService.save); + +const remove = createAsyncThunk(`${name}/remove`, fileService.remove); + +//slice +export const fileSlice = createSlice({ + name, + initialState, + reducers: {}, +}); + +//action creators +export const fileActions = { + ...fileSlice.actions, + save, + remove, +}; diff --git a/src/modules/file/types/file.types.ts b/src/modules/file/types/file.types.ts new file mode 100644 index 0000000..1c36aa4 --- /dev/null +++ b/src/modules/file/types/file.types.ts @@ -0,0 +1,5 @@ +import { ApiBody } from "@/lib/types/api"; + +// Request types +export type FileSaveBody = ApiBody<"FileSave">; +export type FileRemoveBody = ApiBody<"FileRemove">; diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index 45a331d..6dc4df4 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -1,5 +1,23 @@ import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config"; -import { UserUpdateBody } from "../types/user.types"; +import { UserCreateBody, UserUpdateBody } from "../types/user.types"; + +async function fetch(id: string) { + const { data } = await withApiResponseHandling( + apiClient.GET("/users/{userId}", { + params: { path: { userId: id } }, + }) + ); + return data; +} + +async function create(payload: UserCreateBody) { + const { data } = await withApiResponseHandling( + apiClient.POST("/users", { + body: payload, + }) + ); + return data; +} async function update(id: string, payload: UserUpdateBody) { const { data } = await withApiResponseHandling( @@ -21,6 +39,8 @@ async function remove(id: string) { } const userService = { + fetch, + create, update, remove, }; diff --git a/src/modules/user/slices/user.slice.ts b/src/modules/user/slices/user.slice.ts index 8729647..3c6d6b9 100644 --- a/src/modules/user/slices/user.slice.ts +++ b/src/modules/user/slices/user.slice.ts @@ -7,13 +7,21 @@ export const name = "user"; //initial state const initialState: { + fetchStatus: ThunkStatus; + createStatus: ThunkStatus; updateStatus: ThunkStatus; removeStatus: ThunkStatus; } = { + fetchStatus: ThunkStatus.IDLE, + createStatus: ThunkStatus.IDLE, updateStatus: ThunkStatus.IDLE, removeStatus: ThunkStatus.IDLE, }; +const fetch = createAsyncThunk(`${name}/fetch`, userService.fetch); + +const create = createAsyncThunk(`${name}/create`, userService.create); + const update = createAsyncThunk( `${name}/update`, ({ id, ...payload }: UserUpdateBody & { id: string }) => @@ -30,4 +38,10 @@ export const userSlice = createSlice({ }); //action creators -export const userActions = { ...userSlice.actions, update, remove }; +export const userActions = { + ...userSlice.actions, + fetch, + create, + update, + remove, +}; diff --git a/src/modules/user/types/user.types.ts b/src/modules/user/types/user.types.ts index d4077c1..a9e4675 100644 --- a/src/modules/user/types/user.types.ts +++ b/src/modules/user/types/user.types.ts @@ -4,4 +4,5 @@ import { ApiBody, ApiResponse } from "@/lib/types/api"; export type User = ApiResponse<"UserFetch">; // Request types +export type UserCreateBody = ApiBody<"UserCreate">; export type UserUpdateBody = ApiBody<"UserUpdate">; diff --git a/src/pages/auth/forgot-password.tsx b/src/pages/auth/forgot-password.tsx index 697a69d..04927cc 100644 --- a/src/pages/auth/forgot-password.tsx +++ b/src/pages/auth/forgot-password.tsx @@ -7,6 +7,7 @@ import { AuthForgotPasswordBody } from "@/modules/auth/types/auth.types"; import { MailOutlined } from "@ant-design/icons"; import { Button, Form, Input, Typography } from "antd"; import React, { useRef, useState } from "react"; +import { Link } from "react-router-dom"; interface Props {} @@ -63,9 +64,9 @@ const ForgotPassword: React.FC = () => {
- + Back to login - +
diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index 1be59da..7d2d1d8 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -2,8 +2,9 @@ import Validations from "@/lib/utils/validations"; import { useAuth } from "@/modules/auth/hooks/auth.hooks"; import { AuthLoginBody } from "@/modules/auth/types/auth.types"; import { LockOutlined, MailOutlined } from "@ant-design/icons"; -import { Button, Form, Input, Typography } from "antd"; +import { Button, Form, Input } from "antd"; import React from "react"; +import { Link } from "react-router-dom"; interface Props {} @@ -41,9 +42,9 @@ const Login: React.FC = () => {
- + Forgot password? - +
diff --git a/src/pages/auth/reset-password.tsx b/src/pages/auth/reset-password.tsx index 4b48913..6eb6eed 100644 --- a/src/pages/auth/reset-password.tsx +++ b/src/pages/auth/reset-password.tsx @@ -20,7 +20,7 @@ interface Props {} const ResetPassword: React.FC = () => { const dispatch = useAppDispatch(); const loading = useIsLoading("auth", "resetPasswordStatus"); - const [{ email, otp }] = useUrlState({ email: "", otp: "" }); + const [{ email, otp }] = useUrlState(); const navigate = useNavigate(); const onFinish = (values: ResetPasswordForm) => { @@ -75,6 +75,7 @@ const ResetPassword: React.FC = () => { Validations.requiredField("Password"), { validator: Validations.minLen(6) }, ]} + validateFirst > } @@ -96,6 +97,7 @@ const ResetPassword: React.FC = () => { }, }), ]} + validateFirst > } diff --git a/src/pages/posts/posts.tsx b/src/pages/posts/posts.tsx index a4382d3..82aeba4 100644 --- a/src/pages/posts/posts.tsx +++ b/src/pages/posts/posts.tsx @@ -24,7 +24,7 @@ const Posts: React.FC = () => { title: "Date", dataIndex: "created_at", render: (_, record) => ( - + {formattedDateTime(record.created_at)} ), diff --git a/src/pages/settings/settings.tsx b/src/pages/settings/settings.tsx index ef4bd6e..5aac8d3 100644 --- a/src/pages/settings/settings.tsx +++ b/src/pages/settings/settings.tsx @@ -29,10 +29,9 @@ const Settings: React.FC = () => { const changePasswordLoading = useIsLoading("auth", "changePasswordStatus"); const onUpdateProfile = (values: ProfileForm) => { - const data = omit(values, ["photo"]); dispatch( profileActions.update({ - ...data, + ...values, photo: values.photo[0] instanceof File ? values.photo[0] : undefined, }) ) @@ -98,21 +97,25 @@ const Settings: React.FC = () => { > name="oldPassword" + label="Old Password" rules={[Validations.requiredField()]} > - + name="newPassword" + label="New Password" rules={[ Validations.requiredField(), { validator: Validations.minLen(6) }, ]} + validateFirst > - + name="confirmPassword" + label="Confirm Password" rules={[ Validations.requiredField(), ({ getFieldValue }) => ({ @@ -124,8 +127,9 @@ const Settings: React.FC = () => { }, }), ]} + validateFirst > - +