diff --git a/package-lock.json b/package-lock.json index 9043615..56cfff2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.12.0", + "vaul": "^1.1.2", "zustand": "^5.0.10" }, "devDependencies": { @@ -1835,6 +1836,337 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "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-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "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-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "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-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@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-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "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-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "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-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "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-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "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-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "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-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "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-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@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-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "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-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "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-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "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-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "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-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "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-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -2825,7 +3157,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -3298,6 +3630,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -3904,6 +4248,12 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4672,6 +5022,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6036,6 +6395,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.12.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", @@ -6074,6 +6480,28 @@ "react-dom": ">=18" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6836,6 +7264,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -6853,6 +7324,19 @@ "dev": true, "license": "MIT" }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", diff --git a/package.json b/package.json index 4408251..c4329b7 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.12.0", + "vaul": "^1.1.2", "zustand": "^5.0.10" }, "devDependencies": { diff --git a/src/app/App.tsx b/src/app/App.tsx index 453d449..a17dc30 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,5 +1,13 @@ -import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom' +import { + BrowserRouter, + Routes, + Route, + Navigate, + Outlet, +} from 'react-router-dom' import { ManagerHomePage } from '@/pages/manager/home' +import { SocialPage } from '@/pages/manager/social' +import { SocialChatPage } from '@/pages/manager/social-chat' import { LoginPage } from '@/pages/login' import { SignupPage } from '@/pages/signup' import { JobLookupMapPage } from '@/pages/user/job-lookup-map' @@ -43,6 +51,8 @@ export function App() { } /> } /> } /> + } /> + } /> } /> diff --git a/src/assets/icons/Plus.svg b/src/assets/icons/Plus.svg new file mode 100644 index 0000000..4e5bb74 --- /dev/null +++ b/src/assets/icons/Plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/alba/Bookmark.svg b/src/assets/icons/alba/Bookmark.svg new file mode 100644 index 0000000..51109d8 --- /dev/null +++ b/src/assets/icons/alba/Bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/alba/Calendar.svg b/src/assets/icons/alba/Calendar.svg new file mode 100644 index 0000000..8f5544e --- /dev/null +++ b/src/assets/icons/alba/Calendar.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/alba/Clock.svg b/src/assets/icons/alba/Clock.svg new file mode 100644 index 0000000..a89f2a9 --- /dev/null +++ b/src/assets/icons/alba/Clock.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/alba/Thumbsup.svg b/src/assets/icons/alba/Thumbsup.svg new file mode 100644 index 0000000..152006f --- /dev/null +++ b/src/assets/icons/alba/Thumbsup.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/camera.svg b/src/assets/icons/camera.svg new file mode 100644 index 0000000..99b6f7b --- /dev/null +++ b/src/assets/icons/camera.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/chevron-left.svg b/src/assets/icons/chevron-left.svg new file mode 100644 index 0000000..217ce76 --- /dev/null +++ b/src/assets/icons/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/image.svg b/src/assets/icons/image.svg new file mode 100644 index 0000000..75c5e07 --- /dev/null +++ b/src/assets/icons/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 0000000..0bb8b80 --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/socialvector.svg b/src/assets/icons/socialvector.svg new file mode 100644 index 0000000..7d3721c --- /dev/null +++ b/src/assets/icons/socialvector.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/features/social/index.ts b/src/features/social/index.ts new file mode 100644 index 0000000..c291a82 --- /dev/null +++ b/src/features/social/index.ts @@ -0,0 +1,2 @@ +export { AlbaFindDrawer, type AlbaFindDrawerProps } from './ui/AlbaFindDrawer' +export { DrawerPeekStrip } from './ui/DrawerPeekStrip' diff --git a/src/features/social/ui/AlbaFindDrawer.tsx b/src/features/social/ui/AlbaFindDrawer.tsx new file mode 100644 index 0000000..a67ac88 --- /dev/null +++ b/src/features/social/ui/AlbaFindDrawer.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react' + +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerTitle, +} from '@/features/social/ui/drawer' +import { + AlbaFindCategoryBar, + type AlbaFindFilterId, + type AlbaFindMode, +} from '@/shared/ui/manager/alba-find/AlbaFindCategoryBar' +import { AlbaFindList } from '@/shared/ui/manager/alba-find/AlbaFindList' +import { + Albabox, + type AlbaboxProps, +} from '@/shared/ui/manager/alba-find/Albabox' + +export type AlbaFindDrawerProps = { + open: boolean + onOpenChange: (open: boolean) => void + jobs: AlbaboxProps[] +} + +export function AlbaFindDrawer({ + open, + onOpenChange, + jobs, +}: AlbaFindDrawerProps) { + const [mode, setMode] = useState('nearby') + const [activeFilter, setActiveFilter] = useState('sort') + + return ( + + +
+ 알바 찾기 + + 주변 또는 지역 기준으로 알바 공고를 확인할 수 있습니다. + +
+ +
+ +
+ +
+ + {jobs.map((job, index) => ( + + ))} + +
+
+
+ ) +} diff --git a/src/features/social/ui/DrawerHandleBar.tsx b/src/features/social/ui/DrawerHandleBar.tsx new file mode 100644 index 0000000..414714e --- /dev/null +++ b/src/features/social/ui/DrawerHandleBar.tsx @@ -0,0 +1,16 @@ +export type DrawerHandleBarProps = { + className?: string + size?: 'sm' | 'md' +} + +export function DrawerHandleBar({ + className, + size = 'md', +}: DrawerHandleBarProps) { + return ( +
+ ) +} diff --git a/src/features/social/ui/DrawerPeekStrip.tsx b/src/features/social/ui/DrawerPeekStrip.tsx new file mode 100644 index 0000000..52b32dd --- /dev/null +++ b/src/features/social/ui/DrawerPeekStrip.tsx @@ -0,0 +1,82 @@ +import { useRef, type PointerEvent } from 'react' + +import { DrawerHandleBar } from './DrawerHandleBar' + +type DrawerPeekStripProps = { + show: boolean + onRequestOpen: () => void + dragThresholdPx?: number +} + +export function DrawerPeekStrip({ + show, + onRequestOpen, + dragThresholdPx = 40, +}: DrawerPeekStripProps) { + const dragRef = useRef<{ pointerId: number; startY: number } | null>(null) + + const handlePointerDown = (event: PointerEvent) => { + dragRef.current = { + pointerId: event.pointerId, + startY: event.clientY, + } + event.currentTarget.setPointerCapture(event.pointerId) + } + + const handlePointerMove = (event: PointerEvent) => { + if (!dragRef.current || dragRef.current.pointerId !== event.pointerId) + return + const deltaUp = dragRef.current.startY - event.clientY + if (deltaUp >= dragThresholdPx) { + onRequestOpen() + dragRef.current = null + try { + event.currentTarget.releasePointerCapture(event.pointerId) + } catch { + // noop + } + } + } + + const endDrag = (event: PointerEvent) => { + if (!dragRef.current || dragRef.current.pointerId !== event.pointerId) + return + dragRef.current = null + try { + event.currentTarget.releasePointerCapture(event.pointerId) + } catch { + // noop + } + } + + if (!show) return null + + return ( +
{ + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + onRequestOpen() + } + }} + > +
+
+ +
+
+
+
+ ) +} diff --git a/src/features/social/ui/drawer.tsx b/src/features/social/ui/drawer.tsx new file mode 100644 index 0000000..cff7064 --- /dev/null +++ b/src/features/social/ui/drawer.tsx @@ -0,0 +1,112 @@ +import * as React from 'react' +import { Drawer as DrawerPrimitive } from 'vaul' + +import { DrawerHandleBar } from './DrawerHandleBar' + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +) +Drawer.displayName = 'Drawer' + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ +
+ {children} +
+
+)) +DrawerContent.displayName = 'DrawerContent' + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = 'DrawerHeader' + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = 'DrawerFooter' + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/src/pages/manager/social-chat/index.tsx b/src/pages/manager/social-chat/index.tsx new file mode 100644 index 0000000..d179da2 --- /dev/null +++ b/src/pages/manager/social-chat/index.tsx @@ -0,0 +1,109 @@ +import { useNavigate } from 'react-router-dom' +import { useState } from 'react' +import chevronLeftIcon from '@/assets/icons/chevron-left.svg' +import socialVectorIcon from '@/assets/icons/socialvector.svg' +import imageIcon from '@/assets/icons/image.svg' +import cameraIcon from '@/assets/icons/camera.svg' + +export function SocialChatPage() { + const navigate = useNavigate() + const [attachMenuOpen, setAttachMenuOpen] = useState(false) + + return ( +
+
+ +

이수연

+
+ +
+
+
+ 안녕하세요! +
+ 오전 12:00 +
+ +
+
+ 안녕하세요! +
+
+ +
+ 오전 12:00 +
+ 혹시 저 대타 부탁드려도 될까요?? +
+
+
+ +
+
+ + + +
+
+ + {attachMenuOpen && ( +
+
+ + + +
+
+ )} +
+ ) +} diff --git a/src/pages/manager/social/index.tsx b/src/pages/manager/social/index.tsx new file mode 100644 index 0000000..1200820 --- /dev/null +++ b/src/pages/manager/social/index.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { Navbar } from '@/shared/ui/common/Navbar' +import { SocialCategory } from '@/shared/ui/manager/social/SocialCategory' +import { SwipeableSocialItem } from '@/shared/ui/manager/social/SocialList' +import { SocialSearch } from '@/shared/ui/manager/social/SocialSearch' +import { FloatingActionButton } from '@/shared/ui/common/FloatingActionButton' + +import SearchIcon from '@/assets/icons/search.svg' +import MessageIcon from '@/assets/icons/doc/Message.svg' + +const SOCIAL_CATEGORY = [ + { + id: 1, + name: '전체', + }, + { + id: 2, + name: '알바1', + }, + { + id: 3, + name: '알바 2', + }, + { + id: 4, + name: '알바 3', + }, +] + +const SOCIAL_LIST = [ + { + id: 1, + name: '나영채', + message: '메시지 내용 최대 한 줄만 출력 됩니다.', + timeAgo: '1시간 전', + unread: true, + }, + { + id: 2, + name: '나영채', + message: '메시지 내용 최대 한 줄만 출력 됩니다.', + timeAgo: '1시간 전', + unread: false, + }, +] + +export function SocialPage() { + const [searchPopupOpen, setSearchPopupOpen] = useState(false) + const navigate = useNavigate() + + return ( +
+ +
+ {SOCIAL_CATEGORY.map((item, index) => ( + + ))} +
+ +
+
+ {SOCIAL_LIST.map(item => ( + + ))} +
+
+ +
+
+ setSearchPopupOpen(true)} /> + + +
+
+ + {searchPopupOpen && ( +
setSearchPopupOpen(false)} + > +
e.stopPropagation()} + > + + +
+ {Array.from({ length: 5 }).map((_, index) => ( +
+
+
+
+ 나영채 +
+
+ 근무지 이름 +
+
+ +
+ ))} +
+
+
+ )} +
+ ) +} diff --git a/src/shared/ui/common/FloatingActionButton.tsx b/src/shared/ui/common/FloatingActionButton.tsx new file mode 100644 index 0000000..86de7fb --- /dev/null +++ b/src/shared/ui/common/FloatingActionButton.tsx @@ -0,0 +1,12 @@ +import PlusIcon from '@/assets/icons/Plus.svg' +export function FloatingActionButton() { + return ( + + ) +} diff --git a/src/shared/ui/manager/alba-find/AlbaFindCategoryBar.tsx b/src/shared/ui/manager/alba-find/AlbaFindCategoryBar.tsx new file mode 100644 index 0000000..9075eb6 --- /dev/null +++ b/src/shared/ui/manager/alba-find/AlbaFindCategoryBar.tsx @@ -0,0 +1,76 @@ +export type AlbaFindMode = 'nearby' | 'region' + +export type AlbaFindFilterId = 'sort' | 'distance' | 'salary' + +type AlbaFindCategoryBarProps = { + mode: AlbaFindMode + onModeChange: (mode: AlbaFindMode) => void + activeFilter: AlbaFindFilterId + onFilterChange: (id: AlbaFindFilterId) => void +} + +const FILTER_ITEMS: { id: AlbaFindFilterId; label: string }[] = [ + { id: 'sort', label: '최신순' }, + { id: 'distance', label: '거리' }, + { id: 'salary', label: '급여' }, +] + +export function AlbaFindCategoryBar({ + mode, + onModeChange, + activeFilter, + onFilterChange, +}: AlbaFindCategoryBarProps) { + return ( +
+
+ + +
+ +
+ {FILTER_ITEMS.map(({ id, label }) => { + const active = activeFilter === id + return ( + + ) + })} +
+
+ ) +} diff --git a/src/shared/ui/manager/alba-find/AlbaFindList.tsx b/src/shared/ui/manager/alba-find/AlbaFindList.tsx new file mode 100644 index 0000000..c602a20 --- /dev/null +++ b/src/shared/ui/manager/alba-find/AlbaFindList.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from 'react' + +type AlbaFindListProps = { + children: ReactNode + className?: string +} + +export function AlbaFindList({ children, className }: AlbaFindListProps) { + return ( +
+ {children} +
+ ) +} diff --git a/src/shared/ui/manager/alba-find/Albabox.tsx b/src/shared/ui/manager/alba-find/Albabox.tsx new file mode 100644 index 0000000..a80d379 --- /dev/null +++ b/src/shared/ui/manager/alba-find/Albabox.tsx @@ -0,0 +1,68 @@ +import BookmarkIcon from '@/assets/icons/alba/Bookmark.svg?react' + +export type AlbaboxProps = { + storeName: string + title: string + wageAmount: string + timeRange: string + workDays: string + distance: string + postedAgo: string + saved: boolean + likeCount?: string + onBookmarkClick?: () => void +} + +export function Albabox({ + storeName, + title, + wageAmount, + timeRange, + workDays, + distance, + postedAgo, + saved, + likeCount, + onBookmarkClick, +}: AlbaboxProps) { + return ( +
+
+

{storeName}

+ +
+ +

+ {title} +

+ +

+ 시급 {wageAmount}원 +

+ +
+ {timeRange} + {workDays} + {distance} +
+ +
+ {postedAgo} + {likeCount != null && likeCount !== '' && ( + 좋아요 {likeCount} + )} +
+
+ ) +} diff --git a/src/shared/ui/manager/social/SocialCategory.tsx b/src/shared/ui/manager/social/SocialCategory.tsx new file mode 100644 index 0000000..28c2a41 --- /dev/null +++ b/src/shared/ui/manager/social/SocialCategory.tsx @@ -0,0 +1,19 @@ +interface SocialCategoryProps { + label: string + active?: boolean +} + +export function SocialCategory({ label, active = false }: SocialCategoryProps) { + return ( + + ) +} diff --git a/src/shared/ui/manager/social/SocialList.tsx b/src/shared/ui/manager/social/SocialList.tsx new file mode 100644 index 0000000..39a21d0 --- /dev/null +++ b/src/shared/ui/manager/social/SocialList.tsx @@ -0,0 +1,154 @@ +import { useRef, useState, type PointerEventHandler } from 'react' + +interface SocialProfileProps { + name: string + message: string + timeAgo: string + unread?: boolean +} + +interface SocialActionProps { + onRead?: () => void + onDelete?: () => void +} + +interface SwipeableSocialItemProps extends SocialProfileProps { + onRead?: () => void + onDelete?: () => void +} + +const ACTION_WIDTH = 160 +const OPEN_THRESHOLD = ACTION_WIDTH * 0.45 + +export function SocialList({ + name, + message, + timeAgo, + unread = false, +}: SocialProfileProps) { + return ( +
+
+ +
+
+
{name}
+
+ {timeAgo} +
+
+ +
+
+ {message} +
+
+
+
+
+ ) +} + +export function SwipeableSocialItem({ + name, + message, + timeAgo, + unread = false, + onRead, + onDelete, +}: SwipeableSocialItemProps) { + const [translateX, setTranslateX] = useState(0) + const [isDragging, setIsDragging] = useState(false) + + const pointerIdRef = useRef(null) + const startXRef = useRef(0) + const startTranslateXRef = useRef(0) + + const handlePointerDown: PointerEventHandler = event => { + pointerIdRef.current = event.pointerId + startXRef.current = event.clientX + startTranslateXRef.current = translateX + setIsDragging(true) + event.currentTarget.setPointerCapture(event.pointerId) + } + + const handlePointerMove: PointerEventHandler = event => { + if (!isDragging || pointerIdRef.current !== event.pointerId) return + + const deltaX = event.clientX - startXRef.current + const nextTranslateX = Math.min( + 0, + Math.max(-ACTION_WIDTH, startTranslateXRef.current + deltaX) + ) + setTranslateX(nextTranslateX) + } + + const finishDrag = (pointerId: number) => { + if (pointerIdRef.current !== pointerId) return + + pointerIdRef.current = null + setIsDragging(false) + setTranslateX(prev => (Math.abs(prev) > OPEN_THRESHOLD ? -ACTION_WIDTH : 0)) + } + + const handlePointerUp: PointerEventHandler = event => { + finishDrag(event.pointerId) + } + + const handlePointerCancel: PointerEventHandler = event => { + finishDrag(event.pointerId) + } + + return ( +
+
+ +
+ +
+
+ +
+
+
+ ) +} + +export function SocialAction({ onRead, onDelete }: SocialActionProps) { + return ( +
+ + +
+ ) +} diff --git a/src/shared/ui/manager/social/SocialSearch.tsx b/src/shared/ui/manager/social/SocialSearch.tsx new file mode 100644 index 0000000..940c6bc --- /dev/null +++ b/src/shared/ui/manager/social/SocialSearch.tsx @@ -0,0 +1,17 @@ +import SearchIcon from '@/assets/icons/search.svg' + +interface SocialSearchProps { + onClick?: () => void +} + +export function SocialSearch({ onClick }: SocialSearchProps) { + return ( + + ) +} diff --git a/storybook/stories/Albabox.stories.tsx b/storybook/stories/Albabox.stories.tsx new file mode 100644 index 0000000..369cbc4 --- /dev/null +++ b/storybook/stories/Albabox.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import React from 'react' + +import { Albabox } from '../../src/shared/ui/manager/alba-find/Albabox' + +const meta = { + title: 'shared/ui/alba-find/Albabox', + component: Albabox, + parameters: { layout: 'centered' }, + tags: ['autodocs'], + decorators: [ + Story => ( +
+ +
+ ), + ], + argTypes: { + saved: { control: 'boolean' }, + onBookmarkClick: { action: 'bookmark' }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +const baseArgs = { + storeName: '스타벅스 강남역점', + title: '주말 오전 카페 알바 모집 (경력 무관)', + wageAmount: '12,000', + timeRange: '09:00–15:00', + workDays: '토·일', + distance: '도보 5분', + postedAgo: '2시간 전', +} + +export const Default: Story = { + args: { + ...baseArgs, + saved: false, + likeCount: '24', + }, +} + +export const Saved: Story = { + args: { + ...baseArgs, + saved: true, + likeCount: '24', + }, +} + +export const WithoutLikeCount: Story = { + args: { + ...baseArgs, + saved: false, + likeCount: undefined, + }, +} diff --git a/storybook/stories/SocialCategory.stories.tsx b/storybook/stories/SocialCategory.stories.tsx new file mode 100644 index 0000000..3f0f1d8 --- /dev/null +++ b/storybook/stories/SocialCategory.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { SocialCategory } from '../../src/shared/ui/manager/social/SocialCategory' + +const meta = { + title: 'shared/ui/social/SocialCategory', + component: SocialCategory, + parameters: { layout: 'centered' }, + tags: ['autodocs'], + argTypes: { + active: { control: 'boolean' }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + label: '전체', + active: false, + }, +} + +export const Active: Story = { + args: { + label: '전체', + active: true, + }, +} diff --git a/storybook/stories/SocialList.stories.tsx b/storybook/stories/SocialList.stories.tsx new file mode 100644 index 0000000..ed922e4 --- /dev/null +++ b/storybook/stories/SocialList.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import React from 'react' + +import { SocialList } from '../../src/shared/ui/manager/social/SocialList' + +const meta = { + title: 'shared/ui/social/SocialList', + component: SocialList, + parameters: { layout: 'centered' }, + tags: ['autodocs'], + decorators: [ + Story => ( +
+ +
+ ), + ], + argTypes: { + unread: { control: 'boolean' }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + name: '홍길동', + message: '오늘 스케줄 확인 부탁드려요.', + timeAgo: '5분 전', + unread: false, + }, +} + +export const Unread: Story = { + args: { + name: '김철수', + message: '내일 대타 가능하신가요?', + timeAgo: '1시간 전', + unread: true, + }, +} + +export const LongMessage: Story = { + args: { + name: '매장 매니저', + message: + '이번 주 금요일 야간 근무 인원이 부족해서 혹시 가능하시면 연락 주시면 감사하겠습니다.', + timeAgo: '어제', + unread: true, + }, +}