From 27c29949da31712d32c0c92cb91a358b3d3dc747 Mon Sep 17 00:00:00 2001 From: Yehoshua Sandler Date: Tue, 29 Apr 2025 17:23:44 -0500 Subject: [PATCH] feat: ui for approve hold request, still need submit --- package-lock.json | 915 +++++++++++++++++- package.json | 6 +- src/app/(frontend)/serverCalls/requestHold.ts | 1 - src/collections/Checkouts/HoldRequests.ts | 1 + .../Manage/ApproveHoldRequestModal.tsx | 178 ++++ src/components/Manage/HoldRequests.tsx | 35 +- src/components/login-form.tsx | 4 + src/components/ui/combobox.tsx | 76 ++ src/components/ui/command.tsx | 177 ++++ src/components/ui/dialog.tsx | 335 +++++++ src/components/ui/popover.tsx | 48 + src/components/ui/select.tsx | 185 ++++ src/hooks/usePreventScroll.tsx | 360 +++++++ src/serverActions/FindCopiesOnHoldRequest.ts | 56 ++ 14 files changed, 2349 insertions(+), 28 deletions(-) create mode 100644 src/components/Manage/ApproveHoldRequestModal.tsx create mode 100644 src/components/ui/combobox.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/hooks/usePreventScroll.tsx create mode 100644 src/serverActions/FindCopiesOnHoldRequest.ts diff --git a/package-lock.json b/package-lock.json index 9bae96e..642881e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,10 @@ "@payloadcms/next": "3.31.0", "@payloadcms/payload-cloud": "3.31.0", "@payloadcms/richtext-lexical": "3.31.0", + "@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-label": "^2.1.4", + "@radix-ui/react-popover": "^1.1.11", + "@radix-ui/react-select": "^2.2.2", "@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.4", @@ -24,11 +27,12 @@ "@tailwindcss/postcss": "^4.1.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "cross-env": "^7.0.3", "framer-motion": "^12.7.4", "graphql": "^16.8.1", "lucide-react": "^0.488.0", - "motion": "^12.7.4", + "motion": "^12.9.2", "next": "15.2.3", "next-themes": "^0.4.6", "payload": "3.31.0", @@ -4067,12 +4071,64 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "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.2", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", + "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "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-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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-collection": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz", @@ -4129,6 +4185,108 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.11.tgz", + "integrity": "sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@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-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "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-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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-dialog/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-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -4144,6 +4302,119 @@ } } }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", + "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@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-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz", + "integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@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-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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", @@ -4208,6 +4479,211 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.11.tgz", + "integrity": "sha512-yFMfZkVA5G3GJnBgb2PxrrcLKm1ZLWXrbYVgdyTl//0TYEIHS9LJbnyz7WWcZ0qCq7hIlJZpRtxeSeIG5T5oJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@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-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "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-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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-popover/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-popper": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", + "integrity": "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@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-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz", + "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0", + "@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-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz", @@ -4286,6 +4762,117 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.2.tgz", + "integrity": "sha512-HjkVHtBkuq+r3zUAZ/CvNWUGKPfuicGDbgtZgiQuFmNcV5F+Tgy24ep2nsAW2nFgvhGPJVqeBZa6KyVN0EyrBA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@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.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@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.0", + "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-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "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-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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-select/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-separator": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz", @@ -4413,6 +5000,42 @@ } } }, + "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", @@ -4428,6 +5051,109 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "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-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", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "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-visually-hidden": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.0.tgz", + "integrity": "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "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-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "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/@react-aria/focus": { "version": "3.20.2", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.2.tgz", @@ -6337,6 +7063,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -7103,6 +7841,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -7466,6 +8220,12 @@ "node": ">=8" } }, + "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/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -8715,13 +9475,13 @@ } }, "node_modules/framer-motion": { - "version": "12.7.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.7.4.tgz", - "integrity": "sha512-jX0bPsTmU0oPZTYz/dVyD0dmOyEOEJvdn0TaZBE5I8g2GvVnnQnW9f65cJnoVfUkY3WZWNXGXnPbVA9YnaIfVA==", + "version": "12.9.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.9.2.tgz", + "integrity": "sha512-R0O3Jdqbfwywpm45obP+8sTgafmdEcUoShQTAV+rB5pi+Y1Px/FYL5qLLRe5tPtBdN1J4jos7M+xN2VV2oEAbQ==", "license": "MIT", "dependencies": { - "motion-dom": "^12.7.4", - "motion-utils": "^12.7.2", + "motion-dom": "^12.9.1", + "motion-utils": "^12.8.3", "tslib": "^2.4.0" }, "peerDependencies": { @@ -8826,6 +9586,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", @@ -10915,12 +11684,12 @@ "license": "MIT" }, "node_modules/motion": { - "version": "12.7.4", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.7.4.tgz", - "integrity": "sha512-MBGrMbYageHw4iZJn+pGTr7abq5n53jCxYkhFC1It3vYukQPRWg5zij46MnwYGpLR8KG465MLHSASXot9edYOw==", + "version": "12.9.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.9.2.tgz", + "integrity": "sha512-2hwi4wlOpt/zDHcDZATL2FFhYgj2n6t5Hd0UT91swMup6dx6KpFRkTydYJkkV0PUImT1QfC+WT5d0eRekTKpcg==", "license": "MIT", "dependencies": { - "framer-motion": "^12.7.4", + "framer-motion": "^12.9.2", "tslib": "^2.4.0" }, "peerDependencies": { @@ -10941,18 +11710,18 @@ } }, "node_modules/motion-dom": { - "version": "12.7.4", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.7.4.tgz", - "integrity": "sha512-1ZUHAoSUMMxP6jPqyxlk9XUfb6NxMsnWPnH2YGhrOhTURLcXWbETi6eemoKb60Pe32NVJYduL4B62VQSO5Jq8Q==", + "version": "12.9.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.9.1.tgz", + "integrity": "sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==", "license": "MIT", "dependencies": { - "motion-utils": "^12.7.2" + "motion-utils": "^12.8.3" } }, "node_modules/motion-utils": { - "version": "12.7.2", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.7.2.tgz", - "integrity": "sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==", + "version": "12.8.3", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.8.3.tgz", + "integrity": "sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==", "license": "MIT" }, "node_modules/mri": { @@ -12284,6 +13053,53 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "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-select": { "version": "5.9.0", "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.9.0.tgz", @@ -12305,6 +13121,28 @@ "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", + "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/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -14166,6 +15004,27 @@ "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-context-selector": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-2.0.0.tgz", @@ -14190,6 +15049,28 @@ } } }, + "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/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", diff --git a/package.json b/package.json index 38212b5..beb8030 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,10 @@ "@payloadcms/next": "3.31.0", "@payloadcms/payload-cloud": "3.31.0", "@payloadcms/richtext-lexical": "3.31.0", + "@radix-ui/react-dialog": "^1.1.11", "@radix-ui/react-label": "^2.1.4", + "@radix-ui/react-popover": "^1.1.11", + "@radix-ui/react-select": "^2.2.2", "@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.4", @@ -31,11 +34,12 @@ "@tailwindcss/postcss": "^4.1.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "cross-env": "^7.0.3", "framer-motion": "^12.7.4", "graphql": "^16.8.1", "lucide-react": "^0.488.0", - "motion": "^12.7.4", + "motion": "^12.9.2", "next": "15.2.3", "next-themes": "^0.4.6", "payload": "3.31.0", diff --git a/src/app/(frontend)/serverCalls/requestHold.ts b/src/app/(frontend)/serverCalls/requestHold.ts index 89fe748..65f0cfb 100644 --- a/src/app/(frontend)/serverCalls/requestHold.ts +++ b/src/app/(frontend)/serverCalls/requestHold.ts @@ -12,7 +12,6 @@ const requestHold = async (props: Props) => { const headers = await nextHeaders() const authResponse = await payload.auth({ headers }) - console.log(authResponse.user); if (!authResponse.user?.id) return diff --git a/src/collections/Checkouts/HoldRequests.ts b/src/collections/Checkouts/HoldRequests.ts index 3c2f56a..7e204c4 100644 --- a/src/collections/Checkouts/HoldRequests.ts +++ b/src/collections/Checkouts/HoldRequests.ts @@ -1,3 +1,4 @@ +import { HoldRequest } from "@/payload-types"; import { CollectionConfig } from "payload"; const HoldRequests: CollectionConfig = { diff --git a/src/components/Manage/ApproveHoldRequestModal.tsx b/src/components/Manage/ApproveHoldRequestModal.tsx new file mode 100644 index 0000000..7b3ad7e --- /dev/null +++ b/src/components/Manage/ApproveHoldRequestModal.tsx @@ -0,0 +1,178 @@ +'use client' + +import { + Dialog, + DialogDescription, + DialogContent, + DialogHeader, + DialogTitle, + DialogClose, +} from '@/components/ui/dialog' +import { Book, Copy, HoldRequest, Repository, User } from '@/payload-types' +import { findCopiesOfBookInRepository } from '@/serverActions/FindCopiesOnHoldRequest' +import { Variants, Transition } from 'motion/react' +import { useEffect, useState } from 'react' +import { Input } from '../ui/input' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { zodResolver } from '@hookform/resolvers/zod' +import { Button } from '../ui/button' +import { Select } from '@headlessui/react' + +const formSchema = z + .object({ + copyId: z.string(), + untilDate: z.string().date(), + }) + .required() + +type Props = { + holdRequest: HoldRequest + isOpen: boolean + onOpenChange: () => void +} +const ApproveHoldRequestModal = (props: Props) => { + const { holdRequest, isOpen, onOpenChange } = props + + const customVariants: Variants = { + initial: { + scale: 0.9, + filter: 'blur(10px)', + y: '100%', + }, + animate: { + scale: 1, + filter: 'blur(0px)', + y: 0, + }, + } + + const customTransition: Transition = { + type: 'spring', + bounce: 0, + duration: 0.4, + } + + const hold = holdRequest as HoldRequest + const book = hold.book as Book + const repository = hold.repository as Repository + const userRequested = hold.userRequested as User + + const [availableCopies, setAvailableCopies] = useState([]) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + copyId: undefined, + untilDate: undefined, + }, + }) + + useEffect(() => { + if (!isOpen || availableCopies.length) return + + findCopiesOfBookInRepository({ + bookId: book.id, + repositoryId: repository.id, + }) + .then(async (response) => { + const copies = response?.docs || [] + const availableCopies = copies.filter((c) => !c.holdRequests?.docs?.length) + console.log(availableCopies) + setAvailableCopies(availableCopies) + }) + .catch((err) => { + availableCopies + console.error(err) + }) + .finally(() => {}) + }, [availableCopies, setAvailableCopies, isOpen]) + + const onSubmit = (values: z.infer) => { + const acceptHoldPayload = { + copyId: parseInt(values.copyId, 10), + untilDate: values.untilDate, + } + console.log(acceptHoldPayload) + onOpenChange() + } + + return ( + + + + + Hold a copy for {`${userRequested.firstName} ${userRequested.lastName}`}? + + + Select which physical copy, and until when, you would like to hold. + + +
+ + ( + + Copy + + + + + + )} + /> + + ( + + Until + + + + + + )} + /> + + + + + +
+
+ ) +} + +export default ApproveHoldRequestModal diff --git a/src/components/Manage/HoldRequests.tsx b/src/components/Manage/HoldRequests.tsx index a6b7c63..b24748b 100644 --- a/src/components/Manage/HoldRequests.tsx +++ b/src/components/Manage/HoldRequests.tsx @@ -1,7 +1,10 @@ import { PaginatedDocs } from 'payload' -import { Author, Book, HoldRequest, Repository } from '@/payload-types' +import { Author, Book, HoldRequest, Repository, User } from '@/payload-types' import { Button } from '../ui/button' import Image from 'next/image' +import ApproveHoldRequestModal from './ApproveHoldRequestModal' +import { DialogTrigger } from '../ui/dialog' +import { useState } from 'react' type Props = { repos: PaginatedDocs | null @@ -9,6 +12,8 @@ type Props = { const HoldRequestNotifications = (props: Props) => { const { repos } = props + const [openedModalId, setOpenedModalId] = useState(null) + const totalHoldNotifications = repos?.docs.flatMap((r) => r.holdRequests?.docs).length || 0 const holdRequestsByRepoElements = repos?.docs.map((r) => { @@ -18,6 +23,9 @@ const HoldRequestNotifications = (props: Props) => { const hold = h as HoldRequest const book = hold.book as Book const authors = book.authors as Author[] + const dateRequested = hold.dateRequested ? new Date(hold.dateRequested) : new Date() + const userRequested = hold.userRequested as User + return (
  • @@ -37,12 +45,12 @@ const HoldRequestNotifications = (props: Props) => { ) : ( - Requested - {!!hold.dateRequested && ( - - )} + + + {`${userRequested.firstName} ${userRequested.lastName}`} + )}
    @@ -57,15 +65,24 @@ const HoldRequestNotifications = (props: Props) => { />
    -
    +
    - + +
  • diff --git a/src/components/login-form.tsx b/src/components/login-form.tsx index b954298..a51ea61 100644 --- a/src/components/login-form.tsx +++ b/src/components/login-form.tsx @@ -7,9 +7,11 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useRouter } from 'next/navigation' import { useState } from 'react' +import { useGlobal } from '@/providers/GlobalProvider' export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) { const router = useRouter() + const { setUser } = useGlobal() const [isLoading, setIsLoading] = useState(false) const handleSubmit = async (e: React.FormEvent) => { @@ -38,6 +40,8 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) }) const user = await loginReq.json() + setUser(user.user) + if (user.token) router.push('/') } catch (error) { console.error('Login failed:', error) diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx new file mode 100644 index 0000000..acbefeb --- /dev/null +++ b/src/components/ui/combobox.tsx @@ -0,0 +1,76 @@ +'use client' + +import * as React from 'react' +import { Check, ChevronsUpDown } from 'lucide-react' + +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' + +type Option = { + value: any + label: string +} + +type Props = { + options: Option[] + placeholder: string +} +export function Combobox(props: Props) { + const { options, placeholder } = props + + const [open, setOpen] = React.useState(false) + const [value, setValue] = React.useState() + + console.log(options) + console.log(open) + + return ( + + + + + + + + + No Copies found. + + {options.map((o) => ( + { + setValue(currentValue === value ? undefined : currentValue) + setOpen(false) + }} + > + {o.label} + + + ))} + + + + + + ) +} diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..4ca5349 --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,177 @@ +"use client" + +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + ...props +}: React.ComponentProps & { + title?: string + description?: string +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
    + + +
    + ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..670a316 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,335 @@ +'use client'; +import { AnimatePresence, motion, Transition, Variants } from 'motion/react'; +import React, { createContext, useContext, useEffect, useRef } from 'react'; +import { cn } from '@/lib/utils'; +import { useId } from 'react'; +import { createPortal } from 'react-dom'; +import { X } from 'lucide-react'; +import { usePreventScroll } from '@/hooks/usePreventScroll'; + +const DialogContext = createContext<{ + isOpen: boolean; + setIsOpen: (open: boolean) => void; + dialogRef: React.RefObject; + variants: Variants; + transition?: Transition; + ids: { + dialog: string; + title: string; + description: string; + }; + onAnimationComplete: (definition: string) => void; + handleTrigger: () => void; +} | null>(null); + +const defaultVariants: Variants = { + initial: { + opacity: 0, + scale: 0.9, + }, + animate: { + opacity: 1, + scale: 1, + }, +}; + +const defaultTransition: Transition = { + ease: 'easeOut', + duration: 0.2, +}; + +export type DialogProps = { + children: React.ReactNode; + variants?: Variants; + transition?: Transition; + className?: string; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + open?: boolean; +}; + +function Dialog({ + children, + variants = defaultVariants, + transition = defaultTransition, + defaultOpen, + onOpenChange, + open, +}: DialogProps) { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState( + defaultOpen || false + ); + const dialogRef = useRef(null); + const isOpen = open !== undefined ? open : uncontrolledOpen; + + // prevent scroll when dialog is open on iOS + usePreventScroll({ + isDisabled: !isOpen, + }); + + const setIsOpen = React.useCallback( + (value: boolean) => { + setUncontrolledOpen(value); + onOpenChange?.(value); + }, + [onOpenChange] + ); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + + if (isOpen) { + document.body.classList.add('overflow-hidden'); + } else { + document.body.classList.remove('overflow-hidden'); + } + + const handleCancel = (e: Event) => { + e.preventDefault(); + if (isOpen) { + setIsOpen(false); + } + }; + + dialog.addEventListener('cancel', handleCancel); + return () => { + dialog.removeEventListener('cancel', handleCancel); + document.body.classList.remove('overflow-hidden'); + }; + }, [dialogRef, isOpen, setIsOpen]); + + useEffect(() => { + if (isOpen && dialogRef.current) { + dialogRef.current.showModal(); + } + }, [isOpen]); + + const handleTrigger = () => { + setIsOpen(true); + }; + + const onAnimationComplete = (definition: string) => { + if (definition === 'exit' && !isOpen) { + dialogRef.current?.close(); + } + }; + + const baseId = useId(); + const ids = { + dialog: `motion-ui-dialog-${baseId}`, + title: `motion-ui-dialog-title-${baseId}`, + description: `motion-ui-dialog-description-${baseId}`, + }; + + return ( + + {children} + + ); +} + +export type DialogTriggerProps = { + children: React.ReactNode; + className?: string; +}; + +function DialogTrigger({ children, className }: DialogTriggerProps) { + const context = useContext(DialogContext); + if (!context) throw new Error('DialogTrigger must be used within Dialog'); + + return ( + + ); +} + +export type DialogPortalProps = { + children: React.ReactNode; + container?: HTMLElement | null; +}; + +function DialogPortal({ + children, + container = typeof window !== 'undefined' ? document.body : null, +}: DialogPortalProps) { + const [mounted, setMounted] = React.useState(false); + const [portalContainer, setPortalContainer] = + React.useState(null); + + useEffect(() => { + setMounted(true); + setPortalContainer(container || document.body); + return () => setMounted(false); + }, [container]); + + if (!mounted || !portalContainer) { + return null; + } + + return createPortal(children, portalContainer); +} +export type DialogContentProps = { + children: React.ReactNode; + className?: string; + container?: HTMLElement; +}; + +function DialogContent({ children, className, container }: DialogContentProps) { + const context = useContext(DialogContext); + if (!context) throw new Error('DialogContent must be used within Dialog'); + const { + isOpen, + setIsOpen, + dialogRef, + variants, + transition, + ids, + onAnimationComplete, + } = context; + + const content = ( + + {isOpen && ( + } + id={ids.dialog} + aria-labelledby={ids.title} + aria-describedby={ids.description} + aria-modal='true' + role='dialog' + onClick={(e) => { + if (e.target === dialogRef.current) { + setIsOpen(false); + } + }} + initial='initial' + animate='animate' + exit='exit' + variants={variants} + transition={transition} + onAnimationComplete={onAnimationComplete} + className={cn( + 'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform rounded-lg border border-zinc-200 p-0 shadow-lg dark:border dark:border-zinc-700', + 'backdrop:bg-black/50 backdrop:backdrop-blur-xs', + 'open:flex open:flex-col', + className + )} + > +
    {children}
    +
    + )} +
    + ); + + return {content}; +} + +export type DialogHeaderProps = { + children: React.ReactNode; + className?: string; +}; + +function DialogHeader({ children, className }: DialogHeaderProps) { + return ( +
    {children}
    + ); +} + +export type DialogTitleProps = { + children: React.ReactNode; + className?: string; +}; + +function DialogTitle({ children, className }: DialogTitleProps) { + const context = useContext(DialogContext); + if (!context) throw new Error('DialogTitle must be used within Dialog'); + + return ( +

    + {children} +

    + ); +} + +export type DialogDescriptionProps = { + children: React.ReactNode; + className?: string; +}; + +function DialogDescription({ children, className }: DialogDescriptionProps) { + const context = useContext(DialogContext); + if (!context) throw new Error('DialogDescription must be used within Dialog'); + + return ( +

    + {children} +

    + ); +} + +export type DialogCloseProps = { + className?: string; + children?: React.ReactNode; + disabled?: boolean; +}; + +function DialogClose({ className, children, disabled }: DialogCloseProps) { + const context = useContext(DialogContext); + if (!context) throw new Error('DialogClose must be used within Dialog'); + + return ( + + ); +} + +export { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogClose, +}; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..01e468b --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +function Popover({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..dcbbc0c --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,185 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/src/hooks/usePreventScroll.tsx b/src/hooks/usePreventScroll.tsx new file mode 100644 index 0000000..744eb46 --- /dev/null +++ b/src/hooks/usePreventScroll.tsx @@ -0,0 +1,360 @@ +import { useEffect, useLayoutEffect } from 'react'; + +function isMac(): boolean | undefined { + return testPlatform(/^Mac/); +} + +function isIPhone(): boolean | undefined { + return testPlatform(/^iPhone/); +} + +function isIPad(): boolean | undefined { + return ( + testPlatform(/^iPad/) || + // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support. + (isMac() && navigator.maxTouchPoints > 1) + ); +} + +function isIOS(): boolean | undefined { + return isIPhone() || isIPad(); +} + +function testPlatform(re: RegExp): boolean | undefined { + return typeof window !== 'undefined' && window.navigator != null + ? re.test(window.navigator.platform) + : undefined; +} + +const KEYBOARD_BUFFER = 24; + +export const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +interface PreventScrollOptions { + /** Whether the scroll lock is disabled. */ + isDisabled?: boolean; + focusCallback?: () => void; +} + +function chain(...callbacks: any[]): (...args: any[]) => void { + return (...args: any[]) => { + for (let callback of callbacks) { + if (typeof callback === 'function') { + callback(...args); + } + } + }; +} + +// @ts-ignore +const visualViewport = typeof document !== 'undefined' && window.visualViewport; + +export function isScrollable(node: Element): boolean { + let style = window.getComputedStyle(node); + return /(auto|scroll)/.test( + style.overflow + style.overflowX + style.overflowY + ); +} + +export function getScrollParent(node: Element): Element { + if (isScrollable(node)) { + node = node.parentElement as HTMLElement; + } + + while (node && !isScrollable(node)) { + node = node.parentElement as HTMLElement; + } + + return node || document.scrollingElement || document.documentElement; +} + +// HTML input types that do not cause the software keyboard to appear. +const nonTextInputTypes = new Set([ + 'checkbox', + 'radio', + 'range', + 'color', + 'file', + 'image', + 'button', + 'submit', + 'reset', +]); + +// The number of active usePreventScroll calls. Used to determine whether to revert back to the original page style/scroll position +let preventScrollCount = 0; +let restore: () => void; + +/** + * Prevents scrolling on the document body on mount, and + * restores it on unmount. Also ensures that content does not + * shift due to the scrollbars disappearing. + */ +export function usePreventScroll(options: PreventScrollOptions = {}) { + let { isDisabled } = options; + + useIsomorphicLayoutEffect(() => { + if (isDisabled) { + return; + } + + preventScrollCount++; + if (preventScrollCount === 1) { + if (isIOS()) { + restore = preventScrollMobileSafari(); + } + } + + return () => { + preventScrollCount--; + if (preventScrollCount === 0) { + restore?.(); + } + }; + }, [isDisabled]); +} + +// Mobile Safari is a whole different beast. Even with overflow: hidden, +// it still scrolls the page in many situations: +// +// 1. When the bottom toolbar and address bar are collapsed, page scrolling is always allowed. +// 2. When the keyboard is visible, the viewport does not resize. Instead, the keyboard covers part of +// it, so it becomes scrollable. +// 3. When tapping on an input, the page always scrolls so that the input is centered in the visual viewport. +// This may cause even fixed position elements to scroll off the screen. +// 4. When using the next/previous buttons in the keyboard to navigate between inputs, the whole page always +// scrolls, even if the input is inside a nested scrollable element that could be scrolled instead. +// +// In order to work around these cases, and prevent scrolling without jankiness, we do a few things: +// +// 1. Prevent default on `touchmove` events that are not in a scrollable element. This prevents touch scrolling +// on the window. +// 2. Prevent default on `touchmove` events inside a scrollable element when the scroll position is at the +// top or bottom. This avoids the whole page scrolling instead, but does prevent overscrolling. +// 3. Prevent default on `touchend` events on input elements and handle focusing the element ourselves. +// 4. When focusing an input, apply a transform to trick Safari into thinking the input is at the top +// of the page, which prevents it from scrolling the page. After the input is focused, scroll the element +// into view ourselves, without scrolling the whole page. +// 5. Offset the body by the scroll position using a negative margin and scroll to the top. This should appear the +// same visually, but makes the actual scroll position always zero. This is required to make all of the +// above work or Safari will still try to scroll the page when focusing an input. +// 6. As a last resort, handle window scroll events, and scroll back to the top. This can happen when attempting +// to navigate to an input with the next/previous buttons that's outside a modal. +function preventScrollMobileSafari() { + let scrollable: Element; + let lastY = 0; + let onTouchStart = (e: TouchEvent) => { + // Store the nearest scrollable parent element from the element that the user touched. + scrollable = getScrollParent(e.target as Element); + if ( + scrollable === document.documentElement && + scrollable === document.body + ) { + return; + } + + lastY = e.changedTouches[0].pageY; + }; + + let onTouchMove = (e: TouchEvent) => { + // Prevent scrolling the window. + if ( + !scrollable || + scrollable === document.documentElement || + scrollable === document.body + ) { + e.preventDefault(); + return; + } + + // Prevent scrolling up when at the top and scrolling down when at the bottom + // of a nested scrollable area, otherwise mobile Safari will start scrolling + // the window instead. Unfortunately, this disables bounce scrolling when at + // the top but it's the best we can do. + let y = e.changedTouches[0].pageY; + let scrollTop = scrollable.scrollTop; + let bottom = scrollable.scrollHeight - scrollable.clientHeight; + + if (bottom === 0) { + return; + } + + if ((scrollTop <= 0 && y > lastY) || (scrollTop >= bottom && y < lastY)) { + e.preventDefault(); + } + + lastY = y; + }; + + let onTouchEnd = (e: TouchEvent) => { + let target = e.target as HTMLElement; + + // Apply this change if we're not already focused on the target element + if (isInput(target) && target !== document.activeElement) { + e.preventDefault(); + + // Apply a transform to trick Safari into thinking the input is at the top of the page + // so it doesn't try to scroll it into view. When tapping on an input, this needs to + // be done before the "focus" event, so we have to focus the element ourselves. + target.style.transform = 'translateY(-2000px)'; + target.focus(); + requestAnimationFrame(() => { + target.style.transform = ''; + }); + } + }; + + let onFocus = (e: FocusEvent) => { + let target = e.target as HTMLElement; + if (isInput(target)) { + // Transform also needs to be applied in the focus event in cases where focus moves + // other than tapping on an input directly, e.g. the next/previous buttons in the + // software keyboard. In these cases, it seems applying the transform in the focus event + // is good enough, whereas when tapping an input, it must be done before the focus event. 🤷‍♂️ + target.style.transform = 'translateY(-2000px)'; + requestAnimationFrame(() => { + target.style.transform = ''; + + // This will have prevented the browser from scrolling the focused element into view, + // so we need to do this ourselves in a way that doesn't cause the whole page to scroll. + if (visualViewport) { + if (visualViewport.height < window.innerHeight) { + // If the keyboard is already visible, do this after one additional frame + // to wait for the transform to be removed. + requestAnimationFrame(() => { + scrollIntoView(target); + }); + } else { + // Otherwise, wait for the visual viewport to resize before scrolling so we can + // measure the correct position to scroll to. + visualViewport.addEventListener( + 'resize', + () => scrollIntoView(target), + { once: true } + ); + } + } + }); + } + }; + + let onWindowScroll = () => { + // Last resort. If the window scrolled, scroll it back to the top. + // It should always be at the top because the body will have a negative margin (see below). + window.scrollTo(0, 0); + }; + + // Record the original scroll position so we can restore it. + // Then apply a negative margin to the body to offset it by the scroll position. This will + // enable us to scroll the window to the top, which is required for the rest of this to work. + let scrollX = window.pageXOffset; + let scrollY = window.pageYOffset; + + let restoreStyles = chain( + setStyle( + document.documentElement, + 'paddingRight', + `${window.innerWidth - document.documentElement.clientWidth}px` + ) + // setStyle(document.documentElement, 'overflow', 'hidden'), + // setStyle(document.body, 'marginTop', `-${scrollY}px`), + ); + + // Scroll to the top. The negative margin on the body will make this appear the same. + window.scrollTo(0, 0); + + let removeEvents = chain( + addEvent(document, 'touchstart', onTouchStart, { + passive: false, + capture: true, + }), + addEvent(document, 'touchmove', onTouchMove, { + passive: false, + capture: true, + }), + addEvent(document, 'touchend', onTouchEnd, { + passive: false, + capture: true, + }), + addEvent(document, 'focus', onFocus, true), + addEvent(window, 'scroll', onWindowScroll) + ); + + return () => { + // Restore styles and scroll the page back to where it was. + restoreStyles(); + removeEvents(); + window.scrollTo(scrollX, scrollY); + }; +} + +// Sets a CSS property on an element, and returns a function to revert it to the previous value. +function setStyle( + element: HTMLElement, + style: keyof React.CSSProperties, + value: string +) { + // https://github.com/microsoft/TypeScript/issues/17827#issuecomment-391663310 + // @ts-ignore + let cur = element.style[style]; + // @ts-ignore + element.style[style] = value; + + return () => { + // @ts-ignore + element.style[style] = cur; + }; +} + +// Adds an event listener to an element, and returns a function to remove it. +function addEvent( + target: EventTarget, + event: K, + handler: (this: Document, ev: GlobalEventHandlersEventMap[K]) => any, + options?: boolean | AddEventListenerOptions +) { + // @ts-ignore + target.addEventListener(event, handler, options); + + return () => { + // @ts-ignore + target.removeEventListener(event, handler, options); + }; +} + +function scrollIntoView(target: Element) { + let root = document.scrollingElement || document.documentElement; + while (target && target !== root) { + // Find the parent scrollable element and adjust the scroll position if the target is not already in view. + let scrollable = getScrollParent(target); + if ( + scrollable !== document.documentElement && + scrollable !== document.body && + scrollable !== target + ) { + let scrollableTop = scrollable.getBoundingClientRect().top; + let targetTop = target.getBoundingClientRect().top; + let targetBottom = target.getBoundingClientRect().bottom; + // Buffer is needed for some edge cases + const keyboardHeight = + scrollable.getBoundingClientRect().bottom + KEYBOARD_BUFFER; + + if (targetBottom > keyboardHeight) { + scrollable.scrollTop += targetTop - scrollableTop; + } + } + + // @ts-ignore + target = scrollable.parentElement; + } +} + +export function isInput(target: Element) { + return ( + (target instanceof HTMLInputElement && + !nonTextInputTypes.has(target.type)) || + target instanceof HTMLTextAreaElement || + (target instanceof HTMLElement && target.isContentEditable) + ); +} diff --git a/src/serverActions/FindCopiesOnHoldRequest.ts b/src/serverActions/FindCopiesOnHoldRequest.ts new file mode 100644 index 0000000..2785661 --- /dev/null +++ b/src/serverActions/FindCopiesOnHoldRequest.ts @@ -0,0 +1,56 @@ +'use server' + +import { getPayload, PaginatedDocs } from 'payload' +import config from '@/payload.config' +import { Copy } from '@/payload-types' + +type Props = { + bookId: number, + repositoryId: number +} +export const findCopiesOfBookInRepository = async (props: Props): Promise | null> => { + const { bookId, repositoryId } = props + + const payloadConfig = await config + const payload = await getPayload({ config: payloadConfig }) + + try { + const findResponse = (await payload.find({ + collection: 'copies', + depth: 2, + limit: 25, + overrideAccess: false, + where: { + and: [ + { + 'repository.id': { + equals: repositoryId + } + }, + { + 'book.id': { + equals: bookId + } + }, + ] + }, + select: { + id: true, + label: true, + repository: true, + book: true, + holdRequests: true, + }, + joins: { + holdRequests: { + limit: 100, + }, + } + })) as PaginatedDocs + + return findResponse + } catch (err) { + console.log(err) + return null + } +}