Merge pull request #244 from sambecker/key-commmands
Introduce new key commands
This commit is contained in:
commit
866c9fb273
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -20,6 +20,7 @@
|
||||
"exif",
|
||||
"exiftool",
|
||||
"favicons",
|
||||
"Favoriting",
|
||||
"favs",
|
||||
"ghijklmnopqrstuv",
|
||||
"GPSH",
|
||||
@ -53,6 +54,7 @@
|
||||
"thephotoblog",
|
||||
"trpc",
|
||||
"Turbopack",
|
||||
"Unfavoriting",
|
||||
"unnest",
|
||||
"upstash",
|
||||
"UsKSGcbt",
|
||||
|
||||
16
package.json
16
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",
|
||||
|
||||
249
pnpm-lock.yaml
generated
249
pnpm-lock.yaml
generated
@ -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:
|
||||
|
||||
@ -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: <IconFolder
|
||||
size={16}
|
||||
className="translate-x-[1px] translate-y-[0.5px]"
|
||||
className="translate-x-[1px] translate-y-[1px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_UPLOADS,
|
||||
});
|
||||
@ -93,13 +93,13 @@ export default function AdminAppMenu({
|
||||
{photosCountNeedSync}
|
||||
</span>
|
||||
<InsightsIndicatorDot
|
||||
className="inline-block translate-y-[-0.5px]"
|
||||
className="inline-block translate-y-[-1px]"
|
||||
size="small"
|
||||
/>
|
||||
</>,
|
||||
icon: <IconBroom
|
||||
size={17}
|
||||
className="translate-y-[0.5px]"
|
||||
size={18}
|
||||
className="translate-y-[-0.5px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_PHOTOS_UPDATES,
|
||||
});
|
||||
@ -112,7 +112,7 @@ export default function AdminAppMenu({
|
||||
},
|
||||
icon: <IconPhoto
|
||||
size={15}
|
||||
className="translate-x-[-0.5px] translate-y-[0.5px]"
|
||||
className="translate-x-[-0.5px] translate-y-[1px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_PHOTOS,
|
||||
});
|
||||
@ -123,7 +123,7 @@ export default function AdminAppMenu({
|
||||
annotation: `${tagsCount}`,
|
||||
icon: <IconTag
|
||||
size={15}
|
||||
className="translate-y-[0.5px]"
|
||||
className="translate-y-[1.5px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_TAGS,
|
||||
});
|
||||
@ -134,7 +134,7 @@ export default function AdminAppMenu({
|
||||
annotation: `${recipesCount}`,
|
||||
icon: <IconRecipe
|
||||
size={17}
|
||||
className="translate-x-[-0.5px] translate-y-[0.5px]"
|
||||
className="translate-x-[-0.5px] translate-y-[1px]"
|
||||
/>,
|
||||
href: PATH_ADMIN_RECIPES,
|
||||
});
|
||||
@ -147,7 +147,7 @@ export default function AdminAppMenu({
|
||||
icon: isSelecting
|
||||
? <IoCloseSharp
|
||||
size={18}
|
||||
className="translate-x-[-1px] translate-y-[0.5px]"
|
||||
className="translate-x-[-1px] translate-y-[1px]"
|
||||
/>
|
||||
: <IoMdCheckboxOutline
|
||||
size={16}
|
||||
@ -174,7 +174,7 @@ export default function AdminAppMenu({
|
||||
: 'App Configuration',
|
||||
icon: <AdminAppInfoIcon
|
||||
size="small"
|
||||
className="translate-x-[-0.5px] translate-y-[-0.5px]"
|
||||
className="translate-x-[-0.5px] translate-y-[0.5px]"
|
||||
/>,
|
||||
href: showAppInsightsLink
|
||||
? PATH_ADMIN_INSIGHTS
|
||||
@ -189,21 +189,7 @@ export default function AdminAppMenu({
|
||||
|
||||
return (
|
||||
<MoreMenu
|
||||
header={<div className="flex items-center select-none">
|
||||
<span className="inline-flex items-center justify-center w-5 mr-2">
|
||||
{!hasAdminData && isLoadingAdminData
|
||||
? <Spinner
|
||||
className="translate-x-[1px] translate-y-[1px]"
|
||||
size={13}
|
||||
/>
|
||||
:<IconLock
|
||||
size={16}
|
||||
className="translate-x-[1px] translate-y-[0.5px]"
|
||||
narrow
|
||||
/>}
|
||||
</span>
|
||||
<span className="grow">Admin menu</span>
|
||||
</div>}
|
||||
{...{ isOpen, setIsOpen }}
|
||||
icon={<div className={clsx(
|
||||
'w-[28px] h-[28px]',
|
||||
'overflow-hidden',
|
||||
@ -226,7 +212,7 @@ export default function AdminAppMenu({
|
||||
'border-medium',
|
||||
className,
|
||||
)}
|
||||
buttonClassName={clsx(
|
||||
classNameButton={clsx(
|
||||
'p-0!',
|
||||
'w-full h-full',
|
||||
'flex items-center justify-center',
|
||||
@ -237,7 +223,7 @@ export default function AdminAppMenu({
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-gray-400 dark:text-gray-600',
|
||||
)}
|
||||
buttonClassNameOpen={clsx(
|
||||
classNameButtonOpen={clsx(
|
||||
'bg-dim text-main!',
|
||||
'[&>*>*]:translate-y-[6px]',
|
||||
!animateMenuClose && '[&>*>*]:duration-300',
|
||||
|
||||
@ -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<ComponentProps<typeof MoreMenu>, 'sections'> & {
|
||||
photo: Photo
|
||||
revalidatePhoto?: RevalidatePhoto
|
||||
includeFavorite?: boolean
|
||||
showKeyCommands?: boolean
|
||||
}) {
|
||||
const { isUserSignedIn, registerAdminUpdate } = useAppState();
|
||||
|
||||
@ -48,32 +51,39 @@ export default function AdminPhotoMenu({
|
||||
label: 'Edit',
|
||||
icon: <IconEdit
|
||||
size={15}
|
||||
className="translate-x-[0.5px] translate-y-[-0.5px]"
|
||||
className="translate-x-[0.5px]"
|
||||
/>,
|
||||
href: pathForAdminPhotoEdit(photo.id),
|
||||
...showKeyCommands && { keyCommand: KEY_COMMANDS.edit },
|
||||
}];
|
||||
if (includeFavorite) {
|
||||
sectionMain.push({
|
||||
label: isFav ? 'Unfavorite' : 'Favorite',
|
||||
icon: <IconFavs
|
||||
size={14}
|
||||
className="translate-x-[-1px]"
|
||||
className="translate-x-[-1px] translate-y-[0.5px]"
|
||||
highlight={isFav}
|
||||
/>,
|
||||
action: () => toggleFavoritePhotoAction(
|
||||
photo.id,
|
||||
shouldRedirectFav,
|
||||
).then(() => revalidatePhoto?.(photo.id)),
|
||||
...showKeyCommands && {
|
||||
keyCommand: isFav
|
||||
? KEY_COMMANDS.unfavorite
|
||||
: KEY_COMMANDS.favorite,
|
||||
},
|
||||
});
|
||||
}
|
||||
sectionMain.push({
|
||||
label: 'Download',
|
||||
icon: <MdOutlineFileDownload
|
||||
size={17}
|
||||
className="translate-x-[-1px] translate-y-[-0.5px]"
|
||||
className="translate-x-[-1px]"
|
||||
/>,
|
||||
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"
|
||||
/>}
|
||||
</span>,
|
||||
icon: <IconGrSync className="translate-x-[-1px]" />,
|
||||
icon: <IconGrSync
|
||||
className="translate-x-[-1px] translate-y-[0.5px]"
|
||||
/>,
|
||||
action: () => syncPhotoAction(photo.id)
|
||||
.then(() => revalidatePhoto?.(photo.id)),
|
||||
...showKeyCommands && { keyCommand: KEY_COMMANDS.sync },
|
||||
});
|
||||
const sectionDelete = [{
|
||||
const sectionDelete: ComponentProps<typeof MoreMenuItem>[] = [{
|
||||
label: 'Delete',
|
||||
icon: <BiTrash
|
||||
size={15}
|
||||
className="translate-x-[-1px]"
|
||||
/>,
|
||||
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,
|
||||
|
||||
127
src/app/AppViewSwitcher.tsx
Normal file
127
src/app/AppViewSwitcher.tsx
Normal file
@ -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<HTMLAnchorElement>(null);
|
||||
const refHrefGrid = useRef<HTMLAnchorElement>(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 =
|
||||
<SwitcherItem
|
||||
icon={<IconFeed includeTitle={false} />}
|
||||
href={PATH_FEED_INFERRED}
|
||||
hrefRef={refHrefFeed}
|
||||
active={currentSelection === 'feed'}
|
||||
tooltip={{
|
||||
content: 'Feed',
|
||||
keyCommand: KEY_COMMANDS.feed,
|
||||
}}
|
||||
noPadding
|
||||
/>;
|
||||
|
||||
const renderItemGrid =
|
||||
<SwitcherItem
|
||||
icon={<IconGrid includeTitle={false} />}
|
||||
href={PATH_GRID_INFERRED}
|
||||
hrefRef={refHrefGrid}
|
||||
active={currentSelection === 'grid'}
|
||||
tooltip={{
|
||||
content: 'Grid',
|
||||
keyCommand: KEY_COMMANDS.grid,
|
||||
}}
|
||||
noPadding
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex gap-1 sm:gap-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Switcher>
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed}
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid}
|
||||
{/* Show spinner if admin is suspected to be logged in */}
|
||||
{(isUserSignedInEager && !isUserSignedIn) &&
|
||||
<SwitcherItem
|
||||
icon={<Spinner />}
|
||||
isInteractive={false}
|
||||
noPadding
|
||||
tooltip={{ content: 'Admin Menu' }}
|
||||
/>}
|
||||
{isUserSignedIn &&
|
||||
<SwitcherItem
|
||||
icon={<AdminAppMenu
|
||||
isOpen={isAdminMenuOpen}
|
||||
setIsOpen={setIsAdminMenuOpen}
|
||||
/>}
|
||||
tooltip={{
|
||||
content: !isAdminMenuOpen ? 'Admin Menu' : undefined,
|
||||
keyCommand: !isAdminMenuOpen ? KEY_COMMANDS.admin : undefined,
|
||||
}}
|
||||
noPadding
|
||||
/>}
|
||||
</Switcher>
|
||||
<Switcher type="borderless">
|
||||
<SwitcherItem
|
||||
icon={<IconSearch includeTitle={false} />}
|
||||
onClick={() => setIsCommandKOpen?.(true)}
|
||||
tooltip={{
|
||||
content: 'Search',
|
||||
keyCommandModifier: KEY_COMMANDS.search[0],
|
||||
keyCommand: KEY_COMMANDS.search[1],
|
||||
}}
|
||||
/>
|
||||
</Switcher>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
)}>
|
||||
<ViewSwitcher
|
||||
<AppViewSwitcher
|
||||
currentSelection={switcherSelectionForPath()}
|
||||
/>
|
||||
<div className={clsx(
|
||||
|
||||
@ -22,22 +22,22 @@ export default function ThemeSwitcher () {
|
||||
return (
|
||||
<Switcher>
|
||||
<SwitcherItem
|
||||
title="System"
|
||||
icon={<BiDesktop size={16} />}
|
||||
onClick={() => setTheme('system')}
|
||||
active={theme === 'system'}
|
||||
tooltip={{ content: 'System' }}
|
||||
/>
|
||||
<SwitcherItem
|
||||
title="Light"
|
||||
icon={<BiSun size={18} />}
|
||||
onClick={() => setTheme('light')}
|
||||
active={theme === 'light'}
|
||||
tooltip={{ content: 'Light Mode' }}
|
||||
/>
|
||||
<SwitcherItem
|
||||
title="Dark"
|
||||
icon={<BiMoon size={16} />}
|
||||
onClick={() => setTheme('dark')}
|
||||
active={theme === 'dark'}
|
||||
tooltip={{ content: 'Dark Mode' }}
|
||||
/>
|
||||
</Switcher>
|
||||
);
|
||||
|
||||
@ -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 =
|
||||
<SwitcherItem
|
||||
icon={<IconFeed />}
|
||||
href={PATH_FEED_INFERRED}
|
||||
active={currentSelection === 'feed'}
|
||||
noPadding
|
||||
/>;
|
||||
|
||||
const renderItemGrid =
|
||||
<SwitcherItem
|
||||
icon={<IconGrid />}
|
||||
href={PATH_GRID_INFERRED}
|
||||
active={currentSelection === 'grid'}
|
||||
noPadding
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div className={clsx(
|
||||
'flex gap-1 sm:gap-2',
|
||||
className,
|
||||
)}>
|
||||
<Switcher>
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed}
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid}
|
||||
{/* Show spinner if admin is suspected to be logged in */}
|
||||
{(isUserSignedInEager && !isUserSignedIn) &&
|
||||
<SwitcherItem
|
||||
icon={<Spinner />}
|
||||
isInteractive={false}
|
||||
noPadding
|
||||
/>}
|
||||
{isUserSignedIn &&
|
||||
<SwitcherItem
|
||||
className="p-0!"
|
||||
icon={<AdminAppMenu />}
|
||||
noPadding
|
||||
/>}
|
||||
</Switcher>
|
||||
<Switcher type="borderless">
|
||||
<SwitcherItem
|
||||
icon={<IconSearch />}
|
||||
onClick={() => setIsCommandKOpen?.(true)}
|
||||
/>
|
||||
</Switcher>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -61,7 +61,10 @@ export default function Modal({
|
||||
},
|
||||
});
|
||||
|
||||
useEscapeHandler(onClose, true);
|
||||
useEscapeHandler({
|
||||
onKeyDown: onClose,
|
||||
ignoreShouldRespondToKeyboardCommands: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
@ -1,34 +1,40 @@
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { SHOULD_PREFETCH_ALL_LINKS } from '@/app/config';
|
||||
import { ReactNode } from 'react';
|
||||
import { ComponentProps, ReactNode, RefObject } from 'react';
|
||||
import Spinner from './Spinner';
|
||||
import LinkWithIconLoader from './LinkWithIconLoader';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const WIDTH_CLASS = 'w-[42px]';
|
||||
|
||||
export default function SwitcherItem({
|
||||
icon,
|
||||
title,
|
||||
href,
|
||||
hrefRef,
|
||||
className: classNameProp,
|
||||
onClick,
|
||||
active,
|
||||
isInteractive = true,
|
||||
noPadding,
|
||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||
tooltip,
|
||||
}: {
|
||||
icon: ReactNode
|
||||
title?: string
|
||||
href?: string
|
||||
hrefRef?: RefObject<HTMLAnchorElement | null>
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
active?: boolean
|
||||
isInteractive?: boolean
|
||||
noPadding?: boolean
|
||||
prefetch?: boolean
|
||||
tooltip?: ComponentProps<typeof Tooltip>
|
||||
}) {
|
||||
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}
|
||||
</div>;
|
||||
|
||||
const content = href
|
||||
? <LinkWithIconLoader {...{
|
||||
href,
|
||||
ref: hrefRef,
|
||||
title,
|
||||
className,
|
||||
prefetch,
|
||||
icon: renderIcon(),
|
||||
loader: <Spinner />,
|
||||
}} />
|
||||
: <div {...{ title, onClick, className }}>
|
||||
{renderIcon()}
|
||||
</div>;
|
||||
|
||||
return (
|
||||
href
|
||||
? <LinkWithIconLoader {...{
|
||||
title,
|
||||
href,
|
||||
className,
|
||||
prefetch,
|
||||
icon: renderIcon(),
|
||||
loader: <Spinner />,
|
||||
}} />
|
||||
: <div {...{ title, onClick, className }}>
|
||||
{renderIcon()}
|
||||
</div>
|
||||
tooltip
|
||||
? <Tooltip
|
||||
{...tooltip}
|
||||
classNameTrigger={WIDTH_CLASS}
|
||||
delayDuration={500}
|
||||
>
|
||||
{content}
|
||||
</Tooltip>
|
||||
: content
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<svg
|
||||
@ -18,6 +20,7 @@ export default function IconFeed({
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
{includeTitle && <title>Full Frame</title>}
|
||||
<rect x="5.625" y="6.625" width="16.75" height="10.75" rx="1" strokeWidth="1.25"/>
|
||||
|
||||
@ -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 (
|
||||
<svg
|
||||
@ -18,6 +20,7 @@ export default function IconGrid({
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
{includeTitle && <title>Grid</title>}
|
||||
<rect x="5.625" y="6.625" width="16.75" height="10.75" rx="1" strokeWidth="1.25"/>
|
||||
|
||||
@ -19,7 +19,6 @@ export default function ZoomControls({
|
||||
selectImageElement?:
|
||||
(container: HTMLElement | null) => HTMLImageElement | null
|
||||
isEnabled?: boolean
|
||||
shouldZoomOnFKeydown?: boolean
|
||||
}) {
|
||||
const refImageContainer = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
@ -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<HTMLElement | null>
|
||||
} & Omit<ComponentProps<typeof ZoomControls>, '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 = (
|
||||
|
||||
@ -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<typeof DropdownMenu.Content>){
|
||||
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 (
|
||||
<DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenu.Root
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
className={clsx(
|
||||
@ -54,8 +64,8 @@ export default function MoreMenu({
|
||||
'dark:hover:bg-gray-800/75 dark:active:bg-gray-900',
|
||||
'text-dim',
|
||||
'outline-none',
|
||||
buttonClassName,
|
||||
isOpen && buttonClassNameOpen,
|
||||
classNameButton,
|
||||
isOpen && classNameButtonOpen,
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
@ -65,6 +75,7 @@ export default function MoreMenu({
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
{...props}
|
||||
onCloseAutoFocus={e => e.preventDefault()}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={clsx(
|
||||
|
||||
@ -2,33 +2,46 @@
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { ReactNode, useEffect, useState, useTransition } from 'react';
|
||||
import {
|
||||
ComponentProps,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react';
|
||||
import LoaderButton from '../primitives/LoaderButton';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { downloadFileFromBrowser } from '@/utility/url';
|
||||
import KeyCommand from '../primitives/KeyCommand';
|
||||
|
||||
export default function MoreMenuItem({
|
||||
label,
|
||||
labelComplex,
|
||||
annotation,
|
||||
icon,
|
||||
color = 'grey',
|
||||
href,
|
||||
hrefDownloadName,
|
||||
className,
|
||||
action,
|
||||
dismissMenu,
|
||||
shouldPreventDefault = true,
|
||||
keyCommand,
|
||||
keyCommandModifier,
|
||||
}: {
|
||||
label: string
|
||||
labelComplex?: ReactNode
|
||||
annotation?: ReactNode
|
||||
icon?: ReactNode
|
||||
color?: 'grey' | 'red'
|
||||
href?: string
|
||||
hrefDownloadName?: string
|
||||
className?: string
|
||||
action?: () => Promise<void | boolean> | void
|
||||
dismissMenu?: () => void
|
||||
shouldPreventDefault?: boolean
|
||||
keyCommand?: string
|
||||
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
@ -44,18 +57,33 @@ export default function MoreMenuItem({
|
||||
if (transitionDidStart && !isPending) {
|
||||
dismissMenu?.();
|
||||
setTransitionDidStart(false);
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
}, [isPending, dismissMenu, transitionDidStart]);
|
||||
|
||||
const getColorClasses = () => {
|
||||
switch (color) {
|
||||
case 'grey': return clsx(
|
||||
'hover:bg-gray-100/90 active:bg-gray-200/75',
|
||||
'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80',
|
||||
);
|
||||
case 'red': return clsx(
|
||||
'hover:bg-red-100/50 active:bg-red-100/75',
|
||||
'dark:hover:bg-red-950/55 dark:active:bg-red-950/80',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
disabled={isLoading}
|
||||
className={clsx(
|
||||
'flex items-center h-9',
|
||||
'pl-2 pr-3 py-2 rounded-sm',
|
||||
'flex items-center h-8.5 gap-4',
|
||||
'px-2 py-2 rounded-sm',
|
||||
'select-none hover:outline-hidden',
|
||||
'hover:bg-gray-100/90 active:bg-gray-200/75',
|
||||
'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80',
|
||||
getColorClasses(),
|
||||
'whitespace-nowrap',
|
||||
isLoading
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
@ -108,7 +136,8 @@ export default function MoreMenuItem({
|
||||
isLoading={isLoading || isPending}
|
||||
hideTextOnMobile={false}
|
||||
styleAs="link-without-hover"
|
||||
className="translate-y-[1px]"
|
||||
className="translate-y-[0.5px] text-sm grow"
|
||||
classNameIcon="translate-y-[-0.5px]!"
|
||||
>
|
||||
<span>
|
||||
{labelComplex ?? label}
|
||||
@ -118,6 +147,10 @@ export default function MoreMenuItem({
|
||||
{annotation}
|
||||
</span>}
|
||||
</LoaderButton>
|
||||
{keyCommand &&
|
||||
<KeyCommand modifier={keyCommandModifier}>
|
||||
{keyCommand}
|
||||
</KeyCommand>}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
45
src/components/primitives/KeyCommand.tsx
Normal file
45
src/components/primitives/KeyCommand.tsx
Normal file
@ -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 (
|
||||
<span className={clsx('inline-flex items-center gap-0.5', className)}>
|
||||
{keys.map((key) => (
|
||||
<span
|
||||
key={key}
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center',
|
||||
'px-1 h-4 rounded-sm text-xs font-medium',
|
||||
'text-gray-500/90 bg-gray-200/70',
|
||||
'dark:text-gray-300/90 dark:bg-gray-600/50',
|
||||
)}
|
||||
>
|
||||
{key === '⌘'
|
||||
? <PiCommandBold />
|
||||
: key === '⌫'
|
||||
? <HiMiniBackspace
|
||||
className="text-[13px] opacity-80"
|
||||
/>
|
||||
: key}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -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<HTMLButtonElement | null>
|
||||
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
|
||||
? <Spinner
|
||||
|
||||
@ -6,23 +6,31 @@ import MenuSurface from './MenuSurface';
|
||||
import useSupportsHover from '@/utility/useSupportsHover';
|
||||
import clsx from 'clsx/lite';
|
||||
import useClickInsideOutside from '@/utility/useClickInsideOutside';
|
||||
|
||||
import KeyCommand from './KeyCommand';
|
||||
export default function TooltipPrimitive({
|
||||
content,
|
||||
content: contentProp,
|
||||
children,
|
||||
className,
|
||||
classNameTrigger: classNameTriggerProp,
|
||||
sideOffset = 10,
|
||||
delayDuration = 100,
|
||||
skipDelayDuration = 300,
|
||||
supportMobile,
|
||||
color,
|
||||
children,
|
||||
keyCommand,
|
||||
keyCommandModifier,
|
||||
}: {
|
||||
content?: ReactNode
|
||||
children: ReactNode
|
||||
className?: string
|
||||
classNameTrigger?: string
|
||||
sideOffset?: number
|
||||
delayDuration?: number
|
||||
skipDelayDuration?: number
|
||||
supportMobile?: boolean
|
||||
color?: ComponentProps<typeof MenuSurface>['color']
|
||||
children: ReactNode
|
||||
keyCommand?: string
|
||||
keyCommandModifier?: ComponentProps<typeof KeyCommand>['modifier']
|
||||
}) {
|
||||
const refTrigger = useRef<HTMLButtonElement>(null);
|
||||
const refContent = useRef<HTMLDivElement>(null);
|
||||
@ -41,12 +49,22 @@ export default function TooltipPrimitive({
|
||||
});
|
||||
|
||||
const classNameTrigger = clsx(
|
||||
'cursor-default inline-block',
|
||||
'cursor-default inline-flex',
|
||||
classNameTriggerProp,
|
||||
);
|
||||
|
||||
const content = keyCommand
|
||||
? <div className="-mr-0.5 whitespace-nowrap">
|
||||
{contentProp}
|
||||
{' '}
|
||||
<KeyCommand {...{ modifier: keyCommandModifier }}>
|
||||
{keyCommand}
|
||||
</KeyCommand>
|
||||
</div>
|
||||
: contentProp;
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={100}>
|
||||
<Tooltip.Provider {...{ delayDuration, skipDelayDuration }}>
|
||||
<Tooltip.Root open={includeButton ? isOpen : undefined}>
|
||||
<Tooltip.Trigger asChild>
|
||||
{includeButton
|
||||
|
||||
62
src/components/useNavigateOrRunActionWithToast.tsx
Normal file
62
src/components/useNavigateOrRunActionWithToast.tsx
Normal file
@ -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<any> | undefined)
|
||||
toastMessage?: string
|
||||
dismissDelay?: number
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const toastId = useRef<string | number>(undefined);
|
||||
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const dismissToast = useCallback(() => {
|
||||
if (toastId.current) {
|
||||
toast(toastMessage, {
|
||||
id: toastId.current,
|
||||
icon: <FiCheckSquare size={16} />,
|
||||
});
|
||||
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;
|
||||
}
|
||||
@ -139,6 +139,7 @@ export default function PhotoDetailPage({
|
||||
shouldShareRecipe={recipe !== undefined}
|
||||
shouldShareFocalLength={focal !== undefined}
|
||||
includeFavoriteInAdminMenu={includeFavoriteInAdminMenu}
|
||||
showAdminKeyCommands
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
<PhotoPrevNext {...{
|
||||
<PhotoPrevNextActions {...{
|
||||
photo: selectedPhoto,
|
||||
photos,
|
||||
...categories,
|
||||
|
||||
@ -76,6 +76,7 @@ export default function PhotoLarge({
|
||||
shouldShareFocalLength,
|
||||
includeFavoriteInAdminMenu,
|
||||
onVisible,
|
||||
showAdminKeyCommands,
|
||||
}: {
|
||||
photo: Photo
|
||||
className?: string
|
||||
@ -101,6 +102,7 @@ export default function PhotoLarge({
|
||||
shouldShareFocalLength?: boolean
|
||||
includeFavoriteInAdminMenu?: boolean
|
||||
onVisible?: () => void
|
||||
showAdminKeyCommands?: boolean
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const refZoomControls = useRef<ZoomControlsRef>(null);
|
||||
@ -252,6 +254,7 @@ export default function PhotoLarge({
|
||||
revalidatePhoto,
|
||||
includeFavorite: includeFavoriteInAdminMenu,
|
||||
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
|
||||
showKeyCommands: showAdminKeyCommands,
|
||||
}} />;
|
||||
|
||||
const largePhotoContainerClassName = clsx(
|
||||
|
||||
@ -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<HTMLAnchorElement | null>
|
||||
photo?: Photo
|
||||
scroll?: boolean
|
||||
prefetch?: boolean
|
||||
@ -35,6 +37,7 @@ export default function PhotoLink({
|
||||
Omit<ComponentProps<typeof LinkWithStatus>, 'children'> |
|
||||
undefined = photo
|
||||
? {
|
||||
ref,
|
||||
className,
|
||||
href: pathForPhoto({ photo, ...categories }),
|
||||
onClick: () => {
|
||||
|
||||
@ -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 (
|
||||
<div className={clsx(
|
||||
'flex items-center',
|
||||
className,
|
||||
)}>
|
||||
<div className={clsx(
|
||||
'h-4',
|
||||
'flex gap-2 select-none',
|
||||
// Fixes alignment issue when switching from chevrons to text
|
||||
'items-center sm:items-start',
|
||||
'*:select-none',
|
||||
)}>
|
||||
<PhotoLink
|
||||
{...categories}
|
||||
photo={previousPhoto}
|
||||
nextPhotoAnimation={ANIMATION_RIGHT}
|
||||
scroll={false}
|
||||
loaderType="badge"
|
||||
prefetch
|
||||
>
|
||||
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
|
||||
<span className="hidden sm:inline-block">PREV</span>
|
||||
</PhotoLink>
|
||||
<span className="text-extra-extra-dim">
|
||||
/
|
||||
</span>
|
||||
<PhotoLink
|
||||
{...categories}
|
||||
photo={nextPhoto}
|
||||
nextPhotoAnimation={ANIMATION_LEFT}
|
||||
scroll={false}
|
||||
loaderType="badge"
|
||||
prefetch
|
||||
>
|
||||
<FiChevronRight className="sm:hidden text-[1.1rem]" />
|
||||
<span className="hidden sm:inline-block">NEXT</span>
|
||||
</PhotoLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
226
src/photo/PhotoPrevNextActions.tsx
Normal file
226
src/photo/PhotoPrevNextActions.tsx
Normal file
@ -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<HTMLAnchorElement | null>(null);
|
||||
const refNext = useRef<HTMLAnchorElement | null>(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 (
|
||||
<div className={clsx(
|
||||
'flex items-center',
|
||||
className,
|
||||
)}>
|
||||
<div className={clsx(
|
||||
'h-4',
|
||||
'flex gap-2 select-none',
|
||||
// Fixes alignment issue when switching from chevrons to text
|
||||
'items-center sm:items-start',
|
||||
'*:select-none',
|
||||
)}>
|
||||
<Tooltip
|
||||
content={previousPhoto ? 'Previous' : undefined}
|
||||
keyCommand={previousPhoto ? KEY_COMMANDS.prev[0] : undefined}
|
||||
>
|
||||
<PhotoLink
|
||||
{...categories}
|
||||
ref={refPrevious}
|
||||
photo={previousPhoto}
|
||||
nextPhotoAnimation={ANIMATION_RIGHT}
|
||||
scroll={false}
|
||||
loaderType="badge"
|
||||
prefetch
|
||||
>
|
||||
<FiChevronLeft className="sm:hidden text-[1.1rem]" />
|
||||
<span className="hidden sm:inline-block">PREV</span>
|
||||
</PhotoLink>
|
||||
</Tooltip>
|
||||
<span className="text-extra-extra-dim">
|
||||
/
|
||||
</span>
|
||||
<Tooltip
|
||||
content={nextPhoto ? 'Next' : undefined}
|
||||
keyCommand={nextPhoto ? KEY_COMMANDS.next[0] : undefined}
|
||||
>
|
||||
<PhotoLink
|
||||
{...categories}
|
||||
ref={refNext}
|
||||
photo={nextPhoto}
|
||||
nextPhotoAnimation={ANIMATION_LEFT}
|
||||
scroll={false}
|
||||
loaderType="badge"
|
||||
prefetch
|
||||
>
|
||||
<FiChevronRight className="sm:hidden text-[1.1rem]" />
|
||||
<span className="hidden sm:inline-block">NEXT</span>
|
||||
</PhotoLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
14
src/photo/key-commands.ts
Normal file
14
src/photo/key-commands.ts
Normal file
@ -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;
|
||||
@ -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: <Spinner size={16} />,
|
||||
duration,
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import useKeydownHandler from '@/utility/useKeydownHandler';
|
||||
|
||||
export default function useEscapeHandler(
|
||||
onEscape?: () => void,
|
||||
ignoreShouldRespondToKeyboardCommands?: boolean,
|
||||
args: Omit<Parameters<typeof useKeydownHandler>[0], 'keys'>,
|
||||
) {
|
||||
useKeydownHandler(
|
||||
onEscape,
|
||||
['ESCAPE'],
|
||||
ignoreShouldRespondToKeyboardCommands,
|
||||
);
|
||||
useKeydownHandler({
|
||||
...args,
|
||||
keys: ['ESCAPE'],
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user