diff --git a/.env.development b/.env.development
index 3b9e7b2..8390f66 100644
--- a/.env.development
+++ b/.env.development
@@ -1,2 +1,3 @@
-VITE_API_BASE_URL=http://localhost:8000/dev/api
-VITE_ENV=dev
\ No newline at end of file
+VITE_API_BASE_URL=https://be.starters.rayonstudios.com/api/v1
+VITE_ENV=dev
+VITE_HCAPTCHA_SITE_KEY=cd86c190-7a30-4042-9468-018cd91ef63c
\ No newline at end of file
diff --git a/.env.production b/.env.production
index 57b59aa..ba57d70 100644
--- a/.env.production
+++ b/.env.production
@@ -1,2 +1,3 @@
-VITE_API_BASE_URL=http://localhost:8000/api
-VITE_ENV=production
\ No newline at end of file
+VITE_API_BASE_URL=https://be.starters.rayonstudios.com/api
+VITE_ENV=production
+VITE_HCAPTCHA_SITE_KEY=cd86c190-7a30-4042-9468-018cd91ef63c
\ No newline at end of file
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 227d154..eddf588 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -15,5 +15,10 @@ module.exports = {
"warn",
{ allowConstantExport: true },
],
+ "@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/.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/dev-deploy.yml b/.github/workflows/dev-deploy.yml
new file mode 100644
index 0000000..6250fc9
--- /dev/null
+++ b/.github/workflows/dev-deploy.yml
@@ -0,0 +1,25 @@
+name: Deploy to Firebase Hosting - Dev
+"on":
+ push:
+ branches:
+ - dev
+ workflow_dispatch:
+
+jobs:
+ build_and_deploy:
+ permissions: write-all
+ runs-on: ubuntu-latest
+ steps:
+ - 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 }}"
+ firebaseServiceAccount: "${{ secrets.SECRETJSON_DEV }}"
+ channelId: live
+ projectId: rayon-gcp-starter
+ target: dev
diff --git a/.github/workflows/firebase-hosting-merge-dev.yml b/.github/workflows/firebase-hosting-merge-dev.yml
deleted file mode 100644
index f06361e..0000000
--- a/.github/workflows/firebase-hosting-merge-dev.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Deploy to Firebase Hosting on merge (dev)
-"on":
- push:
- branches:
- - dev
-jobs:
- build_and_deploy:
- permissions: write-all
- 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:
- CI: false
-
- - uses: FirebaseExtended/action-hosting-deploy@v0
- with:
- repoToken: "${{ secrets.GITHUB_TOKEN }}"
- firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_DEV }}"
- channelId: live
- projectId: physikomatics-be
- target: dev
diff --git a/.github/workflows/firebase-hosting-merge-main.yml b/.github/workflows/firebase-hosting-merge-main.yml
deleted file mode 100755
index 25c1934..0000000
--- a/.github/workflows/firebase-hosting-merge-main.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Deploy to Firebase Hosting on merge (main)
-"on":
- push:
- branches:
- - main
-jobs:
- build_and_deploy:
- permissions: write-all
- 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
- env:
- CI: false
-
- - uses: FirebaseExtended/action-hosting-deploy@v0
- with:
- repoToken: "${{ secrets.GITHUB_TOKEN }}"
- firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }}"
- channelId: live
- projectId: applied-abbey-378009
- target: prod
diff --git a/.github/workflows/pr-preview-dev.yml b/.github/workflows/pr-preview-dev.yml
new file mode 100644
index 0000000..548c71d
--- /dev/null
+++ b/.github/workflows/pr-preview-dev.yml
@@ -0,0 +1,23 @@
+name: Preview 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@v3
+
+ - run: yarn install --frozen-lockfile && npm run build:dev
+ env:
+ CI: false
+
+ - uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ repoToken: "${{ secrets.GITHUB_TOKEN }}"
+ firebaseServiceAccount: "${{ secrets.SECRETJSON_DEV }}"
+ projectId: rayon-gcp-starter
+ target: dev
diff --git a/.github/workflows/pr-preview-prod.yml b/.github/workflows/pr-preview-prod.yml
new file mode 100644
index 0000000..2c6c521
--- /dev/null
+++ b/.github/workflows/pr-preview-prod.yml
@@ -0,0 +1,23 @@
+name: Preview 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@v3
+
+ - run: yarn install --frozen-lockfile && npm run build:prod
+ env:
+ CI: false
+
+ - uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ repoToken: "${{ secrets.GITHUB_TOKEN }}"
+ firebaseServiceAccount: "${{ secrets.SECRETJSON_PROD }}"
+ projectId: rayon-gcp-starter
+ target: prod
diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml
new file mode 100755
index 0000000..5693f22
--- /dev/null
+++ b/.github/workflows/prod-deploy.yml
@@ -0,0 +1,25 @@
+name: Deploy to Firebase Hosting - Prod
+"on":
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+jobs:
+ build_and_deploy:
+ permissions: write-all
+ runs-on: ubuntu-latest
+ steps:
+ - 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 }}"
+ firebaseServiceAccount: "${{ secrets.SECRETJSON_PROD }}"
+ channelId: live
+ projectId: rayon-gcp-starter
+ target: prod
diff --git a/README.md b/README.md
index 53b753d..ace0fee 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://fe.starters.rayonstudios.com/)
## Tech Stack
@@ -12,7 +12,8 @@ Rayon React Starter is an opinionated starter kit designed to scaffold React pro
- **Redux Toolkit**: State management with slices and thunks
- **Ant Design**: Elegant and consistent UI components
- **Tailwind CSS**: Utility-first CSS framework for rapid UI development
-- **Axios**: Promise-based HTTP client for making requests
+- **OpenAPI Fetch**: Type-safe API client generated from OpenAPI schemas
+- **Axios**: Promise-based HTTP client for making requests (used as the fetch implementation for OpenAPI Fetch)
## Features
@@ -29,6 +30,8 @@ Rayon React Starter is an opinionated starter kit designed to scaffold React pro
- ๐ **Built-in Utilities**: Handy utility functions, hooks, and components to cover common use cases
- ๐งน **Prettier and ESLint Config**: Enforce code style and quality with Prettier and ESLint configurations
- ๐ **CI/CD with Firebase Hosting**: Continuous Integration and Deployment setup using Firebase Hosting and Github Actions for seamless deployment
+- ๐ **ServerPaginatedTable**: Automatic server-side pagination, filtering, and sorting for data tables
+- ๐ **OpenAPI Type Generation**: Automatic type generation from OpenAPI schemas for type-safe API calls
## Getting Started
@@ -44,7 +47,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_react_starter
```
2. Install dependencies:
```bash
@@ -57,26 +60,102 @@ Ensure you have the following installed:
The application should now be running on http://localhost:5173
-## Folde Structure
+## Environment Configuration
+
+The project supports different environments:
+
+```bash
+# Development mode
+yarn dev # Run with development configuration
+yarn build:dev # Build with development configuration
+
+# Production mode
+yarn prod # Run with production configuration
+yarn build:prod # Build with production configuration
+```
+
+## API Integration
+
+### OpenAPI Type Generation
+
+The project includes a script to generate TypeScript types from an OpenAPI schema:
+
+```bash
+yarn gen-api-types:dev # Generate types from development API
+yarn gen-api-types:prod # Generate types from production API
+```
+
+The script fetches the OpenAPI schema from the API endpoint (configured via `VITE_API_BASE_URL` in the environment files) and generates TypeScript types in `src/lib/types/openapi-fetch.d.ts`.
+
+### Making API Calls
+
+The project uses `openapi-fetch` with Axios as the fetch implementation for type-safe API calls:
+
+```typescript
+import apiClient, { withApiResponseHandling } from '@/lib/openapi-fetch.config';
+
+// Example API call with type safety
+const { data } = await withApiResponseHandling(
+ apiClient.GET('/api/users/{id}', {
+ params: { path: { id: userId } }
+ })
+);
+```
+
+## Folder Structure
```bash
โโโ lib/ # Reusable, feature-independent code
+โ โโโ components/ # Shared UI components
+โ โโโ hooks/ # Custom React hooks
+โ โโโ types/ # TypeScript type definitions
+โ โโโ utils/ # Utility functions
โโโ modules/ # Feature-dependent code
+โ โโโ auth/ # Authentication related components and logic
+โ โโโ ... # Other feature modules
โโโ pages/ # Page components
-โโโ ...
+โโโ scripts/ # Build and utility scripts
```
- Use **snake_case** for file and folder names to maintain consistency.
- Feature-based folder structure ensures a clean separation of concerns for better scalability and maintainability.
+## Common Components
+
+### ServerPaginatedTable
+
+`ServerPaginatedTable` is a powerful component for handling server-side paginated data:
+
+```typescript
+import { ServerPaginatedTable } from '@/lib/components/table/server_paginated_table';
+
+// Example usage
+
+```
+
+The component automatically handles:
+- Pagination
+- Sorting
+- Filtering
+- Loading states
+- Error handling
+
## Best Practices
- Reusable code should generally reside in the `lib` folder.
- Feature-dependent code should be organized under `modules`.
-Pages components should be placed under the `pages` folder.
+- Pages components should be placed under the `pages` folder.
+- Use TypeScript for type safety across the application.
+- Follow the established patterns for state management with Redux Toolkit.
+- Use OpenAPI Fetch for API calls to ensure type safety.
+- Use ServerPaginatedTable for displaying data from API endpoints with pagination.
## Roadmap
- [ ] Detailed Developer Documentation
- [ ] Add unit, integration and e2e tests support
-- [ ] Convert to a modular CLI based tools
+- [ ] Convert to a modular CLI based tool
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/package.json b/package.json
index 282b095..d46f382 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,16 @@
{
"name": "rayon_react_starter",
"private": true,
- "version": "0.0.3",
+ "version": "2.1",
+ "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",
- "build:prod": "tsc && vite build",
- "build:dev": "tsc && vite build --mode development",
+ "dev": "npm run gen-types:dev && vite",
+ "prod": "npm run gen-types:prod && vite --mode production",
+ "build:prod": "npm run gen-types:prod && tsc && vite build",
+ "build:dev": "npm run gen-types: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",
@@ -17,6 +20,7 @@
"@ahooksjs/use-url-state": "^3.5.1",
"@ant-design/colors": "^7.0.2",
"@ant-design/icons": "^5.2.6",
+ "@hcaptcha/react-hcaptcha": "^1.12.0",
"@reduxjs/toolkit": "^2.1.0",
"@types/lodash": "^4.17.0",
"@types/qs": "^6.9.16",
@@ -28,6 +32,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",
@@ -40,6 +45,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",
@@ -52,10 +58,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..3de87d9
--- /dev/null
+++ b/scripts/gen-openapi-types.ts
@@ -0,0 +1,143 @@
+#!/usr/bin/env tsx
+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];
+
+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");
+ 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);
+
+ 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
index 3b56446..fd6c2be 100644
--- a/src/lib/axios.config.ts
+++ b/src/lib/axios.config.ts
@@ -17,8 +17,8 @@ const axios = axiosApi.create({
axios.interceptors.request.use((reqConfig) => {
const token = localStorage.getItem("accessToken");
- if (token && !reqConfig.headers.Authorization) {
- reqConfig.headers.Authorization = `Bearer ${token}`;
+ if (token && !reqConfig.headers.toJSON().authorization) {
+ reqConfig.headers.authorization = `Bearer ${token}`;
}
return reqConfig;
});
@@ -45,10 +45,19 @@ axios.interceptors.response.use(undefined, async (error) => {
}${error.message}`;
error.message = msg;
- if (error.response?.status === 401) {
- if (!["/auth/login", "/auth/refresh-token"].includes(error.config.url!)) {
+ if (
+ store.getState().auth.status !== "unauthenticated" &&
+ 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();
+ const { accessToken, refreshToken } = await authService.refreshToken(
+ localStorage.getItem("refreshToken")!
+ );
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
error.config.headers.Authorization = `Bearer ${accessToken}`;
diff --git a/src/lib/components/captcha/captcha.tsx b/src/lib/components/captcha/captcha.tsx
new file mode 100644
index 0000000..d4545bb
--- /dev/null
+++ b/src/lib/components/captcha/captcha.tsx
@@ -0,0 +1,17 @@
+import HCaptcha from "@hcaptcha/react-hcaptcha";
+import React from "react";
+
+interface Props {
+ onChange?: (token: string) => void;
+}
+
+const Captcha: React.FC = ({ onChange }) => {
+ return (
+ onChange?.(token)}
+ />
+ );
+};
+
+export default Captcha;
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/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..fac9cbd
--- /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 (
+
+ }
+ disabled={_value.length === count}
+ onClick={() =>
+ openFile({
+ accept,
+ multiple: true,
+ onChange: (files) =>
+ onChange?.([...(value || []), ...files].slice(0, count) as any),
+ })
+ }
+ >
+ Upload
+
+ {_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/image-picker/image-picker.tsx b/src/lib/components/image-picker/image-picker.tsx
new file mode 100644
index 0000000..24ebc1f
--- /dev/null
+++ b/src/lib/components/image-picker/image-picker.tsx
@@ -0,0 +1,147 @@
+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 | string[];
+ editable?: boolean;
+};
+
+export default function ImagePicker({
+ count,
+ width = 120,
+ height = 120,
+ gutter = 16,
+ listProps = {},
+ imgProps = {},
+ onChange = () => {},
+ value,
+ editable = true,
+}: ImagePickerProps) {
+ const _value = useMemo(
+ () =>
+ Array.isArray(value)
+ ? value
+ .filter((item) => !!item)
+ .map((item) => (typeof item === "string" ? urlToImg(item) : item))
+ : typeof value === "string"
+ ? [urlToImg(value)]
+ : value,
+ [value]
+ );
+
+ const { openFile } = useRootContextValues();
+ const [images, setImages] = useState(
+ _value ? arrayExtend(_value, count) : Array(count).fill({})
+ );
+
+ useUpdateEffect(() => {
+ onChange && onChange(images.filter(isImage).map((img) => img.src));
+ }, [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/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/lazy-select.tsx b/src/lib/components/server-paginated-select/lazy-select.tsx
index a49c380..bfcdd30 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,
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..aee19a4 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({
@@ -65,8 +65,8 @@ 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);
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..5d2a2e6 100644
--- a/src/lib/components/server-paginated-table/server-paginated-table.tsx
+++ b/src/lib/components/server-paginated-table/server-paginated-table.tsx
@@ -2,12 +2,14 @@ import axios from "@/lib/axios.config";
import { GenericObject } from "@/lib/types/misc";
import { isNullish } from "@/lib/utils/misc.utils";
import useUrlState from "@ahooksjs/use-url-state";
+import { ReloadOutlined } from "@ant-design/icons";
import { useDeepCompareEffect, useUpdateEffect } from "ahooks";
-import { Table, TableProps } from "antd";
+import { Button, Table, TableProps, Tooltip } 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 +28,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 & {
@@ -50,11 +52,12 @@ export type ServerPaginatedTableProps = TableProps & {
showSizeChanger?: boolean;
showQuickJumper?: boolean;
showTotal?: (total: number, range: [number, number]) => string;
+ showRefresh?: boolean;
};
export default function ServerPaginatedTable({
url,
- pageSize = 20,
+ pageSize = 10,
dataSource,
onDocsChange,
columns,
@@ -65,30 +68,57 @@ export default function ServerPaginatedTable({
showSizeChanger = true,
showQuickJumper = true,
showTotal = (total, range) => `${range[0]} - ${range[1]} of ${total} items`,
+ showRefresh = true,
...props
}: ServerPaginatedTableProps) {
+ const defaultSortCol = useMemo(
+ () => columns?.find((col) => col.defaultSortOrder),
+ [columns]
+ );
+ const defaultSortField =
+ (defaultSortCol as any)?.dataIndex || defaultSortCol?.key || "";
+ const initialState = {
+ current: 1,
+ pageSize,
+ ...filters?.reduce(
+ (acc, filter) => ({ ...acc, [`filter.${filter.key}`]: "" }),
+ {}
+ ),
+ ["sort.field"]: defaultSortField,
+ ["sort.order"]: defaultSortCol?.defaultSortOrder || "",
+ };
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
- const [tableParams, setTableParams] = useUrlState(
- {
- current: 1,
- pageSize,
- ...filters?.reduce(
- (acc, filter) => ({ ...acc, [`filter.${filter.key}`]: "" }),
- {}
- ),
- },
- { parseOptions: { parseNumbers: true } }
- );
+ const [tableParams, setTableParams] = useUrlState(initialState, {
+ 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) || {};
+
+ // if sorter is removed from a non-default column, set it to default
+ if (
+ defaultSortField &&
+ sortObj.field !== defaultSortField &&
+ !sortObj.order
+ ) {
+ sortObj.field = defaultSortField;
+ sortObj.order = defaultSortCol?.defaultSortOrder;
+ }
+
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 +141,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,18 +158,25 @@ 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;
+ 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)}`)
- .then(({ data }) => {
+ .then(({ data: { data } }) => {
const list = (
Array.isArray(data)
? data
@@ -146,9 +185,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 +207,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, data]);
+
return (
@@ -186,14 +237,21 @@ export default function ServerPaginatedTable({
onFiltersChange={onFiltersChange}
/>
) : null}
+ {showRefresh && (
+
+
+
+
+
+ )}
+
logout().unwrap().catch(console.error),
+ okType: "danger",
});
}, []);
@@ -114,11 +113,14 @@ function Header() {
{
key: "settings",
label: "Settings",
+ icon: ,
onClick: onSettingsClicked,
},
{
key: "logout",
label: "Logout",
+ danger: true,
+ icon: ,
disabled: logoutLoading,
onClick: onLogout,
},
@@ -127,8 +129,7 @@ function Header() {
trigger={["click"]}
>
-
- {profile.name}
+
diff --git a/src/lib/layouts/dashboard-layout/main-menu.tsx b/src/lib/layouts/dashboard-layout/main-menu.tsx
index ad5b84d..9781c82 100644
--- a/src/lib/layouts/dashboard-layout/main-menu.tsx
+++ b/src/lib/layouts/dashboard-layout/main-menu.tsx
@@ -1,7 +1,7 @@
import { store, useAppSelector } from "@/lib/redux/store";
import { getRoutePath } from "@/lib/router/router";
import { RouterConfig, useRouterConfig } from "@/lib/router/router-config";
-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 { Menu } from "antd";
import { useCallback } from "react";
@@ -26,9 +26,12 @@ export default function MainMenu({ closeOnNavigate = false, ...props }) {
(routes: RouterConfig[], prefix = "", basePath = "") => {
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/openapi-fetch.config.ts b/src/lib/openapi-fetch.config.ts
new file mode 100644
index 0000000..c65cfa4
--- /dev/null
+++ b/src/lib/openapi-fetch.config.ts
@@ -0,0 +1,100 @@
+import { globalErrorHandler } from "@/lib/utils/error.utils";
+import { AxiosError } from "axios";
+import createClient from "openapi-fetch";
+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 Axios as the fetch implementation
+const apiClient = createClient({
+ baseUrl: import.meta.env.VITE_API_BASE_URL,
+ // 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 if (contentType?.includes("multipart/form-data")) {
+ data = await input.formData();
+ } else {
+ data = input.body;
+ }
+ }
+
+ // 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",
+ }
+ );
+ }
+ },
+});
+
+apiClient.use({
+ 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 {
+ message: (error as any)?.error ?? error ?? data?.error,
+ name: "AxiosError",
+ };
+ }
+
+ const result = {
+ data: data.data,
+ response,
+ };
+
+ return result;
+}
+
+export default apiClient;
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..c9867f7 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-ignore
export const statusHandlerEnahncer: StoreEnhancer<{}, {}> =
@@ -23,7 +24,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;
@@ -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/redux/store.ts b/src/lib/redux/store.ts
index 4230fbf..7d2a44e 100644
--- a/src/lib/redux/store.ts
+++ b/src/lib/redux/store.ts
@@ -1,5 +1,7 @@
import { authSlice } from "@/modules/auth/slices/auth.slice";
import { profileSlice } from "@/modules/auth/slices/profile.slice";
+import { fileSlice } from "@/modules/file/slices/file.slice";
+import { userSlice } from "@/modules/user/slices/user.slice";
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { statusHandlerEnahncer } from "./enhancers/status.enhancer";
@@ -10,6 +12,8 @@ export const store = configureStore({
reducer: {
auth: authSlice.reducer,
profile: profileSlice.reducer,
+ user: userSlice.reducer,
+ file: fileSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }).concat(
diff --git a/src/lib/router/router-config.tsx b/src/lib/router/router-config.tsx
index 2bda16f..18477f3 100644
--- a/src/lib/router/router-config.tsx
+++ b/src/lib/router/router-config.tsx
@@ -1,8 +1,17 @@
-import { Role } from "@/modules/auth/types/profile.type";
+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 Home from "@/pages/home/home";
-import { DashboardOutlined } from "@ant-design/icons";
+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";
+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";
@@ -32,49 +41,74 @@ 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",
},
},
],
@@ -84,34 +118,33 @@ export const useRouterConfig = (): RouterConfig[] => {
{
layoutType: "dashboard",
authType: "private",
- component: ,
- menuItem: {
- title: "Page 2",
- icon: ,
- },
+ component: ,
route: {
- path: "/page-2",
+ path: "/settings",
},
},
{
- layoutType: "dashboard",
- authType: "private",
- component: ,
- allowedRoles: [Role.ADMIN],
- menuItem: {
- title: "Admin Only Page",
- icon: ,
+ layoutType: "auth",
+ authType: "public",
+ component: ,
+ route: {
+ path: "/login",
},
+ },
+ {
+ layoutType: "auth",
+ authType: "public",
+ component: ,
route: {
- path: "/admin-only",
+ path: "/forgot-password",
},
},
{
layoutType: "auth",
authType: "public",
- component: ,
+ component: ,
route: {
- path: "/login",
+ path: "/reset-password",
},
},
{
diff --git a/src/lib/router/router.tsx b/src/lib/router/router.tsx
index 4a18a1a..dbd7a80 100644
--- a/src/lib/router/router.tsx
+++ b/src/lib/router/router.tsx
@@ -5,15 +5,17 @@ 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 NotFound from "@/pages/404/404";
import _ from "lodash";
import React, {
PropsWithChildren,
ReactNode,
useEffect,
useLayoutEffect,
+ useRef,
useState,
} from "react";
import {
@@ -87,7 +89,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 (
@@ -163,27 +165,40 @@ 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") {
dispatch(profileActions.fetch());
}
- }, [authStatus]);
+ }, []);
useLayoutEffect(() => {
if (authStatus === "authenticated" && !role) {
setIsRendered(false);
- } else if (Array.isArray(allowedRoles) && !allowedRoles.includes(role!)) {
- navigate("/not-found");
- setIsRendered(false);
+ } else if (
+ Array.isArray(allowedRoles) &&
+ !allowedRoles.includes(role as Role)
+ ) {
+ 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..0775462 100644
--- a/src/lib/translations/en-US.json
+++ b/src/lib/translations/en-US.json
@@ -9,11 +9,17 @@
},
"sidebar": {
- "home": "Home"
+ "home": "Home",
+ "users": "Users",
+ "posts": "Posts"
},
"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
new file mode 100644
index 0000000..7fc0937
--- /dev/null
+++ b/src/lib/types/api.ts
@@ -0,0 +1,37 @@
+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<
+ OperationParameters["query" extends keyof OperationParameters
+ ? "query"
+ : never]
+>;
+
+export type ApiBody = NonNullable<
+ OperationRequestBody extends { content: { "application/json": any } }
+ ? OperationRequestBody["content"]["application/json"]
+ : OperationRequestBody extends {
+ content: { "multipart/form-data": any };
+ }
+ ? NonNullable>["content"]["multipart/form-data"]
+ : never
+>;
+
+export type ApiResponse = NonNullable<
+ OperationResponse extends {
+ "200": { content: { "application/json": { data: any } } };
+ }
+ ? OperationResponse["200"]["content"]["application/json"]["data"]
+ : never
+>;
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..a316745
--- /dev/null
+++ b/src/lib/types/openapi-fetch.d.ts
@@ -0,0 +1,1455 @@
+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["FileSave"];
+ 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 | (Date | undefined);
+ updated_at?: string | (Date | undefined);
+ 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;
+ };
+ PaginationSortResponse_SanitizedUser_: {
+ /** Format: double */
+ total?: number;
+ list: components["schemas"]["SanitizedUser"][];
+ };
+ 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 */
+ page?: number;
+ search?: string;
+ role?: components["schemas"]["Role"];
+ };
+ "Expand_Optional_UserMutable.bio-or-photo__": {
+ photo?: string;
+ bio?: string;
+ name: string;
+ role: string;
+ email: string;
+ };
+ UserCreate: components["schemas"]["Expand_Optional_UserMutable.bio-or-photo__"];
+ /** @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__"];
+ "Expand_PostUnlinked-and-_author-SanitizedUser__": {
+ created_at?: string | (Date | undefined);
+ updated_at?: string | (Date | undefined);
+ id?: string;
+ title: string;
+ slug: string;
+ text: string;
+ author_id: string;
+ /** Format: double */
+ views?: number;
+ labels: string[];
+ author: components["schemas"]["SanitizedUser"];
+ };
+ PostType: components["schemas"]["Expand_PostUnlinked-and-_author-SanitizedUser__"];
+ APIResponse_PostType_: {
+ data: components["schemas"]["PostType"] | null;
+ error: string | null;
+ };
+ PaginationSortResponse_PostType_: {
+ /** Format: double */
+ total?: number;
+ list: components["schemas"]["PostType"][];
+ };
+ 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 */
+ page?: number;
+ search?: string;
+ author_id?: string;
+ labels?: string[];
+ populate?: boolean;
+ };
+ "Expand_Omit_PostMutable.author_id__": {
+ title: string;
+ labels: string[];
+ text: string;
+ };
+ PostCreate: components["schemas"]["Expand_Omit_PostMutable.author_id__"];
+ Expand_Partial_PostMutable__: {
+ title?: string;
+ labels?: string[];
+ 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_"];
+ "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 | (Date | undefined);
+ updated_at?: string | (Date | undefined);
+ 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__"];
+ PaginationSortResponse_Notification_: {
+ /** Format: double */
+ total?: number;
+ list: components["schemas"]["Notification"][];
+ };
+ 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 */
+ 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";
+ /** @enum {string} */
+ IMAGE_SIZE: "small" | "medium" | "large";
+ Resizeconfig: {
+ sizes: components["schemas"]["IMAGE_SIZE"][];
+ 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: {
+ newPassword: string;
+ oldPassword: 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?: {
+ sortOrder?: components["schemas"]["SortOrder"];
+ sortField?: components["schemas"]["UserSortFields"];
+ limit?: number;
+ page?: number;
+ search?: string;
+ role?: components["schemas"]["Role"];
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Ok */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["APIResponse_PaginationSortResponse_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?: File;
+ };
+ };
+ };
+ 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?: {
+ sortOrder?: components["schemas"]["SortOrder"];
+ sortField?: components["schemas"]["PostSortFields"];
+ 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_PaginationSortResponse_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?: {
+ sortOrder?: components["schemas"]["SortOrder"];
+ sortField?: components["schemas"]["NotificationSortFields"];
+ 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_PaginationSortResponse_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_"];
+ };
+ };
+ };
+ };
+ FileSave: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "multipart/form-data": {
+ /** Format: binary */
+ file?: File;
+ 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?: File;
+ };
+ };
+ };
+ 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/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/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 db35c8b..134f679 100644
--- a/src/lib/utils/misc.utils.ts
+++ b/src/lib/utils/misc.utils.ts
@@ -11,10 +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/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/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/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/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..b917568 100644
--- a/src/modules/auth/services/auth.service.ts
+++ b/src/modules/auth/services/auth.service.ts
@@ -1,27 +1,56 @@
-import { fakeApi } from "@/lib/utils/misc.utils";
+import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config";
+import {
+ AuthChangePasswordBody,
+ AuthForgotPasswordBody,
+ AuthLoginBody,
+ AuthResetPasswordBody,
+} from "../types/auth.types";
-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: AuthLoginBody) {
+ const { data } = await withApiResponseHandling(
+ apiClient.POST("/auth/login", { body: payload })
+ );
+ return data;
}
-async function logout() {
- return fakeApi(() => true) as Promise;
+async function refreshToken(refreshToken: string) {
+ const { data } = await withApiResponseHandling(
+ apiClient.POST("/auth/refresh", {
+ headers: {
+ authorization: `Bearer ${refreshToken}`,
+ },
+ })
+ );
+ return data;
}
-async function refreshToken() {
- return fakeApi(() => ({
- accessToken: "accessToken",
- refreshToken: "refreshToken",
- })) as Promise<{ accessToken: string; refreshToken: string }>;
+async function changePassword(payload: AuthChangePasswordBody) {
+ const { data: response } = await withApiResponseHandling(
+ apiClient.POST("/auth/change-password", { body: payload })
+ );
+ 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,
- logout,
refreshToken,
+ changePassword,
+ forgotPassword,
+ resetPassword,
};
export default authService;
diff --git a/src/modules/auth/services/profile.service.ts b/src/modules/auth/services/profile.service.ts
index e534a7e..d51e3d8 100644
--- a/src/modules/auth/services/profile.service.ts
+++ b/src/modules/auth/services/profile.service.ts
@@ -1,22 +1,25 @@
-import { fakeApi } from "@/lib/utils/misc.utils";
-import { Profile } from "../types/profile.type";
+import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config";
+import { objectToFormData } from "@/lib/utils/misc.utils";
+import { ProfileUpdateBody } from "../types/profile.types";
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;
+}
+
+async function update(payload: ProfileUpdateBody) {
+ 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/auth.slice.ts b/src/modules/auth/slices/auth.slice.ts
index 0d1a772..0102ac3 100644
--- a/src/modules/auth/slices/auth.slice.ts
+++ b/src/modules/auth/slices/auth.slice.ts
@@ -12,12 +12,18 @@ const initialState: {
status: "processing" | "authenticated" | "unauthenticated";
loginStatus: ThunkStatus;
logoutStatus: ThunkStatus;
+ changePasswordStatus: ThunkStatus;
+ forgotPasswordStatus: ThunkStatus;
+ resetPasswordStatus: ThunkStatus;
} = {
computedRoutes: undefined,
sidebarCollapsed: false,
status: "processing",
loginStatus: ThunkStatus.IDLE,
logoutStatus: ThunkStatus.IDLE,
+ changePasswordStatus: ThunkStatus.IDLE,
+ forgotPasswordStatus: ThunkStatus.IDLE,
+ resetPasswordStatus: ThunkStatus.IDLE,
};
const login = createAsyncThunk(
@@ -29,12 +35,27 @@ const login = createAsyncThunk(
return res;
}
);
+
const logout = createAsyncThunk(`${name}/logout`, async () => {
- await authService.logout();
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
});
+const changePassword = createAsyncThunk(
+ `${name}/changePassword`,
+ authService.changePassword
+);
+
+const forgotPassword = createAsyncThunk(
+ `${name}/forgotPassword`,
+ authService.forgotPassword
+);
+
+const resetPassword = createAsyncThunk(
+ `${name}/resetPassword`,
+ authService.resetPassword
+);
+
//slice
export const authSlice = createSlice({
name,
@@ -61,4 +82,11 @@ export const authSlice = createSlice({
});
//action creators
-export const authActions = { ...authSlice.actions, login, logout };
+export const authActions = {
+ ...authSlice.actions,
+ login,
+ logout,
+ changePassword,
+ forgotPassword,
+ resetPassword,
+};
diff --git a/src/modules/auth/slices/profile.slice.ts b/src/modules/auth/slices/profile.slice.ts
index d586564..44b0b10 100644
--- a/src/modules/auth/slices/profile.slice.ts
+++ b/src/modules/auth/slices/profile.slice.ts
@@ -1,7 +1,7 @@
import { ThunkStatus } from "@/lib/types/misc";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import profileService from "../services/profile.service";
-import { Profile } from "../types/profile.type";
+import { Profile } from "../types/profile.types";
export const name = "profile";
@@ -9,24 +9,32 @@ 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);
+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..f16ec5d
--- /dev/null
+++ b/src/modules/auth/types/auth.types.ts
@@ -0,0 +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.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/src/modules/auth/types/profile.types.ts b/src/modules/auth/types/profile.types.ts
new file mode 100644
index 0000000..d402973
--- /dev/null
+++ b/src/modules/auth/types/profile.types.ts
@@ -0,0 +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/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/posts/post.types.ts b/src/modules/posts/post.types.ts
new file mode 100644
index 0000000..988c807
--- /dev/null
+++ b/src/modules/posts/post.types.ts
@@ -0,0 +1,4 @@
+import { ApiResponse } from "@/lib/types/api";
+
+// Response types
+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..6dc4df4
--- /dev/null
+++ b/src/modules/user/services/user.service.ts
@@ -0,0 +1,48 @@
+import apiClient, { withApiResponseHandling } from "@/lib/openapi-fetch.config";
+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(
+ 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 = {
+ fetch,
+ create,
+ 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..3c6d6b9
--- /dev/null
+++ b/src/modules/user/slices/user.slice.ts
@@ -0,0 +1,47 @@
+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: {
+ 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 }) =>
+ 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,
+ fetch,
+ create,
+ 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..a9e4675
--- /dev/null
+++ b/src/modules/user/types/user.types.ts
@@ -0,0 +1,8 @@
+import { ApiBody, ApiResponse } from "@/lib/types/api";
+
+// Response types
+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
new file mode 100644
index 0000000..da1e580
--- /dev/null
+++ b/src/pages/auth/forgot-password.tsx
@@ -0,0 +1,92 @@
+import Captcha from "@/lib/components/captcha/captcha";
+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";
+import { Link } from "react-router-dom";
+
+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 (
+
+
+ className="mb-1"
+ name="email"
+ rules={[
+ Validations.requiredField("Email"),
+ { validator: Validations.email },
+ ]}
+ validateFirst
+ >
+ } placeholder="Email" size="large" />
+
+
+
+
+ Back to login
+
+
+
+
+
+
+
+
+
+ Continue
+
+
+
+
+ );
+};
+
+export default ForgotPassword;
diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx
index f1c02b0..7d2d1d8 100644
--- a/src/pages/auth/login.tsx
+++ b/src/pages/auth/login.tsx
@@ -1,28 +1,27 @@
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];
+import { Link } from "react-router-dom";
interface Props {}
const Login: React.FC = () => {
const { login, loginLoading } = useAuth();
- const onFinish = (values: FormValues) => {
+ const onFinish = (values: AuthLoginBody) => {
login(values);
};
return (
-
onFinish={onFinish}>
+
name="email"
rules={[
- { required: true, message: Validations.reqd_msg("Email") },
+ Validations.requiredField("Email"),
{ validator: Validations.email },
]}
validateFirst
@@ -30,11 +29,10 @@ const Login: React.FC = () => {
} placeholder="Email" size="large" />
-
name="password"
- rules={[
- { required: true, message: Validations.reqd_msg("Password") },
- ]}
+ rules={[Validations.requiredField("Password")]}
+ className="mb-1"
>
}
@@ -43,8 +41,19 @@ const Login: React.FC = () => {
/>
-
-
+
+
+ Forgot password?
+
+
+
+
+
Log in
diff --git a/src/pages/auth/reset-password.tsx b/src/pages/auth/reset-password.tsx
new file mode 100644
index 0000000..6eb6eed
--- /dev/null
+++ b/src/pages/auth/reset-password.tsx
@@ -0,0 +1,124 @@
+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();
+ 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.
+
+
+
+
+ name="otp"
+ rules={[Validations.requiredField("Recovery code")]}
+ initialValue={otp}
+ label={otp ? "Recovery code" : undefined}
+ >
+
+
+
+
+ name="password"
+ rules={[
+ Validations.requiredField("Password"),
+ { validator: Validations.minLen(6) },
+ ]}
+ validateFirst
+ >
+ }
+ 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");
+ },
+ }),
+ ]}
+ validateFirst
+ >
+ }
+ placeholder="Confirm new password"
+ size="large"
+ />
+
+
+
+
+ Reset password
+
+
+
+
+ );
+};
+
+export default ResetPassword;
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
new file mode 100644
index 0000000..50adff2
--- /dev/null
+++ b/src/pages/posts/posts.tsx
@@ -0,0 +1,73 @@
+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 { 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";
+
+interface Props {}
+
+const Posts: React.FC = () => {
+ const { t } = useLang();
+
+ return (
+
+
{t("sidebar:posts")}
+
+ url="posts?populate=true"
+ columns={[
+ {
+ title: "Title",
+ dataIndex: "title",
+ ellipsis: true,
+ fixed: "left",
+ sorter: true,
+ },
+ {
+ title: "Date",
+ dataIndex: "created_at",
+ sorter: true,
+ defaultSortOrder: "descend",
+ render: (_, record) => (
+
+ {formattedDateTime(record.created_at)}
+
+ ),
+ },
+ {
+ title: "Author",
+ dataIndex: "author",
+ render: (_, record) => ,
+ },
+ {
+ title: "Labels",
+ dataIndex: "labels",
+ render: (_, record) =>
+ record.labels.map((label, ix) => (
+
+ {label}
+
+ )),
+ },
+ { title: "Views", dataIndex: "views", sorter: true },
+ {
+ 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/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
new file mode 100644
index 0000000..5aac8d3
--- /dev/null
+++ b/src/pages/settings/settings.tsx
@@ -0,0 +1,149 @@
+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 { 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 { Profile } from "@/modules/auth/types/profile.types";
+import { Button, Form, Input, message, Typography } from "antd";
+import { omit } from "lodash";
+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) => {
+ dispatch(
+ profileActions.update({
+ ...values,
+ photo: values.photo[0] instanceof File ? values.photo[0] : undefined,
+ })
+ )
+ .unwrap()
+ .then(() => message.success("Profile updated successfully"))
+ .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 (
+
+
+ Update Profile
+
+
name="photo">
+
+
+ name="name">
+
+
+ name="bio">
+
+
+
+
+ Save
+
+
+
+
+
+ Update Password
+
+
+ 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 }) => ({
+ validator(_, value) {
+ if (!value || getFieldValue("newPassword") === value) {
+ return Promise.resolve();
+ }
+ return Promise.reject("Passwords do not match");
+ },
+ }),
+ ]}
+ validateFirst
+ >
+
+
+
+
+ Save
+
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/src/pages/users/components/user-avatar.tsx b/src/pages/users/components/user-avatar.tsx
new file mode 100644
index 0000000..51aa4c9
--- /dev/null
+++ b/src/pages/users/components/user-avatar.tsx
@@ -0,0 +1,38 @@
+import ServerImg from "@/lib/components/server-img/server-img";
+import { kebabCaseToWords } from "@/lib/utils/string.utils";
+import { User } from "@/modules/user/types/user.types";
+import { Space, Typography } from "antd";
+import React from "react";
+
+interface Props {
+ user: Pick;
+ imgPreview?: boolean;
+ showRole?: boolean;
+}
+
+const UserAvatar: React.FC = ({ user, imgPreview, showRole }) => {
+ return (
+
+
+
+
+
+ {user.name}
+ {showRole ? ` (${kebabCaseToWords(user.role ?? "")})` : ""}
+
+
+
+ {user.email}
+
+
+
+ );
+};
+
+export default UserAvatar;
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/modals/edit-user-modal.tsx b/src/pages/users/modals/edit-user-modal.tsx
new file mode 100644
index 0000000..9298fca
--- /dev/null
+++ b/src/pages/users/modals/edit-user-modal.tsx
@@ -0,0 +1,125 @@
+import CustomModal from "@/lib/components/custom-modal/custom-modal";
+import ImagePicker from "@/lib/components/image-picker/image-picker";
+import { useIsLoading } from "@/lib/redux/enhancers/status.enhancer";
+import { useAppDispatch } from "@/lib/redux/store";
+import { Modify } from "@/lib/types/misc";
+import { suppressError } from "@/lib/utils/error.utils";
+import { sizedImg } from "@/lib/utils/image.utils";
+import { kebabCaseToWords } from "@/lib/utils/string.utils";
+import Validations from "@/lib/utils/validations";
+import { Role } from "@/modules/auth/hooks/role.hooks";
+import { fileActions } from "@/modules/file/slices/file.slice";
+import { userActions } from "@/modules/user/slices/user.slice";
+import { User, UserUpdateBody } from "@/modules/user/types/user.types";
+import { Form, Input, message, Select } from "antd";
+import React, { ComponentProps, useEffect, useState } from "react";
+
+type EditUserForm = Modify;
+
+type Props = ComponentProps & {
+ userId?: string;
+ onSuccess?: (user: User) => void;
+};
+
+const EditUserModal: React.FC = ({
+ userId,
+ open,
+ onSuccess,
+ ...props
+}) => {
+ const [form] = Form.useForm();
+ const [submitLoading, setSubmitLoading] = useState(false);
+ const dispatch = useAppDispatch();
+ const loading = useIsLoading("user", "fetchStatus");
+ const isOpen = open && Boolean(userId);
+
+ const onFinish = async (values: EditUserForm) => {
+ try {
+ setSubmitLoading(true);
+
+ // upload photo if it's a file
+ let photo = values.photo?.[0];
+ if (photo instanceof File) {
+ photo = (await dispatch(fileActions.save({ file: photo })).unwrap())
+ .url;
+ }
+
+ const updatedUser = await dispatch(
+ userActions.update({ ...values, id: userId!, photo })
+ ).unwrap();
+
+ message.success("User updated successfully");
+ form.resetFields();
+ onSuccess?.(updatedUser);
+ } catch (error) {
+ } finally {
+ setSubmitLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (isOpen) {
+ dispatch(userActions.fetch(userId!))
+ .unwrap()
+ .then((user) => {
+ form.setFieldsValue({
+ ...user,
+ photo: [sizedImg(user, "photo", "small")],
+ });
+ })
+ .catch(suppressError);
+ }
+ }, [isOpen]);
+
+ return (
+
+ name="photo">
+
+
+
+
+ name="name"
+ label="Name"
+ rules={[
+ Validations.requiredField("name"),
+ { validator: Validations.minLen(3) },
+ ]}
+ validateFirst
+ >
+
+
+
+ name="bio" label="Bio">
+
+
+
+
+ name="role"
+ label="Role"
+ rules={[Validations.requiredField("role")]}
+ >
+ ({
+ label: kebabCaseToWords(item),
+ value: item,
+ }))}
+ />
+
+
+
+ );
+};
+
+export default EditUserModal;
diff --git a/src/pages/users/modals/invite-user-modal.tsx b/src/pages/users/modals/invite-user-modal.tsx
new file mode 100644
index 0000000..454b88c
--- /dev/null
+++ b/src/pages/users/modals/invite-user-modal.tsx
@@ -0,0 +1,85 @@
+import CustomModal from "@/lib/components/custom-modal/custom-modal";
+import { useIsLoading } from "@/lib/redux/enhancers/status.enhancer";
+import { useAppDispatch } from "@/lib/redux/store";
+import { suppressError } from "@/lib/utils/error.utils";
+import { kebabCaseToWords } from "@/lib/utils/string.utils";
+import Validations from "@/lib/utils/validations";
+import { Role } from "@/modules/auth/hooks/role.hooks";
+import { userActions } from "@/modules/user/slices/user.slice";
+import { User, UserCreateBody } from "@/modules/user/types/user.types";
+import { Form, Input, message, Select } from "antd";
+import React, { ComponentProps } from "react";
+
+type Props = ComponentProps & {
+ onSuccess?: (user: User) => void;
+};
+
+const InviteUserModal: React.FC = ({ onSuccess, ...props }) => {
+ const [form] = Form.useForm();
+ const dispatch = useAppDispatch();
+ const loading = useIsLoading("user", "createStatus");
+
+ const onFinish = (values: UserCreateBody) => {
+ dispatch(userActions.create(values))
+ .unwrap()
+ .then((user) => {
+ message.success(
+ "An invitation has been sent to the user's email to join the platform"
+ );
+ form.resetFields();
+ onSuccess?.(user);
+ })
+ .catch(suppressError);
+ };
+
+ return (
+
+
+ name="name"
+ label="Name"
+ rules={[
+ Validations.requiredField("name"),
+ { validator: Validations.minLen(3) },
+ ]}
+ validateFirst
+ >
+
+
+
+
+ name="email"
+ label="Email"
+ rules={[
+ Validations.requiredField("email"),
+ { validator: Validations.email },
+ ]}
+ validateFirst
+ >
+
+
+
+
+ name="role"
+ label="Role"
+ rules={[Validations.requiredField("role")]}
+ >
+ ({
+ label: kebabCaseToWords(item),
+ value: item,
+ }))}
+ />
+
+
+
+ );
+};
+
+export default InviteUserModal;
diff --git a/src/pages/users/users.tsx b/src/pages/users/users.tsx
new file mode 100644
index 0000000..0f36f17
--- /dev/null
+++ b/src/pages/users/users.tsx
@@ -0,0 +1,198 @@
+import AlertPopup from "@/lib/components/alert-popup/alert-popup";
+import PageHeading from "@/lib/components/page-heading/page-heading";
+import ServerPaginatedTable, {
+ GetHelpers,
+} from "@/lib/components/server-paginated-table/server-paginated-table";
+import { useLang } from "@/lib/contexts/root.context";
+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 useUrlState from "@ahooksjs/use-url-state";
+import {
+ CheckCircleFilled,
+ DeleteOutlined,
+ EditOutlined,
+ PlusOutlined,
+} from "@ant-design/icons";
+import { Button, Space, Tag, Tooltip, Typography } from "antd";
+import React, { useRef } from "react";
+import UserAvatar from "./components/user-avatar";
+import EditUserModal from "./modals/edit-user-modal";
+import InviteUserModal from "./modals/invite-user-modal";
+
+interface Props {}
+
+const Users: React.FC = () => {
+ const { t } = useLang();
+ const dispatch = useAppDispatch();
+ const tableHelpers = useRef>();
+ const [{ action, user }, setUrlState] = useUrlState(undefined, {
+ navigateMode: "replace",
+ });
+
+ const onModalCancel = () => {
+ setUrlState({ action: undefined, user: undefined });
+ };
+
+ const onInviteSuccess = (user: User) => {
+ onModalCancel();
+ tableHelpers.current?.setData((data) => [user, ...data]);
+ };
+
+ const onEditSuccess = (user: User) => {
+ onModalCancel();
+ tableHelpers.current?.setData((data) =>
+ data.map((item) => (item.id === user.id ? { ...item, ...user } : item))
+ );
+ };
+
+ const onInvite = () => {
+ setUrlState({ action: "invite" });
+ };
+
+ const onEdit = (user: User) => {
+ setUrlState({ action: "edit", user: user.id });
+ };
+
+ 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(() => {
+ tableHelpers.current?.setData((data) =>
+ data.filter((item) => item.id !== user.id)
+ );
+ })
+ .catch(suppressError),
+ });
+ };
+
+ return (
+
+
{t("sidebar:users")}
+
+ url="users"
+ getHelpers={(helpers) => (tableHelpers.current = helpers)}
+ optionsBar={
+ } onClick={onInvite}>
+ Invite
+
+ }
+ 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",
+ sorter: true,
+ },
+ {
+ title: "Creation Date",
+ dataIndex: "created_at",
+ sorter: true,
+ defaultSortOrder: "descend",
+ render: (_, record) => (
+
+ {formattedDateTime(record.created_at)}
+
+ ),
+ },
+ {
+ 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,
+ },
+ {
+ title: "Actions",
+ key: "actions",
+ fixed: "right",
+ render: (_, user) => {
+ return (
+
+
+ onEdit(user)} />
+
+
+ onDelete(user)}
+ />
+
+
+ );
+ },
+ },
+ ]}
+ />
+
+
+
+
+ );
+};
+
+export default Users;
diff --git a/tailwind.config.js b/tailwind.config.js
index cb608ee..7b2fb5b 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -7,6 +7,7 @@ export default {
colors: {
primary: "#DAA520",
text: "#333",
+ danger: "#EC4949",
},
},
},
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..94ddb25 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"
@@ -121,6 +135,11 @@
dependencies:
regenerator-runtime "^0.14.0"
+"@babel/runtime@^7.17.9":
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.1.tgz#9fce313d12c9a77507f264de74626e87fd0dc541"
+ integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==
+
"@babel/runtime@^7.23.9", "@babel/runtime@^7.24.0":
version "7.24.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e"
@@ -160,116 +179,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"
@@ -302,6 +446,19 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b"
integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==
+"@hcaptcha/loader@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@hcaptcha/loader/-/loader-2.0.0.tgz#674b14262ca3d3302f39628a46bdfbb6f3f903f1"
+ integrity sha512-fFQH6ApU/zCCl6Y1bnbsxsp1Er/lKX+qlgljrpWDeFcenpEtoP68hExlKSXECospzKLeSWcr06cbTjlR/x3IJA==
+
+"@hcaptcha/react-hcaptcha@^1.12.0":
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.12.0.tgz#0d4d416edfc5a4c9b6e7e658f414abdad3c5825d"
+ integrity sha512-QiHnQQ52k8SJJSHkc3cq4TlYzag7oPd4f5ZqnjVSe4fJDSlZaOQFtu5F5AYisVslwaitdDELPVLRsRJxiiI0Aw==
+ dependencies:
+ "@babel/runtime" "^7.17.9"
+ "@hcaptcha/loader" "^2.0.0"
+
"@humanwhocodes/config-array@^0.11.13":
version "0.11.14"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
@@ -473,6 +630,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 +853,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 +1009,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 +1039,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 +1275,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 +1348,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 +1438,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 +1578,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 +1888,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 +2011,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 +2064,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 +2179,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 +2206,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 +2534,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 +2633,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 +2690,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 +2767,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 +3355,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 +3375,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 +3658,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 +3779,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 +3801,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 +3824,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 +3915,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 +3930,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"