diff --git a/package-lock.json b/package-lock.json index 8e2f91c..aa934d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index bf1ef67..c2075f8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/female-portrait.jpg b/public/female-portrait.jpg new file mode 100644 index 0000000..0fcc4a9 Binary files /dev/null and b/public/female-portrait.jpg differ diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/heart-in-fire.svg b/public/heart-in-fire.svg new file mode 100644 index 0000000..00c0857 --- /dev/null +++ b/public/heart-in-fire.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/male-portrait.jpg b/public/male-portrait.jpg new file mode 100644 index 0000000..43aa969 Binary files /dev/null and b/public/male-portrait.jpg differ diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index dc3d160..a2c2082 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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); diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index ea306d7..2a73149 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -22,17 +22,20 @@ function Header({ return (
-
+
{shouldRenderBackButton && ( )}
+ {progressProps && (
@@ -42,4 +45,4 @@ function Header({ ); } -export { Header }; +export { Header }; \ No newline at end of file diff --git a/src/components/layout/LayoutQuestion/LayoutQuestion.tsx b/src/components/layout/LayoutQuestion/LayoutQuestion.tsx index 8e89e96..e64deb4 100644 --- a/src/components/layout/LayoutQuestion/LayoutQuestion.tsx +++ b/src/components/layout/LayoutQuestion/LayoutQuestion.tsx @@ -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, "title" | "content"> { headerProps?: React.ComponentProps; - 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(null); - const [bottomActionButtonHeight, setBottomActionButtonHeight] = - useState(132); - - useEffect(() => { - if (bottomActionButtonRef.current) { - console.log(bottomActionButtonRef.current.clientHeight); - - setBottomActionButtonHeight(bottomActionButtonRef.current.clientHeight); - } - }, [bottomActionButtonProps]); - return (
{headerProps &&
} -
+ +
{title && ( )} + {subtitle && ( )} - {children} - {bottomActionButtonProps && ( - - )} + +
+ {children} +
); } -export { LayoutQuestion }; +export { LayoutQuestion }; \ No newline at end of file diff --git a/src/components/templates/Coupon/Coupon.stories.tsx b/src/components/templates/Coupon/Coupon.stories.tsx new file mode 100644 index 0000000..9700e3a --- /dev/null +++ b/src/components/templates/Coupon/Coupon.stories.tsx @@ -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 = { + headerProps: { + onBack: fn(), + }, + title: { + children: "Тебе повезло!", + align: "center", + }, + subtitle: { + children: "Ты получил специальную эксклюзивную скидку на 94%", + align: "center", + }, +}; + +/** Reusable Coupon page Component */ +const meta: Meta = { + 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: ( + <> + Скопируйте или нажмите Continue + + ), + }, + onCopyPromoCode: fn(), + }, + bottomActionButtonProps: { + actionButtonProps: { + children: "Continue", + onClick: fn(), + }, + }, + }, + argTypes: { + bottomActionButtonProps: { + control: { type: "object" }, + }, + }, + render: (args) => { + return ( + + + + ); + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default = {} satisfies Story; diff --git a/src/components/templates/Coupon/Coupon.tsx b/src/components/templates/Coupon/Coupon.tsx new file mode 100644 index 0000000..3b3b7d0 --- /dev/null +++ b/src/components/templates/Coupon/Coupon.tsx @@ -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, "title"> { + couponProps: React.ComponentProps; + bottomActionButtonProps?: BottomActionButtonProps; +} + +function Coupon({ + couponProps, + bottomActionButtonProps, + ...props +}: CouponProps) { + const { + height: bottomActionButtonHeight, + elementRef: bottomActionButtonRef, + } = useDynamicSize({ + defaultHeight: 132, + }); + + return ( +
+ {/* {title && ( + + )} + {subtitle && ( + + )} */} + + {bottomActionButtonProps && ( + + )} +
+ ); +} + +export { Coupon }; diff --git a/src/components/templates/Email/Email.stories.tsx b/src/components/templates/Email/Email.stories.tsx new file mode 100644 index 0000000..5e3b30d --- /dev/null +++ b/src/components/templates/Email/Email.stories.tsx @@ -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 = { + headerProps: { + onBack: fn(), + }, + title: { + children: "Портрет твоей второй половинки готов! Куда нам его отправить?", + align: "center", + }, + contentProps: { + className: "pt-0!", + }, +}; + +/** Reusable Email page Component */ +const meta: Meta = { + 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: ( + male portrait + ), + 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 ( + + + + ); + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default = {} satisfies Story; + +export const FemalePortrait = { + args: { + image: ( + female portrait + ), + }, +} satisfies Story; diff --git a/src/components/templates/Email/Email.tsx b/src/components/templates/Email/Email.tsx new file mode 100644 index 0000000..b2f44cf --- /dev/null +++ b/src/components/templates/Email/Email.tsx @@ -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, "title"> { + textInputProps: React.ComponentProps; + bottomActionButtonProps?: BottomActionButtonProps; + image?: React.ReactNode; + privacyTermsConsentProps?: React.ComponentProps; + privacySecurityBannerProps?: React.ComponentProps< + typeof PrivacySecurityBanner + >; +} + +function Email({ + textInputProps, + bottomActionButtonProps, + image, + privacyTermsConsentProps, + privacySecurityBannerProps, + ...props +}: EmailProps) { + const { + height: bottomActionButtonHeight, + elementRef: bottomActionButtonRef, + } = useDynamicSize({ + defaultHeight: 132, + }); + const [isTouched, setIsTouched] = useState(false); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: String(textInputProps.value || ""), + }, + }); + + useEffect(() => { + form.setValue("email", String(textInputProps.value || "")); + }, [textInputProps.value, form]); + + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + form.setValue("email", value); + form.trigger("email"); + textInputProps.onChange?.(e); + }; + + const isFormValid = form.formState.isValid && form.getValues("email"); + + return ( +
+ { + setIsTouched(true); + form.trigger("email"); + }} + aria-invalid={isTouched && !!form.formState.errors.email} + aria-errormessage={ + isTouched ? form.formState.errors.email?.message : undefined + } + /> + {image} + {privacySecurityBannerProps && ( + + )} + {bottomActionButtonProps && ( + + ) + } + /> + )} +
+ ); +} + +export { Email }; diff --git a/src/components/templates/Loaders/Loaders.stories.tsx b/src/components/templates/Loaders/Loaders.stories.tsx new file mode 100644 index 0000000..214ad2c --- /dev/null +++ b/src/components/templates/Loaders/Loaders.stories.tsx @@ -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 = { + headerProps: { + onBack: fn(), + }, + title: { + children: "Создаем портрет твоей второй половинки.", + align: "center", + }, + contentProps: { + className: "pt-5", + }, + childrenWrapperProps: { + className: "mt-[57px]", + }, +}; + +/** Reusable Loaders page Component */ +const meta: Meta = { + 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 ( + + + + ); + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default = {} satisfies Story; diff --git a/src/components/templates/Loaders/Loaders.tsx b/src/components/templates/Loaders/Loaders.tsx new file mode 100644 index 0000000..d19890a --- /dev/null +++ b/src/components/templates/Loaders/Loaders.tsx @@ -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, "title"> { + circularProgressbarsListProps: React.ComponentProps< + typeof CircularProgressbarsList + >; + bottomActionButtonProps?: BottomActionButtonProps; +} + +function Loaders({ + circularProgressbarsListProps, + bottomActionButtonProps, + ...props +}: LoadersProps) { + const { + height: bottomActionButtonHeight, + elementRef: bottomActionButtonRef, + } = useDynamicSize({ + defaultHeight: 132, + }); + const [isVisibleButton, setIsVisibleButton] = useState(false); + + const onAnimationEnd = () => { + setIsVisibleButton(true); + circularProgressbarsListProps.onAnimationEnd?.(); + }; + + return ( +
+ + {bottomActionButtonProps && ( + + )} +
+ ); +} + +export { Loaders }; diff --git a/src/components/templates/Question/Question.stories.tsx b/src/components/templates/Question/Question.stories.tsx deleted file mode 100644 index 28ff678..0000000 --- a/src/components/templates/Question/Question.stories.tsx +++ /dev/null @@ -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 = { - 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; - -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 ( - - ); - }, -} satisfies Story; diff --git a/src/components/templates/Question/Question.tsx b/src/components/templates/Question/Question.tsx deleted file mode 100644 index b500478..0000000 --- a/src/components/templates/Question/Question.tsx +++ /dev/null @@ -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, "title" | "content"> { - layoutQuestionProps: Omit; - content: RadioAnswersListProps | SelectAnswersListProps; - contentType: "radio-answers-list" | "select-answers-list"; -} - -function Question({ - layoutQuestionProps, - content, - contentType, - ...props -}: QuestionProps) { - return ( - - {content && ( -
- {contentType === "radio-answers-list" && ( - - )} - {contentType === "select-answers-list" && ( - - )} -
- )} -
- ); -} - -export { Question }; diff --git a/src/components/templates/QuestionDateAnswers/QuestionDateAnswers.stories.tsx b/src/components/templates/QuestionDateAnswers/QuestionDateAnswers.stories.tsx new file mode 100644 index 0000000..ae2ac58 --- /dev/null +++ b/src/components/templates/QuestionDateAnswers/QuestionDateAnswers.stories.tsx @@ -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 = { + 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 ( + + + + ); + }, +}; + +export default meta; +type Story = StoryObj; + +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; diff --git a/src/components/templates/QuestionDateAnswers/QuestionDateAnswers.tsx b/src/components/templates/QuestionDateAnswers/QuestionDateAnswers.tsx new file mode 100644 index 0000000..bf7c754 --- /dev/null +++ b/src/components/templates/QuestionDateAnswers/QuestionDateAnswers.tsx @@ -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, "content"> { + content: DateInputProps; + bottomActionButtonProps?: BottomActionButtonProps; +} + +function QuestionDateAnswers({ + content, + bottomActionButtonProps, + ...props +}: QuestionDateAnswersProps) { + const form = useForm>({ + 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 ( +
+ + {bottomActionButtonProps && ( + + )} +
+ ); +} + +export { QuestionDateAnswers }; diff --git a/src/components/templates/QuestionInformation/QuestionInformation.stories.tsx b/src/components/templates/QuestionInformation/QuestionInformation.stories.tsx new file mode 100644 index 0000000..764c958 --- /dev/null +++ b/src/components/templates/QuestionInformation/QuestionInformation.stories.tsx @@ -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 = { + title: "Templates/QuestionInformation", + component: QuestionInformation, + tags: ["autodocs"], + parameters: { + layout: "fullscreen", + }, + args: { + image: ( + Information + ), + text: ( + + По нашей статистике 51 % женщин Овнов доверяются эмоциям. Но + одной чувствительности мало. Мы покажем, какие качества второй половинки + дадут тепло и уверенность, и изобразим её портрет. + + ), + bottomActionButtonProps: { + actionButtonProps: { + children: "Continue", + onClick: fn(), + }, + }, + }, + argTypes: { + image: { + control: { type: "object" }, + }, + text: { + control: { type: "object" }, + }, + bottomActionButtonProps: { + control: { type: "object" }, + }, + }, + render: (args) => { + return ( + + + + ); + }, +}; + +export default meta; +type Story = StoryObj; + +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; diff --git a/src/components/templates/QuestionInformation/QuestionInformation.tsx b/src/components/templates/QuestionInformation/QuestionInformation.tsx new file mode 100644 index 0000000..b352f2c --- /dev/null +++ b/src/components/templates/QuestionInformation/QuestionInformation.tsx @@ -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({ + defaultHeight: 132, + }); + + return ( +
+ {image} + {text} + {bottomActionButtonProps && ( + + )} +
+ ); +} + +export { QuestionInformation }; diff --git a/src/components/templates/QuestionRadioAnswers/QuestionRadioAnswers.stories.tsx b/src/components/templates/QuestionRadioAnswers/QuestionRadioAnswers.stories.tsx new file mode 100644 index 0000000..8f28ef4 --- /dev/null +++ b/src/components/templates/QuestionRadioAnswers/QuestionRadioAnswers.stories.tsx @@ -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 = { + 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) => ( + + + + ), +}; + +export default meta; +type Story = StoryObj; + +export const Default = {} satisfies Story; diff --git a/src/components/templates/QuestionRadioAnswers/QuestionRadioAnswers.tsx b/src/components/templates/QuestionRadioAnswers/QuestionRadioAnswers.tsx new file mode 100644 index 0000000..a57eb12 --- /dev/null +++ b/src/components/templates/QuestionRadioAnswers/QuestionRadioAnswers.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { + RadioAnswersList, + RadioAnswersListProps, +} from "@/components/widgets/RadioAnswersList/RadioAnswersList"; + +interface QuestionRadioAnswersProps + extends Omit, "content"> { + content: RadioAnswersListProps; +} + +function QuestionRadioAnswers({ + content, + ...props +}: QuestionRadioAnswersProps) { + return ( +
+ +
+ ); +} + +export { QuestionRadioAnswers }; diff --git a/src/components/templates/QuestionSelectAnswers/QuestionSelectAnswers.stories.tsx b/src/components/templates/QuestionSelectAnswers/QuestionSelectAnswers.stories.tsx new file mode 100644 index 0000000..9625bf8 --- /dev/null +++ b/src/components/templates/QuestionSelectAnswers/QuestionSelectAnswers.stories.tsx @@ -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 = { + 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 ( + + + + ); + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default = {} satisfies Story; diff --git a/src/components/templates/QuestionSelectAnswers/QuestionSelectAnswers.tsx b/src/components/templates/QuestionSelectAnswers/QuestionSelectAnswers.tsx new file mode 100644 index 0000000..fae7fed --- /dev/null +++ b/src/components/templates/QuestionSelectAnswers/QuestionSelectAnswers.tsx @@ -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, "content"> { + content: SelectAnswersListProps; + bottomActionButtonProps?: BottomActionButtonProps; +} + +function QuestionSelectAnswers({ + content, + bottomActionButtonProps, + ...props +}: QuestionSelectAnswersProps) { + const { + height: bottomActionButtonHeight, + elementRef: bottomActionButtonRef, + } = useDynamicSize({ + defaultHeight: 132, + }); + const [selectedAnswers, setSelectedAnswers] = useState< + MainButtonProps[] | null + >(content.activeAnswers); + + const handleChangeSelectedAnswers = (answers: MainButtonProps[] | null) => { + setSelectedAnswers(answers); + content.onChangeSelectedAnswers?.(answers); + }; + + return ( +
+ + {bottomActionButtonProps && ( + + )} +
+ ); +} + +export { QuestionSelectAnswers }; diff --git a/src/components/templates/SoulmatePortrait/SoulmatePortrait.stories.tsx b/src/components/templates/SoulmatePortrait/SoulmatePortrait.stories.tsx new file mode 100644 index 0000000..eeff31e --- /dev/null +++ b/src/components/templates/SoulmatePortrait/SoulmatePortrait.stories.tsx @@ -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 = { + 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; + +export const Default = {} satisfies Story; diff --git a/src/components/templates/SoulmatePortrait/SoulmatePortrait.tsx b/src/components/templates/SoulmatePortrait/SoulmatePortrait.tsx new file mode 100644 index 0000000..f16902e --- /dev/null +++ b/src/components/templates/SoulmatePortrait/SoulmatePortrait.tsx @@ -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, "title"> { + bottomActionButtonProps?: React.ComponentProps; + privacyTermsConsentProps?: React.ComponentProps; + title?: TypographyProps<"h2">; +} + +export default function SoulmatePortrait({ + bottomActionButtonProps, + privacyTermsConsentProps, + title, + ...props +}: SoulmatePortraitProps) { + const { + height: bottomActionButtonHeight, + elementRef: bottomActionButtonRef, + } = useDynamicSize({ + defaultHeight: 132, + }); + + return ( +
+
+ {title && ( + + )} +
+ {bottomActionButtonProps && ( + + ) + } + /> + )} +
+ ); +} diff --git a/src/components/ui/SelectInput/SelectInput.stories.tsx b/src/components/ui/SelectInput/SelectInput.stories.tsx new file mode 100644 index 0000000..cfb9b41 --- /dev/null +++ b/src/components/ui/SelectInput/SelectInput.stories.tsx @@ -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 = { + 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 ( +
+ Value: {value} + { + setValue(value); + args.onValueChange?.(value); + }} + options={dayOptions} + /> +
+ ); + }, +}; + +export default meta; +type Story = StoryObj; + +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; diff --git a/src/components/ui/SelectInput/SelectInput.tsx b/src/components/ui/SelectInput/SelectInput.tsx new file mode 100644 index 0000000..a23b553 --- /dev/null +++ b/src/components/ui/SelectInput/SelectInput.tsx @@ -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 { + error?: boolean; + options: Option[]; + placeholder?: string; + label?: string; + labelProps?: React.ComponentProps; + triggerProps?: React.ComponentProps; + contentProps?: React.ComponentProps; + itemProps?: React.ComponentProps; + errorProps?: React.ComponentProps; +} + +export default function SelectInput({ + error, + options, + placeholder, + label, + labelProps, + triggerProps, + contentProps, + itemProps, + errorProps, + ...props +}: SelectInputProps) { + const id = useId(); + + return ( +
+ {label && ( + + )} + + {error && ( + + {errorProps?.children} + + )} +
+ ); +} diff --git a/src/components/ui/TextInput/TextInput.tsx b/src/components/ui/TextInput/TextInput.tsx index 4c45e90..ca8abcd 100644 --- a/src/components/ui/TextInput/TextInput.tsx +++ b/src/components/ui/TextInput/TextInput.tsx @@ -5,14 +5,23 @@ import { useId } from "react"; interface TextInputProps extends React.ComponentProps { 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 ( -
+
{label && (