diff --git a/.vscode/settings.json b/.vscode/settings.json index 2f8184b5..f041c11d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "exif", "exiftool", "favicons", + "Favoriting", "favs", "ghijklmnopqrstuv", "GPSH", @@ -53,6 +54,7 @@ "thephotoblog", "trpc", "Turbopack", + "Unfavoriting", "unnest", "upstash", "UsKSGcbt", diff --git a/package.json b/package.json index 5cdbddaa..e5376072 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,12 @@ "analyze": "ANALYZE=true next build" }, "dependencies": { - "@ai-sdk/openai": "^1.3.16", + "@ai-sdk/openai": "^1.3.18", "@aws-sdk/client-s3": "3.787.0", "@aws-sdk/s3-request-presigner": "3.787.0", - "@radix-ui/react-dialog": "^1.1.10", - "@radix-ui/react-dropdown-menu": "^2.1.11", - "@radix-ui/react-tooltip": "^1.2.3", + "@radix-ui/react-dialog": "^1.1.11", + "@radix-ui/react-dropdown-menu": "^2.1.12", + "@radix-ui/react-tooltip": "^1.2.4", "@radix-ui/react-visually-hidden": "^1.2.0", "@upstash/ratelimit": "^2.0.5", "@upstash/redis": "^1.34.8", @@ -28,12 +28,12 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "fast-deep-equal": "^3.1.3", - "framer-motion": "^12.7.4", + "framer-motion": "^12.9.1", "nanoid": "^5.1.5", "next": "15.3.1", "next-auth": "5.0.0-beta.25", "next-themes": "^0.4.6", - "pg": "^8.14.1", + "pg": "^8.15.5", "react": "19.1.0", "react-dom": "19.1.0", "react-icons": "^5.5.0", @@ -57,12 +57,12 @@ "@testing-library/react": "^16.3.0", "@types/jest": "^29.5.14", "@types/node": "^22.14.1", - "@types/pg": "^8.11.13", + "@types/pg": "^8.11.14", "@types/react": "19.1.2", "@types/react-dom": "19.1.2", "@types/sanitize-html": "^2.15.0", "cross-fetch": "^4.1.0", - "eslint": "9.25.0", + "eslint": "9.25.1", "eslint-config-next": "15.3.1", "eslint-plugin-react-hooks": "^5.2.0", "jest": "^29.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd57a200..05707716 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@ai-sdk/openai': - specifier: ^1.3.16 - version: 1.3.16(zod@3.24.2) + specifier: ^1.3.18 + version: 1.3.18(zod@3.24.2) '@aws-sdk/client-s3': specifier: 3.787.0 version: 3.787.0 @@ -18,14 +18,14 @@ importers: specifier: 3.787.0 version: 3.787.0 '@radix-ui/react-dialog': - specifier: ^1.1.10 - version: 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-dropdown-menu': - specifier: ^2.1.11 - version: 2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^2.1.12 + version: 2.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-tooltip': - specifier: ^1.2.3 - version: 1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-visually-hidden': specifier: ^1.2.0 version: 1.2.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -66,8 +66,8 @@ importers: specifier: ^3.1.3 version: 3.1.3 framer-motion: - specifier: ^12.7.4 - version: 12.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^12.9.1 + version: 12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) nanoid: specifier: ^5.1.5 version: 5.1.5 @@ -81,8 +81,8 @@ importers: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) pg: - specifier: ^8.14.1 - version: 8.14.1 + specifier: ^8.15.5 + version: 8.15.5 react: specifier: 19.1.0 version: 19.1.0 @@ -148,8 +148,8 @@ importers: specifier: ^22.14.1 version: 22.14.1 '@types/pg': - specifier: ^8.11.13 - version: 8.11.13 + specifier: ^8.11.14 + version: 8.11.14 '@types/react': specifier: 19.1.2 version: 19.1.2 @@ -163,14 +163,14 @@ importers: specifier: ^4.1.0 version: 4.1.0 eslint: - specifier: 9.25.0 - version: 9.25.0(jiti@2.4.2) + specifier: 9.25.1 + version: 9.25.1(jiti@2.4.2) eslint-config-next: specifier: 15.3.1 - version: 15.3.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) + version: 15.3.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.25.0(jiti@2.4.2)) + version: 5.2.0(eslint@9.25.1(jiti@2.4.2)) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)) @@ -195,8 +195,8 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} - '@ai-sdk/openai@1.3.16': - resolution: {integrity: sha512-pjtiBKt1GgaSKZryTbM3tqgoegJwgAUlp1+X5uN6T+VPnI4FLSymV65tyloWzDlyqZmi9HXnnSRPu76VoL5D5g==} + '@ai-sdk/openai@1.3.18': + resolution: {integrity: sha512-gqOHTOu62Tm2r4yDQx/Z5tWAgUrcTK8wXnC4A8zF/VOCzIjJDxxPsqJRTtQTMgIdGXhwmsv2sZ2PzvvuLeZeEg==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -610,8 +610,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.25.0': - resolution: {integrity: sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==} + '@eslint/js@9.25.1': + resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -1005,8 +1005,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dialog@1.1.10': - resolution: {integrity: sha512-m6pZb0gEM5uHPSb+i2nKKGQi/HMSVjARMsLMWQfKDP+eJ6B+uqryHnXhpnohTWElw+vEcMk/o4wJODtdRKHwqg==} + '@radix-ui/react-dialog@1.1.11': + resolution: {integrity: sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1040,8 +1040,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.1.11': - resolution: {integrity: sha512-wbPE3cFBfLl+S+LCxChWQGX0k14zUxgvep1HEnLhJ9mNhjyO3ETzRviAeKZ3XomT/iVRRZAWFsnFZ3N0wI8OmA==} + '@radix-ui/react-dropdown-menu@2.1.12': + resolution: {integrity: sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1093,8 +1093,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-menu@2.1.11': - resolution: {integrity: sha512-sbFI4Qaw02J0ogmR9tOMsSqsdrGNpUanlPYAqTE2JJafow8ecHtykg4fSTjNHBdDl4deiKMK+RhTEwyVhP7UDA==} + '@radix-ui/react-menu@2.1.12': + resolution: {integrity: sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1132,8 +1132,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.3': - resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==} + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1202,8 +1202,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-tooltip@1.2.3': - resolution: {integrity: sha512-0KX7jUYFA02np01Y11NWkk6Ip6TqMNmD4ijLelYAzeIndl2aVeltjJFJ2gwjNa1P8U/dgjQ+8cr9Y3Ni+ZNoRA==} + '@radix-ui/react-tooltip@1.2.4': + resolution: {integrity: sha512-DyW8VVeeMSSLFvAmnVnCwvI3H+1tpJFHT50r+tdOoMse9XqYDBCcyux8u3G2y+LOpt7fPQ6KKH0mhs+ce1+Z5w==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1724,8 +1724,8 @@ packages: '@types/node@22.14.1': resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} - '@types/pg@8.11.13': - resolution: {integrity: sha512-6kXByGkvRvwXLuyaWzsebs2du6+XuAB2CuMsuzP7uaihQahshVgSmB22Pmh0vQMkQ1h5+PZU0d+Di1o+WpVWJg==} + '@types/pg@8.11.14': + resolution: {integrity: sha512-qyD11E5R3u0eJmd1lB0WnWKXJGA7s015nyARWljfz5DcX83TKAIlY+QrmvzQTsbIe+hkiFtkyL2gHC6qwF6Fbg==} '@types/react-dom@19.1.2': resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} @@ -2498,8 +2498,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.25.0: - resolution: {integrity: sha512-MsBdObhM4cEwkzCiraDv7A6txFXEqtNXOb877TsSp2FCkBNl8JfVQrmiuDqC1IkejT6JLPzYBXx/xAiYhyzgGA==} + eslint@9.25.1: + resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2611,8 +2611,8 @@ packages: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} - framer-motion@12.7.4: - resolution: {integrity: sha512-jX0bPsTmU0oPZTYz/dVyD0dmOyEOEJvdn0TaZBE5I8g2GvVnnQnW9f65cJnoVfUkY3WZWNXGXnPbVA9YnaIfVA==} + framer-motion@12.9.1: + resolution: {integrity: sha512-dZBp2TO0a39Cc24opshlLoM0/OdTZVKzcXWuhntfwy2Qgz3t9+N4sTyUqNANyHaRFiJUWbwwsXeDvQkEBPky+g==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -3342,11 +3342,11 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - motion-dom@12.7.4: - resolution: {integrity: sha512-1ZUHAoSUMMxP6jPqyxlk9XUfb6NxMsnWPnH2YGhrOhTURLcXWbETi6eemoKb60Pe32NVJYduL4B62VQSO5Jq8Q==} + motion-dom@12.9.1: + resolution: {integrity: sha512-xqXEwRLDYDTzOgXobSoWtytRtGlf7zdkRfFbrrdP7eojaGQZ5Go4OOKtgnx7uF8sAkfr1ZjMvbCJSCIT2h6fkQ==} - motion-utils@12.7.2: - resolution: {integrity: sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==} + motion-utils@12.8.3: + resolution: {integrity: sha512-GYVauZEbca8/zOhEiYOY9/uJeedYQld6co/GJFKOy//0c/4lDqk0zB549sBYqqV2iMuX+uHrY1E5zd8A2L+1Lw==} mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} @@ -3543,11 +3543,11 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - pg-cloudflare@1.1.1: - resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + pg-cloudflare@1.2.5: + resolution: {integrity: sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==} - pg-connection-string@2.7.0: - resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + pg-connection-string@2.8.5: + resolution: {integrity: sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} @@ -3557,14 +3557,17 @@ packages: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} - pg-pool@3.8.0: - resolution: {integrity: sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==} + pg-pool@3.9.5: + resolution: {integrity: sha512-DxyAlOgvUzRFpFAZjbCc8fUfG7BcETDHgepFPf724B0i08k9PAiZV1tkGGgQIL0jbMEuR9jW1YN7eX+WgXxCsQ==} peerDependencies: pg: '>=8.0' pg-protocol@1.8.0: resolution: {integrity: sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==} + pg-protocol@1.9.5: + resolution: {integrity: sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -3573,8 +3576,8 @@ packages: resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} engines: {node: '>=10'} - pg@8.14.1: - resolution: {integrity: sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==} + pg@8.15.5: + resolution: {integrity: sha512-EpAhHFQc+aH9VfeffWIVC+XXk6lmAhS9W1FxtxcPXs94yxhrI1I6w/zkWfIOII/OkBv3Be04X3xMOj0kQ78l6w==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -4357,7 +4360,7 @@ snapshots: '@adobe/css-tools@4.4.2': {} - '@ai-sdk/openai@1.3.16(zod@3.24.2)': + '@ai-sdk/openai@1.3.18(zod@3.24.2)': dependencies: '@ai-sdk/provider': 1.1.3 '@ai-sdk/provider-utils': 2.2.7(zod@3.24.2) @@ -5086,9 +5089,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.25.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.4.1(eslint@9.25.1(jiti@2.4.2))': dependencies: - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -5121,7 +5124,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.25.0': {} + '@eslint/js@9.25.1': {} '@eslint/object-schema@2.1.6': {} @@ -5532,7 +5535,7 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 - '@radix-ui/react-dialog@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-dialog@1.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) @@ -5542,7 +5545,7 @@ snapshots: '@radix-ui/react-focus-scope': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) @@ -5573,13 +5576,13 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) - '@radix-ui/react-dropdown-menu@2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-dropdown-menu@2.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0) - '@radix-ui/react-menu': 2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-menu': 2.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) react: 19.1.0 @@ -5619,7 +5622,7 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 - '@radix-ui/react-menu@2.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-menu@2.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 '@radix-ui/react-collection': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -5632,7 +5635,7 @@ snapshots: '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-popper': 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-roving-focus': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0) @@ -5673,7 +5676,7 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) - '@radix-ui/react-presence@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) @@ -5732,7 +5735,7 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 - '@radix-ui/react-tooltip@1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@radix-ui/react-tooltip@1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) @@ -5741,7 +5744,7 @@ snapshots: '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-popper': 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) @@ -6346,7 +6349,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/pg@8.11.13': + '@types/pg@8.11.14': dependencies: '@types/node': 22.14.1 pg-protocol: 1.8.0 @@ -6374,15 +6377,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.24.1 - '@typescript-eslint/type-utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.24.1 - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -6391,14 +6394,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.24.1 '@typescript-eslint/types': 8.24.1 '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.24.1 debug: 4.4.0 - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6408,12 +6411,12 @@ snapshots: '@typescript-eslint/types': 8.24.1 '@typescript-eslint/visitor-keys': 8.24.1 - '@typescript-eslint/type-utils@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3) - '@typescript-eslint/utils': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) debug: 4.4.0 - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) ts-api-utils: 2.0.1(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -6435,13 +6438,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)': + '@typescript-eslint/utils@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.25.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.25.1(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.24.1 '@typescript-eslint/types': 8.24.1 '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.8.3) - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -6795,7 +6798,7 @@ snapshots: cmdk@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@19.1.0) - '@radix-ui/react-dialog': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': 1.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-id': 1.1.0(@types/react@19.1.2)(react@19.1.0) '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 @@ -7126,19 +7129,19 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@15.3.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3): + eslint-config-next@15.3.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): dependencies: '@next/eslint-plugin-next': 15.3.1 '@rushstack/eslint-patch': 1.10.5 - '@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) - '@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.25.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.0(jiti@2.4.2)) - eslint-plugin-react: 7.37.4(eslint@9.25.0(jiti@2.4.2)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.25.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-react: 7.37.4(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.25.1(jiti@2.4.2)) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -7154,33 +7157,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 enhanced-resolve: 5.18.1 - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) get-tsconfig: 4.10.0 is-bun-module: 1.3.0 stable-hash: 0.0.4 tinyglobby: 0.2.11 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) - eslint: 9.25.0(jiti@2.4.2) + '@typescript-eslint/parser': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7189,9 +7192,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.25.1(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -7203,13 +7206,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.24.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.24.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.25.0(jiti@2.4.2)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.25.1(jiti@2.4.2)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 @@ -7219,7 +7222,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -7228,11 +7231,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.25.0(jiti@2.4.2)): + eslint-plugin-react-hooks@5.2.0(eslint@9.25.1(jiti@2.4.2)): dependencies: - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) - eslint-plugin-react@7.37.4(eslint@9.25.0(jiti@2.4.2)): + eslint-plugin-react@7.37.4(eslint@9.25.1(jiti@2.4.2)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -7240,7 +7243,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.25.0(jiti@2.4.2) + eslint: 9.25.1(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -7263,15 +7266,15 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.25.0(jiti@2.4.2): + eslint@9.25.1(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.25.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.25.1(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.0 '@eslint/config-helpers': 0.2.1 '@eslint/core': 0.13.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.25.0 + '@eslint/js': 9.25.1 '@eslint/plugin-kit': 0.2.8 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -7421,10 +7424,10 @@ snapshots: es-set-tostringtag: 2.1.0 mime-types: 2.1.35 - framer-motion@12.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + framer-motion@12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - motion-dom: 12.7.4 - motion-utils: 12.7.2 + motion-dom: 12.9.1 + motion-utils: 12.8.3 tslib: 2.8.1 optionalDependencies: react: 19.1.0 @@ -8324,11 +8327,11 @@ snapshots: minimist@1.2.8: {} - motion-dom@12.7.4: + motion-dom@12.9.1: dependencies: - motion-utils: 12.7.2 + motion-utils: 12.8.3 - motion-utils@12.7.2: {} + motion-utils@12.8.3: {} mrmime@2.0.1: {} @@ -8506,21 +8509,23 @@ snapshots: path-parse@1.0.7: {} - pg-cloudflare@1.1.1: + pg-cloudflare@1.2.5: optional: true - pg-connection-string@2.7.0: {} + pg-connection-string@2.8.5: {} pg-int8@1.0.1: {} pg-numeric@1.0.2: {} - pg-pool@3.8.0(pg@8.14.1): + pg-pool@3.9.5(pg@8.15.5): dependencies: - pg: 8.14.1 + pg: 8.15.5 pg-protocol@1.8.0: {} + pg-protocol@1.9.5: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -8539,15 +8544,15 @@ snapshots: postgres-interval: 3.0.0 postgres-range: 1.1.4 - pg@8.14.1: + pg@8.15.5: dependencies: - pg-connection-string: 2.7.0 - pg-pool: 3.8.0(pg@8.14.1) - pg-protocol: 1.8.0 + pg-connection-string: 2.8.5 + pg-pool: 3.9.5(pg@8.15.5) + pg-protocol: 1.9.5 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.1.1 + pg-cloudflare: 1.2.5 pgpass@1.0.5: dependencies: diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index c6a09b40..51c19e92 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -24,9 +24,7 @@ import IconRecipe from '@/components/icons/IconRecipe'; import IconTag from '@/components/icons/IconTag'; import IconFolder from '@/components/icons/IconFolder'; import IconSignOut from '@/components/icons/IconSignOut'; -import IconLock from '@/components/icons/IconLock'; import { IoMdCheckboxOutline } from 'react-icons/io'; -import Spinner from '@/components/Spinner'; import IconBroom from '@/components/icons/IconBroom'; import InsightsIndicatorDot from './insights/InsightsIndicatorDot'; import MoreMenuItem from '@/components/more/MoreMenuItem'; @@ -34,10 +32,14 @@ import MoreMenuItem from '@/components/more/MoreMenuItem'; export default function AdminAppMenu({ active, animateMenuClose, + isOpen, + setIsOpen, className, }: { active?: boolean animateMenuClose?: boolean + isOpen?: boolean + setIsOpen?: (isOpen: boolean) => void className?: string }) { const { @@ -46,8 +48,6 @@ export default function AdminAppMenu({ uploadsCount = 0, tagsCount = 0, recipesCount = 0, - hasAdminData, - isLoadingAdminData, selectedPhotoIds, startUpload, setSelectedPhotoIds, @@ -80,7 +80,7 @@ export default function AdminAppMenu({ annotation: `${uploadsCount}`, icon: , href: PATH_ADMIN_UPLOADS, }); @@ -93,13 +93,13 @@ export default function AdminAppMenu({ {photosCountNeedSync} , icon: , href: PATH_ADMIN_PHOTOS_UPDATES, }); @@ -112,7 +112,7 @@ export default function AdminAppMenu({ }, icon: , href: PATH_ADMIN_PHOTOS, }); @@ -123,7 +123,7 @@ export default function AdminAppMenu({ annotation: `${tagsCount}`, icon: , href: PATH_ADMIN_TAGS, }); @@ -134,7 +134,7 @@ export default function AdminAppMenu({ annotation: `${recipesCount}`, icon: , href: PATH_ADMIN_RECIPES, }); @@ -147,7 +147,7 @@ export default function AdminAppMenu({ icon: isSelecting ? : , href: showAppInsightsLink ? PATH_ADMIN_INSIGHTS @@ -189,21 +189,7 @@ export default function AdminAppMenu({ return ( - - {!hasAdminData && isLoadingAdminData - ? - :} - - Admin menu - } + {...{ isOpen, setIsOpen }} icon={
*>*]:translate-y-[6px]', !animateMenuClose && '[&>*>*]:duration-300', diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx index 3d3b7822..eae233e2 100644 --- a/src/admin/AdminPhotoMenu.tsx +++ b/src/admin/AdminPhotoMenu.tsx @@ -25,16 +25,19 @@ import InsightsIndicatorDot from './insights/InsightsIndicatorDot'; import IconFavs from '@/components/icons/IconFavs'; import IconEdit from '@/components/icons/IconEdit'; import { photoNeedsToBeSynced } from '@/photo/sync'; +import { KEY_COMMANDS } from '@/photo/key-commands'; export default function AdminPhotoMenu({ photo, revalidatePhoto, includeFavorite = true, + showKeyCommands, ...props }: Omit, 'sections'> & { photo: Photo revalidatePhoto?: RevalidatePhoto includeFavorite?: boolean + showKeyCommands?: boolean }) { const { isUserSignedIn, registerAdminUpdate } = useAppState(); @@ -48,32 +51,39 @@ export default function AdminPhotoMenu({ label: 'Edit', icon: , href: pathForAdminPhotoEdit(photo.id), + ...showKeyCommands && { keyCommand: KEY_COMMANDS.edit }, }]; if (includeFavorite) { sectionMain.push({ label: isFav ? 'Unfavorite' : 'Favorite', icon: , action: () => toggleFavoritePhotoAction( photo.id, shouldRedirectFav, ).then(() => revalidatePhoto?.(photo.id)), + ...showKeyCommands && { + keyCommand: isFav + ? KEY_COMMANDS.unfavorite + : KEY_COMMANDS.favorite, + }, }); } sectionMain.push({ label: 'Download', icon: , href: photo.url, hrefDownloadName: downloadFileNameForPhoto(photo), + ...showKeyCommands && { keyCommand: KEY_COMMANDS.download }, }); sectionMain.push({ label: 'Sync', @@ -86,17 +96,21 @@ export default function AdminPhotoMenu({ size="small" />} , - icon: , + icon: , action: () => syncPhotoAction(photo.id) .then(() => revalidatePhoto?.(photo.id)), + ...showKeyCommands && { keyCommand: KEY_COMMANDS.sync }, }); - const sectionDelete = [{ + const sectionDelete: ComponentProps[] = [{ label: 'Delete', icon: , className: 'text-error *:hover:text-error', + color: 'red', action: () => { if (confirm(deleteConfirmationTextForPhoto(photo))) { return deletePhotoAction( @@ -109,10 +123,15 @@ export default function AdminPhotoMenu({ }); } }, + ...showKeyCommands && { + keyCommandModifier: KEY_COMMANDS.delete[0], + keyCommand: KEY_COMMANDS.delete[1], + }, }]; return [sectionMain, sectionDelete]; }, [ photo, + showKeyCommands, includeFavorite, isFav, shouldRedirectFav, diff --git a/src/app/AppViewSwitcher.tsx b/src/app/AppViewSwitcher.tsx new file mode 100644 index 00000000..3ce01e24 --- /dev/null +++ b/src/app/AppViewSwitcher.tsx @@ -0,0 +1,127 @@ +import Switcher from '@/components/Switcher'; +import SwitcherItem from '@/components/SwitcherItem'; +import IconFeed from '@/components/icons/IconFeed'; +import IconGrid from '@/components/icons/IconGrid'; +import { + PATH_FEED_INFERRED, + PATH_GRID_INFERRED, +} from '@/app/paths'; +import IconSearch from '../components/icons/IconSearch'; +import { useAppState } from '@/state/AppState'; +import { GRID_HOMEPAGE_ENABLED } from './config'; +import AdminAppMenu from '@/admin/AdminAppMenu'; +import Spinner from '@/components/Spinner'; +import clsx from 'clsx/lite'; +import { useCallback, useRef, useState } from 'react'; +import useKeydownHandler from '@/utility/useKeydownHandler'; +import { usePathname } from 'next/navigation'; +import { KEY_COMMANDS } from '@/photo/key-commands'; + +export type SwitcherSelection = 'feed' | 'grid' | 'admin'; + +export default function AppViewSwitcher({ + currentSelection, + className, +}: { + currentSelection?: SwitcherSelection + className?: string +}) { + const pathname = usePathname(); + + const { + isUserSignedIn, + isUserSignedInEager, + setIsCommandKOpen, + } = useAppState(); + + const refHrefFeed = useRef(null); + const refHrefGrid = useRef(null); + + const onKeyDown = useCallback((e: KeyboardEvent) => { + switch (e.key.toLocaleUpperCase()) { + case KEY_COMMANDS.feed: + if (pathname !== PATH_FEED_INFERRED) { refHrefFeed.current?.click(); } + break; + case KEY_COMMANDS.grid: + if (pathname !== PATH_GRID_INFERRED) { refHrefGrid.current?.click(); } + break; + case KEY_COMMANDS.admin: + if (isUserSignedIn) { setIsAdminMenuOpen(true); } + break; + } + }, [pathname, isUserSignedIn]); + useKeydownHandler({ onKeyDown }); + + const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false); + + const renderItemFeed = + } + href={PATH_FEED_INFERRED} + hrefRef={refHrefFeed} + active={currentSelection === 'feed'} + tooltip={{ + content: 'Feed', + keyCommand: KEY_COMMANDS.feed, + }} + noPadding + />; + + const renderItemGrid = + } + href={PATH_GRID_INFERRED} + hrefRef={refHrefGrid} + active={currentSelection === 'grid'} + tooltip={{ + content: 'Grid', + keyCommand: KEY_COMMANDS.grid, + }} + noPadding + />; + + return ( +
+ + {GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed} + {GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid} + {/* Show spinner if admin is suspected to be logged in */} + {(isUserSignedInEager && !isUserSignedIn) && + } + isInteractive={false} + noPadding + tooltip={{ content: 'Admin Menu' }} + />} + {isUserSignedIn && + } + tooltip={{ + content: !isAdminMenuOpen ? 'Admin Menu' : undefined, + keyCommand: !isAdminMenuOpen ? KEY_COMMANDS.admin : undefined, + }} + noPadding + />} + + + } + onClick={() => setIsCommandKOpen?.(true)} + tooltip={{ + content: 'Search', + keyCommandModifier: KEY_COMMANDS.search[0], + keyCommand: KEY_COMMANDS.search[1], + }} + /> + +
+ ); +} diff --git a/src/app/Nav.tsx b/src/app/Nav.tsx index 060fd2ed..56853ee0 100644 --- a/src/app/Nav.tsx +++ b/src/app/Nav.tsx @@ -4,7 +4,7 @@ import { clsx } from 'clsx/lite'; import { usePathname } from 'next/navigation'; import Link from 'next/link'; import AppGrid from '../components/AppGrid'; -import ViewSwitcher, { SwitcherSelection } from '@/app/ViewSwitcher'; +import AppViewSwitcher, { SwitcherSelection } from '@/app/AppViewSwitcher'; import { PATH_ROOT, isPathAdmin, @@ -80,7 +80,7 @@ export default function Nav({ 'md:w-[calc(100%+8px)] md:translate-x-[-4px] md:px-[4px]', classNameStickyNav, )}> -
} onClick={() => setTheme('system')} active={theme === 'system'} + tooltip={{ content: 'System' }} /> } onClick={() => setTheme('light')} active={theme === 'light'} + tooltip={{ content: 'Light Mode' }} /> } onClick={() => setTheme('dark')} active={theme === 'dark'} + tooltip={{ content: 'Dark Mode' }} /> ); diff --git a/src/app/ViewSwitcher.tsx b/src/app/ViewSwitcher.tsx deleted file mode 100644 index 21f3322e..00000000 --- a/src/app/ViewSwitcher.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import Switcher from '@/components/Switcher'; -import SwitcherItem from '@/components/SwitcherItem'; -import IconFeed from '@/components/icons/IconFeed'; -import IconGrid from '@/components/icons/IconGrid'; -import { - PATH_FEED_INFERRED, - PATH_GRID_INFERRED, -} from '@/app/paths'; -import IconSearch from '../components/icons/IconSearch'; -import { useAppState } from '@/state/AppState'; -import { GRID_HOMEPAGE_ENABLED } from './config'; -import AdminAppMenu from '@/admin/AdminAppMenu'; -import Spinner from '@/components/Spinner'; -import clsx from 'clsx/lite'; - -export type SwitcherSelection = 'feed' | 'grid' | 'admin'; - -export default function ViewSwitcher({ - currentSelection, - className, -}: { - currentSelection?: SwitcherSelection - className?: string -}) { - const { - isUserSignedIn, - isUserSignedInEager, - setIsCommandKOpen, - } = useAppState(); - - const renderItemFeed = - } - href={PATH_FEED_INFERRED} - active={currentSelection === 'feed'} - noPadding - />; - - const renderItemGrid = - } - href={PATH_GRID_INFERRED} - active={currentSelection === 'grid'} - noPadding - />; - - return ( -
- - {GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed} - {GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid} - {/* Show spinner if admin is suspected to be logged in */} - {(isUserSignedInEager && !isUserSignedIn) && - } - isInteractive={false} - noPadding - />} - {isUserSignedIn && - } - noPadding - />} - - - } - onClick={() => setIsCommandKOpen?.(true)} - /> - -
- ); -} diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index a93e82e8..190e33cd 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -61,7 +61,10 @@ export default function Modal({ }, }); - useEscapeHandler(onClose, true); + useEscapeHandler({ + onKeyDown: onClose, + ignoreShouldRespondToKeyboardCommands: true, + }); return ( className?: string onClick?: () => void active?: boolean isInteractive?: boolean noPadding?: boolean prefetch?: boolean + tooltip?: ComponentProps }) { const className = clsx( 'flex items-center justify-center', - 'w-[42px] h-full', - 'py-0.5 px-1.5', + `${WIDTH_CLASS} h-[28px]`, isInteractive && 'cursor-pointer', isInteractive && 'hover:bg-gray-100/60 active:bg-gray-100', isInteractive && 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900', @@ -50,18 +56,29 @@ export default function SwitcherItem({ {icon}
; + const content = href + ? , + }} /> + :
+ {renderIcon()} +
; + return ( - href - ? , - }} /> - :
- {renderIcon()} -
+ tooltip + ? + {content} + + : content ); }; diff --git a/src/components/icons/IconFeed.tsx b/src/components/icons/IconFeed.tsx index 2d5604c2..dd6955bb 100644 --- a/src/components/icons/IconFeed.tsx +++ b/src/components/icons/IconFeed.tsx @@ -6,9 +6,11 @@ const INTRINSIC_HEIGHT = 24; export default function IconFeed({ width = INTRINSIC_WIDTH, includeTitle = true, + className, }: { width?: number includeTitle?: boolean + className?: string }) { return ( {includeTitle && Full Frame} diff --git a/src/components/icons/IconGrid.tsx b/src/components/icons/IconGrid.tsx index df2d355a..3a0c50ba 100644 --- a/src/components/icons/IconGrid.tsx +++ b/src/components/icons/IconGrid.tsx @@ -6,9 +6,11 @@ const INTRINSIC_HEIGHT = 24; export default function IconGrid({ width = INTRINSIC_WIDTH, includeTitle = true, + className, }: { width?: number includeTitle?: boolean + className?: string }) { return ( {includeTitle && Grid} diff --git a/src/components/image/ZoomControls.tsx b/src/components/image/ZoomControls.tsx index a044f5c8..3c13a71e 100644 --- a/src/components/image/ZoomControls.tsx +++ b/src/components/image/ZoomControls.tsx @@ -19,7 +19,6 @@ export default function ZoomControls({ selectImageElement?: (container: HTMLElement | null) => HTMLImageElement | null isEnabled?: boolean - shouldZoomOnFKeydown?: boolean }) { const refImageContainer = useRef(null); diff --git a/src/components/image/useImageZoomControls.ts b/src/components/image/useImageZoomControls.ts index e6c5ca52..17645798 100644 --- a/src/components/image/useImageZoomControls.ts +++ b/src/components/image/useImageZoomControls.ts @@ -1,6 +1,5 @@ import useMetaThemeColor from '@/utility/useMetaThemeColor'; import { useAppState } from '@/state/AppState'; -import useKeydownHandler from '@/utility/useKeydownHandler'; import { ComponentProps, RefObject, @@ -16,7 +15,6 @@ export default function useImageZoomControls({ refImageContainer, selectImageElement, isEnabled, - shouldZoomOnFKeydown, } : { refImageContainer: RefObject } & Omit, 'ref' | 'children'>) { @@ -46,12 +44,6 @@ export default function useImageZoomControls({ viewerRef.current?.reset(); }, []); - // On 'F' keydown, toggle fullscreen - const handleKeyDown = useCallback(() => { - if (shouldZoomOnFKeydown) { open(); } - }, [shouldZoomOnFKeydown, open]); - useKeydownHandler(handleKeyDown, ['F']); - useEffect(() => { if (isEnabled) { const imageRef = ( diff --git a/src/components/more/MoreMenu.tsx b/src/components/more/MoreMenu.tsx index 0afded48..17e56e05 100644 --- a/src/components/more/MoreMenu.tsx +++ b/src/components/more/MoreMenu.tsx @@ -15,12 +15,14 @@ export default function MoreMenu({ icon, header, className, - buttonClassName, - buttonClassNameOpen, + classNameButton, + classNameButtonOpen, ariaLabel, align = 'end', // Prevent errant clicks from trigger being too close to menu sideOffset = 6, + isOpen: isOpenProp, + setIsOpen: setIsOpenProp, onOpen, ...props }: { @@ -28,12 +30,17 @@ export default function MoreMenu({ icon?: ReactNode header?: ReactNode className?: string - buttonClassName?: string - buttonClassNameOpen?: string + classNameButton?: string + classNameButtonOpen?: string ariaLabel: string + isOpen?: boolean + setIsOpen?: (isOpen: boolean) => void onOpen?: () => void } & ComponentProps){ - const [isOpen, setIsOpen] = useState(false); + const [isOpenInternal, setIsOpenInternal] = useState(isOpenProp ?? false); + + const isOpen = isOpenProp ?? isOpenInternal; + const setIsOpen = setIsOpenProp ?? setIsOpenInternal; const dismissMenu = useCallback(() => { setIsOpen(false); @@ -44,7 +51,10 @@ export default function MoreMenu({ }, [isOpen, onOpen]); return ( - + {labelComplex ?? label} @@ -118,6 +147,10 @@ export default function MoreMenuItem({ {annotation} } + {keyCommand && + + {keyCommand} + } ); } diff --git a/src/components/primitives/KeyCommand.tsx b/src/components/primitives/KeyCommand.tsx new file mode 100644 index 00000000..2bf32ad8 --- /dev/null +++ b/src/components/primitives/KeyCommand.tsx @@ -0,0 +1,45 @@ +import clsx from 'clsx/lite'; +import { useMemo } from 'react'; +import { HiMiniBackspace } from 'react-icons/hi2'; +import { PiCommandBold } from 'react-icons/pi'; + +export default function KeyCommand({ + children, + modifier, + className, +}: { + children: string + modifier?: '⌘' | '⌥' | '⇧' | '⌃' | '⏎' + className?: string +}) { + const keys = useMemo(() => { + const childrenFormatted = children === 'BACKSPACE' + ? '⌫' + : children; + return modifier ? [modifier, ...childrenFormatted] : [...childrenFormatted]; + }, [modifier, children]); + + return ( + + {keys.map((key) => ( + + {key === '⌘' + ? + : key === '⌫' + ? + : key} + + ))} + + ); +} diff --git a/src/components/primitives/LoaderButton.tsx b/src/components/primitives/LoaderButton.tsx index 169d8809..d8cb284e 100644 --- a/src/components/primitives/LoaderButton.tsx +++ b/src/components/primitives/LoaderButton.tsx @@ -13,6 +13,7 @@ import Tooltip from '../Tooltip'; export default function LoaderButton({ ref, children, + classNameIcon, isLoading, icon, spinnerColor, @@ -32,6 +33,7 @@ export default function LoaderButton({ ...rest }: { ref?: RefObject + classNameIcon?: string isLoading?: boolean icon?: ReactNode spinnerColor?: SpinnerColor @@ -79,6 +81,7 @@ export default function LoaderButton({ 'min-w-[1.25rem] max-h-5', styleAs === 'button' ? 'translate-y-[-0.5px]' : 'translate-y-[0.5px]', 'inline-flex justify-center shrink-0', + classNameIcon, )}> {isLoading ? ['color'] - children: ReactNode + keyCommand?: string + keyCommandModifier?: ComponentProps['modifier'] }) { const refTrigger = useRef(null); const refContent = useRef(null); @@ -41,12 +49,22 @@ export default function TooltipPrimitive({ }); const classNameTrigger = clsx( - 'cursor-default inline-block', + 'cursor-default inline-flex', classNameTriggerProp, ); + const content = keyCommand + ?
+ {contentProp} + {' '} + + {keyCommand} + +
+ : contentProp; + return ( - + {includeButton diff --git a/src/components/useNavigateOrRunActionWithToast.tsx b/src/components/useNavigateOrRunActionWithToast.tsx new file mode 100644 index 00000000..9a727630 --- /dev/null +++ b/src/components/useNavigateOrRunActionWithToast.tsx @@ -0,0 +1,62 @@ +import { toastWaiting } from '@/toast'; +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect, useRef, useTransition } from 'react'; +import { FiCheckSquare } from 'react-icons/fi'; +import { toast } from 'sonner'; + +export default function useNavigateOrRunActionWithToast({ + pathOrAction, + toastMessage = 'Loading...', + dismissDelay = 1500, +}: { + pathOrAction?: string | (() => Promise | undefined) + toastMessage?: string + dismissDelay?: number +}) { + const router = useRouter(); + + const toastId = useRef(undefined); + + const [isPending, startTransition] = useTransition(); + + const dismissToast = useCallback(() => { + if (toastId.current) { + toast(toastMessage, { + id: toastId.current, + icon: , + }); + return setTimeout(() => { + toast.dismiss(toastId.current); + }, dismissDelay); + } + }, [dismissDelay, toastMessage]); + + useEffect(() => { + if (!isPending) { + const timeout = dismissToast(); + return () => clearTimeout(timeout); + } + return () => { + dismissToast(); + }; + }, [isPending, dismissDelay, dismissToast]); + + const navigateOrRunAction = useCallback(() => { + if (typeof pathOrAction === 'string') { + startTransition(() => { + router.push(pathOrAction); + toastId.current = toastWaiting(toastMessage); + }); + } else if (typeof pathOrAction === 'function') { + const result = pathOrAction(); + if (result instanceof Promise) { + toastId.current = toastWaiting(toastMessage); + result.finally(() => { + dismissToast(); + }); + } + } + }, [dismissToast, pathOrAction, router, toastMessage]); + + return navigateOrRunAction; +} diff --git a/src/photo/PhotoDetailPage.tsx b/src/photo/PhotoDetailPage.tsx index f67c0414..251c0404 100644 --- a/src/photo/PhotoDetailPage.tsx +++ b/src/photo/PhotoDetailPage.tsx @@ -139,6 +139,7 @@ export default function PhotoDetailPage({ shouldShareRecipe={recipe !== undefined} shouldShareFocalLength={focal !== undefined} includeFavoriteInAdminMenu={includeFavoriteInAdminMenu} + showAdminKeyCommands />, ]} /> diff --git a/src/photo/PhotoEscapeHandler.tsx b/src/photo/PhotoEscapeHandler.tsx index c52223af..89047a76 100644 --- a/src/photo/PhotoEscapeHandler.tsx +++ b/src/photo/PhotoEscapeHandler.tsx @@ -12,11 +12,11 @@ export default function PhotoEscapeHandler() { const escapePath = getEscapePath(pathname); - const escapeHandler = useCallback(() => { + const onKeyDown = useCallback(() => { if (escapePath) { router.push(escapePath, { scroll: false }); } }, [escapePath, router]); - useEscapeHandler(escapeHandler); + useEscapeHandler({ onKeyDown }); return null; } diff --git a/src/photo/PhotoHeader.tsx b/src/photo/PhotoHeader.tsx index 0299bf54..18f2e80e 100644 --- a/src/photo/PhotoHeader.tsx +++ b/src/photo/PhotoHeader.tsx @@ -12,7 +12,7 @@ import ShareButton from '@/share/ShareButton'; import AnimateItems from '@/components/AnimateItems'; import { ReactNode } from 'react'; import DivDebugBaselineGrid from '@/components/DivDebugBaselineGrid'; -import PhotoPrevNext from './PhotoPrevNext'; +import PhotoPrevNextActions from './PhotoPrevNextActions'; import PhotoLink from './PhotoLink'; import ResponsiveText from '@/components/primitives/ResponsiveText'; import { useAppState } from '@/state/AppState'; @@ -59,7 +59,7 @@ export default function PhotoHeader({ : 'photo-detail'; const renderPrevNext = - void + showAdminKeyCommands?: boolean }) { const ref = useRef(null); const refZoomControls = useRef(null); @@ -252,6 +254,7 @@ export default function PhotoLarge({ revalidatePhoto, includeFavorite: includeFavoriteInAdminMenu, ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`, + showKeyCommands: showAdminKeyCommands, }} />; const largePhotoContainerClassName = clsx( diff --git a/src/photo/PhotoLink.tsx b/src/photo/PhotoLink.tsx index 2c091fbd..4fd5532d 100644 --- a/src/photo/PhotoLink.tsx +++ b/src/photo/PhotoLink.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ReactNode, ComponentProps } from 'react'; +import { ReactNode, ComponentProps, RefObject } from 'react'; import { Photo, titleForPhoto } from '@/photo'; import { PhotoSetCategory } from '@/category'; import { AnimationConfig } from '../components/AnimateItems'; @@ -12,6 +12,7 @@ import Spinner from '@/components/Spinner'; import LinkWithLoaderBackground from '@/components/LinkWithLoaderBackground'; export default function PhotoLink({ + ref, photo, scroll, prefetch, @@ -21,6 +22,7 @@ export default function PhotoLink({ loaderType = 'spinner', ...categories }: { + ref?: RefObject photo?: Photo scroll?: boolean prefetch?: boolean @@ -35,6 +37,7 @@ export default function PhotoLink({ Omit, 'children'> | undefined = photo ? { + ref, className, href: pathForPhoto({ photo, ...categories }), onClick: () => { diff --git a/src/photo/PhotoPrevNext.tsx b/src/photo/PhotoPrevNext.tsx deleted file mode 100644 index 4ac73b50..00000000 --- a/src/photo/PhotoPrevNext.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { - Photo, - getNextPhoto, - getPreviousPhoto, -} from '@/photo'; -import { PhotoSetCategory } from '../category'; -import PhotoLink from './PhotoLink'; -import { useRouter } from 'next/navigation'; -import { pathForPhoto } from '@/app/paths'; -import { useAppState } from '@/state/AppState'; -import { AnimationConfig } from '@/components/AnimateItems'; -import { clsx } from 'clsx/lite'; -import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'; - -const LISTENER_KEYUP = 'keyup'; - -const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 }; -const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 }; - -export default function PhotoPrevNext({ - photo, - photos = [], - className, - ...categories -}: { - photo?: Photo - photos?: Photo[] - className?: string -} & PhotoSetCategory) { - const router = useRouter(); - - const { - setNextPhotoAnimation, - shouldRespondToKeyboardCommands, - } = useAppState(); - - const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined; - const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined; - - const pathPrevious = previousPhoto - ? pathForPhoto({ photo: previousPhoto, ...categories }) - : undefined; - - const pathNext = nextPhoto - ? pathForPhoto({ photo: nextPhoto, ...categories }) - : undefined; - - useEffect(() => { - if (shouldRespondToKeyboardCommands) { - const onKeyUp = (e: KeyboardEvent) => { - switch (e.key.toUpperCase()) { - case 'ARROWLEFT': - case 'J': - if (pathPrevious) { - setNextPhotoAnimation?.(ANIMATION_RIGHT); - router.push(pathPrevious, { scroll: false }); - } - break; - case 'ARROWRIGHT': - case 'L': - if (pathNext) { - setNextPhotoAnimation?.(ANIMATION_LEFT); - router.push(pathNext, { scroll: false }); - } - break; - }; - }; - window.addEventListener(LISTENER_KEYUP, onKeyUp); - return () => window.removeEventListener(LISTENER_KEYUP, onKeyUp); - } - }, [ - router, - shouldRespondToKeyboardCommands, - setNextPhotoAnimation, - pathPrevious, - pathNext, - ]); - - return ( -
-
- - - PREV - - - / - - - - NEXT - -
-
- ); -}; diff --git a/src/photo/PhotoPrevNextActions.tsx b/src/photo/PhotoPrevNextActions.tsx new file mode 100644 index 00000000..b471d999 --- /dev/null +++ b/src/photo/PhotoPrevNextActions.tsx @@ -0,0 +1,226 @@ +'use client'; + +import { useCallback, useRef } from 'react'; +import { + Photo, + downloadFileNameForPhoto, + getNextPhoto, + getPreviousPhoto, +} from '@/photo'; +import { PhotoSetCategory } from '../category'; +import PhotoLink from './PhotoLink'; +import { pathForAdminPhotoEdit, pathForPhoto } from '@/app/paths'; +import { useAppState } from '@/state/AppState'; +import { AnimationConfig } from '@/components/AnimateItems'; +import { clsx } from 'clsx/lite'; +import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'; +import useNavigateOrRunActionWithToast + from '@/components/useNavigateOrRunActionWithToast'; +import { + deletePhotoAction, + syncPhotoAction, + toggleFavoritePhotoAction, +} from './actions'; +import { isPhotoFav } from '@/tag'; +import Tooltip from '@/components/Tooltip'; +import { ALLOW_PUBLIC_DOWNLOADS } from '@/app/config'; +import { downloadFileFromBrowser } from '@/utility/url'; +import useKeydownHandler from '@/utility/useKeydownHandler'; +import { KEY_COMMANDS } from './key-commands'; + +const ANIMATION_LEFT: AnimationConfig = { type: 'left', duration: 0.3 }; +const ANIMATION_RIGHT: AnimationConfig = { type: 'right', duration: 0.3 }; + +export default function PhotoPrevNextActions({ + photo, + photos = [], + className, + ...categories +}: { + photo?: Photo + photos?: Photo[] + className?: string +} & PhotoSetCategory) { + const { setNextPhotoAnimation, isUserSignedIn } = useAppState(); + + const photoTitle = photo + ? photo.title + ? `'${photo.title}'` + : 'photo' + : undefined; + const downloadUrl = photo?.url; + const downloadFileName = photo + ? downloadFileNameForPhoto(photo) + : undefined; + + const toggleFavorite = useCallback(() => { + if (photo?.id) { return toggleFavoritePhotoAction(photo.id); } + }, [photo?.id]); + + const navigateToPhotoEdit = useNavigateOrRunActionWithToast({ + pathOrAction: photo ? pathForAdminPhotoEdit(photo) : undefined, + toastMessage: `Editing ${photoTitle} ...`, + }); + + const favoritePhoto = useNavigateOrRunActionWithToast({ + pathOrAction: toggleFavorite, + toastMessage: `Favoriting ${photoTitle} ...`, + }); + + const unfavoritePhoto = useNavigateOrRunActionWithToast({ + pathOrAction: toggleFavorite, + toastMessage: `Unfavoriting ${photoTitle} ...`, + }); + + const syncPhoto = useNavigateOrRunActionWithToast({ + pathOrAction: useCallback(() => { + if (photo?.id) { return syncPhotoAction(photo.id); } + }, [photo?.id]), + toastMessage: `Syncing ${photoTitle} ...`, + }); + + const deletePhoto = useNavigateOrRunActionWithToast({ + pathOrAction: useCallback(() => { + if (photo?.id && photo.url) { + return deletePhotoAction(photo.id, photo.url, true); + } + }, [photo?.id, photo?.url]), + toastMessage: `Deleting ${photoTitle} ...`, + }); + + const refPrevious = useRef(null); + const refNext = useRef(null); + + const previousPhoto = photo ? getPreviousPhoto(photo, photos) : undefined; + const nextPhoto = photo ? getNextPhoto(photo, photos) : undefined; + + const pathPrevious = previousPhoto + ? pathForPhoto({ photo: previousPhoto, ...categories }) + : undefined; + + const pathNext = nextPhoto + ? pathForPhoto({ photo: nextPhoto, ...categories }) + : undefined; + + const onKeyDown = useCallback((e: KeyboardEvent) => { + switch (e.key.toUpperCase()) { + // Public commands + case KEY_COMMANDS.prev[0]: + case KEY_COMMANDS.prev[1]: + if (pathPrevious) { + setNextPhotoAnimation?.(ANIMATION_RIGHT); + refPrevious.current?.click(); + } + break; + case KEY_COMMANDS.next[0]: + case KEY_COMMANDS.next[1]: + if (pathNext) { + setNextPhotoAnimation?.(ANIMATION_LEFT); + refNext.current?.click(); + } + break; + // Admin commands + case KEY_COMMANDS.edit: + if (isUserSignedIn) { + navigateToPhotoEdit(); + } + break; + case KEY_COMMANDS.favorite: + if (isUserSignedIn && photo && !isPhotoFav(photo)) { + favoritePhoto(); + } + break; + case KEY_COMMANDS.unfavorite: + if (isUserSignedIn && photo && isPhotoFav(photo)) { + unfavoritePhoto(); + } + break; + case KEY_COMMANDS.download: + if ( + (isUserSignedIn || ALLOW_PUBLIC_DOWNLOADS) && + downloadUrl && + downloadFileName + ) { + downloadFileFromBrowser(downloadUrl, downloadFileName); + } + break; + case KEY_COMMANDS.sync: + if (isUserSignedIn) { + syncPhoto(); + } + break; + case KEY_COMMANDS.delete[1]: + if (e.metaKey && isUserSignedIn) { + deletePhoto(); + } + break; + }; + }, [ + setNextPhotoAnimation, + pathPrevious, + pathNext, + isUserSignedIn, + navigateToPhotoEdit, + photo, + favoritePhoto, + unfavoritePhoto, + downloadUrl, + downloadFileName, + syncPhoto, + deletePhoto, + ]); + useKeydownHandler({ onKeyDown }); + + return ( +
+
+ + + + PREV + + + + / + + + + + NEXT + + +
+
+ ); +}; diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 0768c850..298b569a 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -44,7 +44,7 @@ import { extractImageDataFromBlobPath, propagateRecipeTitleIfNecessary, } from './server'; -import { TAG_FAVS, isTagFavs } from '@/tag'; +import { TAG_FAVS, isPhotoFav, isTagFavs } from '@/tag'; import { convertPhotoToPhotoDbInsert, Photo } from '.'; import { runAuthenticatedAdminServerAction } from '@/auth/server'; import { AiImageQuery, getAiImageQuery } from './ai'; @@ -254,7 +254,7 @@ export const toggleFavoritePhotoAction = async ( const photo = await getPhoto(photoId); if (photo) { const { tags } = photo; - photo.tags = tags.some(tag => tag === TAG_FAVS) + photo.tags = isPhotoFav(photo) ? tags.filter(tag => !isTagFavs(tag)) : [...tags, TAG_FAVS]; await updatePhoto(convertPhotoToPhotoDbInsert(photo)); diff --git a/src/photo/index.ts b/src/photo/index.ts index a93c3675..e822feab 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -213,17 +213,18 @@ export const translatePhotoId = (id: string) => export const titleForPhoto = ( photo: Photo, - preferDateOverUntitled = true, + useDateAsTitle = true, + fallback = 'Untitled', ) => { if (photo.title) { return photo.title; - } else if (preferDateOverUntitled && (photo.takenAt || photo.createdAt)) { + } else if (useDateAsTitle && (photo.takenAt || photo.createdAt)) { return formatDate({ date: photo.takenAt || photo.createdAt, length: 'tiny', }).toLocaleUpperCase(); } else { - return 'Untitled'; + return fallback; } }; diff --git a/src/photo/key-commands.ts b/src/photo/key-commands.ts new file mode 100644 index 00000000..a0c20076 --- /dev/null +++ b/src/photo/key-commands.ts @@ -0,0 +1,14 @@ +export const KEY_COMMANDS = { + feed: 'F', + grid: 'G', + admin: 'A', + prev: ['J', 'ARROWLEFT'], + next: ['L', 'ARROWRIGHT'], + edit: 'E', + favorite: 'P', + unfavorite: 'X', + download: 'D', + sync: 'S', + search: ['⌘', 'K'], + delete: ['⌘', 'BACKSPACE'], +} as const; diff --git a/src/toast/index.tsx b/src/toast/index.tsx index d453ca10..1935080b 100644 --- a/src/toast/index.tsx +++ b/src/toast/index.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react'; import { PiWarningBold } from 'react-icons/pi'; import { FiCheckSquare } from 'react-icons/fi'; import { toast } from 'sonner'; +import Spinner from '@/components/Spinner'; const DEFAULT_DURATION = 4000; @@ -24,3 +25,13 @@ export const toastWarning = ( duration, }, ); + +export const toastWaiting = ( + message: ReactNode, + duration = Infinity, +) => toast( + message, { + icon: , + duration, + }, +); diff --git a/src/utility/useEscapeHandler.ts b/src/utility/useEscapeHandler.ts index 2218f0d5..fd37d3f3 100644 --- a/src/utility/useEscapeHandler.ts +++ b/src/utility/useEscapeHandler.ts @@ -1,12 +1,10 @@ import useKeydownHandler from '@/utility/useKeydownHandler'; export default function useEscapeHandler( - onEscape?: () => void, - ignoreShouldRespondToKeyboardCommands?: boolean, + args: Omit[0], 'keys'>, ) { - useKeydownHandler( - onEscape, - ['ESCAPE'], - ignoreShouldRespondToKeyboardCommands, - ); + useKeydownHandler({ + ...args, + keys: ['ESCAPE'], + }); } diff --git a/src/utility/useKeydownHandler.ts b/src/utility/useKeydownHandler.ts index b6f2af40..9ee57483 100644 --- a/src/utility/useKeydownHandler.ts +++ b/src/utility/useKeydownHandler.ts @@ -3,30 +3,34 @@ import { useCallback, useEffect } from 'react'; const LISTENER_KEYDOWN = 'keydown'; -export default function useKeydownHandler( - onKeydown?: (e: KeyboardEvent) => void, - keys: string[] = [], - ignoreShouldRespondToKeyboardCommands?: boolean, -) { +export default function useKeydownHandler({ + onKeyDown: onKeyDownArg, + keys, + ignoreShouldRespondToKeyboardCommands = false, +}: { + onKeyDown?: (e: KeyboardEvent) => void + keys?: string[] + ignoreShouldRespondToKeyboardCommands?: boolean +}) { const { shouldRespondToKeyboardCommands } = useAppState(); - const onKeyUp = useCallback((e: KeyboardEvent) => { - if (keys.some(key => key.toUpperCase() === e.key?.toUpperCase())) { - onKeydown?.(e); + const onKeyDown = useCallback((e: KeyboardEvent) => { + if (!keys || keys.some(key => key.toUpperCase() === e.key?.toUpperCase())) { + onKeyDownArg?.(e); } - }, [onKeydown, keys]); + }, [onKeyDownArg, keys]); useEffect(() => { if ( shouldRespondToKeyboardCommands || ignoreShouldRespondToKeyboardCommands ) { - window.addEventListener(LISTENER_KEYDOWN, onKeyUp); - return () => window.removeEventListener(LISTENER_KEYDOWN, onKeyUp); + window.addEventListener(LISTENER_KEYDOWN, onKeyDown); + return () => window.removeEventListener(LISTENER_KEYDOWN, onKeyDown); } }, [ shouldRespondToKeyboardCommands, ignoreShouldRespondToKeyboardCommands, - onKeyUp, + onKeyDown, ]); }