Compare commits
31 Commits
5e6cfe8070
...
detached
| Author | SHA1 | Date | |
|---|---|---|---|
| 095f3dae76 | |||
| cd034bcce6 | |||
| 316cca712d | |||
| 74f7cc7664 | |||
| 7bb8ab97c7 | |||
| abbf782945 | |||
| c13cc4a0a5 | |||
| 236c0dcf10 | |||
| c98a5cc811 | |||
| 24de9f5446 | |||
| f13417e73a | |||
| 2ae9c619c7 | |||
| 315e7db3db | |||
| 5435750fb5 | |||
| 624c94155c | |||
| 3e1d544b33 | |||
| 459487a896 | |||
| 8ae198897d | |||
| 586af488da | |||
| 943b2d63f5 | |||
| 921ab4c89f | |||
| d13997ba80 | |||
| 5137836265 | |||
| 5fe9ea6747 | |||
| 8af4a908e6 | |||
| 128a1b3c4f | |||
| cb168b6415 | |||
| b8d431ae99 | |||
| cb6a814918 | |||
| fe6e87f97c | |||
| 948480c219 |
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
NEXT_PUBLIC_API_URL=http://api.example.com/api
|
||||||
@ -1,3 +0,0 @@
|
|||||||
*.js
|
|
||||||
*.mjs
|
|
||||||
*.cjs
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals"
|
|
||||||
}
|
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -102,7 +102,6 @@ dist
|
|||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
# vuepress v2.x temp and cache directory
|
||||||
.temp
|
.temp
|
||||||
.cache
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
# Docusaurus cache and generated files
|
||||||
.docusaurus
|
.docusaurus
|
||||||
@ -123,11 +122,9 @@ dist
|
|||||||
.vscode-test
|
.vscode-test
|
||||||
|
|
||||||
# yarn v2
|
# yarn v2
|
||||||
.yarn/cache
|
.yarn
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
.yarnrc.yml
|
||||||
@ -1,6 +1 @@
|
|||||||
# Ignore artifacts:
|
.next
|
||||||
build
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# Ignore all HTML files:
|
|
||||||
**/*.html
|
|
||||||
@ -1,6 +1,39 @@
|
|||||||
{
|
{
|
||||||
|
"singleAttributePerLine": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"semi": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"bracketSpacing": true,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"semi": false,
|
"bracketSameLine": true,
|
||||||
"singleQuote": false
|
"arrowParens": "avoid",
|
||||||
|
"plugins": [
|
||||||
|
"@ianvs/prettier-plugin-sort-imports"
|
||||||
|
],
|
||||||
|
"importOrder": [
|
||||||
|
".*styles.css$",
|
||||||
|
"dayjs",
|
||||||
|
"^react$",
|
||||||
|
"^next$",
|
||||||
|
"^next/.*$",
|
||||||
|
"<BUILTIN_MODULES>",
|
||||||
|
"<THIRD_PARTY_MODULES>",
|
||||||
|
"^@mantine/(.*)$",
|
||||||
|
"^@mantinex/(.*)$",
|
||||||
|
"^@mantine-tests/(.*)$",
|
||||||
|
"^@docs/(.*)$",
|
||||||
|
"^@/.*$",
|
||||||
|
"^../(?!.*.css$).*$",
|
||||||
|
"^./(?!.*.css$).*$",
|
||||||
|
"\\.css$"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.mdx",
|
||||||
|
"options": {
|
||||||
|
"printWidth": 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
.stylelintignore
Normal file
2
.stylelintignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.next
|
||||||
|
out
|
||||||
28
.stylelintrc.json
Normal file
28
.stylelintrc.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"extends": ["stylelint-config-standard-scss"],
|
||||||
|
"rules": {
|
||||||
|
"custom-property-pattern": null,
|
||||||
|
"selector-class-pattern": null,
|
||||||
|
"scss/no-duplicate-mixins": null,
|
||||||
|
"declaration-empty-line-before": null,
|
||||||
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
|
"alpha-value-notation": null,
|
||||||
|
"custom-property-empty-line-before": null,
|
||||||
|
"property-no-vendor-prefix": null,
|
||||||
|
"color-function-notation": null,
|
||||||
|
"length-zero-no-unit": null,
|
||||||
|
"selector-not-notation": null,
|
||||||
|
"no-descending-specificity": null,
|
||||||
|
"comment-empty-line-before": null,
|
||||||
|
"scss/at-mixin-pattern": null,
|
||||||
|
"scss/at-rule-no-unknown": null,
|
||||||
|
"value-keyword-case": null,
|
||||||
|
"media-feature-range-notation": null,
|
||||||
|
"selector-pseudo-class-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignorePseudoClasses": ["global"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
38
README.md
38
README.md
@ -1,5 +1,37 @@
|
|||||||
# Mantine Next Template
|
# Mantine Next.js template
|
||||||
|
|
||||||
Get started with the template by clicking `Use this template` button on the top of the page.
|
This is a template for [Next.js](https://nextjs.org/) app router + [Mantine](https://mantine.dev/).
|
||||||
|
If you want to use pages router instead, see [next-pages-template](https://github.com/mantinedev/next-pages-template).
|
||||||
|
|
||||||
[Documentation](https://mantine.dev/guides/next/)
|
## Features
|
||||||
|
|
||||||
|
This template comes with the following features:
|
||||||
|
|
||||||
|
- [PostCSS](https://postcss.org/) with [mantine-postcss-preset](https://mantine.dev/styles/postcss-preset)
|
||||||
|
- [TypeScript](https://www.typescriptlang.org/)
|
||||||
|
- [Storybook](https://storybook.js.org/)
|
||||||
|
- [Jest](https://jestjs.io/) setup with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
|
||||||
|
- ESLint setup with [eslint-config-mantine](https://github.com/mantinedev/eslint-config-mantine)
|
||||||
|
|
||||||
|
## npm scripts
|
||||||
|
|
||||||
|
### Build and dev scripts
|
||||||
|
|
||||||
|
- `dev` – start dev server
|
||||||
|
- `build` – bundle application for production
|
||||||
|
- `analyze` – analyzes application bundle with [@next/bundle-analyzer](https://www.npmjs.com/package/@next/bundle-analyzer)
|
||||||
|
|
||||||
|
### Testing scripts
|
||||||
|
|
||||||
|
- `typecheck` – checks TypeScript types
|
||||||
|
- `lint` – runs ESLint
|
||||||
|
- `prettier:check` – checks files with Prettier
|
||||||
|
- `jest` – runs jest tests
|
||||||
|
- `jest:watch` – starts jest watch
|
||||||
|
- `test` – runs `jest`, `prettier:check`, `lint` and `typecheck` scripts
|
||||||
|
|
||||||
|
### Other scripts
|
||||||
|
|
||||||
|
- `storybook` – starts storybook dev server
|
||||||
|
- `storybook:build` – build production storybook bundle to `storybook-static`
|
||||||
|
- `prettier:write` – formats all files with Prettier
|
||||||
|
|||||||
30
eslint.config.mjs
Normal file
30
eslint.config.mjs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import mantine from "eslint-config-mantine";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
...mantine,
|
||||||
|
{ ignores: ["**/*.{mjs,cjs,js,d.ts,d.mts}"] },
|
||||||
|
{
|
||||||
|
files: ["**/*.story.tsx"],
|
||||||
|
rules: {
|
||||||
|
"no-console": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
rules: {
|
||||||
|
"no-console": "off",
|
||||||
|
"react/jsx-curly-brace-presence": "off",
|
||||||
|
"curly": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["src/client/**/*.{ts,tsx}"],
|
||||||
|
rules: {
|
||||||
|
"import/no-useless-path-segments": "off",
|
||||||
|
},
|
||||||
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
16
jest.config.cjs
Normal file
16
jest.config.cjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const nextJest = require('next/jest');
|
||||||
|
|
||||||
|
const createJestConfig = nextJest({
|
||||||
|
dir: './',
|
||||||
|
});
|
||||||
|
|
||||||
|
const customJestConfig = {
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@/components/(.*)$': '<rootDir>/components/$1',
|
||||||
|
'^@/pages/(.*)$': '<rootDir>/pages/$1',
|
||||||
|
},
|
||||||
|
testEnvironment: 'jest-environment-jsdom',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = createJestConfig(customJestConfig);
|
||||||
27
jest.setup.cjs
Normal file
27
jest.setup.cjs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
require('@testing-library/jest-dom');
|
||||||
|
|
||||||
|
const { getComputedStyle } = window;
|
||||||
|
window.getComputedStyle = (elt) => getComputedStyle(elt);
|
||||||
|
window.HTMLElement.prototype.scrollIntoView = () => {};
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: jest.fn().mockImplementation((query) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
dispatchEvent: jest.fn(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
class ResizeObserver {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ResizeObserver = ResizeObserver;
|
||||||
@ -1,12 +1,16 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
import bundleAnalyzer from '@next/bundle-analyzer';
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
experimental: {
|
|
||||||
optimizePackageImports: [
|
|
||||||
"@mantine/core",
|
|
||||||
"@mantine/hooks",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default nextConfig
|
const withBundleAnalyzer = bundleAnalyzer({
|
||||||
|
enabled: process.env.ANALYZE === 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withBundleAnalyzer({
|
||||||
|
output: "standalone",
|
||||||
|
reactStrictMode: false,
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
optimizePackageImports: ['@mantine/core', '@mantine/hooks'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
28
openapi-ts.config.ts
Normal file
28
openapi-ts.config.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig } from "@hey-api/openapi-ts";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
input: "http://localhost:8000/openapi.json",
|
||||||
|
output: "src/lib/client",
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
"@hey-api/client-axios",
|
||||||
|
"@tanstack/react-query",
|
||||||
|
"@hey-api/typescript",
|
||||||
|
{
|
||||||
|
name: "zod",
|
||||||
|
requests: true,
|
||||||
|
responses: true,
|
||||||
|
definitions: true,
|
||||||
|
metadata: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "@hey-api/sdk",
|
||||||
|
asClass: false,
|
||||||
|
validator: "zod",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "@hey-api/client-next",
|
||||||
|
runtimeConfigPath: "./src/hey-api-config.ts",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
77
package.json
77
package.json
@ -6,41 +6,78 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"generate-client": "openapi-ts && prettier --write ./src/client/**/*.ts && git add ./src/client"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^8.2.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@mantine/dates": "^8.2.1",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@mantine/dropzone": "^8.2.1",
|
"@mantine/core": "8.1.2",
|
||||||
"@mantine/form": "^8.2.1",
|
"@mantine/form": "^8.1.3",
|
||||||
"@mantine/hooks": "^8.2.1",
|
"@mantine/hooks": "8.1.2",
|
||||||
"@mantine/modals": "^8.2.1",
|
"@mantine/modals": "^8.2.1",
|
||||||
"@mantine/notifications": "^8.2.1",
|
"@mantine/notifications": "^8.2.1",
|
||||||
|
"@next/bundle-analyzer": "^15.3.3",
|
||||||
"@reduxjs/toolkit": "^2.8.2",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.34.0",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
"dayjs": "^1.11.13",
|
"axios": "^1.11.0",
|
||||||
"lodash": "^4.17.21",
|
"classnames": "^2.5.1",
|
||||||
|
"framer-motion": "^12.23.7",
|
||||||
|
"i18n-iso-countries": "^7.14.0",
|
||||||
|
"lexorank": "^1.0.5",
|
||||||
|
"libphonenumber-js": "^1.12.10",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-imask": "^7.6.1",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"tailwind-preset-mantine": "^2.1.0",
|
"redux-persist": "^6.0.0",
|
||||||
"tailwindcss": "^4.1.11"
|
"sharp": "^0.34.3",
|
||||||
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash": "^4",
|
"@babel/core": "^7.27.4",
|
||||||
"@types/node": "22.13.11",
|
"@eslint/js": "^9.29.0",
|
||||||
"@types/react": "19.0.12",
|
"@hey-api/client-axios": "^0.9.1",
|
||||||
"@types/react-dom": "19.0.4",
|
"@hey-api/client-next": "^0.5.1",
|
||||||
"eslint": "9.23.0",
|
"@hey-api/openapi-ts": "^0.80.1",
|
||||||
"eslint-config-next": "15.2.3",
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
|
||||||
"postcss": "^8.5.3",
|
"@storybook/nextjs": "^8.6.8",
|
||||||
|
"@storybook/react": "^8.6.8",
|
||||||
|
"@testing-library/dom": "^10.4.0",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/eslint-plugin-jsx-a11y": "^6",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
|
"@types/node": "^22.13.11",
|
||||||
|
"@types/react": "19.1.8",
|
||||||
|
"@types/react-redux": "^7.1.34",
|
||||||
|
"@types/redux-persist": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"babel-loader": "^10.0.0",
|
||||||
|
"eslint": "^9.29.0",
|
||||||
|
"eslint-config-mantine": "^4.0.3",
|
||||||
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"jest-environment-jsdom": "^30.0.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
"postcss-preset-mantine": "1.17.0",
|
"postcss-preset-mantine": "1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "3.6.2",
|
"prettier": "^3.5.3",
|
||||||
"typescript": "5.8.2"
|
"storybook": "^8.6.8",
|
||||||
|
"storybook-dark-mode": "^4.0.2",
|
||||||
|
"stylelint": "^16.20.0",
|
||||||
|
"stylelint-config-standard-scss": "^15.0.1",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"ts-jest": "^29.4.0",
|
||||||
|
"typescript": "5.8.3",
|
||||||
|
"typescript-eslint": "^8.34.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.2"
|
"packageManager": "yarn@4.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
"postcss-preset-mantine": {},
|
"postcss-preset-mantine": {},
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
"postcss-simple-vars": {
|
"postcss-simple-vars": {
|
||||||
variables: {
|
variables: {
|
||||||
"mantine-breakpoint-xs": "36em",
|
"mantine-breakpoint-xs": "36em",
|
||||||
@ -12,4 +12,4 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1 +1,12 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><g fill="none" fill-rule="evenodd"><rect width="500" height="500" fill="#339AF0" rx="250"/><g fill="#FFF"><path fill-rule="nonzero" d="M202.055 135.706c-6.26 8.373-4.494 20.208 3.944 26.42 29.122 21.45 45.824 54.253 45.824 90.005 0 35.752-16.702 68.559-45.824 90.005-8.436 6.215-10.206 18.043-3.944 26.42 6.26 8.378 18.173 10.13 26.611 3.916a153.835 153.835 0 0024.509-22.54h53.93c10.506 0 19.023-8.455 19.023-18.885 0-10.43-8.517-18.886-19.023-18.886h-29.79c8.196-18.594 12.553-38.923 12.553-60.03s-4.357-41.436-12.552-60.03h29.79c10.505 0 19.022-8.455 19.022-18.885 0-10.43-8.517-18.886-19.023-18.886h-53.93a153.835 153.835 0 00-24.509-22.54c-8.438-6.215-20.351-4.46-26.61 3.916z"/><path d="M171.992 246.492c0-15.572 12.624-28.195 28.196-28.195 15.572 0 28.195 12.623 28.195 28.195 0 15.572-12.623 28.196-28.195 28.196-15.572 0-28.196-12.624-28.196-28.196z"/></g></g></svg>
|
<svg width="41" height="47" viewBox="0 0 41 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_431_24446)">
|
||||||
|
<path opacity="0.958" fill-rule="evenodd" clip-rule="evenodd" d="M20.2179 -0.0939941C20.406 -0.0939941 20.5941 -0.0939941 20.7822 -0.0939941C27.0194 3.59767 33.2885 7.26367 39.5895 10.904C33.2187 14.6831 26.8242 18.4118 20.406 22.09C13.9877 18.4118 7.59324 14.6831 1.22253 10.904C7.56616 7.23297 13.898 3.56697 20.2179 -0.0939941ZM19.6537 3.85401C19.9938 3.85239 20.3073 3.94639 20.5941 4.13601C24.2301 6.39201 27.8663 8.64801 31.5024 10.904C23.6659 11.0293 15.8296 11.0293 7.99318 10.904C11.9233 8.59642 15.8101 6.24642 19.6537 3.85401Z" fill="#44A8C6"/>
|
||||||
|
<path opacity="0.962" fill-rule="evenodd" clip-rule="evenodd" d="M-0.0939941 13.442C6.3424 16.991 12.7369 20.6257 19.0895 24.346C19.2776 31.8649 19.3402 39.3849 19.2776 46.906C19.0895 46.906 18.9014 46.906 18.7133 46.906C12.4971 43.203 6.22796 39.5684 -0.0939941 36.002C-0.0939941 28.482 -0.0939941 20.962 -0.0939941 13.442ZM2.91518 19.646C6.9531 26.4762 10.9653 33.3382 14.9519 40.232C10.9005 38.3163 6.91964 36.2169 3.00922 33.934C2.91518 29.1718 2.88385 24.4092 2.91518 19.646Z" fill="#334B63"/>
|
||||||
|
<path opacity="0.972" fill-rule="evenodd" clip-rule="evenodd" d="M40.906 13.442C40.906 21.0246 40.906 28.6074 40.906 36.19C34.5741 39.6675 28.305 43.2395 22.0986 46.906C21.9732 46.906 21.8479 46.906 21.7225 46.906C21.6911 39.3858 21.7225 31.8658 21.8165 24.346C28.1747 20.6832 34.5378 17.0485 40.906 13.442ZM25.8601 40.326C29.6787 33.4443 33.5969 26.6137 37.6147 19.834C37.7401 24.534 37.7401 29.234 37.6147 33.934C33.7364 36.1387 29.8183 38.2693 25.8601 40.326Z" fill="#3C83B4"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_431_24446">
|
||||||
|
<rect width="41" height="47" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 1.8 KiB |
16
src/.storybook/main.ts
Normal file
16
src/.storybook/main.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/nextjs';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
core: {
|
||||||
|
disableWhatsNewNotifications: true,
|
||||||
|
disableTelemetry: true,
|
||||||
|
enableCrashReports: false,
|
||||||
|
},
|
||||||
|
stories: ['../components/**/*.(stories|story).@(js|jsx|ts|tsx)'],
|
||||||
|
addons: ['storybook-dark-mode'],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/nextjs',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
36
src/.storybook/preview.tsx
Normal file
36
src/.storybook/preview.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import '@mantine/core/styles.css';
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { addons } from '@storybook/preview-api';
|
||||||
|
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
|
||||||
|
import { MantineProvider, useMantineColorScheme } from '@mantine/core';
|
||||||
|
import { theme } from '../theme';
|
||||||
|
|
||||||
|
export const parameters = {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
options: {
|
||||||
|
showPanel: false,
|
||||||
|
storySort: (a, b) => {
|
||||||
|
return a.title.localeCompare(b.title, undefined, { numeric: true });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const channel = addons.getChannel();
|
||||||
|
|
||||||
|
function ColorSchemeWrapper({ children }: { children: React.ReactNode }) {
|
||||||
|
const { setColorScheme } = useMantineColorScheme();
|
||||||
|
const handleColorScheme = (value: boolean) => setColorScheme(value ? 'dark' : 'light');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
channel.on(DARK_MODE_EVENT_NAME, handleColorScheme);
|
||||||
|
return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme);
|
||||||
|
}, [channel]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decorators = [
|
||||||
|
(renderStory: any) => <ColorSchemeWrapper>{renderStory()}</ColorSchemeWrapper>,
|
||||||
|
(renderStory: any) => <MantineProvider theme={theme}>{renderStory()}</MantineProvider>,
|
||||||
|
];
|
||||||
13
src/app/deals/components/Board/Board.tsx
Normal file
13
src/app/deals/components/Board/Board.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
board: BoardSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Board: FC<Props> = ({ board }) => {
|
||||||
|
return <Box miw={100} style={{ borderWidth: 1, margin: 0 }}>{board.name}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Board;
|
||||||
65
src/app/deals/components/Boards/Boards.tsx
Normal file
65
src/app/deals/components/Boards/Boards.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { ScrollArea } from "@mantine/core";
|
||||||
|
import Board from "@/app/deals/components/Board/Board";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
import { updateBoardMutation } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import SortableDnd from "@/components/SortableDnd";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
|
const Boards = () => {
|
||||||
|
const { boards, setSelectedBoard, refetchBoards } = useBoardsContext();
|
||||||
|
|
||||||
|
const updateBoard = useMutation({
|
||||||
|
...updateBoardMutation(),
|
||||||
|
onError: error => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
refetchBoards();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderBoard = (board: BoardSchema) => {
|
||||||
|
return <Board board={board} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||||
|
updateBoard.mutate({
|
||||||
|
path: {
|
||||||
|
boardId: itemId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
board: {
|
||||||
|
lexorank: newLexorank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectBoard = (board: BoardSchema) => {
|
||||||
|
setSelectedBoard(board);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea
|
||||||
|
offsetScrollbars={"x"}
|
||||||
|
scrollbars={"x"}
|
||||||
|
scrollbarSize={0}
|
||||||
|
w={"100%"}>
|
||||||
|
<SortableDnd
|
||||||
|
initialItems={boards}
|
||||||
|
renderItem={renderBoard}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onItemClick={selectBoard}
|
||||||
|
rowStyle={{ flexWrap: "nowrap" }}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Boards;
|
||||||
12
src/app/deals/components/DealCard/DealCard.tsx
Normal file
12
src/app/deals/components/DealCard/DealCard.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Card } from "@mantine/core";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
deal: DealSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DealCard = ({ deal }: Props) => {
|
||||||
|
return <Card>{deal.name}</Card>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DealCard;
|
||||||
25
src/app/deals/components/DealContainer/DealContainer.tsx
Normal file
25
src/app/deals/components/DealContainer/DealContainer.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, { FC, useMemo } from "react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
deal: DealSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DealContainer: FC<Props> = ({ deal }) => {
|
||||||
|
const dealBody = useMemo(() => <DealCard deal={deal} />, [deal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<SortableItem
|
||||||
|
dragHandleStyle={{ cursor: "pointer" }}
|
||||||
|
id={deal.id}>
|
||||||
|
{dealBody}
|
||||||
|
</SortableItem>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DealContainer;
|
||||||
36
src/app/deals/components/DndOverlay/DndOverlay.tsx
Normal file
36
src/app/deals/components/DndOverlay/DndOverlay.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
||||||
|
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
|
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activeDeal: DealSchema | null;
|
||||||
|
activeStatus: StatusSchema | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DndOverlay = ({ activeStatus, activeDeal }: Props) => {
|
||||||
|
const { deals } = useStatusesContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragOverlay dropAnimation={defaultDropAnimation}>
|
||||||
|
<div style={{ cursor: "grabbing" }}>
|
||||||
|
{activeDeal ? (
|
||||||
|
<DealCard deal={activeDeal} />
|
||||||
|
) : activeStatus ? (
|
||||||
|
<StatusColumn
|
||||||
|
id={`${activeStatus.id}-status`}
|
||||||
|
status={activeStatus}
|
||||||
|
deals={deals.filter(
|
||||||
|
deal => deal.statusId === activeStatus.id
|
||||||
|
)}
|
||||||
|
isDragging
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</DragOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DndOverlay;
|
||||||
24
src/app/deals/components/Header/Header.tsx
Normal file
24
src/app/deals/components/Header/Header.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const { projects, setSelectedProject, selectedProject } =
|
||||||
|
useProjectsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify={"flex-end"}
|
||||||
|
w={"100%"}>
|
||||||
|
<ProjectSelect
|
||||||
|
data={projects}
|
||||||
|
value={selectedProject}
|
||||||
|
onChange={value => value && setSelectedProject(value)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
37
src/app/deals/components/SortableItem/SortableItem.tsx
Normal file
37
src/app/deals/components/SortableItem/SortableItem.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortableItem = ({ children, id }: Props) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableItem;
|
||||||
67
src/app/deals/components/StatusColumn/StatusColumn.tsx
Normal file
67
src/app/deals/components/StatusColumn/StatusColumn.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useDroppable } from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { Box, Stack, Text } from "@mantine/core";
|
||||||
|
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
|
||||||
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
status: StatusSchema;
|
||||||
|
deals: DealSchema[];
|
||||||
|
isDragging?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusColumn = ({ id, status, deals, isDragging }: Props) => {
|
||||||
|
const { setNodeRef } = useDroppable({ id });
|
||||||
|
const sortedDeals = useMemo(
|
||||||
|
() => sortByLexorank(deals.filter(deal => deal.statusId === status.id)),
|
||||||
|
[deals]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnBody = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<SortableContext
|
||||||
|
id={id}
|
||||||
|
items={sortedDeals}
|
||||||
|
strategy={verticalListSortingStrategy}>
|
||||||
|
<Stack
|
||||||
|
gap={"xs"}
|
||||||
|
ref={setNodeRef}>
|
||||||
|
{sortedDeals.map(deal => (
|
||||||
|
<DealContainer
|
||||||
|
key={deal.id}
|
||||||
|
deal={deal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</SortableContext>
|
||||||
|
);
|
||||||
|
}, [sortedDeals]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#eee",
|
||||||
|
padding: 2,
|
||||||
|
width: "15vw",
|
||||||
|
minWidth: 150,
|
||||||
|
}}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
cursor: "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
}}>
|
||||||
|
{status.name}
|
||||||
|
</Text>
|
||||||
|
{columnBody}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusColumn;
|
||||||
76
src/app/deals/components/StatusColumns/StatusColumns.tsx
Normal file
76
src/app/deals/components/StatusColumns/StatusColumns.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import StatusColumnsDnd from "@/app/deals/components/StatusColumnsDnd/StatusColumnsDnd";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import {
|
||||||
|
updateDealMutation,
|
||||||
|
updateStatusMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
|
const StatusColumns = () => {
|
||||||
|
const { refetchStatuses, refetchDeals } = useStatusesContext();
|
||||||
|
|
||||||
|
const updateStatus = useMutation({
|
||||||
|
...updateStatusMutation(),
|
||||||
|
onError: error => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
refetchStatuses();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDeals = useMutation({
|
||||||
|
...updateDealMutation(),
|
||||||
|
onError: error => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
refetchDeals();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDealDragEnd = (
|
||||||
|
dealId: number,
|
||||||
|
statusId: number,
|
||||||
|
lexorank?: string
|
||||||
|
) => {
|
||||||
|
updateDeals.mutate({
|
||||||
|
path: {
|
||||||
|
dealId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
deal: {
|
||||||
|
statusId,
|
||||||
|
lexorank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
||||||
|
updateStatus.mutate({
|
||||||
|
path: {
|
||||||
|
statusId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
status: {
|
||||||
|
lexorank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusColumnsDnd
|
||||||
|
onDealDragEnd={onDealDragEnd}
|
||||||
|
onStatusDragEnd={onStatusDragEnd}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusColumns;
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { closestCorners, DndContext } from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
horizontalListSortingStrategy,
|
||||||
|
SortableContext,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { Group, ScrollArea } from "@mantine/core";
|
||||||
|
import DndOverlay from "@/app/deals/components/DndOverlay/DndOverlay";
|
||||||
|
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||||
|
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
|
import useDndSensors from "../../hooks/useSensors";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDealDragEnd: (
|
||||||
|
dealId: number,
|
||||||
|
statusId: number,
|
||||||
|
lexorank?: string
|
||||||
|
) => void;
|
||||||
|
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusColumnsDnd: FC<Props> = props => {
|
||||||
|
const { deals } = useStatusesContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
sortedStatuses,
|
||||||
|
handleDragStart,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragEnd,
|
||||||
|
activeStatus,
|
||||||
|
activeDeal,
|
||||||
|
} = useDealsAndStatusesDnd(props);
|
||||||
|
|
||||||
|
const sensors = useDndSensors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea
|
||||||
|
offsetScrollbars={"x"}
|
||||||
|
scrollbarSize={"0.5rem"}>
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCorners}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnd={handleDragEnd}>
|
||||||
|
<SortableContext
|
||||||
|
items={sortedStatuses.map(status => `${status.id}-status`)}
|
||||||
|
strategy={horizontalListSortingStrategy}>
|
||||||
|
<Group
|
||||||
|
gap={"xs"}
|
||||||
|
wrap={"nowrap"}
|
||||||
|
align={"start"}>
|
||||||
|
{sortedStatuses.map(status => (
|
||||||
|
<SortableItem
|
||||||
|
key={status.id}
|
||||||
|
id={`${status.id}-status`}>
|
||||||
|
<StatusColumn
|
||||||
|
id={`${status.id}-status`}
|
||||||
|
status={status}
|
||||||
|
deals={deals}
|
||||||
|
isDragging={activeStatus?.id === status.id}
|
||||||
|
/>
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
<DndOverlay
|
||||||
|
activeStatus={activeStatus}
|
||||||
|
activeDeal={activeDeal}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusColumnsDnd;
|
||||||
80
src/app/deals/contexts/BoardsContext.tsx
Normal file
80
src/app/deals/contexts/BoardsContext.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
FC,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
import useBoardsList from "@/hooks/useBoardsList";
|
||||||
|
|
||||||
|
type BoardsContextState = {
|
||||||
|
boards: BoardSchema[];
|
||||||
|
setBoards: React.Dispatch<React.SetStateAction<BoardSchema[]>>;
|
||||||
|
selectedBoard: BoardSchema | null;
|
||||||
|
setSelectedBoard: React.Dispatch<React.SetStateAction<BoardSchema | null>>;
|
||||||
|
refetchBoards: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoardsContext = createContext<BoardsContextState | undefined>(undefined);
|
||||||
|
|
||||||
|
const useBoardsContextState = () => {
|
||||||
|
const { selectedProject: project } = useProjectsContext();
|
||||||
|
const { boards, setBoards, refetch: refetchBoards } = useBoardsList({ projectId: project?.id });
|
||||||
|
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (boards.length > 0 && selectedBoard === null) {
|
||||||
|
setSelectedBoard(boards[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedBoard) {
|
||||||
|
let newBoard = boards.find(board => board.id === selectedBoard.id);
|
||||||
|
|
||||||
|
if (!newBoard && boards.length > 0) {
|
||||||
|
newBoard = boards[0];
|
||||||
|
}
|
||||||
|
setSelectedBoard(newBoard ?? null);
|
||||||
|
}
|
||||||
|
}, [boards]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
boards,
|
||||||
|
setBoards,
|
||||||
|
selectedBoard,
|
||||||
|
setSelectedBoard,
|
||||||
|
refetchBoards,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type BoardsContextProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BoardsContextProvider: FC<BoardsContextProviderProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const state = useBoardsContextState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoardsContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
</BoardsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBoardsContext = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useBoardsContext must be used within a BoardsContextProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
77
src/app/deals/contexts/ProjectsContext.tsx
Normal file
77
src/app/deals/contexts/ProjectsContext.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
FC,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { ProjectSchema } from "@/lib/client";
|
||||||
|
import useProjectsList from "@/hooks/useProjectsList";
|
||||||
|
|
||||||
|
type ProjectsContextState = {
|
||||||
|
selectedProject: ProjectSchema | null;
|
||||||
|
setSelectedProject: React.Dispatch<
|
||||||
|
React.SetStateAction<ProjectSchema | null>
|
||||||
|
>;
|
||||||
|
projects: ProjectSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectsContext = createContext<ProjectsContextState | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const useProjectsContextState = () => {
|
||||||
|
const { projects } = useProjectsList();
|
||||||
|
const [selectedProject, setSelectedProject] =
|
||||||
|
useState<ProjectSchema | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projects.length > 0) {
|
||||||
|
if (selectedProject) {
|
||||||
|
setSelectedProject(
|
||||||
|
projects.find(
|
||||||
|
project => project.id === selectedProject.id
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedProject(projects[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedProject(null);
|
||||||
|
}, [projects]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
selectedProject,
|
||||||
|
setSelectedProject,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectsContextProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectsContextProvider: FC<ProjectsContextProviderProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const state = useProjectsContextState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProjectsContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
</ProjectsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProjectsContext = () => {
|
||||||
|
const context = useContext(ProjectsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useProjectsContext must be used within a ProjectsContextProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
76
src/app/deals/contexts/StatusesContext.tsx
Normal file
76
src/app/deals/contexts/StatusesContext.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { createContext, FC, useContext, useEffect } from "react";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
|
import useDealsList from "@/hooks/useDealsList";
|
||||||
|
import useStatusesList from "@/hooks/useStatusesList";
|
||||||
|
|
||||||
|
type StatusesContextState = {
|
||||||
|
statuses: StatusSchema[];
|
||||||
|
setStatuses: React.Dispatch<React.SetStateAction<StatusSchema[]>>;
|
||||||
|
deals: DealSchema[];
|
||||||
|
setDeals: React.Dispatch<React.SetStateAction<DealSchema[]>>;
|
||||||
|
refetchStatuses: () => void;
|
||||||
|
refetchDeals: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusesContext = createContext<StatusesContextState | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const useStatusesContextState = () => {
|
||||||
|
const { selectedBoard } = useBoardsContext();
|
||||||
|
const {
|
||||||
|
statuses,
|
||||||
|
setStatuses,
|
||||||
|
refetch: refetchStatuses,
|
||||||
|
} = useStatusesList({
|
||||||
|
boardId: selectedBoard?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
deals,
|
||||||
|
setDeals,
|
||||||
|
refetch: refetchDeals,
|
||||||
|
} = useDealsList({ boardId: selectedBoard?.id });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetchStatuses();
|
||||||
|
}, [selectedBoard]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statuses,
|
||||||
|
setStatuses,
|
||||||
|
deals,
|
||||||
|
setDeals,
|
||||||
|
refetchStatuses,
|
||||||
|
refetchDeals,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatusesContextProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatusesContextProvider: FC<StatusesContextProviderProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const state = useStatusesContextState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusesContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
</StatusesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStatusesContext = () => {
|
||||||
|
const context = useContext(StatusesContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useStatusesContext must be used within a StatusesContextProvider"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
221
src/app/deals/hooks/useDealsAndStatusesDnd.ts
Normal file
221
src/app/deals/hooks/useDealsAndStatusesDnd.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
||||||
|
import { useDebouncedCallback } from "@mantine/hooks";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
||||||
|
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
||||||
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDealDragEnd: (
|
||||||
|
dealId: number,
|
||||||
|
statusId: number,
|
||||||
|
lexorank?: string
|
||||||
|
) => void;
|
||||||
|
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDealsAndStatusesDnd = (props: Props) => {
|
||||||
|
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||||
|
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||||
|
const { statuses, deals, setDeals, setStatuses } = useStatusesContext();
|
||||||
|
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getNewRankForSameStatus,
|
||||||
|
getNewRankForAnotherStatus,
|
||||||
|
getNewStatusRank,
|
||||||
|
} = useGetNewRank();
|
||||||
|
|
||||||
|
const debouncedSetStatuses = useDebouncedCallback(setStatuses, 200);
|
||||||
|
const debouncedSetDeals = useDebouncedCallback(setDeals, 200);
|
||||||
|
|
||||||
|
const getStatusByDealId = (dealId: number) => {
|
||||||
|
const deal = deals.find(deal => deal.id === dealId);
|
||||||
|
if (!deal) return;
|
||||||
|
return statuses.find(status => status.id === deal.statusId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = ({ active, over }: DragOverEvent) => {
|
||||||
|
if (!over) return;
|
||||||
|
const activeId = active.id as string | number;
|
||||||
|
|
||||||
|
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||||
|
handleColumnDragOver(activeId, over);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleDealDragOver(activeId, over);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDealDragOver = (activeId: string | number, over: Over) => {
|
||||||
|
const activeDealId = Number(activeId);
|
||||||
|
const activeStatusId = getStatusByDealId(activeDealId)?.id;
|
||||||
|
if (!activeStatusId) return;
|
||||||
|
|
||||||
|
const { overStatusId, newLexorank } = getDropTarget(
|
||||||
|
over.id,
|
||||||
|
activeDealId,
|
||||||
|
activeStatusId
|
||||||
|
);
|
||||||
|
if (!overStatusId) return;
|
||||||
|
|
||||||
|
debouncedSetDeals(deals =>
|
||||||
|
deals.map(deal =>
|
||||||
|
deal.id === activeDealId
|
||||||
|
? {
|
||||||
|
...deal,
|
||||||
|
statusId: overStatusId,
|
||||||
|
lexorank: newLexorank || deal.lexorank,
|
||||||
|
}
|
||||||
|
: deal
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnDragOver = (activeId: string, over: Over) => {
|
||||||
|
const activeStatusId = getStatusId(activeId);
|
||||||
|
let overStatusId: number;
|
||||||
|
|
||||||
|
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||||
|
overStatusId = getStatusId(over.id);
|
||||||
|
} else {
|
||||||
|
const deal = deals.find(deal => deal.id === over.id);
|
||||||
|
if (!deal) return;
|
||||||
|
overStatusId = deal.statusId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overStatusId || activeStatusId === overStatusId) return;
|
||||||
|
|
||||||
|
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
||||||
|
if (!newRank) return;
|
||||||
|
|
||||||
|
debouncedSetStatuses(statuses =>
|
||||||
|
statuses.map(status =>
|
||||||
|
status.id === activeStatusId
|
||||||
|
? { ...status, lexorank: newRank }
|
||||||
|
: status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDropTarget = (
|
||||||
|
overId: string | number,
|
||||||
|
activeDealId: number,
|
||||||
|
activeStatusId: number,
|
||||||
|
isOnDragEnd: boolean = false
|
||||||
|
) => {
|
||||||
|
if (typeof overId === "string") {
|
||||||
|
return {
|
||||||
|
overStatusId: getStatusId(overId),
|
||||||
|
newLexorank: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const overDealId = Number(overId);
|
||||||
|
const overStatusId = getStatusByDealId(overDealId)?.id;
|
||||||
|
|
||||||
|
if (!overStatusId || (!isOnDragEnd && activeDealId === overDealId)) {
|
||||||
|
return { overStatusId: undefined, newLexorank: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusDeals = sortByLexorank(
|
||||||
|
deals.filter(deal => deal.statusId === overStatusId)
|
||||||
|
);
|
||||||
|
const overDealIndex = statusDeals.findIndex(
|
||||||
|
deal => deal.id === overDealId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeStatusId === overStatusId) {
|
||||||
|
const newLexorank = getNewRankForSameStatus(
|
||||||
|
statusDeals,
|
||||||
|
overDealIndex,
|
||||||
|
activeDealId
|
||||||
|
);
|
||||||
|
return { overStatusId, newLexorank };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLexorank = getNewRankForAnotherStatus(
|
||||||
|
statusDeals,
|
||||||
|
overDealIndex
|
||||||
|
);
|
||||||
|
return { overStatusId, newLexorank };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = ({ active, over }: DragOverEvent) => {
|
||||||
|
setActiveDeal(null);
|
||||||
|
setActiveStatus(null);
|
||||||
|
if (!over) return;
|
||||||
|
|
||||||
|
const activeId: string | number = active.id;
|
||||||
|
|
||||||
|
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||||
|
handleStatusColumnDragEnd(activeId, over);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleDealDragEnd(activeId, over);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusColumnDragEnd = (activeId: string, over: Over) => {
|
||||||
|
const activeStatusId = getStatusId(activeId);
|
||||||
|
let overStatusId: number;
|
||||||
|
|
||||||
|
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||||
|
overStatusId = getStatusId(over.id);
|
||||||
|
} else {
|
||||||
|
const deal = deals.find(deal => deal.statusId === over.id);
|
||||||
|
if (!deal) return;
|
||||||
|
overStatusId = deal.statusId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overStatusId) return;
|
||||||
|
|
||||||
|
const newRank = getNewStatusRank(activeStatusId, overStatusId);
|
||||||
|
if (!newRank) return;
|
||||||
|
|
||||||
|
props.onStatusDragEnd?.(activeStatusId, newRank);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDealDragEnd = (activeId: number | string, over: Over) => {
|
||||||
|
const activeDealId = Number(activeId);
|
||||||
|
const activeStatusId = getStatusByDealId(activeDealId)?.id;
|
||||||
|
if (!activeStatusId) return;
|
||||||
|
|
||||||
|
const { overStatusId, newLexorank } = getDropTarget(
|
||||||
|
over.id,
|
||||||
|
activeDealId,
|
||||||
|
activeStatusId,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (!overStatusId) return;
|
||||||
|
|
||||||
|
props.onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||||
|
const activeId = active.id as string | number;
|
||||||
|
|
||||||
|
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||||
|
const statusId = getStatusId(activeId);
|
||||||
|
setActiveStatus(
|
||||||
|
statuses.find(status => status.id === statusId) ?? null
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveDeal(
|
||||||
|
deals.find(deal => deal.id === (activeId as number)) ?? null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortedStatuses,
|
||||||
|
handleDragStart,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragEnd,
|
||||||
|
activeStatus,
|
||||||
|
activeDeal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDealsAndStatusesDnd;
|
||||||
84
src/app/deals/hooks/useGetNewRank.ts
Normal file
84
src/app/deals/hooks/useGetNewRank.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { LexoRank } from "lexorank";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
const useGetNewRank = () => {
|
||||||
|
const { statuses } = useStatusesContext();
|
||||||
|
|
||||||
|
const getNewRankForSameStatus = (
|
||||||
|
statusDeals: DealSchema[],
|
||||||
|
overDealIndex: number,
|
||||||
|
activeDealId: number
|
||||||
|
) => {
|
||||||
|
const activeDealIndex = statusDeals.findIndex(
|
||||||
|
deal => deal.id === activeDealId
|
||||||
|
);
|
||||||
|
const [leftIndex, rightIndex] =
|
||||||
|
overDealIndex < activeDealIndex
|
||||||
|
? [overDealIndex - 1, overDealIndex]
|
||||||
|
: [overDealIndex, overDealIndex + 1];
|
||||||
|
|
||||||
|
const leftLexorank =
|
||||||
|
leftIndex >= 0
|
||||||
|
? LexoRank.parse(statusDeals[leftIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
const rightLexorank =
|
||||||
|
rightIndex < statusDeals.length
|
||||||
|
? LexoRank.parse(statusDeals[rightIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewRankForAnotherStatus = (
|
||||||
|
statusDeals: DealSchema[],
|
||||||
|
overDealIndex: number
|
||||||
|
) => {
|
||||||
|
const leftLexorank =
|
||||||
|
overDealIndex > 0
|
||||||
|
? LexoRank.parse(statusDeals[overDealIndex - 1].lexorank)
|
||||||
|
: null;
|
||||||
|
const rightLexorank = LexoRank.parse(
|
||||||
|
statusDeals[overDealIndex].lexorank
|
||||||
|
);
|
||||||
|
|
||||||
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewStatusRank = (activeStatusId: number, overStatusId: number) => {
|
||||||
|
const sortedStatusList = sortByLexorank(statuses);
|
||||||
|
const overIndex = sortedStatusList.findIndex(
|
||||||
|
s => s.id === overStatusId
|
||||||
|
);
|
||||||
|
const activeIndex = sortedStatusList.findIndex(
|
||||||
|
s => s.id === activeStatusId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overIndex === -1 || activeIndex === -1) return null;
|
||||||
|
|
||||||
|
const [leftIndex, rightIndex] =
|
||||||
|
overIndex < activeIndex
|
||||||
|
? [overIndex - 1, overIndex]
|
||||||
|
: [overIndex, overIndex + 1];
|
||||||
|
|
||||||
|
const leftLexorank =
|
||||||
|
leftIndex >= 0
|
||||||
|
? LexoRank.parse(statuses[leftIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
const rightLexorank =
|
||||||
|
rightIndex < statuses.length
|
||||||
|
? LexoRank.parse(statuses[rightIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getNewRankForSameStatus,
|
||||||
|
getNewRankForAnotherStatus,
|
||||||
|
getNewStatusRank,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGetNewRank;
|
||||||
26
src/app/deals/hooks/useSensors.ts
Normal file
26
src/app/deals/hooks/useSensors.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
TouchSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
||||||
|
|
||||||
|
const useDndSensors = () => {
|
||||||
|
const sensorOptions = {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return useSensors(
|
||||||
|
useSensor(PointerSensor, sensorOptions),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, sensorOptions)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDndSensors;
|
||||||
28
src/app/deals/page.tsx
Normal file
28
src/app/deals/page.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Divider } from "@mantine/core";
|
||||||
|
import Boards from "@/app/deals/components/Boards/Boards";
|
||||||
|
import Header from "@/app/deals/components/Header/Header";
|
||||||
|
import StatusColumns from "@/app/deals/components/StatusColumns/StatusColumns";
|
||||||
|
import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import PageBlock from "@/components/PageBlock/PageBlock";
|
||||||
|
import PageContainer from "@/components/PageContainer/PageContainer";
|
||||||
|
|
||||||
|
export default function DealsPage() {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageBlock>
|
||||||
|
<ProjectsContextProvider>
|
||||||
|
<BoardsContextProvider>
|
||||||
|
<Header />
|
||||||
|
<Boards />
|
||||||
|
<Divider my={"xl"} />
|
||||||
|
<StatusesContextProvider>
|
||||||
|
<StatusColumns />
|
||||||
|
</StatusesContextProvider>
|
||||||
|
</BoardsContextProvider>
|
||||||
|
</ProjectsContextProvider>
|
||||||
|
</PageBlock>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
src/app/deals/utils/statusId.ts
Normal file
6
src/app/deals/utils/statusId.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const STATUS_POSTFIX = "-status";
|
||||||
|
|
||||||
|
export const isStatusId = (rawId: string) => rawId.endsWith(STATUS_POSTFIX);
|
||||||
|
|
||||||
|
export const getStatusId = (rawId: string) =>
|
||||||
|
Number(rawId.replace(STATUS_POSTFIX, ""));
|
||||||
7
src/app/global.css
Normal file
7
src/app/global.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
body {
|
||||||
|
@mixin light {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +0,0 @@
|
|||||||
@import "tailwind-preset-mantine";
|
|
||||||
@import "./theme.css";
|
|
||||||
@ -1,41 +1,71 @@
|
|||||||
import "@mantine/core/styles.css"
|
import "@mantine/core/styles.css";
|
||||||
import "@mantine/dates/styles.css"
|
import "@mantine/notifications/styles.css";
|
||||||
import "@mantine/notifications/styles.css"
|
import { ReactNode } from "react";
|
||||||
import '@mantine/dropzone/styles.css';
|
|
||||||
|
|
||||||
import React from "react"
|
|
||||||
import {
|
import {
|
||||||
ColorSchemeScript,
|
ColorSchemeScript,
|
||||||
mantineHtmlProps,
|
mantineHtmlProps,
|
||||||
MantineProvider,
|
MantineProvider,
|
||||||
} from "@mantine/core"
|
} from "@mantine/core";
|
||||||
import { theme } from "./theme"
|
import { theme } from "@/theme";
|
||||||
import "./globals.css"
|
import "@/app/global.css";
|
||||||
import { Notifications } from "@mantine/notifications"
|
import { ModalsProvider } from "@mantine/modals";
|
||||||
import { ModalsProvider } from "@mantine/modals"
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
import { modals } from "@/modals/modals";
|
||||||
|
import { ReactQueryProvider } from "@/providers/ReactQueryProvider";
|
||||||
|
import ReduxProvider from "@/providers/ReduxProvider";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Mantine Next.js template",
|
title: "CRM LogiDex",
|
||||||
description: "I am using Mantine with Next.js!",
|
description: "CRM LogiDex",
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module "@mantine/modals" {
|
||||||
|
export interface MantineModalsOverride {
|
||||||
|
modals: typeof modals;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: any }) {
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: Props) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" {...mantineHtmlProps}>
|
<html
|
||||||
|
lang="ru"
|
||||||
|
{...mantineHtmlProps}>
|
||||||
<head>
|
<head>
|
||||||
<ColorSchemeScript />
|
<ColorSchemeScript defaultColorScheme={"auto"} />
|
||||||
<link rel="shortcut icon" href="/favicon.svg" />
|
<link
|
||||||
|
rel="shortcut icon"
|
||||||
|
href="/favicon.svg"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="global.css"
|
||||||
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
|
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
|
||||||
/>
|
/>
|
||||||
|
<title />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<MantineProvider theme={theme}>
|
<MantineProvider
|
||||||
<Notifications />
|
theme={theme}
|
||||||
<ModalsProvider>{children}</ModalsProvider>
|
defaultColorScheme={"auto"}>
|
||||||
|
<ReactQueryProvider>
|
||||||
|
<ReduxProvider>
|
||||||
|
<ModalsProvider
|
||||||
|
labels={{ confirm: "Да", cancel: "Нет" }}
|
||||||
|
modals={modals}>
|
||||||
|
{children}
|
||||||
|
</ModalsProvider>
|
||||||
|
</ReduxProvider>
|
||||||
|
<Notifications position="bottom-right" />
|
||||||
|
</ReactQueryProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,52 +1,5 @@
|
|||||||
import { ColorSchemesSwitcher } from "@/components/color-schemes-switcher"
|
import { redirect } from "next/navigation";
|
||||||
import {
|
|
||||||
AppShell,
|
|
||||||
AppShellHeader,
|
|
||||||
AppShellMain,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core"
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function HomePage() {
|
||||||
return (
|
redirect("/deals");
|
||||||
<AppShell header={{ height: 60 }} padding="md">
|
|
||||||
<AppShellHeader></AppShellHeader>
|
|
||||||
<AppShellMain>
|
|
||||||
<Title className="text-center mt-20">
|
|
||||||
Welcome to{" "}
|
|
||||||
<Text
|
|
||||||
inherit
|
|
||||||
variant="gradient"
|
|
||||||
component="span"
|
|
||||||
gradient={{ from: "pink", to: "yellow" }}
|
|
||||||
>
|
|
||||||
Mantine
|
|
||||||
</Text>{" "}
|
|
||||||
+
|
|
||||||
<Text
|
|
||||||
inherit
|
|
||||||
variant="gradient"
|
|
||||||
component="span"
|
|
||||||
gradient={{ from: "blue", to: "green" }}
|
|
||||||
>
|
|
||||||
TailwindCSS
|
|
||||||
</Text>
|
|
||||||
</Title>
|
|
||||||
<Text
|
|
||||||
className="text-bold text-center text-gray-700 dark:text-gray-300 max-w-[500px] mx-auto mt-xl"
|
|
||||||
ta="center"
|
|
||||||
size="lg"
|
|
||||||
maw={580}
|
|
||||||
mx="auto"
|
|
||||||
mt="xl"
|
|
||||||
>
|
|
||||||
This starter Next.js project includes a minimal setup for
|
|
||||||
Mantine with TailwindCSS. To get started edit page.tsx file.
|
|
||||||
</Text>
|
|
||||||
<div className="flex justify-center mt-10">
|
|
||||||
<ColorSchemesSwitcher />
|
|
||||||
</div>
|
|
||||||
</AppShellMain>
|
|
||||||
</AppShell>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
/** This file is autogenerated by the script. Do not edit it manually. */
|
|
||||||
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
|
|
||||||
|
|
||||||
/* colors - all */
|
|
||||||
|
|
||||||
|
|
||||||
/* colors - variant specific */
|
|
||||||
|
|
||||||
|
|
||||||
/* breakpoints */
|
|
||||||
--breakpoint-*: initial;
|
|
||||||
--breakpoint-xs: 36em;
|
|
||||||
--breakpoint-sm: 48em;
|
|
||||||
--breakpoint-md: 62em;
|
|
||||||
--breakpoint-lg: 75em;
|
|
||||||
--breakpoint-xl: 88em;
|
|
||||||
|
|
||||||
/* readd back tailwind's default containers vars to fix #24 */
|
|
||||||
--size-3xs: 16rem;
|
|
||||||
--size-2xs: 18rem;
|
|
||||||
--size-xs: 20rem;
|
|
||||||
--size-sm: 24rem;
|
|
||||||
--size-md: 28rem;
|
|
||||||
--size-lg: 32rem;
|
|
||||||
--size-xl: 36rem;
|
|
||||||
--size-2xl: 42rem;
|
|
||||||
--size-3xl: 48rem;
|
|
||||||
--size-4xl: 56rem;
|
|
||||||
--size-5xl: 64rem;
|
|
||||||
--size-6xl: 72rem;
|
|
||||||
--size-7xl: 80rem;
|
|
||||||
|
|
||||||
--container-3xs: var(--size-3xs);
|
|
||||||
--container-2xs: var(--size-2xs);
|
|
||||||
--container-xs: var(--size-xs);
|
|
||||||
--container-sm: var(--size-sm);
|
|
||||||
--container-md: var(--size-md);
|
|
||||||
--container-lg: var(--size-lg);
|
|
||||||
--container-xl: var(--size-xl);
|
|
||||||
--container-2xl: var(--size-2xl);
|
|
||||||
--container-3xl: var(--size-3xl);
|
|
||||||
--container-4xl: var(--size-4xl);
|
|
||||||
--container-5xl: var(--size-5xl);
|
|
||||||
--container-6xl: var(--size-6xl);
|
|
||||||
--container-7xl: var(--size-7xl);
|
|
||||||
|
|
||||||
--width-3xs: var(--size-3xs);
|
|
||||||
--width-2xs: var(--size-2xs);
|
|
||||||
--width-xs: var(--size-xs);
|
|
||||||
--width-sm: var(--size-sm);
|
|
||||||
--width-md: var(--size-md);
|
|
||||||
--width-lg: var(--size-lg);
|
|
||||||
--width-xl: var(--size-xl);
|
|
||||||
--width-2xl: var(--size-2xl);
|
|
||||||
--width-3xl: var(--size-3xl);
|
|
||||||
--width-4xl: var(--size-4xl);
|
|
||||||
--width-5xl: var(--size-5xl);
|
|
||||||
--width-6xl: var(--size-6xl);
|
|
||||||
--width-7xl: var(--size-7xl);
|
|
||||||
|
|
||||||
--min-width-3xs: var(--size-3xs);
|
|
||||||
--min-width-2xs: var(--size-2xs);
|
|
||||||
--min-width-xs: var(--size-xs);
|
|
||||||
--min-width-sm: var(--size-sm);
|
|
||||||
--min-width-md: var(--size-md);
|
|
||||||
--min-width-lg: var(--size-lg);
|
|
||||||
--min-width-xl: var(--size-xl);
|
|
||||||
--min-width-2xl: var(--size-2xl);
|
|
||||||
--min-width-3xl: var(--size-3xl);
|
|
||||||
--min-width-4xl: var(--size-4xl);
|
|
||||||
--min-width-5xl: var(--size-5xl);
|
|
||||||
--min-width-6xl: var(--size-6xl);
|
|
||||||
--min-width-7xl: var(--size-7xl);
|
|
||||||
|
|
||||||
--max-width-3xs: var(--size-3xs);
|
|
||||||
--max-width-2xs: var(--size-2xs);
|
|
||||||
--max-width-xs: var(--size-xs);
|
|
||||||
--max-width-sm: var(--size-sm);
|
|
||||||
--max-width-md: var(--size-md);
|
|
||||||
--max-width-lg: var(--size-lg);
|
|
||||||
--max-width-xl: var(--size-xl);
|
|
||||||
--max-width-2xl: var(--size-2xl);
|
|
||||||
--max-width-3xl: var(--size-3xl);
|
|
||||||
--max-width-4xl: var(--size-4xl);
|
|
||||||
--max-width-5xl: var(--size-5xl);
|
|
||||||
--max-width-6xl: var(--size-6xl);
|
|
||||||
--max-width-7xl: var(--size-7xl);
|
|
||||||
|
|
||||||
--height-3xs: var(--size-3xs);
|
|
||||||
--height-2xs: var(--size-2xs);
|
|
||||||
--height-xs: var(--size-xs);
|
|
||||||
--height-sm: var(--size-sm);
|
|
||||||
--height-md: var(--size-md);
|
|
||||||
--height-lg: var(--size-lg);
|
|
||||||
--height-xl: var(--size-xl);
|
|
||||||
--height-2xl: var(--size-2xl);
|
|
||||||
--height-3xl: var(--size-3xl);
|
|
||||||
--height-4xl: var(--size-4xl);
|
|
||||||
--height-5xl: var(--size-5xl);
|
|
||||||
--height-6xl: var(--size-6xl);
|
|
||||||
--height-7xl: var(--size-7xl);
|
|
||||||
|
|
||||||
--min-height-3xs: var(--size-3xs);
|
|
||||||
--min-height-2xs: var(--size-2xs);
|
|
||||||
--min-height-xs: var(--size-xs);
|
|
||||||
--min-height-sm: var(--size-sm);
|
|
||||||
--min-height-md: var(--size-md);
|
|
||||||
--min-height-lg: var(--size-lg);
|
|
||||||
--min-height-xl: var(--size-xl);
|
|
||||||
--min-height-2xl: var(--size-2xl);
|
|
||||||
--min-height-3xl: var(--size-3xl);
|
|
||||||
--min-height-4xl: var(--size-4xl);
|
|
||||||
--min-height-5xl: var(--size-5xl);
|
|
||||||
--min-height-6xl: var(--size-6xl);
|
|
||||||
--min-height-7xl: var(--size-7xl);
|
|
||||||
|
|
||||||
--max-height-3xs: var(--size-3xs);
|
|
||||||
--max-height-2xs: var(--size-2xs);
|
|
||||||
--max-height-xs: var(--size-xs);
|
|
||||||
--max-height-sm: var(--size-sm);
|
|
||||||
--max-height-md: var(--size-md);
|
|
||||||
--max-height-lg: var(--size-lg);
|
|
||||||
--max-height-xl: var(--size-xl);
|
|
||||||
--max-height-2xl: var(--size-2xl);
|
|
||||||
--max-height-3xl: var(--size-3xl);
|
|
||||||
--max-height-4xl: var(--size-4xl);
|
|
||||||
--max-height-5xl: var(--size-5xl);
|
|
||||||
--max-height-6xl: var(--size-6xl);
|
|
||||||
--max-height-7xl: var(--size-7xl);
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { createTheme } from "@mantine/core"
|
|
||||||
|
|
||||||
export const theme = createTheme({
|
|
||||||
breakpoints: {
|
|
||||||
xs: "36em",
|
|
||||||
sm: "48em",
|
|
||||||
md: "62em",
|
|
||||||
lg: "75em",
|
|
||||||
xl: "88em",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default theme
|
|
||||||
28
src/components/ColorSchemeToggle/ColorSchemeToggle.tsx
Normal file
28
src/components/ColorSchemeToggle/ColorSchemeToggle.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button, Group, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import { modals } from "@mantine/modals";
|
||||||
|
|
||||||
|
export function ColorSchemeToggle() {
|
||||||
|
const { setColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
const openTestModal = () => {
|
||||||
|
modals.openContextModal({
|
||||||
|
modal: "testModal",
|
||||||
|
title: "Тест",
|
||||||
|
withCloseButton: false,
|
||||||
|
innerProps: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify="center"
|
||||||
|
mt="xl">
|
||||||
|
<Button onClick={() => setColorScheme("light")}>Light</Button>
|
||||||
|
<Button onClick={() => setColorScheme("dark")}>Dark</Button>
|
||||||
|
<Button onClick={() => setColorScheme("auto")}>Auto</Button>
|
||||||
|
<Button onClick={() => openTestModal()}>Modal</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/components/Draggable/Draggable.tsx
Normal file
30
src/components/Draggable/Draggable.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { FC, ReactNode } from "react";
|
||||||
|
import { useDraggable } from "@dnd-kit/core";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Draggable: FC<Props> = props => {
|
||||||
|
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||||
|
id: "draggable",
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = transform
|
||||||
|
? {
|
||||||
|
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...listeners}
|
||||||
|
{...attributes}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Draggable;
|
||||||
25
src/components/Droppable/Droppable.tsx
Normal file
25
src/components/Droppable/Droppable.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, { FC, ReactNode } from "react";
|
||||||
|
import { useDroppable } from "@dnd-kit/core";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Droppable: FC<Props> = ({ children }) => {
|
||||||
|
const { isOver, setNodeRef } = useDroppable({
|
||||||
|
id: "droppable",
|
||||||
|
});
|
||||||
|
const style = {
|
||||||
|
color: isOver ? "green" : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Droppable;
|
||||||
41
src/components/PageBlock/PageBlock.module.css
Normal file
41
src/components/PageBlock/PageBlock.module.css
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
.container {
|
||||||
|
border-radius: rem(40);
|
||||||
|
background-color: white;
|
||||||
|
@mixin dark {
|
||||||
|
background-color: var(--mantine-color-dark-8);
|
||||||
|
box-shadow: 5px 5px 30px 1px var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
|
@mixin light {
|
||||||
|
box-shadow: 5px 5px 24px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
padding: rem(35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-full-height {
|
||||||
|
min-height: calc(100vh - (rem(20) * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-full-height-fixed {
|
||||||
|
height: calc(100vh - (rem(20) * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-no-border-radius {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-full-screen-mobile {
|
||||||
|
@media (max-width: 48em) {
|
||||||
|
min-height: 100vh;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
padding: rem(40) rem(20) rem(20);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/components/PageBlock/PageBlock.tsx
Normal file
36
src/components/PageBlock/PageBlock.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { CSSProperties, FC, ReactNode } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import styles from "./PageBlock.module.css";
|
||||||
|
5
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
fullHeight?: boolean;
|
||||||
|
fullHeightFixed?: boolean;
|
||||||
|
noBorderRadius?: boolean;
|
||||||
|
fullScreenMobile?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageBlock: FC<Props> = ({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
fullHeight = false,
|
||||||
|
fullHeightFixed = false,
|
||||||
|
noBorderRadius = false,
|
||||||
|
fullScreenMobile = false,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className={classNames(
|
||||||
|
styles.container,
|
||||||
|
fullHeight && styles["container-full-height"],
|
||||||
|
fullHeightFixed && styles["container-full-height-fixed"],
|
||||||
|
noBorderRadius && styles["container-no-border-radius"],
|
||||||
|
fullScreenMobile && styles["container-full-screen-mobile"]
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default PageBlock;
|
||||||
7
src/components/PageContainer/PageContainer.module.css
Normal file
7
src/components/PageContainer/PageContainer.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: rem(10);
|
||||||
|
min-height: 86vh;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
24
src/components/PageContainer/PageContainer.tsx
Normal file
24
src/components/PageContainer/PageContainer.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { CSSProperties, FC, ReactNode } from "react";
|
||||||
|
import styles from "./PageContainer.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
center?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageContainer: FC<Props> = ({ children, style, center }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
alignItems: center ? "center" : "",
|
||||||
|
justifyContent: center ? "center" : "",
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageContainer;
|
||||||
27
src/components/SortableDnd/DragHandle.tsx
Normal file
27
src/components/SortableDnd/DragHandle.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { CSSProperties, ReactNode, useContext } from "react";
|
||||||
|
import SortableItemContext from "@/components/SortableDnd/SortableItemContext";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DragHandle = ({ children, style }: Props) => {
|
||||||
|
const { attributes, listeners, ref } = useContext(SortableItemContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
cursor: "grab",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
ref={ref}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DragHandle;
|
||||||
130
src/components/SortableDnd/SortableDnd.tsx
Normal file
130
src/components/SortableDnd/SortableDnd.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Active, DndContext, DragEndEvent } from "@dnd-kit/core";
|
||||||
|
import { SortableContext } from "@dnd-kit/sortable";
|
||||||
|
import { LexoRank } from "lexorank";
|
||||||
|
import { Box, Group } from "@mantine/core";
|
||||||
|
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||||
|
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
|
import { SortableOverlay } from "@/components/SortableDnd/SortableOverlay";
|
||||||
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
type BaseItem = {
|
||||||
|
id: number;
|
||||||
|
lexorank: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props<T extends BaseItem> = {
|
||||||
|
initialItems: T[];
|
||||||
|
renderItem: (item: T) => ReactNode;
|
||||||
|
onDragEnd: (itemId: number, newLexorank: string) => void;
|
||||||
|
onItemClick: (item: T) => void;
|
||||||
|
rowStyle?: CSSProperties;
|
||||||
|
itemStyle?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortableDnd = <T extends BaseItem>({
|
||||||
|
initialItems,
|
||||||
|
renderItem,
|
||||||
|
onDragEnd,
|
||||||
|
onItemClick,
|
||||||
|
rowStyle,
|
||||||
|
itemStyle,
|
||||||
|
}: Props<T>) => {
|
||||||
|
const [active, setActive] = useState<Active | null>(null);
|
||||||
|
const [items, setItems] = useState<T[]>([]);
|
||||||
|
const activeItem = useMemo(
|
||||||
|
() => initialItems.find(item => item.id === active?.id),
|
||||||
|
[active, items]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItems(sortByLexorank(initialItems));
|
||||||
|
}, [initialItems]);
|
||||||
|
|
||||||
|
const sensors = useDndSensors();
|
||||||
|
|
||||||
|
const onDragEndLocal = ({ active, over }: DragEndEvent) => {
|
||||||
|
if (over && active.id !== over?.id && activeItem) {
|
||||||
|
const overIndex: number = items.findIndex(
|
||||||
|
({ id }) => id === over.id
|
||||||
|
);
|
||||||
|
const activeIndex: number = items.findIndex(
|
||||||
|
({ id }) => id === activeItem.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let leftIndex = overIndex;
|
||||||
|
let rightIndex = overIndex + 1;
|
||||||
|
if (overIndex < activeIndex) {
|
||||||
|
leftIndex = overIndex - 1;
|
||||||
|
rightIndex = overIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftLexorank: LexoRank | null =
|
||||||
|
leftIndex >= 0
|
||||||
|
? LexoRank.parse(items[leftIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
const rightLexorank: LexoRank | null =
|
||||||
|
rightIndex < items.length
|
||||||
|
? LexoRank.parse(items[rightIndex].lexorank)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const newLexorank = getNewLexorank(
|
||||||
|
leftLexorank,
|
||||||
|
rightLexorank
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
items[activeIndex].lexorank = newLexorank;
|
||||||
|
onDragEnd(items[activeIndex].id, newLexorank);
|
||||||
|
const sortedItems = sortByLexorank(items);
|
||||||
|
setItems([...sortedItems]);
|
||||||
|
}
|
||||||
|
setActive(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
onDragStart={({ active }) => setActive(active)}
|
||||||
|
onDragEnd={onDragEndLocal}
|
||||||
|
onDragCancel={() => setActive(null)}>
|
||||||
|
<SortableContext items={items}>
|
||||||
|
<Group
|
||||||
|
gap={0}
|
||||||
|
style={rowStyle}
|
||||||
|
role="application">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onItemClick(item);
|
||||||
|
}}>
|
||||||
|
<SortableItem
|
||||||
|
dragHandleStyle={{ cursor: "pointer" }}
|
||||||
|
itemStyle={itemStyle}
|
||||||
|
id={item.id}>
|
||||||
|
{renderItem(item)}
|
||||||
|
</SortableItem>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</SortableContext>
|
||||||
|
<SortableOverlay>
|
||||||
|
<div style={{ cursor: "grabbing" }}>
|
||||||
|
{activeItem ? renderItem(activeItem) : null}
|
||||||
|
</div>
|
||||||
|
</SortableOverlay>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableDnd;
|
||||||
54
src/components/SortableDnd/SortableItem.tsx
Normal file
54
src/components/SortableDnd/SortableItem.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React, { CSSProperties, PropsWithChildren, useMemo } from "react";
|
||||||
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import DragHandle from "@/components/SortableDnd/DragHandle";
|
||||||
|
import SortableItemContext from "./SortableItemContext";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: number | string;
|
||||||
|
itemStyle?: CSSProperties;
|
||||||
|
dragHandleStyle?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SortableItem = ({
|
||||||
|
children,
|
||||||
|
itemStyle,
|
||||||
|
id,
|
||||||
|
dragHandleStyle,
|
||||||
|
}: PropsWithChildren<Props>) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
isDragging,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
setActivatorNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
} = useSortable({ id });
|
||||||
|
|
||||||
|
const context = useMemo(
|
||||||
|
() => ({
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
ref: setActivatorNodeRef,
|
||||||
|
}),
|
||||||
|
[attributes, listeners, setActivatorNodeRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
const style: CSSProperties = {
|
||||||
|
opacity: isDragging ? 0.4 : undefined,
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition,
|
||||||
|
...itemStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableItemContext.Provider value={context}>
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}>
|
||||||
|
<DragHandle style={dragHandleStyle}>{children}</DragHandle>
|
||||||
|
</div>
|
||||||
|
</SortableItemContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
src/components/SortableDnd/SortableItemContext.tsx
Normal file
16
src/components/SortableDnd/SortableItemContext.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { DraggableSyntheticListeners } from "@dnd-kit/core";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
listeners: DraggableSyntheticListeners;
|
||||||
|
ref: (node: HTMLElement | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableItemContext = createContext<Context>({
|
||||||
|
attributes: {},
|
||||||
|
listeners: undefined,
|
||||||
|
ref() {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SortableItemContext;
|
||||||
24
src/components/SortableDnd/SortableOverlay.tsx
Normal file
24
src/components/SortableDnd/SortableOverlay.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
import {
|
||||||
|
defaultDropAnimationSideEffects,
|
||||||
|
DragOverlay,
|
||||||
|
DropAnimation,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
|
||||||
|
const dropAnimationConfig: DropAnimation = {
|
||||||
|
sideEffects: defaultDropAnimationSideEffects({
|
||||||
|
styles: {
|
||||||
|
active: {
|
||||||
|
opacity: "0.4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SortableOverlay({ children }: PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<DragOverlay dropAnimation={dropAnimationConfig}>
|
||||||
|
{children}
|
||||||
|
</DragOverlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/components/SortableDnd/index.ts
Normal file
3
src/components/SortableDnd/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import SortableDnd from "@/components/SortableDnd/SortableDnd";
|
||||||
|
|
||||||
|
export default SortableDnd;
|
||||||
@ -1,18 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Button, Group, useMantineColorScheme } from "@mantine/core"
|
|
||||||
|
|
||||||
export function ColorSchemesSwitcher() {
|
|
||||||
const { setColorScheme, clearColorScheme } = useMantineColorScheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group>
|
|
||||||
<Button variant={"filled"} onClick={() => setColorScheme("light")}>
|
|
||||||
Light
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setColorScheme("dark")}>Dark</Button>
|
|
||||||
<Button onClick={() => setColorScheme("auto")}>Auto</Button>
|
|
||||||
<Button onClick={clearColorScheme}>Clear</Button>
|
|
||||||
</Group>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
110
src/components/selects/ObjectSelect/ObjectSelect.tsx
Normal file
110
src/components/selects/ObjectSelect/ObjectSelect.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { groupBy, omit } from "lodash";
|
||||||
|
import { Select, SelectProps } from "@mantine/core";
|
||||||
|
|
||||||
|
interface ObjectWithIdAndName {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectObjectType<T> = T;
|
||||||
|
|
||||||
|
type ControlledValueProps<T> = {
|
||||||
|
value: SelectObjectType<T>;
|
||||||
|
onChange: (value: SelectObjectType<T>) => void;
|
||||||
|
};
|
||||||
|
type CustomLabelAndKeyProps<T> = {
|
||||||
|
getLabelFn: (item: SelectObjectType<T>) => string;
|
||||||
|
getValueFn: (item: SelectObjectType<T>) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RestProps<T> = {
|
||||||
|
defaultValue?: SelectObjectType<T>;
|
||||||
|
onChange: (value: SelectObjectType<T>) => void;
|
||||||
|
data: SelectObjectType<T>[];
|
||||||
|
groupBy?: (item: SelectObjectType<T>) => string;
|
||||||
|
filterBy?: (item: SelectObjectType<T>) => boolean;
|
||||||
|
};
|
||||||
|
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||||
|
return item.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||||
|
if (!item) return item;
|
||||||
|
return item.id.toString();
|
||||||
|
};
|
||||||
|
export type ObjectSelectProps<T> = (RestProps<T> &
|
||||||
|
Partial<ControlledValueProps<T>>) &
|
||||||
|
Omit<SelectProps, "value" | "onChange" | "data"> &
|
||||||
|
(T extends ObjectWithIdAndName
|
||||||
|
? Partial<CustomLabelAndKeyProps<T>>
|
||||||
|
: CustomLabelAndKeyProps<T>);
|
||||||
|
|
||||||
|
const ObjectSelect = <T,>(props: ObjectSelectProps<T>) => {
|
||||||
|
const isControlled = "value" in props;
|
||||||
|
const haveGetValueFn = "getValueFn" in props;
|
||||||
|
const haveGetLabelFn = "getLabelFn" in props;
|
||||||
|
const [internalValue, setInternalValue] = useState<
|
||||||
|
SelectObjectType<T> | undefined
|
||||||
|
>(props.defaultValue);
|
||||||
|
|
||||||
|
const value = isControlled ? props.value : internalValue;
|
||||||
|
|
||||||
|
const getValueFn =
|
||||||
|
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||||
|
const getLabelFn =
|
||||||
|
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
const propsData = props.filterBy
|
||||||
|
? props.data.filter(props.filterBy)
|
||||||
|
: props.data;
|
||||||
|
if (props.groupBy) {
|
||||||
|
const groupedData = groupBy(propsData, props.groupBy);
|
||||||
|
return Object.entries(groupedData).map(([group, items]) => ({
|
||||||
|
group,
|
||||||
|
items: items.map(item => ({
|
||||||
|
label: getLabelFn(item),
|
||||||
|
value: getValueFn(item),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return propsData.map(item => ({
|
||||||
|
label: getLabelFn(item),
|
||||||
|
value: getValueFn(item),
|
||||||
|
}));
|
||||||
|
}, [props.data, props.groupBy]);
|
||||||
|
|
||||||
|
const handleOnChange = (event: string | null) => {
|
||||||
|
if (!event) return;
|
||||||
|
const object = props.data.find(item => event === getValueFn(item));
|
||||||
|
if (!object) return;
|
||||||
|
if (isControlled) {
|
||||||
|
props.onChange(object);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInternalValue(object);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isControlled || !internalValue) return;
|
||||||
|
props.onChange(internalValue);
|
||||||
|
}, [internalValue]);
|
||||||
|
|
||||||
|
const restProps = omit(props, [
|
||||||
|
"filterBy",
|
||||||
|
"groupBy",
|
||||||
|
"getValueFn",
|
||||||
|
"getLabelFn",
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
{...restProps}
|
||||||
|
value={value && getValueFn(value)}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ObjectSelect;
|
||||||
28
src/components/selects/ProjectSelect/ProjectSelect.tsx
Normal file
28
src/components/selects/ProjectSelect/ProjectSelect.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import { ProjectSchema } from "@/lib/client";
|
||||||
|
import ObjectSelect, {
|
||||||
|
ObjectSelectProps,
|
||||||
|
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
||||||
|
|
||||||
|
type Props = Omit<
|
||||||
|
ObjectSelectProps<ProjectSchema | null>,
|
||||||
|
"getLabelFn" | "getValueFn"
|
||||||
|
>;
|
||||||
|
|
||||||
|
const ProjectSelect: FC<Props> = ({ data, ...props }) => {
|
||||||
|
const onClear = () => props.onChange(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ObjectSelect
|
||||||
|
data={data}
|
||||||
|
searchable
|
||||||
|
placeholder={"Выберите проект"}
|
||||||
|
onClear={onClear}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectSelect;
|
||||||
6
src/hey-api-config.ts
Normal file
6
src/hey-api-config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type {CreateClientConfig} from "@/lib/client/client.gen";
|
||||||
|
|
||||||
|
export const createClientConfig: CreateClientConfig = config => ({
|
||||||
|
...config,
|
||||||
|
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
||||||
|
});
|
||||||
31
src/hooks/useBoardsList.ts
Normal file
31
src/hooks/useBoardsList.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
import { getBoardsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
projectId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useBoardsList = ({ projectId }: Props) => {
|
||||||
|
const [boards, setBoards] = useState<BoardSchema[]>([]);
|
||||||
|
|
||||||
|
const { data, refetch, isLoading } = useQuery({
|
||||||
|
...getBoardsOptions({ path: { projectId: projectId! } }),
|
||||||
|
enabled: projectId !== undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectId === undefined) {
|
||||||
|
setBoards([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data?.boards) {
|
||||||
|
setBoards(data.boards);
|
||||||
|
}
|
||||||
|
}, [data?.boards, projectId]);
|
||||||
|
|
||||||
|
return { boards, setBoards, refetch, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBoardsList;
|
||||||
31
src/hooks/useDealsList.ts
Normal file
31
src/hooks/useDealsList.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
import { getDealsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
boardId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDealsList = ({ boardId }: Props) => {
|
||||||
|
const [deals, setDeals] = useState<DealSchema[]>([]);
|
||||||
|
|
||||||
|
const { data, refetch, isLoading } = useQuery({
|
||||||
|
...getDealsOptions({ path: { boardId: boardId! } }),
|
||||||
|
enabled: boardId !== undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (boardId === undefined) {
|
||||||
|
setDeals([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data?.deals) {
|
||||||
|
setDeals(data.deals);
|
||||||
|
}
|
||||||
|
}, [data?.deals, boardId]);
|
||||||
|
|
||||||
|
return { deals, setDeals, refetch, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDealsList;
|
||||||
12
src/hooks/useProjectsList.ts
Normal file
12
src/hooks/useProjectsList.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getProjectsOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
const useProjectsList = () => {
|
||||||
|
const { data, refetch, isLoading } = useQuery({
|
||||||
|
...getProjectsOptions(),
|
||||||
|
});
|
||||||
|
const projects = !data ? [] : data.projects;
|
||||||
|
return { projects, refetch, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useProjectsList;
|
||||||
31
src/hooks/useStatusesList.ts
Normal file
31
src/hooks/useStatusesList.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { StatusSchema } from "@/lib/client";
|
||||||
|
import { getStatusesOptions } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
boardId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStatusesList = ({ boardId }: Props) => {
|
||||||
|
const [statuses, setStatuses] = useState<StatusSchema[]>([]);
|
||||||
|
|
||||||
|
const { data, refetch, isLoading } = useQuery({
|
||||||
|
...getStatusesOptions({ path: { boardId: boardId! } }),
|
||||||
|
enabled: boardId !== undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (boardId === undefined) {
|
||||||
|
setStatuses([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data?.statuses) {
|
||||||
|
setStatuses(data.statuses);
|
||||||
|
}
|
||||||
|
}, [data?.statuses, boardId]);
|
||||||
|
|
||||||
|
return { statuses, setStatuses, refetch, isLoading };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useStatusesList;
|
||||||
169
src/lib/client/@tanstack/react-query.gen.ts
Normal file
169
src/lib/client/@tanstack/react-query.gen.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import { type Options, getBoards, updateBoard, getDeals, updateDeal, getProjects, getStatuses, updateStatus } from '../sdk.gen';
|
||||||
|
import { queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||||
|
import type { GetBoardsData, UpdateBoardData, UpdateBoardError, UpdateBoardResponse2, GetDealsData, UpdateDealData, UpdateDealError, UpdateDealResponse2, GetProjectsData, GetStatusesData, UpdateStatusData, UpdateStatusError, UpdateStatusResponse2 } from '../types.gen';
|
||||||
|
import type { AxiosError } from 'axios';
|
||||||
|
import { client as _heyApiClient } from '../client.gen';
|
||||||
|
|
||||||
|
export type QueryKey<TOptions extends Options> = [
|
||||||
|
Pick<TOptions, 'baseURL' | 'body' | 'headers' | 'path' | 'query'> & {
|
||||||
|
_id: string;
|
||||||
|
_infinite?: boolean;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions, infinite?: boolean): [
|
||||||
|
QueryKey<TOptions>[0]
|
||||||
|
] => {
|
||||||
|
const params: QueryKey<TOptions>[0] = { _id: id, baseURL: options?.baseURL || (options?.client ?? _heyApiClient).getConfig().baseURL } as QueryKey<TOptions>[0];
|
||||||
|
if (infinite) {
|
||||||
|
params._infinite = infinite;
|
||||||
|
}
|
||||||
|
if (options?.body) {
|
||||||
|
params.body = options.body;
|
||||||
|
}
|
||||||
|
if (options?.headers) {
|
||||||
|
params.headers = options.headers;
|
||||||
|
}
|
||||||
|
if (options?.path) {
|
||||||
|
params.path = options.path;
|
||||||
|
}
|
||||||
|
if (options?.query) {
|
||||||
|
params.query = options.query;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
params
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBoardsQueryKey = (options: Options<GetBoardsData>) => createQueryKey('getBoards', options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Boards
|
||||||
|
*/
|
||||||
|
export const getBoardsOptions = (options: Options<GetBoardsData>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await getBoards({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: getBoardsQueryKey(options)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Board
|
||||||
|
*/
|
||||||
|
export const updateBoardMutation = (options?: Partial<Options<UpdateBoardData>>): UseMutationOptions<UpdateBoardResponse2, AxiosError<UpdateBoardError>, Options<UpdateBoardData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<UpdateBoardResponse2, AxiosError<UpdateBoardError>, Options<UpdateBoardData>> = {
|
||||||
|
mutationFn: async (localOptions) => {
|
||||||
|
const { data } = await updateBoard({
|
||||||
|
...options,
|
||||||
|
...localOptions,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDealsQueryKey = (options: Options<GetDealsData>) => createQueryKey('getDeals', options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Deals
|
||||||
|
*/
|
||||||
|
export const getDealsOptions = (options: Options<GetDealsData>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await getDeals({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: getDealsQueryKey(options)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Deal
|
||||||
|
*/
|
||||||
|
export const updateDealMutation = (options?: Partial<Options<UpdateDealData>>): UseMutationOptions<UpdateDealResponse2, AxiosError<UpdateDealError>, Options<UpdateDealData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<UpdateDealResponse2, AxiosError<UpdateDealError>, Options<UpdateDealData>> = {
|
||||||
|
mutationFn: async (localOptions) => {
|
||||||
|
const { data } = await updateDeal({
|
||||||
|
...options,
|
||||||
|
...localOptions,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProjectsQueryKey = (options?: Options<GetProjectsData>) => createQueryKey('getProjects', options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Projects
|
||||||
|
*/
|
||||||
|
export const getProjectsOptions = (options?: Options<GetProjectsData>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await getProjects({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: getProjectsQueryKey(options)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStatusesQueryKey = (options: Options<GetStatusesData>) => createQueryKey('getStatuses', options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Statuses
|
||||||
|
*/
|
||||||
|
export const getStatusesOptions = (options: Options<GetStatusesData>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: async ({ queryKey, signal }) => {
|
||||||
|
const { data } = await getStatuses({
|
||||||
|
...options,
|
||||||
|
...queryKey[0],
|
||||||
|
signal,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
queryKey: getStatusesQueryKey(options)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Status
|
||||||
|
*/
|
||||||
|
export const updateStatusMutation = (options?: Partial<Options<UpdateStatusData>>): UseMutationOptions<UpdateStatusResponse2, AxiosError<UpdateStatusError>, Options<UpdateStatusData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<UpdateStatusResponse2, AxiosError<UpdateStatusError>, Options<UpdateStatusData>> = {
|
||||||
|
mutationFn: async (localOptions) => {
|
||||||
|
const { data } = await updateStatus({
|
||||||
|
...options,
|
||||||
|
...localOptions,
|
||||||
|
throwOnError: true
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
19
src/lib/client/client.gen.ts
Normal file
19
src/lib/client/client.gen.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { ClientOptions } from './types.gen';
|
||||||
|
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client';
|
||||||
|
import { createClientConfig } from '../../hey-api-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `createClientConfig()` function will be called on client initialization
|
||||||
|
* and the returned object will become the client's initial configuration.
|
||||||
|
*
|
||||||
|
* You may want to initialize your client this way instead of calling
|
||||||
|
* `setConfig()`. This is useful for example if you're using Next.js
|
||||||
|
* to ensure your client always has the correct values.
|
||||||
|
*/
|
||||||
|
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
|
||||||
|
|
||||||
|
export const client = createClient(createClientConfig(createConfig<ClientOptions>({
|
||||||
|
baseURL: '/api'
|
||||||
|
})));
|
||||||
115
src/lib/client/client/client.ts
Normal file
115
src/lib/client/client/client.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import type { Client, Config } from './types';
|
||||||
|
import {
|
||||||
|
buildUrl,
|
||||||
|
createConfig,
|
||||||
|
mergeConfigs,
|
||||||
|
mergeHeaders,
|
||||||
|
setAuthParams,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
export const createClient = (config: Config = {}): Client => {
|
||||||
|
let _config = mergeConfigs(createConfig(), config);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { auth, ...configWithoutAuth } = _config;
|
||||||
|
const instance = axios.create(configWithoutAuth);
|
||||||
|
|
||||||
|
const getConfig = (): Config => ({ ..._config });
|
||||||
|
|
||||||
|
const setConfig = (config: Config): Config => {
|
||||||
|
_config = mergeConfigs(_config, config);
|
||||||
|
instance.defaults = {
|
||||||
|
...instance.defaults,
|
||||||
|
..._config,
|
||||||
|
// @ts-expect-error
|
||||||
|
headers: mergeHeaders(instance.defaults.headers, _config.headers),
|
||||||
|
};
|
||||||
|
return getConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
const request: Client['request'] = async (options) => {
|
||||||
|
const opts = {
|
||||||
|
..._config,
|
||||||
|
...options,
|
||||||
|
axios: options.axios ?? _config.axios ?? instance,
|
||||||
|
headers: mergeHeaders(_config.headers, options.headers),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.security) {
|
||||||
|
await setAuthParams({
|
||||||
|
...opts,
|
||||||
|
security: opts.security,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.requestValidator) {
|
||||||
|
await opts.requestValidator(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.body && opts.bodySerializer) {
|
||||||
|
opts.body = opts.bodySerializer(opts.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildUrl(opts);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// assign Axios here for consistency with fetch
|
||||||
|
const _axios = opts.axios!;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { auth, ...optsWithoutAuth } = opts;
|
||||||
|
const response = await _axios({
|
||||||
|
...optsWithoutAuth,
|
||||||
|
baseURL: opts.baseURL as string,
|
||||||
|
data: opts.body,
|
||||||
|
headers: opts.headers as RawAxiosRequestHeaders,
|
||||||
|
// let `paramsSerializer()` handle query params if it exists
|
||||||
|
params: opts.paramsSerializer ? opts.query : undefined,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
|
let { data } = response;
|
||||||
|
|
||||||
|
if (opts.responseType === 'json') {
|
||||||
|
if (opts.responseValidator) {
|
||||||
|
await opts.responseValidator(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.responseTransformer) {
|
||||||
|
data = await opts.responseTransformer(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
data: data ?? {},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const e = error as AxiosError;
|
||||||
|
if (opts.throwOnError) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
// @ts-expect-error
|
||||||
|
e.error = e.response?.data ?? {};
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
buildUrl,
|
||||||
|
delete: (options) => request({ ...options, method: 'DELETE' }),
|
||||||
|
get: (options) => request({ ...options, method: 'GET' }),
|
||||||
|
getConfig,
|
||||||
|
head: (options) => request({ ...options, method: 'HEAD' }),
|
||||||
|
instance,
|
||||||
|
options: (options) => request({ ...options, method: 'OPTIONS' }),
|
||||||
|
patch: (options) => request({ ...options, method: 'PATCH' }),
|
||||||
|
post: (options) => request({ ...options, method: 'POST' }),
|
||||||
|
put: (options) => request({ ...options, method: 'PUT' }),
|
||||||
|
request,
|
||||||
|
setConfig,
|
||||||
|
} as Client;
|
||||||
|
};
|
||||||
21
src/lib/client/client/index.ts
Normal file
21
src/lib/client/client/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export type { Auth } from '../core/auth';
|
||||||
|
export type { QuerySerializerOptions } from '../core/bodySerializer';
|
||||||
|
export {
|
||||||
|
formDataBodySerializer,
|
||||||
|
jsonBodySerializer,
|
||||||
|
urlSearchParamsBodySerializer,
|
||||||
|
} from '../core/bodySerializer';
|
||||||
|
export { buildClientParams } from '../core/params';
|
||||||
|
export { createClient } from './client';
|
||||||
|
export type {
|
||||||
|
Client,
|
||||||
|
ClientOptions,
|
||||||
|
Config,
|
||||||
|
CreateClientConfig,
|
||||||
|
Options,
|
||||||
|
OptionsLegacyParser,
|
||||||
|
RequestOptions,
|
||||||
|
RequestResult,
|
||||||
|
TDataShape,
|
||||||
|
} from './types';
|
||||||
|
export { createConfig } from './utils';
|
||||||
179
src/lib/client/client/types.ts
Normal file
179
src/lib/client/client/types.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import type {
|
||||||
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosRequestHeaders,
|
||||||
|
AxiosResponse,
|
||||||
|
AxiosStatic,
|
||||||
|
CreateAxiosDefaults,
|
||||||
|
} from 'axios';
|
||||||
|
|
||||||
|
import type { Auth } from '../core/auth';
|
||||||
|
import type {
|
||||||
|
Client as CoreClient,
|
||||||
|
Config as CoreConfig,
|
||||||
|
} from '../core/types';
|
||||||
|
|
||||||
|
export interface Config<T extends ClientOptions = ClientOptions>
|
||||||
|
extends Omit<CreateAxiosDefaults, 'auth' | 'baseURL' | 'headers' | 'method'>,
|
||||||
|
CoreConfig {
|
||||||
|
/**
|
||||||
|
* Axios implementation. You can use this option to provide a custom
|
||||||
|
* Axios instance.
|
||||||
|
*
|
||||||
|
* @default axios
|
||||||
|
*/
|
||||||
|
axios?: AxiosStatic;
|
||||||
|
/**
|
||||||
|
* Base URL for all requests made by this client.
|
||||||
|
*/
|
||||||
|
baseURL?: T['baseURL'];
|
||||||
|
/**
|
||||||
|
* An object containing any HTTP headers that you want to pre-populate your
|
||||||
|
* `Headers` object with.
|
||||||
|
*
|
||||||
|
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
||||||
|
*/
|
||||||
|
headers?:
|
||||||
|
| AxiosRequestHeaders
|
||||||
|
| Record<
|
||||||
|
string,
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| (string | number | boolean)[]
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| unknown
|
||||||
|
>;
|
||||||
|
/**
|
||||||
|
* Throw an error instead of returning it in the response?
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
throwOnError?: T['throwOnError'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestOptions<
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
Url extends string = string,
|
||||||
|
> extends Config<{
|
||||||
|
throwOnError: ThrowOnError;
|
||||||
|
}> {
|
||||||
|
/**
|
||||||
|
* Any body that you want to add to your request.
|
||||||
|
*
|
||||||
|
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
||||||
|
*/
|
||||||
|
body?: unknown;
|
||||||
|
path?: Record<string, unknown>;
|
||||||
|
query?: Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* Security mechanism(s) to use for the request.
|
||||||
|
*/
|
||||||
|
security?: ReadonlyArray<Auth>;
|
||||||
|
url: Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestResult<
|
||||||
|
TData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
> = ThrowOnError extends true
|
||||||
|
? Promise<
|
||||||
|
AxiosResponse<
|
||||||
|
TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||||
|
>
|
||||||
|
>
|
||||||
|
: Promise<
|
||||||
|
| (AxiosResponse<
|
||||||
|
TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||||
|
> & { error: undefined })
|
||||||
|
| (AxiosError<
|
||||||
|
TError extends Record<string, unknown> ? TError[keyof TError] : TError
|
||||||
|
> & {
|
||||||
|
data: undefined;
|
||||||
|
error: TError extends Record<string, unknown>
|
||||||
|
? TError[keyof TError]
|
||||||
|
: TError;
|
||||||
|
})
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface ClientOptions {
|
||||||
|
baseURL?: string;
|
||||||
|
throwOnError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MethodFn = <
|
||||||
|
TData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
ThrowOnError extends boolean = false,
|
||||||
|
>(
|
||||||
|
options: Omit<RequestOptions<ThrowOnError>, 'method'>,
|
||||||
|
) => RequestResult<TData, TError, ThrowOnError>;
|
||||||
|
|
||||||
|
type RequestFn = <
|
||||||
|
TData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
ThrowOnError extends boolean = false,
|
||||||
|
>(
|
||||||
|
options: Omit<RequestOptions<ThrowOnError>, 'method'> &
|
||||||
|
Pick<Required<RequestOptions<ThrowOnError>>, 'method'>,
|
||||||
|
) => RequestResult<TData, TError, ThrowOnError>;
|
||||||
|
|
||||||
|
type BuildUrlFn = <
|
||||||
|
TData extends {
|
||||||
|
body?: unknown;
|
||||||
|
path?: Record<string, unknown>;
|
||||||
|
query?: Record<string, unknown>;
|
||||||
|
url: string;
|
||||||
|
},
|
||||||
|
>(
|
||||||
|
options: Pick<TData, 'url'> & Omit<Options<TData>, 'axios'>,
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
|
||||||
|
instance: AxiosInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `createClientConfig()` function will be called on client initialization
|
||||||
|
* and the returned object will become the client's initial configuration.
|
||||||
|
*
|
||||||
|
* You may want to initialize your client this way instead of calling
|
||||||
|
* `setConfig()`. This is useful for example if you're using Next.js
|
||||||
|
* to ensure your client always has the correct values.
|
||||||
|
*/
|
||||||
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
||||||
|
override?: Config<ClientOptions & T>,
|
||||||
|
) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
|
export interface TDataShape {
|
||||||
|
body?: unknown;
|
||||||
|
headers?: unknown;
|
||||||
|
path?: unknown;
|
||||||
|
query?: unknown;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||||
|
|
||||||
|
export type Options<
|
||||||
|
TData extends TDataShape = TDataShape,
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
> = OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'path' | 'query' | 'url'> &
|
||||||
|
Omit<TData, 'url'>;
|
||||||
|
|
||||||
|
export type OptionsLegacyParser<
|
||||||
|
TData = unknown,
|
||||||
|
ThrowOnError extends boolean = boolean,
|
||||||
|
> = TData extends { body?: any }
|
||||||
|
? TData extends { headers?: any }
|
||||||
|
? OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'headers' | 'url'> & TData
|
||||||
|
: OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'url'> &
|
||||||
|
TData &
|
||||||
|
Pick<RequestOptions<ThrowOnError>, 'headers'>
|
||||||
|
: TData extends { headers?: any }
|
||||||
|
? OmitKeys<RequestOptions<ThrowOnError>, 'headers' | 'url'> &
|
||||||
|
TData &
|
||||||
|
Pick<RequestOptions<ThrowOnError>, 'body'>
|
||||||
|
: OmitKeys<RequestOptions<ThrowOnError>, 'url'> & TData;
|
||||||
286
src/lib/client/client/utils.ts
Normal file
286
src/lib/client/client/utils.ts
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
import { getAuthToken } from '../core/auth';
|
||||||
|
import type {
|
||||||
|
QuerySerializer,
|
||||||
|
QuerySerializerOptions,
|
||||||
|
} from '../core/bodySerializer';
|
||||||
|
import type { ArraySeparatorStyle } from '../core/pathSerializer';
|
||||||
|
import {
|
||||||
|
serializeArrayParam,
|
||||||
|
serializeObjectParam,
|
||||||
|
serializePrimitiveParam,
|
||||||
|
} from '../core/pathSerializer';
|
||||||
|
import type { Client, ClientOptions, Config, RequestOptions } from './types';
|
||||||
|
|
||||||
|
interface PathSerializer {
|
||||||
|
path: Record<string, unknown>;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
||||||
|
|
||||||
|
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||||
|
let url = _url;
|
||||||
|
const matches = _url.match(PATH_PARAM_RE);
|
||||||
|
if (matches) {
|
||||||
|
for (const match of matches) {
|
||||||
|
let explode = false;
|
||||||
|
let name = match.substring(1, match.length - 1);
|
||||||
|
let style: ArraySeparatorStyle = 'simple';
|
||||||
|
|
||||||
|
if (name.endsWith('*')) {
|
||||||
|
explode = true;
|
||||||
|
name = name.substring(0, name.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.startsWith('.')) {
|
||||||
|
name = name.substring(1);
|
||||||
|
style = 'label';
|
||||||
|
} else if (name.startsWith(';')) {
|
||||||
|
name = name.substring(1);
|
||||||
|
style = 'matrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = path[name];
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
url = url.replace(
|
||||||
|
match,
|
||||||
|
serializeArrayParam({ explode, name, style, value }),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
url = url.replace(
|
||||||
|
match,
|
||||||
|
serializeObjectParam({
|
||||||
|
explode,
|
||||||
|
name,
|
||||||
|
style,
|
||||||
|
value: value as Record<string, unknown>,
|
||||||
|
valueOnly: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'matrix') {
|
||||||
|
url = url.replace(
|
||||||
|
match,
|
||||||
|
`;${serializePrimitiveParam({
|
||||||
|
name,
|
||||||
|
value: value as string,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceValue = encodeURIComponent(
|
||||||
|
style === 'label' ? `.${value as string}` : (value as string),
|
||||||
|
);
|
||||||
|
url = url.replace(match, replaceValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createQuerySerializer = <T = unknown>({
|
||||||
|
allowReserved,
|
||||||
|
array,
|
||||||
|
object,
|
||||||
|
}: QuerySerializerOptions = {}) => {
|
||||||
|
const querySerializer = (queryParams: T) => {
|
||||||
|
const search: string[] = [];
|
||||||
|
if (queryParams && typeof queryParams === 'object') {
|
||||||
|
for (const name in queryParams) {
|
||||||
|
const value = queryParams[name];
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const serializedArray = serializeArrayParam({
|
||||||
|
allowReserved,
|
||||||
|
explode: true,
|
||||||
|
name,
|
||||||
|
style: 'form',
|
||||||
|
value,
|
||||||
|
...array,
|
||||||
|
});
|
||||||
|
if (serializedArray) search.push(serializedArray);
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
const serializedObject = serializeObjectParam({
|
||||||
|
allowReserved,
|
||||||
|
explode: true,
|
||||||
|
name,
|
||||||
|
style: 'deepObject',
|
||||||
|
value: value as Record<string, unknown>,
|
||||||
|
...object,
|
||||||
|
});
|
||||||
|
if (serializedObject) search.push(serializedObject);
|
||||||
|
} else {
|
||||||
|
const serializedPrimitive = serializePrimitiveParam({
|
||||||
|
allowReserved,
|
||||||
|
name,
|
||||||
|
value: value as string,
|
||||||
|
});
|
||||||
|
if (serializedPrimitive) search.push(serializedPrimitive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return search.join('&');
|
||||||
|
};
|
||||||
|
return querySerializer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setAuthParams = async ({
|
||||||
|
security,
|
||||||
|
...options
|
||||||
|
}: Pick<Required<RequestOptions>, 'security'> &
|
||||||
|
Pick<RequestOptions, 'auth' | 'query'> & {
|
||||||
|
headers: Record<any, unknown>;
|
||||||
|
}) => {
|
||||||
|
for (const auth of security) {
|
||||||
|
const token = await getAuthToken(auth, options.auth);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = auth.name ?? 'Authorization';
|
||||||
|
|
||||||
|
switch (auth.in) {
|
||||||
|
case 'query':
|
||||||
|
if (!options.query) {
|
||||||
|
options.query = {};
|
||||||
|
}
|
||||||
|
options.query[name] = token;
|
||||||
|
break;
|
||||||
|
case 'cookie': {
|
||||||
|
const value = `${name}=${token}`;
|
||||||
|
if ('Cookie' in options.headers && options.headers['Cookie']) {
|
||||||
|
options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`;
|
||||||
|
} else {
|
||||||
|
options.headers['Cookie'] = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'header':
|
||||||
|
default:
|
||||||
|
options.headers[name] = token;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildUrl: Client['buildUrl'] = (options) => {
|
||||||
|
const url = getUrl({
|
||||||
|
path: options.path,
|
||||||
|
// let `paramsSerializer()` handle query params if it exists
|
||||||
|
query: !options.paramsSerializer ? options.query : undefined,
|
||||||
|
querySerializer:
|
||||||
|
typeof options.querySerializer === 'function'
|
||||||
|
? options.querySerializer
|
||||||
|
: createQuerySerializer(options.querySerializer),
|
||||||
|
url: options.url,
|
||||||
|
});
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUrl = ({
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
querySerializer,
|
||||||
|
url: _url,
|
||||||
|
}: {
|
||||||
|
path?: Record<string, unknown>;
|
||||||
|
query?: Record<string, unknown>;
|
||||||
|
querySerializer: QuerySerializer;
|
||||||
|
url: string;
|
||||||
|
}) => {
|
||||||
|
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
||||||
|
let url = pathUrl;
|
||||||
|
if (path) {
|
||||||
|
url = defaultPathSerializer({ path, url });
|
||||||
|
}
|
||||||
|
let search = query ? querySerializer(query) : '';
|
||||||
|
if (search.startsWith('?')) {
|
||||||
|
search = search.substring(1);
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
url += `?${search}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeConfigs = (a: Config, b: Config): Config => {
|
||||||
|
const config = { ...a, ...b };
|
||||||
|
config.headers = mergeHeaders(a.headers, b.headers);
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special Axios headers keywords allowing to set headers by request method.
|
||||||
|
*/
|
||||||
|
export const axiosHeadersKeywords = [
|
||||||
|
'common',
|
||||||
|
'delete',
|
||||||
|
'get',
|
||||||
|
'head',
|
||||||
|
'patch',
|
||||||
|
'post',
|
||||||
|
'put',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const mergeHeaders = (
|
||||||
|
...headers: Array<Required<Config>['headers'] | undefined>
|
||||||
|
): Record<any, unknown> => {
|
||||||
|
const mergedHeaders: Record<any, unknown> = {};
|
||||||
|
for (const header of headers) {
|
||||||
|
if (!header || typeof header !== 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iterator = Object.entries(header);
|
||||||
|
|
||||||
|
for (const [key, value] of iterator) {
|
||||||
|
if (
|
||||||
|
axiosHeadersKeywords.includes(
|
||||||
|
key as (typeof axiosHeadersKeywords)[number],
|
||||||
|
) &&
|
||||||
|
typeof value === 'object'
|
||||||
|
) {
|
||||||
|
mergedHeaders[key] = {
|
||||||
|
...(mergedHeaders[key] as Record<any, unknown>),
|
||||||
|
...value,
|
||||||
|
};
|
||||||
|
} else if (value === null) {
|
||||||
|
delete mergedHeaders[key];
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
for (const v of value) {
|
||||||
|
// @ts-expect-error
|
||||||
|
mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string];
|
||||||
|
}
|
||||||
|
} else if (value !== undefined) {
|
||||||
|
// assume object headers are meant to be JSON stringified, i.e. their
|
||||||
|
// content value in OpenAPI specification is 'application/json'
|
||||||
|
mergedHeaders[key] =
|
||||||
|
typeof value === 'object' ? JSON.stringify(value) : (value as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||||
|
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||||
|
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||||
|
...override,
|
||||||
|
});
|
||||||
40
src/lib/client/core/auth.ts
Normal file
40
src/lib/client/core/auth.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export type AuthToken = string | undefined;
|
||||||
|
|
||||||
|
export interface Auth {
|
||||||
|
/**
|
||||||
|
* Which part of the request do we use to send the auth?
|
||||||
|
*
|
||||||
|
* @default 'header'
|
||||||
|
*/
|
||||||
|
in?: 'header' | 'query' | 'cookie';
|
||||||
|
/**
|
||||||
|
* Header or query parameter name.
|
||||||
|
*
|
||||||
|
* @default 'Authorization'
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
scheme?: 'basic' | 'bearer';
|
||||||
|
type: 'apiKey' | 'http';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAuthToken = async (
|
||||||
|
auth: Auth,
|
||||||
|
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const token =
|
||||||
|
typeof callback === 'function' ? await callback(auth) : callback;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.scheme === 'bearer') {
|
||||||
|
return `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.scheme === 'basic') {
|
||||||
|
return `Basic ${btoa(token)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
};
|
||||||
88
src/lib/client/core/bodySerializer.ts
Normal file
88
src/lib/client/core/bodySerializer.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import type {
|
||||||
|
ArrayStyle,
|
||||||
|
ObjectStyle,
|
||||||
|
SerializerOptions,
|
||||||
|
} from './pathSerializer';
|
||||||
|
|
||||||
|
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
||||||
|
|
||||||
|
export type BodySerializer = (body: any) => any;
|
||||||
|
|
||||||
|
export interface QuerySerializerOptions {
|
||||||
|
allowReserved?: boolean;
|
||||||
|
array?: SerializerOptions<ArrayStyle>;
|
||||||
|
object?: SerializerOptions<ObjectStyle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializeFormDataPair = (
|
||||||
|
data: FormData,
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
): void => {
|
||||||
|
if (typeof value === 'string' || value instanceof Blob) {
|
||||||
|
data.append(key, value);
|
||||||
|
} else {
|
||||||
|
data.append(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializeUrlSearchParamsPair = (
|
||||||
|
data: URLSearchParams,
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
): void => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
data.append(key, value);
|
||||||
|
} else {
|
||||||
|
data.append(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formDataBodySerializer = {
|
||||||
|
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||||
|
body: T,
|
||||||
|
): FormData => {
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
Object.entries(body).forEach(([key, value]) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => serializeFormDataPair(data, key, v));
|
||||||
|
} else {
|
||||||
|
serializeFormDataPair(data, key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const jsonBodySerializer = {
|
||||||
|
bodySerializer: <T>(body: T): string =>
|
||||||
|
JSON.stringify(body, (_key, value) =>
|
||||||
|
typeof value === 'bigint' ? value.toString() : value,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const urlSearchParamsBodySerializer = {
|
||||||
|
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||||
|
body: T,
|
||||||
|
): string => {
|
||||||
|
const data = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(body).forEach(([key, value]) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
||||||
|
} else {
|
||||||
|
serializeUrlSearchParamsPair(data, key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.toString();
|
||||||
|
},
|
||||||
|
};
|
||||||
151
src/lib/client/core/params.ts
Normal file
151
src/lib/client/core/params.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
type Slot = 'body' | 'headers' | 'path' | 'query';
|
||||||
|
|
||||||
|
export type Field =
|
||||||
|
| {
|
||||||
|
in: Exclude<Slot, 'body'>;
|
||||||
|
/**
|
||||||
|
* Field name. This is the name we want the user to see and use.
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* Field mapped name. This is the name we want to use in the request.
|
||||||
|
* If omitted, we use the same value as `key`.
|
||||||
|
*/
|
||||||
|
map?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
in: Extract<Slot, 'body'>;
|
||||||
|
/**
|
||||||
|
* Key isn't required for bodies.
|
||||||
|
*/
|
||||||
|
key?: string;
|
||||||
|
map?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Fields {
|
||||||
|
allowExtra?: Partial<Record<Slot, boolean>>;
|
||||||
|
args?: ReadonlyArray<Field>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
||||||
|
|
||||||
|
const extraPrefixesMap: Record<string, Slot> = {
|
||||||
|
$body_: 'body',
|
||||||
|
$headers_: 'headers',
|
||||||
|
$path_: 'path',
|
||||||
|
$query_: 'query',
|
||||||
|
};
|
||||||
|
const extraPrefixes = Object.entries(extraPrefixesMap);
|
||||||
|
|
||||||
|
type KeyMap = Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
in: Slot;
|
||||||
|
map?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
||||||
|
if (!map) {
|
||||||
|
map = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const config of fields) {
|
||||||
|
if ('in' in config) {
|
||||||
|
if (config.key) {
|
||||||
|
map.set(config.key, {
|
||||||
|
in: config.in,
|
||||||
|
map: config.map,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (config.args) {
|
||||||
|
buildKeyMap(config.args, map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
body: unknown;
|
||||||
|
headers: Record<string, unknown>;
|
||||||
|
path: Record<string, unknown>;
|
||||||
|
query: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripEmptySlots = (params: Params) => {
|
||||||
|
for (const [slot, value] of Object.entries(params)) {
|
||||||
|
if (value && typeof value === 'object' && !Object.keys(value).length) {
|
||||||
|
delete params[slot as Slot];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildClientParams = (
|
||||||
|
args: ReadonlyArray<unknown>,
|
||||||
|
fields: FieldsConfig,
|
||||||
|
) => {
|
||||||
|
const params: Params = {
|
||||||
|
body: {},
|
||||||
|
headers: {},
|
||||||
|
path: {},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = buildKeyMap(fields);
|
||||||
|
|
||||||
|
let config: FieldsConfig[number] | undefined;
|
||||||
|
|
||||||
|
for (const [index, arg] of args.entries()) {
|
||||||
|
if (fields[index]) {
|
||||||
|
config = fields[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('in' in config) {
|
||||||
|
if (config.key) {
|
||||||
|
const field = map.get(config.key)!;
|
||||||
|
const name = field.map || config.key;
|
||||||
|
(params[field.in] as Record<string, unknown>)[name] = arg;
|
||||||
|
} else {
|
||||||
|
params.body = arg;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(arg ?? {})) {
|
||||||
|
const field = map.get(key);
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
const name = field.map || key;
|
||||||
|
(params[field.in] as Record<string, unknown>)[name] = value;
|
||||||
|
} else {
|
||||||
|
const extra = extraPrefixes.find(([prefix]) =>
|
||||||
|
key.startsWith(prefix),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extra) {
|
||||||
|
const [prefix, slot] = extra;
|
||||||
|
(params[slot] as Record<string, unknown>)[
|
||||||
|
key.slice(prefix.length)
|
||||||
|
] = value;
|
||||||
|
} else {
|
||||||
|
for (const [slot, allowed] of Object.entries(
|
||||||
|
config.allowExtra ?? {},
|
||||||
|
)) {
|
||||||
|
if (allowed) {
|
||||||
|
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stripEmptySlots(params);
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
||||||
179
src/lib/client/core/pathSerializer.ts
Normal file
179
src/lib/client/core/pathSerializer.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
interface SerializeOptions<T>
|
||||||
|
extends SerializePrimitiveOptions,
|
||||||
|
SerializerOptions<T> {}
|
||||||
|
|
||||||
|
interface SerializePrimitiveOptions {
|
||||||
|
allowReserved?: boolean;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerializerOptions<T> {
|
||||||
|
/**
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
explode: boolean;
|
||||||
|
style: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
|
||||||
|
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
||||||
|
type MatrixStyle = 'label' | 'matrix' | 'simple';
|
||||||
|
export type ObjectStyle = 'form' | 'deepObject';
|
||||||
|
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
||||||
|
|
||||||
|
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
||||||
|
switch (style) {
|
||||||
|
case 'label':
|
||||||
|
return '.';
|
||||||
|
case 'matrix':
|
||||||
|
return ';';
|
||||||
|
case 'simple':
|
||||||
|
return ',';
|
||||||
|
default:
|
||||||
|
return '&';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
||||||
|
switch (style) {
|
||||||
|
case 'form':
|
||||||
|
return ',';
|
||||||
|
case 'pipeDelimited':
|
||||||
|
return '|';
|
||||||
|
case 'spaceDelimited':
|
||||||
|
return '%20';
|
||||||
|
default:
|
||||||
|
return ',';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
||||||
|
switch (style) {
|
||||||
|
case 'label':
|
||||||
|
return '.';
|
||||||
|
case 'matrix':
|
||||||
|
return ';';
|
||||||
|
case 'simple':
|
||||||
|
return ',';
|
||||||
|
default:
|
||||||
|
return '&';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serializeArrayParam = ({
|
||||||
|
allowReserved,
|
||||||
|
explode,
|
||||||
|
name,
|
||||||
|
style,
|
||||||
|
value,
|
||||||
|
}: SerializeOptions<ArraySeparatorStyle> & {
|
||||||
|
value: unknown[];
|
||||||
|
}) => {
|
||||||
|
if (!explode) {
|
||||||
|
const joinedValues = (
|
||||||
|
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
||||||
|
).join(separatorArrayNoExplode(style));
|
||||||
|
switch (style) {
|
||||||
|
case 'label':
|
||||||
|
return `.${joinedValues}`;
|
||||||
|
case 'matrix':
|
||||||
|
return `;${name}=${joinedValues}`;
|
||||||
|
case 'simple':
|
||||||
|
return joinedValues;
|
||||||
|
default:
|
||||||
|
return `${name}=${joinedValues}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = separatorArrayExplode(style);
|
||||||
|
const joinedValues = value
|
||||||
|
.map((v) => {
|
||||||
|
if (style === 'label' || style === 'simple') {
|
||||||
|
return allowReserved ? v : encodeURIComponent(v as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializePrimitiveParam({
|
||||||
|
allowReserved,
|
||||||
|
name,
|
||||||
|
value: v as string,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.join(separator);
|
||||||
|
return style === 'label' || style === 'matrix'
|
||||||
|
? separator + joinedValues
|
||||||
|
: joinedValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serializePrimitiveParam = ({
|
||||||
|
allowReserved,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}: SerializePrimitiveParam) => {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
throw new Error(
|
||||||
|
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serializeObjectParam = ({
|
||||||
|
allowReserved,
|
||||||
|
explode,
|
||||||
|
name,
|
||||||
|
style,
|
||||||
|
value,
|
||||||
|
valueOnly,
|
||||||
|
}: SerializeOptions<ObjectSeparatorStyle> & {
|
||||||
|
value: Record<string, unknown> | Date;
|
||||||
|
valueOnly?: boolean;
|
||||||
|
}) => {
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style !== 'deepObject' && !explode) {
|
||||||
|
let values: string[] = [];
|
||||||
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
|
values = [
|
||||||
|
...values,
|
||||||
|
key,
|
||||||
|
allowReserved ? (v as string) : encodeURIComponent(v as string),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const joinedValues = values.join(',');
|
||||||
|
switch (style) {
|
||||||
|
case 'form':
|
||||||
|
return `${name}=${joinedValues}`;
|
||||||
|
case 'label':
|
||||||
|
return `.${joinedValues}`;
|
||||||
|
case 'matrix':
|
||||||
|
return `;${name}=${joinedValues}`;
|
||||||
|
default:
|
||||||
|
return joinedValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const separator = separatorObjectExplode(style);
|
||||||
|
const joinedValues = Object.entries(value)
|
||||||
|
.map(([key, v]) =>
|
||||||
|
serializePrimitiveParam({
|
||||||
|
allowReserved,
|
||||||
|
name: style === 'deepObject' ? `${name}[${key}]` : key,
|
||||||
|
value: v as string,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.join(separator);
|
||||||
|
return style === 'label' || style === 'matrix'
|
||||||
|
? separator + joinedValues
|
||||||
|
: joinedValues;
|
||||||
|
};
|
||||||
118
src/lib/client/core/types.ts
Normal file
118
src/lib/client/core/types.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import type { Auth, AuthToken } from './auth';
|
||||||
|
import type {
|
||||||
|
BodySerializer,
|
||||||
|
QuerySerializer,
|
||||||
|
QuerySerializerOptions,
|
||||||
|
} from './bodySerializer';
|
||||||
|
|
||||||
|
export interface Client<
|
||||||
|
RequestFn = never,
|
||||||
|
Config = unknown,
|
||||||
|
MethodFn = never,
|
||||||
|
BuildUrlFn = never,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Returns the final request URL.
|
||||||
|
*/
|
||||||
|
buildUrl: BuildUrlFn;
|
||||||
|
connect: MethodFn;
|
||||||
|
delete: MethodFn;
|
||||||
|
get: MethodFn;
|
||||||
|
getConfig: () => Config;
|
||||||
|
head: MethodFn;
|
||||||
|
options: MethodFn;
|
||||||
|
patch: MethodFn;
|
||||||
|
post: MethodFn;
|
||||||
|
put: MethodFn;
|
||||||
|
request: RequestFn;
|
||||||
|
setConfig: (config: Config) => Config;
|
||||||
|
trace: MethodFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
/**
|
||||||
|
* Auth token or a function returning auth token. The resolved value will be
|
||||||
|
* added to the request payload as defined by its `security` array.
|
||||||
|
*/
|
||||||
|
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
||||||
|
/**
|
||||||
|
* A function for serializing request body parameter. By default,
|
||||||
|
* {@link JSON.stringify()} will be used.
|
||||||
|
*/
|
||||||
|
bodySerializer?: BodySerializer | null;
|
||||||
|
/**
|
||||||
|
* An object containing any HTTP headers that you want to pre-populate your
|
||||||
|
* `Headers` object with.
|
||||||
|
*
|
||||||
|
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
||||||
|
*/
|
||||||
|
headers?:
|
||||||
|
| RequestInit['headers']
|
||||||
|
| Record<
|
||||||
|
string,
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| (string | number | boolean)[]
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| unknown
|
||||||
|
>;
|
||||||
|
/**
|
||||||
|
* The request method.
|
||||||
|
*
|
||||||
|
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
||||||
|
*/
|
||||||
|
method?:
|
||||||
|
| 'CONNECT'
|
||||||
|
| 'DELETE'
|
||||||
|
| 'GET'
|
||||||
|
| 'HEAD'
|
||||||
|
| 'OPTIONS'
|
||||||
|
| 'PATCH'
|
||||||
|
| 'POST'
|
||||||
|
| 'PUT'
|
||||||
|
| 'TRACE';
|
||||||
|
/**
|
||||||
|
* A function for serializing request query parameters. By default, arrays
|
||||||
|
* will be exploded in form style, objects will be exploded in deepObject
|
||||||
|
* style, and reserved characters are percent-encoded.
|
||||||
|
*
|
||||||
|
* This method will have no effect if the native `paramsSerializer()` Axios
|
||||||
|
* API function is used.
|
||||||
|
*
|
||||||
|
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
||||||
|
*/
|
||||||
|
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
||||||
|
/**
|
||||||
|
* A function validating request data. This is useful if you want to ensure
|
||||||
|
* the request conforms to the desired shape, so it can be safely sent to
|
||||||
|
* the server.
|
||||||
|
*/
|
||||||
|
requestValidator?: (data: unknown) => Promise<unknown>;
|
||||||
|
/**
|
||||||
|
* A function transforming response data before it's returned. This is useful
|
||||||
|
* for post-processing data, e.g. converting ISO strings into Date objects.
|
||||||
|
*/
|
||||||
|
responseTransformer?: (data: unknown) => Promise<unknown>;
|
||||||
|
/**
|
||||||
|
* A function validating response data. This is useful if you want to ensure
|
||||||
|
* the response conforms to the desired shape, so it can be safely passed to
|
||||||
|
* the transformers and returned to the user.
|
||||||
|
*/
|
||||||
|
responseValidator?: (data: unknown) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||||
|
? true
|
||||||
|
: [T] extends [never | undefined]
|
||||||
|
? [undefined] extends [T]
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
: false;
|
||||||
|
|
||||||
|
export type OmitNever<T extends Record<string, unknown>> = {
|
||||||
|
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
||||||
|
? never
|
||||||
|
: K]: T[K];
|
||||||
|
};
|
||||||
3
src/lib/client/index.ts
Normal file
3
src/lib/client/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
export * from './types.gen';
|
||||||
|
export * from './sdk.gen';
|
||||||
151
src/lib/client/sdk.gen.ts
Normal file
151
src/lib/client/sdk.gen.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import type { Options as ClientOptions, TDataShape, Client } from './client';
|
||||||
|
import type { GetBoardsData, GetBoardsResponses, GetBoardsErrors, UpdateBoardData, UpdateBoardResponses, UpdateBoardErrors, GetDealsData, GetDealsResponses, GetDealsErrors, UpdateDealData, UpdateDealResponses, UpdateDealErrors, GetProjectsData, GetProjectsResponses, GetStatusesData, GetStatusesResponses, GetStatusesErrors, UpdateStatusData, UpdateStatusResponses, UpdateStatusErrors } from './types.gen';
|
||||||
|
import { zGetBoardsData, zGetBoardsResponse2, zUpdateBoardData, zUpdateBoardResponse2, zGetDealsData, zGetDealsResponse2, zUpdateDealData, zUpdateDealResponse2, zGetProjectsData, zGetProjectsResponse2, zGetStatusesData, zGetStatusesResponse2, zUpdateStatusData, zUpdateStatusResponse2 } from './zod.gen';
|
||||||
|
import { client as _heyApiClient } from './client.gen';
|
||||||
|
|
||||||
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||||
|
/**
|
||||||
|
* You can provide a client instance returned by `createClient()` instead of
|
||||||
|
* individual options. This might be also useful if you want to implement a
|
||||||
|
* custom client.
|
||||||
|
*/
|
||||||
|
client?: Client;
|
||||||
|
/**
|
||||||
|
* You can pass arbitrary values through the `meta` object. This can be
|
||||||
|
* used to access values that aren't defined as part of the SDK function.
|
||||||
|
*/
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Boards
|
||||||
|
*/
|
||||||
|
export const getBoards = <ThrowOnError extends boolean = false>(options: Options<GetBoardsData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).get<GetBoardsResponses, GetBoardsErrors, ThrowOnError>({
|
||||||
|
requestValidator: async (data) => {
|
||||||
|
return await zGetBoardsData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
responseValidator: async (data) => {
|
||||||
|
return await zGetBoardsResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: '/board/{projectId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Board
|
||||||
|
*/
|
||||||
|
export const updateBoard = <ThrowOnError extends boolean = false>(options: Options<UpdateBoardData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).patch<UpdateBoardResponses, UpdateBoardErrors, ThrowOnError>({
|
||||||
|
requestValidator: async (data) => {
|
||||||
|
return await zUpdateBoardData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
responseValidator: async (data) => {
|
||||||
|
return await zUpdateBoardResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: '/board/{boardId}',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Deals
|
||||||
|
*/
|
||||||
|
export const getDeals = <ThrowOnError extends boolean = false>(options: Options<GetDealsData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).get<GetDealsResponses, GetDealsErrors, ThrowOnError>({
|
||||||
|
requestValidator: async (data) => {
|
||||||
|
return await zGetDealsData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
responseValidator: async (data) => {
|
||||||
|
return await zGetDealsResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: '/deal/{boardId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Deal
|
||||||
|
*/
|
||||||
|
export const updateDeal = <ThrowOnError extends boolean = false>(options: Options<UpdateDealData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).patch<UpdateDealResponses, UpdateDealErrors, ThrowOnError>({
|
||||||
|
requestValidator: async (data) => {
|
||||||
|
return await zUpdateDealData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
responseValidator: async (data) => {
|
||||||
|
return await zUpdateDealResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: '/deal/{dealId}',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Projects
|
||||||
|
*/
|
||||||
|
export const getProjects = <ThrowOnError extends boolean = false>(options?: Options<GetProjectsData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? _heyApiClient).get<GetProjectsResponses, unknown, ThrowOnError>({
|
||||||
|
requestValidator: async (data) => {
|
||||||
|
return await zGetProjectsData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
responseValidator: async (data) => {
|
||||||
|
return await zGetProjectsResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: '/project/',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Statuses
|
||||||
|
*/
|
||||||
|
export const getStatuses = <ThrowOnError extends boolean = false>(options: Options<GetStatusesData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).get<GetStatusesResponses, GetStatusesErrors, ThrowOnError>({
|
||||||
|
requestValidator: async (data) => {
|
||||||
|
return await zGetStatusesData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
responseValidator: async (data) => {
|
||||||
|
return await zGetStatusesResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: '/status/{boardId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Status
|
||||||
|
*/
|
||||||
|
export const updateStatus = <ThrowOnError extends boolean = false>(options: Options<UpdateStatusData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).patch<UpdateStatusResponses, UpdateStatusErrors, ThrowOnError>({
|
||||||
|
requestValidator: async (data) => {
|
||||||
|
return await zUpdateStatusData.parseAsync(data);
|
||||||
|
},
|
||||||
|
responseType: 'json',
|
||||||
|
responseValidator: async (data) => {
|
||||||
|
return await zUpdateStatusResponse2.parseAsync(data);
|
||||||
|
},
|
||||||
|
url: '/status/{statusId}',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
438
src/lib/client/types.gen.ts
Normal file
438
src/lib/client/types.gen.ts
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BoardSchema
|
||||||
|
*/
|
||||||
|
export type BoardSchema = {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Lexorank
|
||||||
|
*/
|
||||||
|
lexorank: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DealSchema
|
||||||
|
*/
|
||||||
|
export type DealSchema = {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Lexorank
|
||||||
|
*/
|
||||||
|
lexorank: string;
|
||||||
|
/**
|
||||||
|
* Statusid
|
||||||
|
*/
|
||||||
|
statusId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetBoardsResponse
|
||||||
|
*/
|
||||||
|
export type GetBoardsResponse = {
|
||||||
|
/**
|
||||||
|
* Boards
|
||||||
|
*/
|
||||||
|
boards: Array<BoardSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetDealsResponse
|
||||||
|
*/
|
||||||
|
export type GetDealsResponse = {
|
||||||
|
/**
|
||||||
|
* Deals
|
||||||
|
*/
|
||||||
|
deals: Array<DealSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetProjectsResponse
|
||||||
|
*/
|
||||||
|
export type GetProjectsResponse = {
|
||||||
|
/**
|
||||||
|
* Projects
|
||||||
|
*/
|
||||||
|
projects: Array<ProjectSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetStatusesResponse
|
||||||
|
*/
|
||||||
|
export type GetStatusesResponse = {
|
||||||
|
/**
|
||||||
|
* Statuses
|
||||||
|
*/
|
||||||
|
statuses: Array<StatusSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTPValidationError
|
||||||
|
*/
|
||||||
|
export type HttpValidationError = {
|
||||||
|
/**
|
||||||
|
* Detail
|
||||||
|
*/
|
||||||
|
detail?: Array<ValidationError>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectSchema
|
||||||
|
*/
|
||||||
|
export type ProjectSchema = {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StatusSchema
|
||||||
|
*/
|
||||||
|
export type StatusSchema = {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Lexorank
|
||||||
|
*/
|
||||||
|
lexorank: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateBoardRequest
|
||||||
|
*/
|
||||||
|
export type UpdateBoardRequest = {
|
||||||
|
board: UpdateBoardSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateBoardResponse
|
||||||
|
*/
|
||||||
|
export type UpdateBoardResponse = {
|
||||||
|
/**
|
||||||
|
* Message
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateBoardSchema
|
||||||
|
*/
|
||||||
|
export type UpdateBoardSchema = {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name?: string | null;
|
||||||
|
/**
|
||||||
|
* Lexorank
|
||||||
|
*/
|
||||||
|
lexorank?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealRequest
|
||||||
|
*/
|
||||||
|
export type UpdateDealRequest = {
|
||||||
|
deal: UpdateDealSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealResponse
|
||||||
|
*/
|
||||||
|
export type UpdateDealResponse = {
|
||||||
|
/**
|
||||||
|
* Message
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealSchema
|
||||||
|
*/
|
||||||
|
export type UpdateDealSchema = {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name?: string | null;
|
||||||
|
/**
|
||||||
|
* Lexorank
|
||||||
|
*/
|
||||||
|
lexorank?: string | null;
|
||||||
|
/**
|
||||||
|
* Statusid
|
||||||
|
*/
|
||||||
|
statusId?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateStatusRequest
|
||||||
|
*/
|
||||||
|
export type UpdateStatusRequest = {
|
||||||
|
status: UpdateStatusSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateStatusResponse
|
||||||
|
*/
|
||||||
|
export type UpdateStatusResponse = {
|
||||||
|
/**
|
||||||
|
* Message
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateStatusSchema
|
||||||
|
*/
|
||||||
|
export type UpdateStatusSchema = {
|
||||||
|
/**
|
||||||
|
* Name
|
||||||
|
*/
|
||||||
|
name?: string | null;
|
||||||
|
/**
|
||||||
|
* Lexorank
|
||||||
|
*/
|
||||||
|
lexorank?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ValidationError
|
||||||
|
*/
|
||||||
|
export type ValidationError = {
|
||||||
|
/**
|
||||||
|
* Location
|
||||||
|
*/
|
||||||
|
loc: Array<string | number>;
|
||||||
|
/**
|
||||||
|
* Message
|
||||||
|
*/
|
||||||
|
msg: string;
|
||||||
|
/**
|
||||||
|
* Error Type
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetBoardsData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Projectid
|
||||||
|
*/
|
||||||
|
projectId: number;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/board/{projectId}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetBoardsErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetBoardsError = GetBoardsErrors[keyof GetBoardsErrors];
|
||||||
|
|
||||||
|
export type GetBoardsResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: GetBoardsResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetBoardsResponse2 = GetBoardsResponses[keyof GetBoardsResponses];
|
||||||
|
|
||||||
|
export type UpdateBoardData = {
|
||||||
|
body: UpdateBoardRequest;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Boardid
|
||||||
|
*/
|
||||||
|
boardId: number;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/board/{boardId}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateBoardErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateBoardError = UpdateBoardErrors[keyof UpdateBoardErrors];
|
||||||
|
|
||||||
|
export type UpdateBoardResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: UpdateBoardResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateBoardResponse2 = UpdateBoardResponses[keyof UpdateBoardResponses];
|
||||||
|
|
||||||
|
export type GetDealsData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Boardid
|
||||||
|
*/
|
||||||
|
boardId: number;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/deal/{boardId}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetDealsErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetDealsError = GetDealsErrors[keyof GetDealsErrors];
|
||||||
|
|
||||||
|
export type GetDealsResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: GetDealsResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetDealsResponse2 = GetDealsResponses[keyof GetDealsResponses];
|
||||||
|
|
||||||
|
export type UpdateDealData = {
|
||||||
|
body: UpdateDealRequest;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Dealid
|
||||||
|
*/
|
||||||
|
dealId: number;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/deal/{dealId}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateDealErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateDealError = UpdateDealErrors[keyof UpdateDealErrors];
|
||||||
|
|
||||||
|
export type UpdateDealResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: UpdateDealResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateDealResponse2 = UpdateDealResponses[keyof UpdateDealResponses];
|
||||||
|
|
||||||
|
export type GetProjectsData = {
|
||||||
|
body?: never;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/project/';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetProjectsResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: GetProjectsResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetProjectsResponse2 = GetProjectsResponses[keyof GetProjectsResponses];
|
||||||
|
|
||||||
|
export type GetStatusesData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Boardid
|
||||||
|
*/
|
||||||
|
boardId: number;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/status/{boardId}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetStatusesErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetStatusesError = GetStatusesErrors[keyof GetStatusesErrors];
|
||||||
|
|
||||||
|
export type GetStatusesResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: GetStatusesResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetStatusesResponse2 = GetStatusesResponses[keyof GetStatusesResponses];
|
||||||
|
|
||||||
|
export type UpdateStatusData = {
|
||||||
|
body: UpdateStatusRequest;
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Statusid
|
||||||
|
*/
|
||||||
|
statusId: number;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: '/status/{statusId}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateStatusErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateStatusError = UpdateStatusErrors[keyof UpdateStatusErrors];
|
||||||
|
|
||||||
|
export type UpdateStatusResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: UpdateStatusResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateStatusResponse2 = UpdateStatusResponses[keyof UpdateStatusResponses];
|
||||||
|
|
||||||
|
export type ClientOptions = {
|
||||||
|
baseURL: `${string}://${string}/api` | (string & {});
|
||||||
|
};
|
||||||
263
src/lib/client/zod.gen.ts
Normal file
263
src/lib/client/zod.gen.ts
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BoardSchema
|
||||||
|
*/
|
||||||
|
export const zBoardSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.int(),
|
||||||
|
lexorank: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DealSchema
|
||||||
|
*/
|
||||||
|
export const zDealSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.int(),
|
||||||
|
lexorank: z.string(),
|
||||||
|
statusId: z.int()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetBoardsResponse
|
||||||
|
*/
|
||||||
|
export const zGetBoardsResponse = z.object({
|
||||||
|
boards: z.array(zBoardSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetDealsResponse
|
||||||
|
*/
|
||||||
|
export const zGetDealsResponse = z.object({
|
||||||
|
deals: z.array(zDealSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectSchema
|
||||||
|
*/
|
||||||
|
export const zProjectSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.int()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetProjectsResponse
|
||||||
|
*/
|
||||||
|
export const zGetProjectsResponse = z.object({
|
||||||
|
projects: z.array(zProjectSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StatusSchema
|
||||||
|
*/
|
||||||
|
export const zStatusSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.int(),
|
||||||
|
lexorank: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetStatusesResponse
|
||||||
|
*/
|
||||||
|
export const zGetStatusesResponse = z.object({
|
||||||
|
statuses: z.array(zStatusSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ValidationError
|
||||||
|
*/
|
||||||
|
export const zValidationError = z.object({
|
||||||
|
loc: z.array(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.int()
|
||||||
|
])),
|
||||||
|
msg: z.string(),
|
||||||
|
type: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTPValidationError
|
||||||
|
*/
|
||||||
|
export const zHttpValidationError = z.object({
|
||||||
|
detail: z.optional(z.array(zValidationError))
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateBoardSchema
|
||||||
|
*/
|
||||||
|
export const zUpdateBoardSchema = z.object({
|
||||||
|
name: z.optional(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.null()
|
||||||
|
])),
|
||||||
|
lexorank: z.optional(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.null()
|
||||||
|
]))
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateBoardRequest
|
||||||
|
*/
|
||||||
|
export const zUpdateBoardRequest = z.object({
|
||||||
|
board: zUpdateBoardSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateBoardResponse
|
||||||
|
*/
|
||||||
|
export const zUpdateBoardResponse = z.object({
|
||||||
|
message: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealSchema
|
||||||
|
*/
|
||||||
|
export const zUpdateDealSchema = z.object({
|
||||||
|
name: z.optional(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.null()
|
||||||
|
])),
|
||||||
|
lexorank: z.optional(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.null()
|
||||||
|
])),
|
||||||
|
statusId: z.optional(z.union([
|
||||||
|
z.int(),
|
||||||
|
z.null()
|
||||||
|
]))
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealRequest
|
||||||
|
*/
|
||||||
|
export const zUpdateDealRequest = z.object({
|
||||||
|
deal: zUpdateDealSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateDealResponse
|
||||||
|
*/
|
||||||
|
export const zUpdateDealResponse = z.object({
|
||||||
|
message: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateStatusSchema
|
||||||
|
*/
|
||||||
|
export const zUpdateStatusSchema = z.object({
|
||||||
|
name: z.optional(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.null()
|
||||||
|
])),
|
||||||
|
lexorank: z.optional(z.union([
|
||||||
|
z.string(),
|
||||||
|
z.null()
|
||||||
|
]))
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateStatusRequest
|
||||||
|
*/
|
||||||
|
export const zUpdateStatusRequest = z.object({
|
||||||
|
status: zUpdateStatusSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateStatusResponse
|
||||||
|
*/
|
||||||
|
export const zUpdateStatusResponse = z.object({
|
||||||
|
message: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zGetBoardsData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.object({
|
||||||
|
projectId: z.int()
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never())
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zGetBoardsResponse2 = zGetBoardsResponse;
|
||||||
|
|
||||||
|
export const zUpdateBoardData = z.object({
|
||||||
|
body: zUpdateBoardRequest,
|
||||||
|
path: z.object({
|
||||||
|
boardId: z.int()
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never())
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zUpdateBoardResponse2 = zUpdateBoardResponse;
|
||||||
|
|
||||||
|
export const zGetDealsData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.object({
|
||||||
|
boardId: z.int()
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never())
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zGetDealsResponse2 = zGetDealsResponse;
|
||||||
|
|
||||||
|
export const zUpdateDealData = z.object({
|
||||||
|
body: zUpdateDealRequest,
|
||||||
|
path: z.object({
|
||||||
|
dealId: z.int()
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never())
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zUpdateDealResponse2 = zUpdateDealResponse;
|
||||||
|
|
||||||
|
export const zGetProjectsData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.optional(z.never()),
|
||||||
|
query: z.optional(z.never())
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zGetProjectsResponse2 = zGetProjectsResponse;
|
||||||
|
|
||||||
|
export const zGetStatusesData = z.object({
|
||||||
|
body: z.optional(z.never()),
|
||||||
|
path: z.object({
|
||||||
|
boardId: z.int()
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never())
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zGetStatusesResponse2 = zGetStatusesResponse;
|
||||||
|
|
||||||
|
export const zUpdateStatusData = z.object({
|
||||||
|
body: zUpdateStatusRequest,
|
||||||
|
path: z.object({
|
||||||
|
statusId: z.int()
|
||||||
|
}),
|
||||||
|
query: z.optional(z.never())
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
export const zUpdateStatusResponse2 = zUpdateStatusResponse;
|
||||||
1
src/lib/notifications/index.ts
Normal file
1
src/lib/notifications/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./notifications";
|
||||||
46
src/lib/notifications/notifications.ts
Normal file
46
src/lib/notifications/notifications.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
type CustomNotifications = {
|
||||||
|
notify: (...params: Parameters<typeof notifications.show>) => void;
|
||||||
|
success: (...params: Parameters<typeof notifications.show>) => void;
|
||||||
|
warn: (...params: Parameters<typeof notifications.show>) => void;
|
||||||
|
error: (...params: Parameters<typeof notifications.show>) => void;
|
||||||
|
guess: (
|
||||||
|
ok: boolean,
|
||||||
|
...params: Parameters<typeof notifications.show>
|
||||||
|
) => void;
|
||||||
|
} & typeof notifications;
|
||||||
|
|
||||||
|
const customNotifications: CustomNotifications = {
|
||||||
|
...notifications,
|
||||||
|
notify: params => {
|
||||||
|
return notifications.show({
|
||||||
|
...params,
|
||||||
|
color: "blue",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
success: params => {
|
||||||
|
return notifications.show({
|
||||||
|
...params,
|
||||||
|
color: "green",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
warn: params => {
|
||||||
|
return notifications.show({
|
||||||
|
...params,
|
||||||
|
color: "yellow",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: params => {
|
||||||
|
return notifications.show({
|
||||||
|
...params,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
guess: (ok: boolean, params) => {
|
||||||
|
if (ok) return customNotifications.success(params);
|
||||||
|
return customNotifications.error(params);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { customNotifications as notifications };
|
||||||
23
src/lib/store/features/auth/authSlice.ts
Normal file
23
src/lib/store/features/auth/authSlice.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
phoneNumber: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AuthState = {
|
||||||
|
phoneNumber: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authSlice = createSlice({
|
||||||
|
name: "authentication",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setPhoneNumber: (state, action: PayloadAction<string | null>) => {
|
||||||
|
state.phoneNumber = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setPhoneNumber } = authSlice.actions;
|
||||||
|
|
||||||
|
export default authSlice.reducer;
|
||||||
8
src/lib/store/features/rootReducer.ts
Normal file
8
src/lib/store/features/rootReducer.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { combineReducers } from "@reduxjs/toolkit";
|
||||||
|
import authReducer from "@/lib/store/features/auth/authSlice";
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
auth: authReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
||||||
28
src/lib/store/store.ts
Normal file
28
src/lib/store/store.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { persistReducer, persistStore } from "redux-persist";
|
||||||
|
import storage from "redux-persist/lib/storage";
|
||||||
|
import rootReducer from "@/lib/store/features/rootReducer";
|
||||||
|
|
||||||
|
const persistConfig = {
|
||||||
|
key: "root",
|
||||||
|
storage,
|
||||||
|
whitelist: ["targetService", "verification", "auth"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: persistedReducer,
|
||||||
|
middleware: getDefaultMiddleware =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const persistor = persistStore(store);
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
16
src/modals/TestModal/TestModal.tsx
Normal file
16
src/modals/TestModal/TestModal.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Flex, rem, Text } from "@mantine/core";
|
||||||
|
import { ContextModalProps } from "@mantine/modals";
|
||||||
|
|
||||||
|
const TestModal = ({ id, context, innerProps }: ContextModalProps) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
gap={rem(10)}
|
||||||
|
direction={"column"}>
|
||||||
|
<Text>Hi</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestModal;
|
||||||
5
src/modals/modals.ts
Normal file
5
src/modals/modals.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import TestModal from "@/modals/TestModal/TestModal";
|
||||||
|
|
||||||
|
export const modals = {
|
||||||
|
testModal: TestModal,
|
||||||
|
};
|
||||||
27
src/providers/ReactQueryProvider.tsx
Normal file
27
src/providers/ReactQueryProvider.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactNode, useState } from "react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ReactQueryProvider({ children }: Props) {
|
||||||
|
const [queryClient] = useState(
|
||||||
|
() =>
|
||||||
|
new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 3 * 60 * 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/providers/ReduxProvider.tsx
Normal file
23
src/providers/ReduxProvider.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
import { persistor, store } from "@/lib/store/store";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReduxProvider({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
{" "}
|
||||||
|
<PersistGate
|
||||||
|
loading={null}
|
||||||
|
persistor={persistor}>
|
||||||
|
{children}
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/redux-persist.d.ts
vendored
Normal file
5
src/redux-persist.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
declare module "redux-persist/lib/storage" {
|
||||||
|
import { WebStorage } from "redux-persist/es/types";
|
||||||
|
const localStorage: WebStorage;
|
||||||
|
export default localStorage;
|
||||||
|
}
|
||||||
43
src/theme.ts
Normal file
43
src/theme.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { createTheme, MantineColorsTuple } from "@mantine/core";
|
||||||
|
|
||||||
|
export const myColor: MantineColorsTuple = [
|
||||||
|
"#e2faff",
|
||||||
|
"#d4eff8",
|
||||||
|
"#afdce9",
|
||||||
|
"#87c8db",
|
||||||
|
"#65b7cf",
|
||||||
|
"#4aaac7",
|
||||||
|
"#3fa7c6",
|
||||||
|
"#2c92af",
|
||||||
|
"#1b829e",
|
||||||
|
"#00718c",
|
||||||
|
];
|
||||||
|
|
||||||
|
const radius = "md";
|
||||||
|
const size = "md";
|
||||||
|
|
||||||
|
export const theme = createTheme({
|
||||||
|
colors: {
|
||||||
|
myColor,
|
||||||
|
},
|
||||||
|
primaryColor: "myColor",
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultProps: {
|
||||||
|
radius,
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputBase: {
|
||||||
|
defaultProps: {
|
||||||
|
radius,
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Modal: {
|
||||||
|
defaultProps: {
|
||||||
|
radius,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
36
src/utils/lexorank.ts
Normal file
36
src/utils/lexorank.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { LexoRank } from "lexorank";
|
||||||
|
|
||||||
|
type LexorankSortable = {
|
||||||
|
lexorank: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function compareByLexorank<T extends LexorankSortable>(
|
||||||
|
a: T,
|
||||||
|
b: T
|
||||||
|
): -1 | 1 | 0 {
|
||||||
|
if (a.lexorank < b.lexorank) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.lexorank > b.lexorank) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortByLexorank<T extends LexorankSortable>(items: T[]): T[] {
|
||||||
|
return items.sort(compareByLexorank);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewLexorank(
|
||||||
|
left?: LexoRank | null,
|
||||||
|
right?: LexoRank | null
|
||||||
|
): LexoRank {
|
||||||
|
if (right) {
|
||||||
|
if (left) return left?.between(right);
|
||||||
|
return right.between(LexoRank.min());
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
return left.between(LexoRank.max());
|
||||||
|
}
|
||||||
|
return LexoRank.middle();
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"types": ["node", "jest", "@testing-library/jest-dom"],
|
||||||
|
"target": "ES2020",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@ -14,14 +15,10 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"plugins": [{ "name": "next" }]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|||||||
Reference in New Issue
Block a user