594
package-lock.json
generated
@ -8,10 +8,12 @@
|
||||
"name": "witlab-funnel",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@ -21,8 +23,12 @@
|
||||
"mongoose": "^8.18.2",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.0",
|
||||
"react-circular-progressbar": "^2.2.0",
|
||||
"react-dom": "19.1.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"react-hook-form": "^7.63.0",
|
||||
"recharts": "^2.15.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^4.1.1",
|
||||
@ -313,7 +319,6 @@
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -1014,6 +1019,56 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
||||
"integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -1912,12 +1967,41 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
|
||||
@ -1948,6 +2032,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@ -2122,6 +2232,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-rect": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1",
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
@ -2217,6 +2359,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
||||
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-visually-hidden": "1.2.3",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||
@ -2358,6 +2543,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
@ -2376,6 +2579,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
|
||||
@ -2727,6 +2959,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-a11y": {
|
||||
"version": "9.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-9.1.6.tgz",
|
||||
@ -3574,6 +3812,69 @@
|
||||
"@types/deep-eql": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
@ -5431,9 +5732,129 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
@ -5512,6 +5933,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
@ -6412,6 +6839,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@ -6440,6 +6873,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz",
|
||||
"integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||
@ -7025,6 +7467,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@ -7637,7 +8088,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@ -8066,6 +8516,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@ -8077,7 +8533,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
@ -8558,7 +9013,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -9019,7 +9473,6 @@
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
@ -9077,6 +9530,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-circular-progressbar": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz",
|
||||
"integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-docgen-typescript": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz",
|
||||
@ -9099,11 +9561,26 @@
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.63.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
|
||||
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
@ -9153,6 +9630,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-equals": "^5.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@ -9175,6 +9667,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recast": {
|
||||
"version": "0.23.11",
|
||||
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz",
|
||||
@ -9202,6 +9710,44 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
|
||||
"integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react-is": "^18.3.1",
|
||||
"react-smooth": "^4.0.4",
|
||||
"recharts-scale": "^0.4.4",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"victory-vendor": "^36.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts-scale": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
|
||||
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
@ -10424,7 +10970,6 @@
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
@ -10914,6 +11459,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "36.9.2",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
|
||||
@ -11576,6 +12143,15 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz",
|
||||
"integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,10 +14,12 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@ -27,8 +29,12 @@
|
||||
"mongoose": "^8.18.2",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.0",
|
||||
"react-circular-progressbar": "^2.2.0",
|
||||
"react-dom": "19.1.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"react-hook-form": "^7.63.0",
|
||||
"recharts": "^2.15.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^4.1.1",
|
||||
|
||||
BIN
public/female-portrait.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
@ -1 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
213
public/heart-in-fire.svg
Normal file
@ -0,0 +1,213 @@
|
||||
<svg width="252" height="167" viewBox="0 0 252 167" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_109_2162)">
|
||||
<path d="M131.693 52.3306C131.693 52.3306 150.168 55.6056 163.131 64.1717C176.094 72.7378 182.204 61.5075 170.91 58.2512C170.91 58.2512 174.181 51.695 182.788 49.9344C191.396 48.1738 196.508 38.8629 191.618 32.1166C191.618 32.1166 200.888 42.8952 196.033 53.3434C191.177 63.7916 187.203 65.6207 191.177 76.0347C195.151 86.4486 209.274 110.757 198.168 129.338C189.492 143.853 192.737 148.035 192.737 148.035C192.737 148.035 194.047 137.478 209.274 138.138C209.274 138.138 200.666 143.124 202.433 153.828H129.357C129.357 153.828 113.262 68.3629 131.693 52.3306Z" fill="url(#paint0_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M131.693 52.3306C131.693 52.3306 150.168 55.6056 163.131 64.1717C176.094 72.7378 182.204 61.5075 170.91 58.2512C170.91 58.2512 174.181 51.695 182.788 49.9344C191.396 48.1738 196.417 38.7165 191.618 32.1166C191.618 32.1166 200.888 42.8952 196.033 53.3434C191.177 63.7916 187.203 65.6207 191.177 76.0347C195.151 86.4486 209.274 110.757 198.168 129.338C189.492 143.853 192.737 148.035 192.737 148.035C192.737 148.035 194.047 137.478 209.274 138.138C209.274 138.138 200.666 143.124 202.433 153.828H129.357C129.357 153.828 113.262 68.3629 131.693 52.3306Z" fill="url(#paint1_linear_109_2162)"/>
|
||||
<path d="M71.3808 153.828C71.3808 153.828 58.5804 150.023 61.8165 139.026C65.0525 128.03 50.5481 124.945 48.8691 120.311C46.1115 112.698 54.1656 110.125 49.8978 102.372C49.8978 102.372 53.5028 104.628 53.7248 112.028C53.9468 119.429 61.2287 120.557 70.0582 120.417C70.0582 120.417 51.5768 96.2362 57.4048 83.9059C62.8858 72.3109 78.8878 67.9765 79.3287 65.2966C79.7695 62.6168 73.4444 61.4919 73.4444 61.4919C73.4444 61.4919 80.5449 55.8175 86.5011 59.5194C92.4604 63.2213 101.287 67.2379 110.116 63.0094C110.116 63.0094 111.477 58.6033 104.489 53.8824C98.2509 49.6695 95.5495 44.9299 106.549 34.569C115.891 25.7692 111.111 21.0359 111.111 21.0359C111.111 21.0359 118.283 20.0855 117.842 32.771C117.401 45.4597 121.932 50.3208 131.693 52.3306C141.455 54.3405 148.74 64.3836 150.506 76.2247C152.273 88.0658 147.011 93.494 154.514 104.771C162.018 116.048 165.845 128.453 164.667 138.322C164.667 138.322 160.921 125.951 151.056 119.678C138.587 111.748 138.147 103.715 138.147 103.715C138.147 103.715 140.023 108.685 136.493 113.97C132.963 119.255 127.994 125.178 132.519 137.019L133.732 130.043C133.732 130.043 144.547 137.019 145.869 143.152C147.192 149.284 141.548 153.831 141.548 153.831H71.3839L71.3808 153.828Z" fill="url(#paint2_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M71.3808 153.828C71.3808 153.828 58.5804 150.023 61.8165 139.026C65.0525 128.03 50.5481 124.945 48.8691 120.311C46.1115 112.698 54.1656 110.125 49.8978 102.372C49.8978 102.372 53.5028 104.628 53.7248 112.028C53.9468 119.429 61.2287 120.557 70.0582 120.417C70.0582 120.417 51.5768 96.2362 57.4048 83.9059C62.8858 72.3109 78.8878 67.9765 79.3287 65.2966C79.7695 62.6168 73.4444 61.4919 73.4444 61.4919C73.4444 61.4919 80.5449 55.8175 86.5011 59.5194C92.4604 63.2213 101.287 67.2379 110.116 63.0094C110.116 63.0094 111.477 58.6033 104.489 53.8824C98.2509 49.6695 95.5495 44.9299 106.549 34.569C115.891 25.7692 111.111 21.0359 111.111 21.0359C111.111 21.0359 118.283 20.0855 117.842 32.771C117.401 45.4597 121.932 50.3208 131.693 52.3306C141.455 54.3405 148.74 64.3836 150.506 76.2247C152.273 88.0658 147.011 93.494 154.514 104.771C162.018 116.048 165.845 128.453 164.667 138.322C164.667 138.322 160.921 125.951 151.056 119.678C138.587 111.748 138.147 103.715 138.147 103.715C138.147 103.715 140.023 108.685 136.493 113.97C132.963 119.255 127.994 125.178 132.519 137.019L133.732 130.043C133.732 130.043 144.547 137.019 145.869 143.152C147.192 149.284 141.548 153.831 141.548 153.831H71.3839L71.3808 153.828Z" fill="url(#paint3_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M193.262 153.827C193.262 153.827 181.766 145.13 183.229 134.386C184.817 122.735 209.565 99.1614 182.701 77.567C159.073 58.5714 133.838 55.8698 133.838 55.8698C133.838 55.8698 160.495 61.4943 167.196 85.7717C172.877 106.35 156.781 124.202 157.828 138.134C157.828 138.134 160.968 135.165 163.713 140.225C166.455 145.286 159.767 150.895 159.767 150.895C159.767 150.895 155.874 131.475 143.399 126.776C143.399 126.776 149.408 143.671 141.545 153.827H193.262Z" fill="url(#paint4_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M169.769 153.833C169.769 153.833 170.935 137.078 180.906 119.207C190.877 101.336 183.095 79.4428 169.178 69.6334C169.178 69.6334 205.522 84.6653 196.398 120.566C196.398 120.566 187.734 125.688 185.289 134.385C181.647 147.336 190.733 153.827 190.733 153.827L169.766 153.833H169.769Z" fill="url(#paint5_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M71.3806 153.827C71.3806 153.827 63.7673 149.704 64.7585 140.505C65.7527 131.307 57.3734 126.935 52.9524 123.062C48.5313 119.185 50.967 112.454 50.967 112.454C50.967 112.454 48.869 119.995 57.4047 122.04C65.9372 124.084 77.2681 129.724 77.2681 129.724C77.2681 129.724 70.1675 137.124 74.5823 144.896C78.9971 152.667 104.048 153.83 104.048 153.83H71.3838L71.3806 153.827Z" fill="url(#paint6_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M104.044 153.826C104.044 153.826 78.7376 154.531 68.5855 142.266C68.5855 142.266 67.8508 129.496 81.8267 123.264C95.8058 117.032 91.8319 101.729 91.8319 101.729C91.8319 101.729 101.249 124.644 92.4197 135.781C92.4197 135.781 79.5443 131.833 76.6772 137.542C73.807 143.251 80.9794 152.343 104.044 153.823V153.826Z" fill="url(#paint7_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M148.977 153.827C148.977 153.827 154.495 143.678 145.456 130.285C136.417 116.892 144.015 108.516 144.631 94.0261C145.247 79.5364 139.803 71.0762 139.803 71.0762C139.803 71.0762 145.459 91.2154 134.066 109.13C124.186 124.663 116.704 138.034 135.539 147.199C135.539 147.199 136.68 143.266 141.554 144.625C147.42 146.264 148.983 153.823 148.983 153.823L148.977 153.827Z" fill="url(#paint8_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M139.825 126.093C139.825 126.093 150.005 129.49 153.492 133.369L151.553 133.348C151.553 133.348 153.826 139.072 150.653 147.111C150.653 147.111 147.479 135.731 139.825 126.093Z" fill="url(#paint9_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M124.13 144.31C112.433 142.512 108.35 140.608 108.35 140.608C108.35 140.608 111.22 145.578 120.931 148.432C120.931 148.432 113.318 154.777 99.3013 151.075C85.2878 147.373 80.4322 153.823 80.4322 153.823H71.3838C71.3838 153.823 108.903 141.219 122.585 118.552C136.267 95.886 127.882 76.9964 116.407 67.4082C104.929 57.8232 108.203 45.2934 112.139 38.6499C117.364 29.8408 113.612 22.437 113.612 22.437C113.612 22.437 117.733 24.6213 116.848 34.7704C115.966 44.9195 119.79 51.4757 128.838 54.2957C128.838 54.2957 119.165 58.6645 125.565 67.4425C131.965 76.2174 141.439 98.1608 133.131 114.53C124.824 130.898 116.626 139.795 124.13 144.307V144.31Z" fill="url(#paint10_linear_109_2162)"/>
|
||||
<path d="M103.898 26.461C103.898 26.461 106.808 21.0733 101.102 17.1003C97.957 14.9129 91.2285 15.8913 91.2285 15.8913C91.2285 15.8913 98.4823 24.1177 103.898 26.4579V26.461Z" fill="url(#paint11_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M89.9216 63.9596C89.9216 63.9596 101.95 69.1417 111.108 63.9596C111.108 63.9596 112.099 58.8742 104.592 53.069C97.0847 47.2637 100.111 39.9472 108.406 33.5623C108.406 33.5623 104.038 43.8641 108.975 52.1871C114.788 61.9841 115.153 73.1209 115.153 73.1209C115.153 73.1209 98.526 71.5691 89.9185 63.9565L89.9216 63.9596Z" fill="url(#paint12_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M111.992 21.456C111.992 21.456 114.863 25.7562 111.552 30.2652C108.24 34.7773 99.1827 41.7386 100.805 48.3353C102.428 54.9352 110.742 53.5953 112.286 64.1681C112.286 64.1681 110.889 54.1593 106.88 49.7905C102.872 45.4218 114.05 34.7991 115.157 30.689C115.157 30.689 115.672 24.0642 111.992 21.456Z" fill="url(#paint13_linear_109_2162)"/>
|
||||
<path d="M125.849 44.5742C125.849 44.5742 123.173 42.8261 125.849 38.8655C125.849 38.8655 128.888 41.078 125.849 44.5742Z" fill="url(#paint14_linear_109_2162)"/>
|
||||
<path d="M64.1956 62.862C64.1956 62.862 51.7517 67.6701 49.9883 60.3878C49.9883 60.3878 50.2634 59.3284 53.4838 58.5743C53.4838 58.5743 57.3202 62.7311 64.1956 62.862Z" fill="url(#paint15_linear_109_2162)"/>
|
||||
<path d="M47.8404 98.286C47.8404 98.286 43.5726 95.6779 44.3104 89.265C45.0421 82.877 42.9128 79.3278 42.9128 79.3278C42.9128 79.3278 46.8836 83.064 48.2812 89.4083C48.2812 89.4083 45.855 92.7924 47.8404 98.2891V98.286Z" fill="url(#paint16_linear_109_2162)"/>
|
||||
<path d="M65.5713 26.1212C65.5713 26.1212 63.3795 24.0926 64.527 21.017C64.527 21.017 67.5098 23.0674 68.1976 26.7319L65.5713 26.1212Z" fill="url(#paint17_linear_109_2162)"/>
|
||||
<path d="M189.632 37.9462C189.632 37.9462 191.177 33.3282 185.383 25.4071C181.569 20.1939 182.569 13.8589 182.569 13.8589C182.569 13.8589 178.595 20.7891 180.915 28.707C180.915 28.707 186.765 31.3463 189.632 37.9462Z" fill="url(#paint18_linear_109_2162)"/>
|
||||
<path d="M203.899 130.544C203.899 130.544 206.713 127.873 208.258 123.75C208.258 123.75 208.589 126.39 208.148 129.085L203.899 130.547V130.544Z" fill="url(#paint19_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M187.453 76.987C187.453 76.987 176.941 67.5297 179.368 60.9299C181.794 54.33 192.94 48.9424 195.148 43.4425C195.148 43.4425 193.86 54.4048 190.292 59.0602C185.658 65.1085 187.453 76.987 187.453 76.987Z" fill="url(#paint20_linear_109_2162)"/>
|
||||
<g filter="url(#filter0_d_109_2162)">
|
||||
<path d="M160.577 67.6125C142.162 54.1791 126.576 70.6071 126.576 70.6071C126.576 70.6071 110.992 54.176 92.5736 67.6125C74.9645 80.457 76.6028 128.367 126.576 148.391C176.545 128.367 178.187 80.457 160.577 67.6125Z" fill="url(#paint21_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:multiply" d="M126.579 148.391C176.548 128.367 178.19 80.4573 160.581 67.6128C154.518 63.1911 148.765 62.0039 143.772 62.4027C146.079 62.2563 155.741 62.4152 157.554 75.2067C159.614 89.7525 144.332 122.44 118.659 125.606C96.535 128.336 87.1552 113.12 86.8237 112.568C92.8331 126.323 105.336 139.878 126.582 148.391H126.579Z" fill="url(#paint22_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M100.368 66.0109C100.368 66.0109 88.734 69.2547 85.301 84.2773C81.868 99.2999 90.2379 114.369 90.2379 114.369C90.2379 114.369 88.5214 96.3895 100.368 66.0078V66.0109Z" fill="url(#paint23_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M118.915 142.999C118.915 142.999 140.004 142.043 158.051 119.704C158.051 119.704 148.28 135.016 127.673 146.851C127.673 146.851 122.33 145.76 118.915 142.999Z" fill="url(#paint24_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:multiply" d="M120.976 71.1864C126.813 79.7431 134.971 82.5663 134.971 82.5663C134.971 82.5663 139.917 61.9161 131.678 66.6057C128.458 68.6281 126.579 70.6068 126.579 70.6068C126.579 70.6068 117.868 61.4237 105.774 62.4053C105.918 62.4084 115.182 62.6951 120.976 71.1864Z" fill="url(#paint25_linear_109_2162)"/>
|
||||
<path style="mix-blend-mode:color-dodge" opacity="0.7" d="M104.939 66.0109C96.5693 66.0109 86.8268 71.6977 84.4724 88.135C82.3995 102.609 90.2254 128.416 116.708 122.131C143.193 115.846 157.904 75.3373 148.524 69.8374C139.144 64.3375 134.398 79.3508 135.061 89.3876C135.061 89.3876 119.722 66.0078 104.936 66.0078L104.939 66.0109Z" fill="url(#paint26_linear_109_2162)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_109_2162" x="52.3962" y="32.3292" width="148.361" height="146.062" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="15"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_109_2162"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_109_2162" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_109_2162" x1="175.713" y1="47.3387" x2="149.622" y2="213.434" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_109_2162" x1="206.535" y1="42.0195" x2="162.603" y2="109.905" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_109_2162" x1="78.7002" y1="50.38" x2="139.683" y2="185.508" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_109_2162" x1="36.0969" y1="49.7505" x2="152.379" y2="129.299" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_109_2162" x1="237.341" y1="95.8989" x2="99.59" y2="112.722" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_109_2162" x1="221.505" y1="103.726" x2="129.052" y2="121.657" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_109_2162" x1="56.4198" y1="90.1465" x2="110.346" y2="210.924" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_109_2162" x1="54.7877" y1="87.6098" x2="117.816" y2="173.443" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_109_2162" x1="119.946" y1="61.0051" x2="145.702" y2="148.701" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_109_2162" x1="143.408" y1="117.25" x2="152.163" y2="151.111" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_109_2162" x1="44.3948" y1="45.8792" x2="173.975" y2="160.396" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_109_2162" x1="93.7392" y1="13.0027" x2="142.813" y2="76.5534" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_109_2162" x1="82.0519" y1="25.6631" x2="132.445" y2="94.1456" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_109_2162" x1="101.815" y1="18.0377" x2="119.667" y2="75.7349" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint14_linear_109_2162" x1="126.037" y1="31.7889" x2="125.804" y2="53.8103" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint15_linear_109_2162" x1="50.9035" y1="59.9929" x2="69.5698" y2="68.2737" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint16_linear_109_2162" x1="42.694" y1="82.2102" x2="52.4335" y2="105.582" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint17_linear_109_2162" x1="64.6145" y1="21.4751" x2="70.441" y2="34.4278" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint18_linear_109_2162" x1="179.858" y1="17.1401" x2="194.565" y2="49.7311" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint19_linear_109_2162" x1="205.319" y1="125.474" x2="208.167" y2="131.788" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500"/>
|
||||
<stop offset="0.36" stop-color="#FF8004"/>
|
||||
<stop offset="1" stop-color="#FF540D"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint20_linear_109_2162" x1="177.711" y1="88.2266" x2="193.831" y2="33.3169" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF9500" stop-opacity="0"/>
|
||||
<stop offset="0.11" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="0.34" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.67" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="1" stop-color="#FCEA10"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint21_linear_109_2162" x1="163.567" y1="127.367" x2="114.633" y2="86.3865" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EF4B9F"/>
|
||||
<stop offset="1" stop-color="#E6332A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint22_linear_109_2162" x1="105.909" y1="77.9052" x2="243.813" y2="201.203" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0C5DA5" stop-opacity="0"/>
|
||||
<stop offset="0.27" stop-color="#14529D" stop-opacity="0.24"/>
|
||||
<stop offset="0.87" stop-color="#2A3989" stop-opacity="0.86"/>
|
||||
<stop offset="1" stop-color="#2F3485"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint23_linear_109_2162" x1="78.9946" y1="58.4357" x2="134.591" y2="167.436" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCEA10"/>
|
||||
<stop offset="0.33" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="0.66" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.89" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="1" stop-color="#FF9500" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint24_linear_109_2162" x1="94.6246" y1="175.756" x2="163.584" y2="106.894" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCEA10"/>
|
||||
<stop offset="0.33" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="0.66" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.89" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="1" stop-color="#FF9500" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint25_linear_109_2162" x1="150.016" y1="80.2978" x2="32.4088" y2="33.8186" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0C5DA5" stop-opacity="0"/>
|
||||
<stop offset="0.27" stop-color="#14529D" stop-opacity="0.24"/>
|
||||
<stop offset="0.87" stop-color="#2A3989" stop-opacity="0.86"/>
|
||||
<stop offset="1" stop-color="#2F3485"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint26_linear_109_2162" x1="76.0368" y1="43.9023" x2="149.33" y2="125.17" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCEA10"/>
|
||||
<stop offset="0.33" stop-color="#FDCA0A" stop-opacity="0.63"/>
|
||||
<stop offset="0.66" stop-color="#FEAD04" stop-opacity="0.29"/>
|
||||
<stop offset="0.89" stop-color="#FE9B01" stop-opacity="0.08"/>
|
||||
<stop offset="1" stop-color="#FF9500" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_109_2162">
|
||||
<rect width="251" height="167" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
BIN
public/male-portrait.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 128 B |
@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
Before Width: | Height: | Size: 385 B |
@ -25,7 +25,9 @@
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-secondary: var(--chart-secondary);
|
||||
--color-ring: var(--ring);
|
||||
--color-placeholder-foreground: var(--placeholder-foreground);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
@ -53,6 +55,7 @@
|
||||
0px 0px 0px 0px rgba(59, 130, 246, 0.2);
|
||||
--shadow-black-glow: 0px 8px 15px 0px #00000026, 0px 4px 6px 0px #00000014;
|
||||
--shadow-coupon: 0px 20px 40px 0px #0000004d, 0px 8px 16px 0px #00000033;
|
||||
--shadow-destructive: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
:root {
|
||||
@ -80,7 +83,7 @@
|
||||
|
||||
/* Muted - для второстепенного контента */
|
||||
--muted: oklch(0.97 0 0); /* Светло-серый фон */
|
||||
--muted-foreground: oklch(0.59 0.02 260.8); /* #64748B - серый текст */
|
||||
--muted-foreground: oklch(0.5544 0.0407 257.42); /* #64748B - серый текст */
|
||||
|
||||
/* Accent - для акцентов */
|
||||
--accent: oklch(0.97 0 0); /* Светло-серый фон */
|
||||
@ -94,8 +97,12 @@
|
||||
|
||||
/* Border и Input */
|
||||
--border: oklch(0.9288 0.0126 255.51); /* Светло-серая граница */
|
||||
--border-black: oklch(0 0 0); /* Черная граница */
|
||||
--input: oklch(0.922 0 0); /* Светло-серый фон инпутов */
|
||||
--ring: oklch(0.6231 0.188 259.81); /* Синий фокус */
|
||||
--placeholder-foreground: oklch(
|
||||
0.7544 0.0199 282.65
|
||||
); /* #ADAEBC - placeholder текст */
|
||||
|
||||
/* Chart цвета - можно оставить как есть или переопределить */
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
@ -103,6 +110,7 @@
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--chart-secondary: oklch(0.881 0.0536 260.65) /* #C4D9FC */;
|
||||
|
||||
/* Sidebar - можно оставить как есть */
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
|
||||
@ -22,17 +22,20 @@ function Header({
|
||||
|
||||
return (
|
||||
<header className={cn("w-full p-6 pb-3 min-h-[96px]", className)} {...props}>
|
||||
<div className="w-full flex justify-left items-center min-h-9">
|
||||
<div className="w-full flex justify-start items-center min-h-9">
|
||||
{shouldRenderBackButton && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="hover:bg-transparent rounded-full p-0! ml-[-13px] mb-[-9px]"
|
||||
className="hover:bg-transparent rounded-full !p-0 -ml-[13px] -mb-[9px]"
|
||||
onClick={onBack}
|
||||
aria-label="Назад"
|
||||
>
|
||||
<ChevronLeft size={36} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{progressProps && (
|
||||
<div className="w-full flex justify-center items-center mt-3">
|
||||
<Progress {...progressProps} />
|
||||
@ -42,4 +45,4 @@ function Header({
|
||||
);
|
||||
}
|
||||
|
||||
export { Header };
|
||||
export { Header };
|
||||
@ -2,22 +2,16 @@
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Header } from "@/components/layout/Header/Header";
|
||||
import Typography, {
|
||||
TypographyProps,
|
||||
} from "@/components/ui/Typography/Typography";
|
||||
import {
|
||||
BottomActionButton,
|
||||
BottomActionButtonProps,
|
||||
} from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Typography, { TypographyProps } from "@/components/ui/Typography/Typography";
|
||||
|
||||
export interface LayoutQuestionProps
|
||||
extends Omit<React.ComponentProps<"section">, "title" | "content"> {
|
||||
headerProps?: React.ComponentProps<typeof Header>;
|
||||
title: TypographyProps<"h2">;
|
||||
title?: TypographyProps<"h2">;
|
||||
subtitle?: TypographyProps<"p">;
|
||||
children: React.ReactNode;
|
||||
bottomActionButtonProps?: BottomActionButtonProps;
|
||||
contentProps?: React.ComponentProps<"div">;
|
||||
childrenWrapperProps?: React.ComponentProps<"div">;
|
||||
}
|
||||
|
||||
function LayoutQuestion({
|
||||
@ -26,32 +20,29 @@ function LayoutQuestion({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
bottomActionButtonProps,
|
||||
contentProps,
|
||||
childrenWrapperProps,
|
||||
...props
|
||||
}: LayoutQuestionProps) {
|
||||
const bottomActionButtonRef = useRef<HTMLDivElement | null>(null);
|
||||
const [bottomActionButtonHeight, setBottomActionButtonHeight] =
|
||||
useState<number>(132);
|
||||
|
||||
useEffect(() => {
|
||||
if (bottomActionButtonRef.current) {
|
||||
console.log(bottomActionButtonRef.current.clientHeight);
|
||||
|
||||
setBottomActionButtonHeight(bottomActionButtonRef.current.clientHeight);
|
||||
}
|
||||
}, [bottomActionButtonProps]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(`block min-h-dvh w-full`, className)}
|
||||
className={cn("block min-h-dvh w-full", className)}
|
||||
{...props}
|
||||
// Безопаснее, чем JS-константа: если CSS-переменная не задана — будет 0px
|
||||
style={{
|
||||
paddingBottom: `${bottomActionButtonHeight}px`,
|
||||
...props.style,
|
||||
paddingBottom: "var(--bottom-action-button-height, 0px)",
|
||||
...(props.style ?? {}),
|
||||
}}
|
||||
>
|
||||
{headerProps && <Header {...headerProps} />}
|
||||
<div className="w-full flex flex-col justify-center items-center p-6 pt-[30px]">
|
||||
|
||||
<div
|
||||
{...contentProps}
|
||||
className={cn(
|
||||
"w-full flex flex-col justify-center items-center p-6 pt-[30px]",
|
||||
contentProps?.className
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<Typography
|
||||
as="h2"
|
||||
@ -62,6 +53,7 @@ function LayoutQuestion({
|
||||
className={cn(title.className, "w-full text-[25px] leading-[38px]")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{subtitle && (
|
||||
<Typography
|
||||
as="p"
|
||||
@ -74,17 +66,16 @@ function LayoutQuestion({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
className="max-w-[560px]"
|
||||
ref={bottomActionButtonRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
{...childrenWrapperProps}
|
||||
className={cn("w-full mt-[30px]", childrenWrapperProps?.className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export { LayoutQuestion };
|
||||
export { LayoutQuestion };
|
||||
80
src/components/templates/Coupon/Coupon.stories.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Coupon } from "./Coupon";
|
||||
import { fn } from "storybook/test";
|
||||
import {
|
||||
LayoutQuestion,
|
||||
LayoutQuestionProps,
|
||||
} from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
|
||||
const layoutQuestionProps: Omit<LayoutQuestionProps, "children"> = {
|
||||
headerProps: {
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "Тебе повезло!",
|
||||
align: "center",
|
||||
},
|
||||
subtitle: {
|
||||
children: "Ты получил специальную эксклюзивную скидку на 94%",
|
||||
align: "center",
|
||||
},
|
||||
};
|
||||
|
||||
/** Reusable Coupon page Component */
|
||||
const meta: Meta<typeof Coupon> = {
|
||||
title: "Templates/Coupon",
|
||||
component: Coupon,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
couponProps: {
|
||||
title: {
|
||||
children: "Special Offer",
|
||||
},
|
||||
offer: {
|
||||
title: {
|
||||
children: "94% OFF",
|
||||
},
|
||||
description: {
|
||||
children: "Одноразовая эксклюзивная скидка",
|
||||
},
|
||||
},
|
||||
promoCode: {
|
||||
children: "HAIR50",
|
||||
},
|
||||
footer: {
|
||||
children: (
|
||||
<>
|
||||
Скопируйте или нажмите <b>Continue</b>
|
||||
</>
|
||||
),
|
||||
},
|
||||
onCopyPromoCode: fn(),
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
bottomActionButtonProps: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
<Coupon {...args} />
|
||||
</LayoutQuestion>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
70
src/components/templates/Coupon/Coupon.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BottomActionButton,
|
||||
BottomActionButtonProps,
|
||||
} from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import { Coupon as CouponWidget } from "@/components/widgets/Coupon/Coupon";
|
||||
import { useDynamicSize } from "@/hooks/DOM/useDynamicSize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CouponProps extends Omit<React.ComponentProps<"div">, "title"> {
|
||||
couponProps: React.ComponentProps<typeof CouponWidget>;
|
||||
bottomActionButtonProps?: BottomActionButtonProps;
|
||||
}
|
||||
|
||||
function Coupon({
|
||||
couponProps,
|
||||
bottomActionButtonProps,
|
||||
...props
|
||||
}: CouponProps) {
|
||||
const {
|
||||
height: bottomActionButtonHeight,
|
||||
elementRef: bottomActionButtonRef,
|
||||
} = useDynamicSize<HTMLDivElement>({
|
||||
defaultHeight: 132,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full flex flex-col items-center gap-[22px]"
|
||||
{...props}
|
||||
style={{
|
||||
paddingBottom: `${bottomActionButtonHeight}px`,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
{/* {title && (
|
||||
<Typography
|
||||
as="h2"
|
||||
weight="bold"
|
||||
font="manrope"
|
||||
{...title}
|
||||
className={cn(title.className, "text-[25px] leading-[38px]")}
|
||||
/>
|
||||
)}
|
||||
{subtitle && (
|
||||
<Typography
|
||||
as="p"
|
||||
weight="medium"
|
||||
font="inter"
|
||||
{...subtitle}
|
||||
className={cn(subtitle.className, "text-[17px] leading-[26px]")}
|
||||
/>
|
||||
)} */}
|
||||
<CouponWidget {...couponProps} />
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
className={cn(
|
||||
"max-w-[560px] z-10",
|
||||
bottomActionButtonProps.className
|
||||
)}
|
||||
ref={bottomActionButtonRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Coupon };
|
||||
104
src/components/templates/Email/Email.stories.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Email } from "./Email";
|
||||
import { fn } from "storybook/test";
|
||||
import {
|
||||
LayoutQuestion,
|
||||
LayoutQuestionProps,
|
||||
} from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
import Image from "next/image";
|
||||
|
||||
const layoutQuestionProps: Omit<LayoutQuestionProps, "children"> = {
|
||||
headerProps: {
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "Портрет твоей второй половинки готов! Куда нам его отправить?",
|
||||
align: "center",
|
||||
},
|
||||
contentProps: {
|
||||
className: "pt-0!",
|
||||
},
|
||||
};
|
||||
|
||||
/** Reusable Email page Component */
|
||||
const meta: Meta<typeof Email> = {
|
||||
title: "Templates/Email",
|
||||
component: Email,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
textInputProps: {
|
||||
label: "Email",
|
||||
placeholder: "Enter your Email",
|
||||
type: "email",
|
||||
onChange: fn(),
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: fn(),
|
||||
},
|
||||
},
|
||||
image: (
|
||||
<Image
|
||||
src="/male-portrait.jpg"
|
||||
alt="male portrait"
|
||||
width={164}
|
||||
height={245}
|
||||
className="mt-3.5 rounded-[50px] blur-sm"
|
||||
/>
|
||||
),
|
||||
privacyTermsConsentProps: {
|
||||
privacyPolicy: {
|
||||
children: "Privacy Policy",
|
||||
href: "#privacy-policy",
|
||||
},
|
||||
termsOfUse: {
|
||||
children: "Terms of use",
|
||||
href: "#terms-of-use",
|
||||
},
|
||||
},
|
||||
privacySecurityBannerProps: {
|
||||
text: {
|
||||
children:
|
||||
"Мы не передаем личную информацию, она остаётся в безопасности и под вашим контролем.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
textInputProps: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
<Email {...args} />
|
||||
</LayoutQuestion>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const FemalePortrait = {
|
||||
args: {
|
||||
image: (
|
||||
<Image
|
||||
src="/female-portrait.jpg"
|
||||
alt="female portrait"
|
||||
width={164}
|
||||
height={245}
|
||||
className="mt-3.5 rounded-[50px] blur-sm"
|
||||
/>
|
||||
),
|
||||
},
|
||||
} satisfies Story;
|
||||
126
src/components/templates/Email/Email.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
||||
import {
|
||||
BottomActionButton,
|
||||
BottomActionButtonProps,
|
||||
} from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import { useDynamicSize } from "@/hooks/DOM/useDynamicSize";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import PrivacyTermsConsent from "@/components/widgets/PrivacyTermsConsent/PrivacyTermsConsent";
|
||||
import PrivacySecurityBanner from "@/components/widgets/PrivacySecurityBanner/PrivacySecurityBanner";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.email({
|
||||
message: "Please enter a valid email address",
|
||||
}),
|
||||
});
|
||||
|
||||
interface EmailProps extends Omit<React.ComponentProps<"div">, "title"> {
|
||||
textInputProps: React.ComponentProps<typeof TextInput>;
|
||||
bottomActionButtonProps?: BottomActionButtonProps;
|
||||
image?: React.ReactNode;
|
||||
privacyTermsConsentProps?: React.ComponentProps<typeof PrivacyTermsConsent>;
|
||||
privacySecurityBannerProps?: React.ComponentProps<
|
||||
typeof PrivacySecurityBanner
|
||||
>;
|
||||
}
|
||||
|
||||
function Email({
|
||||
textInputProps,
|
||||
bottomActionButtonProps,
|
||||
image,
|
||||
privacyTermsConsentProps,
|
||||
privacySecurityBannerProps,
|
||||
...props
|
||||
}: EmailProps) {
|
||||
const {
|
||||
height: bottomActionButtonHeight,
|
||||
elementRef: bottomActionButtonRef,
|
||||
} = useDynamicSize<HTMLDivElement>({
|
||||
defaultHeight: 132,
|
||||
});
|
||||
const [isTouched, setIsTouched] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: String(textInputProps.value || ""),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("email", String(textInputProps.value || ""));
|
||||
}, [textInputProps.value, form]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
form.setValue("email", value);
|
||||
form.trigger("email");
|
||||
textInputProps.onChange?.(e);
|
||||
};
|
||||
|
||||
const isFormValid = form.formState.isValid && form.getValues("email");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full flex flex-col items-center gap-[26px]"
|
||||
{...props}
|
||||
style={{
|
||||
paddingBottom: `${bottomActionButtonHeight}px`,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="Enter your Email"
|
||||
type="email"
|
||||
{...textInputProps}
|
||||
onChange={handleChange}
|
||||
onBlur={() => {
|
||||
setIsTouched(true);
|
||||
form.trigger("email");
|
||||
}}
|
||||
aria-invalid={isTouched && !!form.formState.errors.email}
|
||||
aria-errormessage={
|
||||
isTouched ? form.formState.errors.email?.message : undefined
|
||||
}
|
||||
/>
|
||||
{image}
|
||||
{privacySecurityBannerProps && (
|
||||
<PrivacySecurityBanner
|
||||
{...privacySecurityBannerProps}
|
||||
className={cn(privacySecurityBannerProps.className, "mt-[26px]")}
|
||||
/>
|
||||
)}
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
actionButtonProps={{
|
||||
...bottomActionButtonProps.actionButtonProps,
|
||||
disabled: !isFormValid,
|
||||
}}
|
||||
className={cn(
|
||||
"max-w-[560px] z-10",
|
||||
bottomActionButtonProps.className
|
||||
)}
|
||||
ref={bottomActionButtonRef}
|
||||
childrenUnderButton={
|
||||
privacyTermsConsentProps && (
|
||||
<PrivacyTermsConsent
|
||||
{...privacyTermsConsentProps}
|
||||
className={cn(privacyTermsConsentProps.className, "mt-5")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Email };
|
||||
108
src/components/templates/Loaders/Loaders.stories.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Loaders } from "./Loaders";
|
||||
import { fn } from "storybook/test";
|
||||
import {
|
||||
LayoutQuestion,
|
||||
LayoutQuestionProps,
|
||||
} from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
|
||||
const layoutQuestionProps: Omit<LayoutQuestionProps, "children"> = {
|
||||
headerProps: {
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "Создаем портрет твоей второй половинки.",
|
||||
align: "center",
|
||||
},
|
||||
contentProps: {
|
||||
className: "pt-5",
|
||||
},
|
||||
childrenWrapperProps: {
|
||||
className: "mt-[57px]",
|
||||
},
|
||||
};
|
||||
|
||||
/** Reusable Loaders page Component */
|
||||
const meta: Meta<typeof Loaders> = {
|
||||
title: "Templates/Loaders",
|
||||
component: Loaders,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
circularProgressbarsListProps: {
|
||||
progressbarItems: [
|
||||
{
|
||||
processing: {
|
||||
title: { children: "Анализ твоих ответов" },
|
||||
text: {
|
||||
children: "Processing...",
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
title: { children: "Анализ твоих ответов" },
|
||||
text: {
|
||||
children: "Complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
processing: {
|
||||
title: { children: "Portrait of the Soulmate" },
|
||||
text: {
|
||||
children: "Processing...",
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
title: { children: "Portrait of the Soulmate" },
|
||||
text: {
|
||||
children: "Complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
processing: {
|
||||
title: { children: "Portrait of the Soulmate" },
|
||||
text: {
|
||||
children: "Processing...",
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
title: { children: "Connection Insights" },
|
||||
text: {
|
||||
children: "Complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
onAnimationEnd: fn(),
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
circularProgressbarsListProps: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
<Loaders {...args} />
|
||||
</LayoutQuestion>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
67
src/components/templates/Loaders/Loaders.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BottomActionButton,
|
||||
BottomActionButtonProps,
|
||||
} from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import CircularProgressbarsList from "@/components/widgets/CircularProgressbarsList/CircularProgressbarsList";
|
||||
import { useDynamicSize } from "@/hooks/DOM/useDynamicSize";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
interface LoadersProps extends Omit<React.ComponentProps<"div">, "title"> {
|
||||
circularProgressbarsListProps: React.ComponentProps<
|
||||
typeof CircularProgressbarsList
|
||||
>;
|
||||
bottomActionButtonProps?: BottomActionButtonProps;
|
||||
}
|
||||
|
||||
function Loaders({
|
||||
circularProgressbarsListProps,
|
||||
bottomActionButtonProps,
|
||||
...props
|
||||
}: LoadersProps) {
|
||||
const {
|
||||
height: bottomActionButtonHeight,
|
||||
elementRef: bottomActionButtonRef,
|
||||
} = useDynamicSize<HTMLDivElement>({
|
||||
defaultHeight: 132,
|
||||
});
|
||||
const [isVisibleButton, setIsVisibleButton] = useState(false);
|
||||
|
||||
const onAnimationEnd = () => {
|
||||
setIsVisibleButton(true);
|
||||
circularProgressbarsListProps.onAnimationEnd?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full flex flex-col items-center gap-[22px]"
|
||||
{...props}
|
||||
style={{
|
||||
paddingBottom: `${bottomActionButtonHeight}px`,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<CircularProgressbarsList
|
||||
{...circularProgressbarsListProps}
|
||||
transitionDurationItem={3000}
|
||||
onAnimationEnd={onAnimationEnd}
|
||||
/>
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
className={cn(
|
||||
"max-w-[560px] z-10 transition-opacity duration-300",
|
||||
!isVisibleButton && "opacity-0 pointer-events-none",
|
||||
isVisibleButton && "opacity-100 pointer-events-auto",
|
||||
bottomActionButtonProps.className
|
||||
)}
|
||||
ref={bottomActionButtonRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Loaders };
|
||||
@ -1,174 +0,0 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { Question } from "./Question";
|
||||
import { fn } from "storybook/test";
|
||||
import { useState } from "react";
|
||||
import { MainButtonProps } from "@/components/ui/MainButton/MainButton";
|
||||
import { SelectAnswersListProps } from "@/components/widgets/SelectAnswersList/SelectAnswersList";
|
||||
|
||||
/** Reusable Question page Component */
|
||||
const meta: Meta<typeof Question> = {
|
||||
title: "Templates/Question",
|
||||
component: Question,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
layoutQuestionProps: {
|
||||
headerProps: {
|
||||
progressProps: {
|
||||
value: (5 / 15) * 100,
|
||||
label: "5 of 15",
|
||||
className: "max-w-[198px]",
|
||||
},
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "Which best represents your hair loss and goals?",
|
||||
},
|
||||
subtitle: {
|
||||
children: "Let's personalize your hair care journey",
|
||||
},
|
||||
},
|
||||
contentType: "radio-answers-list",
|
||||
},
|
||||
argTypes: {
|
||||
contentType: {
|
||||
control: { type: "select" },
|
||||
options: ["radio-answers-list", "select-answers-list"],
|
||||
},
|
||||
content: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const RadioAnswers = {
|
||||
args: {
|
||||
contentType: "radio-answers-list",
|
||||
content: {
|
||||
answers: [
|
||||
{
|
||||
children: "FEMALE",
|
||||
emoji: "👩",
|
||||
id: "female",
|
||||
},
|
||||
{
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
isCheckbox: true,
|
||||
id: "male",
|
||||
},
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
id: "without-emoji",
|
||||
},
|
||||
],
|
||||
activeAnswer: {
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
isCheckbox: true,
|
||||
id: "male",
|
||||
},
|
||||
onAnswerClick: fn(),
|
||||
onChangeSelectedAnswer: fn(),
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const SelectAnswers = {
|
||||
args: {
|
||||
contentType: "select-answers-list",
|
||||
content: {
|
||||
answers: [
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
isCheckbox: true,
|
||||
id: "hairline",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, exploring",
|
||||
isCheckbox: true,
|
||||
id: "exploring",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
id: "ready-to-start-text",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
id: "ready-to-start-emoji",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start-emoji-checkbox",
|
||||
},
|
||||
],
|
||||
activeAnswers: [
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
id: "ready-to-start-emoji",
|
||||
},
|
||||
],
|
||||
onChangeSelectedAnswers: fn(),
|
||||
onAnswerClick: fn(),
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
const { layoutQuestionProps, content, ...rest } = args;
|
||||
const [selectedAnswers, setSelectedAnswers] = useState<
|
||||
MainButtonProps[] | null
|
||||
>((content as SelectAnswersListProps).activeAnswers);
|
||||
|
||||
const onActionButtonClick = () => {
|
||||
fn()(selectedAnswers);
|
||||
};
|
||||
|
||||
const layoutQuestionArgs = {
|
||||
...layoutQuestionProps,
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: onActionButtonClick,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const onChangeSelectedAnswers = (answers: MainButtonProps[] | null) => {
|
||||
setSelectedAnswers(answers);
|
||||
fn()(answers);
|
||||
};
|
||||
|
||||
const contentArgs = {
|
||||
...content,
|
||||
onChangeSelectedAnswers,
|
||||
};
|
||||
|
||||
return (
|
||||
<Question
|
||||
{...rest}
|
||||
layoutQuestionProps={layoutQuestionArgs}
|
||||
content={contentArgs}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} satisfies Story;
|
||||
@ -1,45 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RadioAnswersList,
|
||||
RadioAnswersListProps,
|
||||
} from "@/components/widgets/RadioAnswersList/RadioAnswersList";
|
||||
import {
|
||||
LayoutQuestion,
|
||||
LayoutQuestionProps,
|
||||
} from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
import {
|
||||
SelectAnswersList,
|
||||
SelectAnswersListProps,
|
||||
} from "@/components/widgets/SelectAnswersList/SelectAnswersList";
|
||||
|
||||
interface QuestionProps
|
||||
extends Omit<React.ComponentProps<"div">, "title" | "content"> {
|
||||
layoutQuestionProps: Omit<LayoutQuestionProps, "children">;
|
||||
content: RadioAnswersListProps | SelectAnswersListProps;
|
||||
contentType: "radio-answers-list" | "select-answers-list";
|
||||
}
|
||||
|
||||
function Question({
|
||||
layoutQuestionProps,
|
||||
content,
|
||||
contentType,
|
||||
...props
|
||||
}: QuestionProps) {
|
||||
return (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
{content && (
|
||||
<div className="w-full mt-[30px]" {...props}>
|
||||
{contentType === "radio-answers-list" && (
|
||||
<RadioAnswersList {...(content as RadioAnswersListProps)} />
|
||||
)}
|
||||
{contentType === "select-answers-list" && (
|
||||
<SelectAnswersList {...(content as SelectAnswersListProps)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</LayoutQuestion>
|
||||
);
|
||||
}
|
||||
|
||||
export { Question };
|
||||
@ -0,0 +1,127 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { QuestionDateAnswers } from "./QuestionDateAnswers";
|
||||
import { fn } from "storybook/test";
|
||||
import { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
|
||||
const layoutQuestionProps = {
|
||||
headerProps: {
|
||||
progressProps: {
|
||||
value: (5 / 15) * 100,
|
||||
label: "5 of 15",
|
||||
className: "max-w-[198px]",
|
||||
},
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "When is your birthday?",
|
||||
},
|
||||
subtitle: {
|
||||
children: "We need this information to personalize your experience",
|
||||
},
|
||||
};
|
||||
|
||||
/** Reusable QuestionDateAnswers page Component */
|
||||
const meta: Meta<typeof QuestionDateAnswers> = {
|
||||
title: "Templates/QuestionDateAnswers",
|
||||
component: QuestionDateAnswers,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
content: {
|
||||
value: null,
|
||||
onChange: fn(),
|
||||
maxYear: new Date().getFullYear() - 11,
|
||||
yearsRange: 100,
|
||||
locale: "en",
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
content: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
<QuestionDateAnswers {...args} />
|
||||
</LayoutQuestion>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const WithInitialValue = {
|
||||
args: {
|
||||
content: {
|
||||
value: "1990-05-15",
|
||||
onChange: fn(),
|
||||
maxYear: new Date().getFullYear() - 11,
|
||||
yearsRange: 100,
|
||||
locale: "en",
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithError = {
|
||||
args: {
|
||||
content: {
|
||||
value: "",
|
||||
onChange: fn(),
|
||||
maxYear: new Date().getFullYear() - 11,
|
||||
yearsRange: 100,
|
||||
locale: "en",
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithCustomLocale = {
|
||||
args: {
|
||||
content: {
|
||||
value: null,
|
||||
onChange: fn(),
|
||||
maxYear: new Date().getFullYear() - 11,
|
||||
yearsRange: 100,
|
||||
locale: "ru",
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithCustomYearRange = {
|
||||
args: {
|
||||
content: {
|
||||
value: null,
|
||||
onChange: fn(),
|
||||
maxYear: 2000,
|
||||
yearsRange: 50,
|
||||
locale: "en",
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithoutBottomButton = {
|
||||
args: {
|
||||
content: {
|
||||
value: null,
|
||||
onChange: fn(),
|
||||
maxYear: new Date().getFullYear() - 11,
|
||||
yearsRange: 100,
|
||||
locale: "en",
|
||||
},
|
||||
bottomActionButtonProps: undefined,
|
||||
},
|
||||
} satisfies Story;
|
||||
@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BottomActionButton,
|
||||
BottomActionButtonProps,
|
||||
} from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import DateInput, {
|
||||
DateInputProps,
|
||||
} from "@/components/widgets/DateInput/DateInput";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const formSchema = z.object({
|
||||
date: z.string().min(1, {
|
||||
message: "Please select a date",
|
||||
}),
|
||||
});
|
||||
|
||||
interface QuestionDateAnswersProps
|
||||
extends Omit<React.ComponentProps<"div">, "content"> {
|
||||
content: DateInputProps;
|
||||
bottomActionButtonProps?: BottomActionButtonProps;
|
||||
}
|
||||
|
||||
function QuestionDateAnswers({
|
||||
content,
|
||||
bottomActionButtonProps,
|
||||
...props
|
||||
}: QuestionDateAnswersProps) {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
date: content.value || "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("date", content.value || "");
|
||||
}, [content.value, form]);
|
||||
|
||||
const handleChange = (value: string | null) => {
|
||||
form.setValue("date", value || "");
|
||||
form.trigger("date");
|
||||
content.onChange?.(value);
|
||||
};
|
||||
|
||||
const isFormValid = form.formState.isValid && form.getValues("date");
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<DateInput
|
||||
{...content}
|
||||
onChange={handleChange}
|
||||
error={form.formState.errors.date?.message}
|
||||
/>
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
actionButtonProps={{
|
||||
...bottomActionButtonProps.actionButtonProps,
|
||||
disabled: !isFormValid,
|
||||
}}
|
||||
className={cn("max-w-[560px]", bottomActionButtonProps.className)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { QuestionDateAnswers };
|
||||
@ -0,0 +1,92 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { QuestionInformation } from "./QuestionInformation";
|
||||
import { fn } from "storybook/test";
|
||||
import { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
import Image from "next/image";
|
||||
|
||||
const layoutQuestionProps = {
|
||||
headerProps: {
|
||||
progressProps: {
|
||||
value: (3 / 15) * 100,
|
||||
label: "3 of 15",
|
||||
className: "max-w-[198px]",
|
||||
},
|
||||
onBack: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
/** Reusable QuestionInformation page Component */
|
||||
const meta: Meta<typeof QuestionInformation> = {
|
||||
title: "Templates/QuestionInformation",
|
||||
component: QuestionInformation,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
image: (
|
||||
<Image
|
||||
className=""
|
||||
src="/heart-in-fire.svg"
|
||||
alt="Information"
|
||||
width={251}
|
||||
height={167}
|
||||
/>
|
||||
),
|
||||
text: (
|
||||
<Typography as="p" size="2xl" className="leading-[40px]">
|
||||
По нашей статистике <b>51 % женщин Овнов</b> доверяются эмоциям. Но
|
||||
одной чувствительности мало. Мы покажем, какие качества второй половинки
|
||||
дадут тепло и уверенность, и изобразим её портрет.
|
||||
</Typography>
|
||||
),
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
image: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
text: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
<QuestionInformation {...args} />
|
||||
</LayoutQuestion>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const WithoutImage = {
|
||||
args: {
|
||||
image: undefined,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithoutText = {
|
||||
args: {
|
||||
text: undefined,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithoutBottomButton = {
|
||||
args: {
|
||||
bottomActionButtonProps: undefined,
|
||||
},
|
||||
} satisfies Story;
|
||||
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BottomActionButton,
|
||||
BottomActionButtonProps,
|
||||
} from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import { useDynamicSize } from "@/hooks/DOM/useDynamicSize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface QuestionInformationProps extends React.ComponentProps<"div"> {
|
||||
image?: React.ReactNode;
|
||||
text?: React.ReactNode;
|
||||
bottomActionButtonProps?: BottomActionButtonProps;
|
||||
}
|
||||
|
||||
function QuestionInformation({
|
||||
image,
|
||||
text,
|
||||
bottomActionButtonProps,
|
||||
...props
|
||||
}: QuestionInformationProps) {
|
||||
const {
|
||||
height: bottomActionButtonHeight,
|
||||
elementRef: bottomActionButtonRef,
|
||||
} = useDynamicSize<HTMLDivElement>({
|
||||
defaultHeight: 132,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full flex flex-col items-center gap-[22px]"
|
||||
{...props}
|
||||
style={{
|
||||
paddingBottom: `${bottomActionButtonHeight}px`,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
{image}
|
||||
{text}
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
className={cn("max-w-[560px]", bottomActionButtonProps.className)}
|
||||
ref={bottomActionButtonRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { QuestionInformation };
|
||||
@ -0,0 +1,75 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { QuestionRadioAnswers } from "./QuestionRadioAnswers";
|
||||
import { fn } from "storybook/test";
|
||||
import { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
|
||||
const layoutQuestionProps = {
|
||||
headerProps: {
|
||||
progressProps: {
|
||||
value: (5 / 15) * 100,
|
||||
label: "5 of 15",
|
||||
className: "max-w-[198px]",
|
||||
},
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "Which best represents your hair loss and goals?",
|
||||
},
|
||||
subtitle: {
|
||||
children: "Let's personalize your hair care journey",
|
||||
},
|
||||
};
|
||||
|
||||
/** Reusable QuestionRadioAnswers page Component */
|
||||
const meta: Meta<typeof QuestionRadioAnswers> = {
|
||||
title: "Templates/QuestionRadioAnswers",
|
||||
component: QuestionRadioAnswers,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
content: {
|
||||
answers: [
|
||||
{
|
||||
children: "FEMALE",
|
||||
emoji: "👩",
|
||||
id: "female",
|
||||
},
|
||||
{
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
isCheckbox: true,
|
||||
id: "male",
|
||||
},
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
id: "without-emoji",
|
||||
},
|
||||
],
|
||||
activeAnswer: {
|
||||
children: "MALE",
|
||||
emoji: "👨",
|
||||
isCheckbox: true,
|
||||
id: "male",
|
||||
},
|
||||
onAnswerClick: fn(),
|
||||
onChangeSelectedAnswer: fn(),
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
content: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
render: (args) => (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
<QuestionRadioAnswers {...args} />
|
||||
</LayoutQuestion>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RadioAnswersList,
|
||||
RadioAnswersListProps,
|
||||
} from "@/components/widgets/RadioAnswersList/RadioAnswersList";
|
||||
|
||||
interface QuestionRadioAnswersProps
|
||||
extends Omit<React.ComponentProps<"div">, "content"> {
|
||||
content: RadioAnswersListProps;
|
||||
}
|
||||
|
||||
function QuestionRadioAnswers({
|
||||
content,
|
||||
...props
|
||||
}: QuestionRadioAnswersProps) {
|
||||
return (
|
||||
<div {...props}>
|
||||
<RadioAnswersList {...content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { QuestionRadioAnswers };
|
||||
@ -0,0 +1,104 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { QuestionSelectAnswers } from "./QuestionSelectAnswers";
|
||||
import { fn } from "storybook/test";
|
||||
import { LayoutQuestion } from "@/components/layout/LayoutQuestion/LayoutQuestion";
|
||||
|
||||
const layoutQuestionProps = {
|
||||
headerProps: {
|
||||
progressProps: {
|
||||
value: (5 / 15) * 100,
|
||||
label: "5 of 15",
|
||||
className: "max-w-[198px]",
|
||||
},
|
||||
onBack: fn(),
|
||||
},
|
||||
title: {
|
||||
children: "Which best represents your hair loss and goals?",
|
||||
},
|
||||
subtitle: {
|
||||
children: "Let's personalize your hair care journey",
|
||||
},
|
||||
};
|
||||
|
||||
/** Reusable QuestionSelectAnswers page Component */
|
||||
const meta: Meta<typeof QuestionSelectAnswers> = {
|
||||
title: "Templates/QuestionSelectAnswers",
|
||||
component: QuestionSelectAnswers,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
content: {
|
||||
answers: [
|
||||
{
|
||||
children: "Receding hairline, want to slow its progress",
|
||||
isCheckbox: true,
|
||||
id: "hairline",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, exploring",
|
||||
isCheckbox: true,
|
||||
id: "exploring",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
id: "ready-to-start-text",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
id: "ready-to-start-emoji",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start-emoji-checkbox",
|
||||
},
|
||||
],
|
||||
activeAnswers: [
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
isCheckbox: true,
|
||||
id: "ready-to-start",
|
||||
},
|
||||
{
|
||||
children: "Experiencing hair loss, ready to start",
|
||||
emoji: "👩🏼",
|
||||
id: "ready-to-start-emoji",
|
||||
},
|
||||
],
|
||||
onChangeSelectedAnswers: fn(),
|
||||
onAnswerClick: fn(),
|
||||
},
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
content: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
return (
|
||||
<LayoutQuestion {...layoutQuestionProps}>
|
||||
<QuestionSelectAnswers {...args} />
|
||||
</LayoutQuestion>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { MainButtonProps } from "@/components/ui/MainButton/MainButton";
|
||||
import {
|
||||
BottomActionButton,
|
||||
BottomActionButtonProps,
|
||||
} from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import {
|
||||
SelectAnswersList,
|
||||
SelectAnswersListProps,
|
||||
} from "@/components/widgets/SelectAnswersList/SelectAnswersList";
|
||||
import { useDynamicSize } from "@/hooks/DOM/useDynamicSize";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
interface QuestionSelectAnswersProps
|
||||
extends Omit<React.ComponentProps<"div">, "content"> {
|
||||
content: SelectAnswersListProps;
|
||||
bottomActionButtonProps?: BottomActionButtonProps;
|
||||
}
|
||||
|
||||
function QuestionSelectAnswers({
|
||||
content,
|
||||
bottomActionButtonProps,
|
||||
...props
|
||||
}: QuestionSelectAnswersProps) {
|
||||
const {
|
||||
height: bottomActionButtonHeight,
|
||||
elementRef: bottomActionButtonRef,
|
||||
} = useDynamicSize<HTMLDivElement>({
|
||||
defaultHeight: 132,
|
||||
});
|
||||
const [selectedAnswers, setSelectedAnswers] = useState<
|
||||
MainButtonProps[] | null
|
||||
>(content.activeAnswers);
|
||||
|
||||
const handleChangeSelectedAnswers = (answers: MainButtonProps[] | null) => {
|
||||
setSelectedAnswers(answers);
|
||||
content.onChangeSelectedAnswers?.(answers);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
{...props}
|
||||
style={{
|
||||
paddingBottom: `${bottomActionButtonHeight}px`,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<SelectAnswersList
|
||||
{...content}
|
||||
onChangeSelectedAnswers={handleChangeSelectedAnswers}
|
||||
/>
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
className={cn("max-w-[560px]", bottomActionButtonProps.className)}
|
||||
ref={bottomActionButtonRef}
|
||||
actionButtonProps={{
|
||||
...bottomActionButtonProps.actionButtonProps,
|
||||
disabled: selectedAnswers?.length === 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { QuestionSelectAnswers };
|
||||
@ -0,0 +1,40 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import SoulmatePortrait from "./SoulmatePortrait";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable SoulmatePortrait page Component */
|
||||
const meta: Meta<typeof SoulmatePortrait> = {
|
||||
title: "Templates/SoulmatePortrait",
|
||||
component: SoulmatePortrait,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
args: {
|
||||
bottomActionButtonProps: {
|
||||
actionButtonProps: {
|
||||
children: "Continue",
|
||||
onClick: fn(),
|
||||
},
|
||||
},
|
||||
privacyTermsConsentProps: {
|
||||
privacyPolicy: {
|
||||
children: "Privacy Policy",
|
||||
href: "#privacy-policy",
|
||||
},
|
||||
termsOfUse: {
|
||||
children: "Terms of use",
|
||||
href: "#terms-of-use",
|
||||
},
|
||||
},
|
||||
title: {
|
||||
children: "Soulmate Portrait",
|
||||
},
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,69 @@
|
||||
import Typography, {
|
||||
TypographyProps,
|
||||
} from "@/components/ui/Typography/Typography";
|
||||
import { BottomActionButton } from "@/components/widgets/BottomActionButton/BottomActionButton";
|
||||
import PrivacyTermsConsent from "@/components/widgets/PrivacyTermsConsent/PrivacyTermsConsent";
|
||||
import { useDynamicSize } from "@/hooks/DOM/useDynamicSize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface SoulmatePortraitProps
|
||||
extends Omit<React.ComponentProps<"div">, "title"> {
|
||||
bottomActionButtonProps?: React.ComponentProps<typeof BottomActionButton>;
|
||||
privacyTermsConsentProps?: React.ComponentProps<typeof PrivacyTermsConsent>;
|
||||
title?: TypographyProps<"h2">;
|
||||
}
|
||||
|
||||
export default function SoulmatePortrait({
|
||||
bottomActionButtonProps,
|
||||
privacyTermsConsentProps,
|
||||
title,
|
||||
...props
|
||||
}: SoulmatePortraitProps) {
|
||||
const {
|
||||
height: bottomActionButtonHeight,
|
||||
elementRef: bottomActionButtonRef,
|
||||
} = useDynamicSize<HTMLDivElement>({
|
||||
defaultHeight: 132,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full flex flex-col items-center gap-[30px]"
|
||||
{...props}
|
||||
style={{
|
||||
paddingBottom: `${bottomActionButtonHeight}px`,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<div className="w-full pt-6 pb-3">
|
||||
{title && (
|
||||
<Typography
|
||||
as="h2"
|
||||
size="xl"
|
||||
weight="bold"
|
||||
{...title}
|
||||
className={cn(title.className, "leading-[125%] text-primary")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{bottomActionButtonProps && (
|
||||
<BottomActionButton
|
||||
{...bottomActionButtonProps}
|
||||
className={cn(
|
||||
"max-w-[560px] z-10",
|
||||
bottomActionButtonProps.className
|
||||
)}
|
||||
ref={bottomActionButtonRef}
|
||||
childrenUnderButton={
|
||||
privacyTermsConsentProps && (
|
||||
<PrivacyTermsConsent
|
||||
{...privacyTermsConsentProps}
|
||||
className={cn(privacyTermsConsentProps.className, "mt-5")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
src/components/ui/SelectInput/SelectInput.stories.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import SelectInput from "./SelectInput";
|
||||
import { fn } from "storybook/test";
|
||||
import { useState } from "react";
|
||||
import { useDateInput } from "@/hooks/useDateInput";
|
||||
import Typography from "../Typography/Typography";
|
||||
|
||||
/** Reusable SelectInput Component */
|
||||
const meta: Meta<typeof SelectInput> = {
|
||||
title: "UI/SelectInput",
|
||||
component: SelectInput,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
defaultValue: "01",
|
||||
onValueChange: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
value: {
|
||||
control: { type: "text" },
|
||||
},
|
||||
options: {
|
||||
control: { type: "object" },
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
const { dayOptions } = useDateInput({});
|
||||
const [value, setValue] = useState(args.defaultValue);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography>Value: {value}</Typography>
|
||||
<SelectInput
|
||||
{...args}
|
||||
value={value}
|
||||
onValueChange={(value: string) => {
|
||||
setValue(value);
|
||||
args.onValueChange?.(value);
|
||||
}}
|
||||
options={dayOptions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithLabel = {
|
||||
args: {
|
||||
label: "Month",
|
||||
placeholder: "MM",
|
||||
defaultValue: undefined,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithPlaceholder = {
|
||||
args: {
|
||||
placeholder: "Placeholder",
|
||||
defaultValue: undefined,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithError = {
|
||||
args: {
|
||||
placeholder: "With Error",
|
||||
// "aria-invalid": true,
|
||||
error: true,
|
||||
errorProps: {
|
||||
children: "Error",
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
99
src/components/ui/SelectInput/SelectInput.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useId } from "react";
|
||||
import Typography from "../Typography/Typography";
|
||||
import { Label } from "../label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../select";
|
||||
|
||||
type Option = {
|
||||
value: string | number;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export interface SelectInputProps extends React.ComponentProps<typeof Select> {
|
||||
error?: boolean;
|
||||
options: Option[];
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
labelProps?: React.ComponentProps<typeof Label>;
|
||||
triggerProps?: React.ComponentProps<typeof SelectTrigger>;
|
||||
contentProps?: React.ComponentProps<typeof SelectContent>;
|
||||
itemProps?: React.ComponentProps<typeof SelectItem>;
|
||||
errorProps?: React.ComponentProps<typeof Typography>;
|
||||
}
|
||||
|
||||
export default function SelectInput({
|
||||
error,
|
||||
options,
|
||||
placeholder,
|
||||
label,
|
||||
labelProps,
|
||||
triggerProps,
|
||||
contentProps,
|
||||
itemProps,
|
||||
errorProps,
|
||||
...props
|
||||
}: SelectInputProps) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className={cn("w-full flex flex-col gap-2")}>
|
||||
{label && (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
"text-muted-foreground font-inter font-medium text-base",
|
||||
labelProps?.className
|
||||
)}
|
||||
{...labelProps}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<Select {...props}>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
"w-full min-w-[106px] h-fit! min-h-14",
|
||||
"px-4 py-3.5",
|
||||
"font-inter text-[18px]/[28px] font-medium text-foreground",
|
||||
"data-[placeholder]:text-placeholder-foreground",
|
||||
"rounded-2xl outline-2 outline-primary/30",
|
||||
"duration-200",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
// placeholder && !props.value && "text-placeholder-foreground",
|
||||
error &&
|
||||
"outline-destructive focus-visible:shadow-destructive focus-visible:ring-destructive/30",
|
||||
triggerProps?.className
|
||||
)}
|
||||
{...triggerProps}
|
||||
>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent {...contentProps}>
|
||||
{options.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value.toString()}
|
||||
{...itemProps}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{error && (
|
||||
<Typography size="xs" color="destructive" {...errorProps}>
|
||||
{errorProps?.children}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -5,14 +5,23 @@ import { useId } from "react";
|
||||
|
||||
interface TextInputProps extends React.ComponentProps<typeof Input> {
|
||||
label?: string;
|
||||
containerProps?: React.ComponentProps<"div">;
|
||||
}
|
||||
|
||||
function TextInput({ className, label, ...props }: TextInputProps) {
|
||||
function TextInput({
|
||||
className,
|
||||
label,
|
||||
containerProps,
|
||||
...props
|
||||
}: TextInputProps) {
|
||||
const id = useId();
|
||||
const inputId = props.id || id;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div
|
||||
{...containerProps}
|
||||
className={cn("w-full flex flex-col gap-2", containerProps?.className)}
|
||||
>
|
||||
{label && (
|
||||
<Label
|
||||
htmlFor={inputId}
|
||||
|
||||
@ -22,7 +22,7 @@ const buttonVariants = cva(
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
link: "inline-block underline underline-offset-2 p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
92
src/components/ui/card.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
357
src/components/ui/chart.tsx
Normal file
@ -0,0 +1,357 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
}) {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
202
src/components/ui/select.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
isIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default";
|
||||
isIcon?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"data-[placeholder]:text-muted-foreground",
|
||||
"[&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] ",
|
||||
"aria-invalid:ring-destructive/20",
|
||||
"dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50",
|
||||
"flex w-fit items-center justify-between gap-2",
|
||||
"rounded-md",
|
||||
"bg-transparent",
|
||||
"px-3 py-2",
|
||||
"text-sm whitespace-nowrap shadow-xs",
|
||||
"transition-[color,box-shadow]",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
|
||||
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{isIcon && (
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
)}
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@ -1,36 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { forwardRef } from "react";
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
||||
|
||||
import { GradientBlur } from "../GradientBlur/GradientBlur";
|
||||
import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
|
||||
|
||||
export interface BottomActionButtonProps extends React.ComponentProps<"div"> {
|
||||
actionButtonProps?: React.ComponentProps<typeof ActionButton>;
|
||||
/** Контент над кнопкой (например подсказка) */
|
||||
childrenAboveButton?: React.ReactNode;
|
||||
/** Контент под кнопкой (например дисклеймер) */
|
||||
childrenUnderButton?: React.ReactNode;
|
||||
/** Управление блюром подложки */
|
||||
showGradientBlur?: boolean;
|
||||
/** Синхронизировать CSS-переменную --bottom-action-button-height на <html> */
|
||||
syncCssVar?: boolean;
|
||||
}
|
||||
|
||||
const BottomActionButton = forwardRef<HTMLDivElement, BottomActionButtonProps>(
|
||||
function BottomActionButton(
|
||||
{ actionButtonProps, showGradientBlur = true, className, ...props },
|
||||
{
|
||||
actionButtonProps,
|
||||
childrenAboveButton,
|
||||
childrenUnderButton,
|
||||
showGradientBlur = true,
|
||||
className,
|
||||
syncCssVar = true,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
useImperativeHandle(ref, () => innerRef.current as HTMLDivElement, []);
|
||||
|
||||
const hasButton = Boolean(actionButtonProps);
|
||||
const hasExtra =
|
||||
Boolean(childrenAboveButton) || Boolean(childrenUnderButton);
|
||||
const hasContent = hasButton || hasExtra;
|
||||
|
||||
// Ничего не рендерим, если нет контента
|
||||
if (!hasContent) return null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!syncCssVar || typeof window === "undefined") return;
|
||||
const el = innerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const setVar = () => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--bottom-action-button-height",
|
||||
`${el.offsetHeight}px`
|
||||
);
|
||||
};
|
||||
|
||||
setVar();
|
||||
|
||||
if ("ResizeObserver" in window) {
|
||||
const ro = new ResizeObserver(setVar);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
} else {
|
||||
const onResize = () => setVar();
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}
|
||||
}, [syncCssVar]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
ref={innerRef}
|
||||
className={cn(
|
||||
"fixed bottom-0 left-[50%] translate-x-[-50%] w-full",
|
||||
"fixed bottom-0 left-1/2 -translate-x-1/2 w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<GradientBlur className="p-6 pt-11" isActiveBlur={showGradientBlur}>
|
||||
{actionButtonProps ? <ActionButton {...actionButtonProps} /> : null}
|
||||
{childrenAboveButton}
|
||||
{hasButton ? <ActionButton {...actionButtonProps} /> : null}
|
||||
{childrenUnderButton}
|
||||
</GradientBlur>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { BottomActionButton };
|
||||
export { BottomActionButton };
|
||||
@ -0,0 +1,18 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import CircularProgressbar from "./CircularProgressbar";
|
||||
|
||||
/** Reusable CircularProgressbar Component */
|
||||
const meta: Meta<typeof CircularProgressbar> = {
|
||||
title: "Widgets/CircularProgressbar",
|
||||
component: CircularProgressbar,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
value: 75,
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import Typography, {
|
||||
TypographyProps,
|
||||
} from "@/components/ui/Typography/Typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMemo } from "react";
|
||||
import { CircularProgressbar as CircularProgressbarComponent } from "react-circular-progressbar";
|
||||
|
||||
export interface CircularProgressbarProps extends React.ComponentProps<"div"> {
|
||||
pathColor?: string;
|
||||
trailColor?: string;
|
||||
strokeWidth?: number;
|
||||
value?: number;
|
||||
maxValue?: number;
|
||||
minValue?: number;
|
||||
transitionDuration?: string;
|
||||
transitionDelay?: string;
|
||||
text?: TypographyProps<"span">;
|
||||
isShowText?: boolean;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function CircularProgressbar({
|
||||
pathColor = "var(--primary)",
|
||||
trailColor = "var(--chart-secondary)",
|
||||
strokeWidth = 13,
|
||||
value = 50,
|
||||
maxValue = 100,
|
||||
minValue = 0,
|
||||
transitionDuration = "0.5s",
|
||||
transitionDelay = "0s",
|
||||
text,
|
||||
children,
|
||||
isShowText = true,
|
||||
size = 60,
|
||||
...props
|
||||
}: CircularProgressbarProps) {
|
||||
const textValue = useMemo(() => {
|
||||
return `${value.toFixed(0)}%`;
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(`relative`, props.className)}
|
||||
style={{ width: size, height: size, ...props.style }}
|
||||
>
|
||||
<CircularProgressbarComponent
|
||||
styles={{
|
||||
path: {
|
||||
stroke: pathColor,
|
||||
strokeLinecap: "round",
|
||||
transitionDuration: transitionDuration,
|
||||
transitionDelay: transitionDelay,
|
||||
},
|
||||
trail: {
|
||||
stroke: trailColor,
|
||||
},
|
||||
}}
|
||||
// className="path:stroke-primary trail:stroke-muted"
|
||||
maxValue={maxValue}
|
||||
minValue={minValue}
|
||||
strokeWidth={strokeWidth}
|
||||
value={value}
|
||||
/>
|
||||
{(text || isShowText) && (
|
||||
<Typography
|
||||
as="span"
|
||||
size="sm"
|
||||
weight="semiBold"
|
||||
color="muted"
|
||||
{...text}
|
||||
className={cn(
|
||||
text?.className,
|
||||
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||
children && "hidden"
|
||||
)}
|
||||
>
|
||||
{text?.children || textValue}
|
||||
</Typography>
|
||||
)}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
.divider {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--primary) 0%,
|
||||
var(--primary) 33.33%,
|
||||
var(--chart-secondary) 66.66%,
|
||||
var(--chart-secondary) 100%
|
||||
);
|
||||
background-size: 300% 100%;
|
||||
background-position: 100% 0;
|
||||
animation: divider 1s linear forwards;
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
@keyframes divider {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import CircularProgressbarsList from "./CircularProgressbarsList";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
/** Reusable CircularProgressbarsList Component */
|
||||
const meta: Meta<typeof CircularProgressbarsList> = {
|
||||
title: "Widgets/CircularProgressbarsList",
|
||||
component: CircularProgressbarsList,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
progressbarItems: [
|
||||
{
|
||||
processing: {
|
||||
title: { children: "Анализ твоих ответов" },
|
||||
text: {
|
||||
children: "Processing...",
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
title: { children: "Анализ твоих ответов" },
|
||||
text: {
|
||||
children: "Complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
processing: {
|
||||
title: { children: "Portrait of the Soulmate" },
|
||||
text: {
|
||||
children: "Processing...",
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
title: { children: "Portrait of the Soulmate" },
|
||||
text: {
|
||||
children: "Complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
processing: {
|
||||
title: { children: "Portrait of the Soulmate" },
|
||||
text: {
|
||||
children: "Processing...",
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
title: { children: "Connection Insights" },
|
||||
text: {
|
||||
children: "Complete",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
onAnimationEnd: fn(),
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,169 @@
|
||||
import Typography, {
|
||||
TypographyProps,
|
||||
} from "@/components/ui/Typography/Typography";
|
||||
import CircularProgressbar, {
|
||||
CircularProgressbarProps,
|
||||
} from "../CircularProgressbar/CircularProgressbar";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import styles from "./CircularProgressbarsList.module.css";
|
||||
|
||||
const getItemProgress = (index: number, progress: number) => {
|
||||
const integerDivision = Math.floor(progress / 100);
|
||||
if (integerDivision > index) {
|
||||
return 100;
|
||||
}
|
||||
if (integerDivision === index) {
|
||||
return progress % 100;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
interface ProgressbarItem {
|
||||
circularProgressbarProps?: Omit<
|
||||
CircularProgressbarProps,
|
||||
"maxValue" | "minValue" | "value" | "size"
|
||||
>;
|
||||
processing?: {
|
||||
title?: TypographyProps<"p">;
|
||||
text?: TypographyProps<"span">;
|
||||
};
|
||||
completed?: {
|
||||
title?: TypographyProps<"p">;
|
||||
text?: TypographyProps<"span">;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CircularProgressbarsListProps
|
||||
extends React.ComponentProps<"div"> {
|
||||
progressbarItems: ProgressbarItem[];
|
||||
transitionDurationItem?: number; // in milliseconds
|
||||
animationDurationDivider?: number; // in milliseconds
|
||||
onAnimationEnd?: () => void;
|
||||
}
|
||||
|
||||
export default function CircularProgressbarsList({
|
||||
progressbarItems,
|
||||
transitionDurationItem = 5_000,
|
||||
animationDurationDivider = 1000,
|
||||
onAnimationEnd,
|
||||
...props
|
||||
}: CircularProgressbarsListProps) {
|
||||
const id = useId();
|
||||
const progressbarItemId = `${id}-progressbar-item`;
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let delay = transitionDurationItem / 100;
|
||||
if (progress && progress % 100 === 0) {
|
||||
delay = animationDurationDivider;
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
if (progress < progressbarItems.length * 100) {
|
||||
setProgress((prev) => prev + 1);
|
||||
} else {
|
||||
onAnimationEnd?.();
|
||||
}
|
||||
}, delay);
|
||||
return () => clearTimeout(timeout);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progress]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"w-full grid gap-1.5 items-start justify-items-center",
|
||||
props.className
|
||||
)}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${progressbarItems.length * 2 - 1}, 1fr)`,
|
||||
maxWidth: `${progressbarItems.length * 120}px`,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: progressbarItems.length * 2 - 1 }).map(
|
||||
(_, index) => {
|
||||
if (index % 2 === 0) {
|
||||
const progressbarItem = progressbarItems[index / 2];
|
||||
const itemProgress = getItemProgress(index / 2, progress);
|
||||
const isItemCompleted = itemProgress >= 100;
|
||||
const itemState = isItemCompleted ? "completed" : "processing";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${progressbarItemId}-${index}`}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<CircularProgressbar
|
||||
{...progressbarItem.circularProgressbarProps}
|
||||
transitionDuration={`${transitionDurationItem / 100}ms`}
|
||||
// transitionDelay={`${
|
||||
// ((transitionDurationItem + animationDurationDivider) *
|
||||
// index) /
|
||||
// 2
|
||||
// }ms`}
|
||||
value={itemProgress}
|
||||
size={60}
|
||||
// text={{
|
||||
// children: `${itemProgress}%`,
|
||||
// }}
|
||||
>
|
||||
{isItemCompleted && (
|
||||
<CheckIcon className="size-4" color="var(--primary)" />
|
||||
)}
|
||||
</CircularProgressbar>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 w-[110px] mt-2",
|
||||
"max-[415px]:w-[80px]"
|
||||
)}
|
||||
>
|
||||
{progressbarItem[itemState]?.title && (
|
||||
<Typography
|
||||
as="p"
|
||||
weight="semiBold"
|
||||
size="sm"
|
||||
{...progressbarItem[itemState]?.title}
|
||||
/>
|
||||
)}
|
||||
{progressbarItem[itemState]?.text && (
|
||||
<Typography
|
||||
size="xs"
|
||||
color="muted"
|
||||
{...progressbarItem[itemState]?.text}
|
||||
className={cn(
|
||||
"mt-[-4px]",
|
||||
progressbarItem[itemState]?.text.className
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${progressbarItemId}-divider-${index}`}
|
||||
className={cn(
|
||||
"w-full h-[2px] mt-[31px] mx-[-25px]",
|
||||
"max-[415px]:mx-[-35px] max-w-[40px]",
|
||||
styles.divider
|
||||
)}
|
||||
style={{
|
||||
animationDuration: `${animationDurationDivider}ms`,
|
||||
animationDelay: `${
|
||||
((transitionDurationItem + animationDurationDivider) *
|
||||
(index + 1)) /
|
||||
2 -
|
||||
animationDurationDivider
|
||||
}ms`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -10,12 +10,12 @@ import { Files } from "lucide-react";
|
||||
interface CouponProps extends Omit<React.ComponentProps<"div">, "title"> {
|
||||
title: TypographyProps<"h3">;
|
||||
offer: {
|
||||
title: TypographyProps<"h3">;
|
||||
description: TypographyProps<"p">;
|
||||
title?: TypographyProps<"h3">;
|
||||
description?: TypographyProps<"p">;
|
||||
};
|
||||
promoCode: TypographyProps<"span">;
|
||||
footer: TypographyProps<"p">;
|
||||
onCopyPromoCode: (code: string) => void;
|
||||
promoCode?: TypographyProps<"span">;
|
||||
footer?: TypographyProps<"p">;
|
||||
onCopyPromoCode?: (code: string) => void;
|
||||
}
|
||||
|
||||
function Coupon({
|
||||
@ -55,14 +55,16 @@ function Coupon({
|
||||
fill="#FCD34D"
|
||||
/>
|
||||
</svg>
|
||||
<Typography
|
||||
as="h3"
|
||||
size="xl"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
{...title}
|
||||
className={cn(title.className, "leading-[140%]")}
|
||||
/>
|
||||
{title && (
|
||||
<Typography
|
||||
as="h3"
|
||||
size="xl"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
{...title}
|
||||
className={cn(title.className, "leading-[140%]")}
|
||||
/>
|
||||
)}
|
||||
{offer && (
|
||||
<div
|
||||
className={cn(
|
||||
@ -72,24 +74,28 @@ function Coupon({
|
||||
"px-5 py-4 mt-3.5"
|
||||
)}
|
||||
>
|
||||
<Typography
|
||||
as="h3"
|
||||
size="4xl"
|
||||
weight="black"
|
||||
color="card"
|
||||
{...offer.title}
|
||||
/>
|
||||
<Typography
|
||||
as="p"
|
||||
weight="semiBold"
|
||||
color="card"
|
||||
{...offer.description}
|
||||
className={cn(
|
||||
"text-[17px] leading-[100%]",
|
||||
"mt-2",
|
||||
offer.description.className
|
||||
)}
|
||||
/>
|
||||
{offer.title && (
|
||||
<Typography
|
||||
as="h3"
|
||||
size="4xl"
|
||||
weight="black"
|
||||
color="card"
|
||||
{...offer.title}
|
||||
/>
|
||||
)}
|
||||
{offer.description && (
|
||||
<Typography
|
||||
as="p"
|
||||
weight="semiBold"
|
||||
color="card"
|
||||
{...offer.description}
|
||||
className={cn(
|
||||
"text-[17px] leading-[100%]",
|
||||
"mt-2",
|
||||
offer.description.className
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@ -107,7 +113,7 @@ function Coupon({
|
||||
{promoCode && (
|
||||
<div
|
||||
className="w-full flex items-center justify-center gap-2 mt2 cursor-pointer"
|
||||
onClick={() => onCopyPromoCode(promoCode.children as string)}
|
||||
onClick={() => onCopyPromoCode?.(promoCode.children as string)}
|
||||
>
|
||||
<Typography
|
||||
size="lg"
|
||||
|
||||
120
src/components/widgets/DateInput/DateInput.stories.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { useState } from "react";
|
||||
import DateInput from "./DateInput";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
|
||||
/** Reusable DateInput Component */
|
||||
const meta: Meta<typeof DateInput> = {
|
||||
title: "Widgets/DateInput",
|
||||
component: DateInput,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
value: null,
|
||||
onChange: fn(),
|
||||
// onBlur: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
value: {
|
||||
control: { type: "text" },
|
||||
description: "Значение даты в формате YYYY-MM-DD",
|
||||
},
|
||||
maxYear: {
|
||||
control: { type: "number" },
|
||||
description: "Максимальный год для выбора",
|
||||
},
|
||||
yearsRange: {
|
||||
control: { type: "number" },
|
||||
description: "Диапазон лет для выбора",
|
||||
},
|
||||
locale: {
|
||||
control: { type: "select" },
|
||||
options: ["en", "ru", "de", "fr"],
|
||||
description: "Локаль для отображения месяцев",
|
||||
},
|
||||
error: {
|
||||
control: { type: "text" },
|
||||
description: "Сообщение об ошибке",
|
||||
},
|
||||
},
|
||||
render: (args) => {
|
||||
const [value, setValue] = useState<string | null>(args.value);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Typography>Выбранная дата: {value || "Не выбрана"}</Typography>
|
||||
<DateInput
|
||||
{...args}
|
||||
// value={args.value}
|
||||
onChange={(newValue) => {
|
||||
setValue(newValue);
|
||||
args.onChange?.(newValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
|
||||
export const WithValue = {
|
||||
args: {
|
||||
value: "1990-05-15",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithError = {
|
||||
args: {
|
||||
value: "2025-01-01",
|
||||
error: "Дата рождения не может быть в будущем",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const RussianLocale = {
|
||||
args: {
|
||||
value: null,
|
||||
locale: "ru",
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const CustomYearRange = {
|
||||
args: {
|
||||
value: null,
|
||||
maxYear: 2000,
|
||||
yearsRange: 50,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const WithCustomPlaceholders = {
|
||||
args: {
|
||||
value: null,
|
||||
daySelectProps: {
|
||||
placeholder: "День",
|
||||
},
|
||||
monthSelectProps: {
|
||||
placeholder: "Месяц",
|
||||
},
|
||||
yearSelectProps: {
|
||||
placeholder: "Год",
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Disabled = {
|
||||
args: {
|
||||
value: "1990-05-15",
|
||||
daySelectProps: {
|
||||
disabled: true,
|
||||
},
|
||||
monthSelectProps: {
|
||||
disabled: true,
|
||||
},
|
||||
yearSelectProps: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
} satisfies Story;
|
||||
104
src/components/widgets/DateInput/DateInput.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import SelectInput, {
|
||||
SelectInputProps,
|
||||
} from "@/components/ui/SelectInput/SelectInput";
|
||||
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
import { useDateInput } from "@/hooks/useDateInput";
|
||||
|
||||
type LocalSelectInputProps = Omit<
|
||||
SelectInputProps,
|
||||
"value" | "onValueChange" | "options"
|
||||
>;
|
||||
|
||||
export interface DateInputProps {
|
||||
value: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
error?: string;
|
||||
maxYear?: number;
|
||||
yearsRange?: number;
|
||||
// onBlur?: () => void;
|
||||
locale?: string;
|
||||
daySelectProps?: LocalSelectInputProps;
|
||||
monthSelectProps?: LocalSelectInputProps;
|
||||
yearSelectProps?: LocalSelectInputProps;
|
||||
}
|
||||
|
||||
export default function DateInput({
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
maxYear = new Date().getFullYear() - 11,
|
||||
yearsRange = 100,
|
||||
// onBlur,
|
||||
locale = "en",
|
||||
daySelectProps,
|
||||
monthSelectProps,
|
||||
yearSelectProps,
|
||||
}: DateInputProps) {
|
||||
const {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
yearOptions,
|
||||
monthOptions,
|
||||
dayOptions,
|
||||
localeFormat,
|
||||
handleYearChange,
|
||||
handleMonthChange,
|
||||
handleDayChange,
|
||||
} = useDateInput({
|
||||
value,
|
||||
onChange,
|
||||
maxYear,
|
||||
yearsRange,
|
||||
locale,
|
||||
});
|
||||
|
||||
const inputs = {
|
||||
d: (
|
||||
<SelectInput
|
||||
key="d"
|
||||
value={day}
|
||||
onValueChange={handleDayChange}
|
||||
options={dayOptions}
|
||||
placeholder="DD"
|
||||
{...daySelectProps}
|
||||
/>
|
||||
),
|
||||
m: (
|
||||
<SelectInput
|
||||
key="m"
|
||||
value={month}
|
||||
onValueChange={handleMonthChange}
|
||||
options={monthOptions}
|
||||
placeholder="MM"
|
||||
{...monthSelectProps}
|
||||
/>
|
||||
),
|
||||
y: (
|
||||
<SelectInput
|
||||
key="y"
|
||||
value={year}
|
||||
onValueChange={handleYearChange}
|
||||
options={yearOptions}
|
||||
placeholder="YYYY"
|
||||
{...yearSelectProps}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-3 max-[390px]:flex-col">
|
||||
{localeFormat.map((format) => inputs[format])}
|
||||
</div>
|
||||
{error && (
|
||||
<Typography as="p" size="xs" color="destructive" align="left">
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import PrivacySecurityBanner from "./PrivacySecurityBanner";
|
||||
|
||||
/** Privacy Security Banner Component */
|
||||
const meta: Meta<typeof PrivacySecurityBanner> = {
|
||||
title: "Widgets/PrivacySecurityBanner",
|
||||
component: PrivacySecurityBanner,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: ["autodocs"],
|
||||
},
|
||||
args: {
|
||||
text: {
|
||||
children:
|
||||
"Мы не передаем личную информацию, она остаётся в безопасности и под вашим контролем.",
|
||||
},
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,48 @@
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface PrivacySecurityBannerProps
|
||||
extends React.ComponentProps<"div"> {
|
||||
text?: React.ComponentProps<typeof Typography>;
|
||||
}
|
||||
|
||||
export default function PrivacySecurityBanner({
|
||||
text,
|
||||
...props
|
||||
}: PrivacySecurityBannerProps) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"w-full grid grid-cols-[18px_1fr] gap-[11px]",
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="19"
|
||||
height="19"
|
||||
viewBox="0 0 19 19"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.5 0C9.6725 0 9.845 0.0373134 10.0025 0.108209L17.0637 3.08955C17.8887 3.43657 18.5037 4.24627 18.5 5.22388C18.4812 8.92537 16.9512 15.6978 10.49 18.7761C9.86375 19.0746 9.13625 19.0746 8.51 18.7761C2.04876 15.6978 0.518767 8.92537 0.500017 5.22388C0.496267 4.24627 1.11127 3.43657 1.93626 3.08955L9.00125 0.108209C9.155 0.0373134 9.3275 0 9.5 0Z"
|
||||
fill="#3F83F8"
|
||||
/>
|
||||
<path
|
||||
d="M8.87116 12.38C8.86942 12.38 8.86767 12.38 8.86614 12.38C8.72515 12.3785 8.59338 12.3106 8.51 12.1972L6.58711 9.58194C6.44046 9.38253 6.48336 9.10188 6.68278 8.95523C6.88219 8.80792 7.16306 8.85167 7.30948 9.05091L8.87968 11.1866L11.973 7.17469C12.1241 6.97855 12.4058 6.94201 12.602 7.09345C12.7979 7.24471 12.8345 7.52621 12.6832 7.72235L9.22623 12.2056C9.14128 12.3156 9.01014 12.38 8.87116 12.38Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
{text && (
|
||||
<Typography
|
||||
as="p"
|
||||
align="left"
|
||||
weight="medium"
|
||||
{...text}
|
||||
className={cn("[&_a]:font-medium w-fit", text.className)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { Meta, StoryObj } from "@storybook/nextjs-vite";
|
||||
import PrivacyTermsConsent from "./PrivacyTermsConsent";
|
||||
|
||||
/** Privacy Terms Consent Component */
|
||||
const meta: Meta<typeof PrivacyTermsConsent> = {
|
||||
title: "Widgets/PrivacyTermsConsent",
|
||||
component: PrivacyTermsConsent,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: ["autodocs"],
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default = {} satisfies Story;
|
||||
@ -0,0 +1,42 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Typography from "@/components/ui/Typography/Typography";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
export interface PrivacyTermsConsentProps
|
||||
extends Omit<React.ComponentProps<typeof Typography>, "children"> {
|
||||
privacyPolicy?: React.ComponentProps<typeof Button> & { href: string };
|
||||
termsOfUse?: React.ComponentProps<typeof Button> & { href: string };
|
||||
}
|
||||
|
||||
export default function PrivacyTermsConsent({
|
||||
privacyPolicy,
|
||||
termsOfUse,
|
||||
...props
|
||||
}: PrivacyTermsConsentProps) {
|
||||
return (
|
||||
<Typography
|
||||
as="p"
|
||||
size="xs"
|
||||
font="inter"
|
||||
color="muted"
|
||||
{...props}
|
||||
className={cn("[&_a]:font-medium", props.className)}
|
||||
>
|
||||
I agree to the{" "}
|
||||
{privacyPolicy && (
|
||||
<Button variant="link" asChild {...privacyPolicy}>
|
||||
<Link href={privacyPolicy.href}>{privacyPolicy.children}</Link>
|
||||
</Button>
|
||||
)}
|
||||
{", "}
|
||||
{termsOfUse && (
|
||||
<Button variant="link" asChild {...termsOfUse}>
|
||||
<Link href={termsOfUse.href}>{termsOfUse.children}</Link>
|
||||
</Button>
|
||||
)}{" "}
|
||||
and to the use of cookies and tracking technologies, that require your
|
||||
consent
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
43
src/hooks/DOM/useDynamicSize.ts
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
interface IUseDynamicSize {
|
||||
defaultWidth?: number;
|
||||
defaultHeight?: number;
|
||||
}
|
||||
|
||||
export function useDynamicSize<T extends HTMLElement>({
|
||||
defaultWidth = 0,
|
||||
defaultHeight = 0,
|
||||
}: IUseDynamicSize) {
|
||||
const [width, setWidth] = useState<number>(defaultWidth);
|
||||
const [height, setHeight] = useState<number>(defaultHeight);
|
||||
|
||||
const elementRef = useRef<T>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!elementRef.current) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (elementRef.current?.clientWidth !== width) {
|
||||
setWidth(elementRef.current?.clientWidth || 0);
|
||||
}
|
||||
if (elementRef.current?.clientHeight !== height) {
|
||||
setHeight(elementRef.current?.clientHeight || 0);
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(elementRef.current);
|
||||
|
||||
return function cleanup() {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [elementRef.current]);
|
||||
|
||||
return useMemo(
|
||||
() => ({ width, height, elementRef }),
|
||||
[width, height, elementRef]
|
||||
);
|
||||
}
|
||||
162
src/hooks/useDateInput.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { SelectInputProps } from "@/components/ui/SelectInput/SelectInput";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
const isValidDate = (year: number, month: number, day: number) => {
|
||||
if (!year || !month || !day) return false;
|
||||
const date = new Date(year, month - 1, day);
|
||||
return (
|
||||
date.getFullYear() === year &&
|
||||
date.getMonth() === month - 1 &&
|
||||
date.getDate() === day
|
||||
);
|
||||
};
|
||||
|
||||
const parseDateValue = (
|
||||
value?: string | null
|
||||
): { year: string; month: string; day: string } | null => {
|
||||
if (!value) return null;
|
||||
|
||||
// Поддерживаем форматы: "2003-04-09" и "2003-04-09 12:00"
|
||||
const dateMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:\s+\d{2}:\d{2})?$/);
|
||||
|
||||
if (!dateMatch) return null;
|
||||
|
||||
const [, year, month, day] = dateMatch;
|
||||
return { year, month, day };
|
||||
};
|
||||
|
||||
// Упрощенное определение порядка полей даты на основе локали.
|
||||
// В реальном приложении здесь лучше использовать данные из next-intl.
|
||||
const getDateInputLocaleFormat = (locale: string): ("d" | "m" | "y")[] => {
|
||||
const format = new Intl.DateTimeFormat(locale).format(new Date(2001, 1, 3)); // Используем 3/Feb/2001
|
||||
if (/^3.*2/.test(format)) return ["d", "m", "y"]; // 3/2/2001 -> d/m/y
|
||||
if (/^2.*3/.test(format)) return ["m", "d", "y"]; // 2/3/2001 -> m/d/y
|
||||
return ["y", "m", "d"]; // 2001/2/3 -> y/m/d
|
||||
};
|
||||
|
||||
interface UseDateInputProps {
|
||||
value?: string | null;
|
||||
onChange?: (value: string | null) => void;
|
||||
maxYear?: number;
|
||||
yearsRange?: number;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export const useDateInput = ({
|
||||
value,
|
||||
onChange,
|
||||
maxYear = new Date().getFullYear() - 11,
|
||||
yearsRange = 100,
|
||||
locale = "en",
|
||||
}: UseDateInputProps) => {
|
||||
const [year, setYear] = useState("");
|
||||
const [month, setMonth] = useState("");
|
||||
const [day, setDay] = useState("");
|
||||
|
||||
const lastEmittedValue = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const parsedDate = parseDateValue(value);
|
||||
|
||||
if (parsedDate) {
|
||||
setYear(parsedDate.year);
|
||||
setMonth(parsedDate.month);
|
||||
setDay(parsedDate.day);
|
||||
} else {
|
||||
setYear("");
|
||||
setMonth("");
|
||||
setDay("");
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const updateValue = useCallback(
|
||||
(newValue: string | null) => {
|
||||
if (newValue !== lastEmittedValue.current) {
|
||||
lastEmittedValue.current = newValue;
|
||||
onChange?.(newValue);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const numericYear = Number(year);
|
||||
const numericMonth = Number(month);
|
||||
const numericDay = Number(day);
|
||||
|
||||
if (isValidDate(numericYear, numericMonth, numericDay)) {
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
updateValue(formattedDate);
|
||||
} else {
|
||||
if (year || month || day) {
|
||||
updateValue(null);
|
||||
}
|
||||
}
|
||||
}, [year, month, day, updateValue]);
|
||||
|
||||
const yearOptions = useMemo(
|
||||
() =>
|
||||
Array.from({ length: yearsRange }, (_, i) => ({
|
||||
value: maxYear - i,
|
||||
label: String(maxYear - i),
|
||||
})),
|
||||
[maxYear, yearsRange]
|
||||
);
|
||||
|
||||
const monthOptions = useMemo(
|
||||
() =>
|
||||
Array.from({ length: 12 }, (_, i) => ({
|
||||
value: String(i + 1).padStart(2, "0"),
|
||||
label: String(i + 1),
|
||||
})),
|
||||
[]
|
||||
);
|
||||
|
||||
const dayOptions = useMemo(() => {
|
||||
const daysInMonth =
|
||||
year && month ? new Date(Number(year), Number(month), 0).getDate() : 31;
|
||||
return Array.from({ length: daysInMonth }, (_, i) => ({
|
||||
value: String(i + 1).padStart(2, "0"),
|
||||
label: String(i + 1),
|
||||
}));
|
||||
}, [year, month]);
|
||||
|
||||
const localeFormat = useMemo(
|
||||
() => getDateInputLocaleFormat(locale),
|
||||
[locale]
|
||||
);
|
||||
|
||||
const handleYearChange: SelectInputProps["onValueChange"] = useCallback(
|
||||
(value: string) => {
|
||||
setYear(value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleMonthChange: SelectInputProps["onValueChange"] = useCallback(
|
||||
(value: string) => {
|
||||
setMonth(value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDayChange: SelectInputProps["onValueChange"] = useCallback(
|
||||
(value: string) => {
|
||||
setDay(value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
yearOptions,
|
||||
monthOptions,
|
||||
dayOptions,
|
||||
localeFormat,
|
||||
handleYearChange,
|
||||
handleMonthChange,
|
||||
handleDayChange,
|
||||
};
|
||||
};
|
||||