Merge branch 'main' into static
This commit is contained in:
commit
7fa7dce66e
@ -39,8 +39,8 @@
|
|||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"nanoid": "^5.0.6",
|
"nanoid": "^5.0.6",
|
||||||
"next": "14.2.0-canary.23",
|
"next": "14.2.0-canary.31",
|
||||||
"next-auth": "5.0.0-beta.13",
|
"next-auth": "5.0.0-beta.15",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"postcss": "8.4.35",
|
"postcss": "8.4.35",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
|||||||
96
pnpm-lock.yaml
generated
96
pnpm-lock.yaml
generated
@ -49,7 +49,7 @@ dependencies:
|
|||||||
version: 7.2.0(eslint@8.57.0)(typescript@5.4.2)
|
version: 7.2.0(eslint@8.57.0)(typescript@5.4.2)
|
||||||
'@vercel/analytics':
|
'@vercel/analytics':
|
||||||
specifier: ^1.2.2
|
specifier: ^1.2.2
|
||||||
version: 1.2.2(next@14.2.0-canary.23)(react@18.2.0)
|
version: 1.2.2(next@14.2.0-canary.31)(react@18.2.0)
|
||||||
'@vercel/blob':
|
'@vercel/blob':
|
||||||
specifier: ^0.22.1
|
specifier: ^0.22.1
|
||||||
version: 0.22.1
|
version: 0.22.1
|
||||||
@ -58,7 +58,7 @@ dependencies:
|
|||||||
version: 0.7.2
|
version: 0.7.2
|
||||||
'@vercel/speed-insights':
|
'@vercel/speed-insights':
|
||||||
specifier: ^1.0.10
|
specifier: ^1.0.10
|
||||||
version: 1.0.10(next@14.2.0-canary.23)(react@18.2.0)
|
version: 1.0.10(next@14.2.0-canary.31)(react@18.2.0)
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: 10.4.18
|
specifier: 10.4.18
|
||||||
version: 10.4.18(postcss@8.4.35)
|
version: 10.4.18(postcss@8.4.35)
|
||||||
@ -96,11 +96,11 @@ dependencies:
|
|||||||
specifier: ^5.0.6
|
specifier: ^5.0.6
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
next:
|
next:
|
||||||
specifier: 14.2.0-canary.23
|
specifier: 14.2.0-canary.31
|
||||||
version: 14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
version: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: 5.0.0-beta.13
|
specifier: 5.0.0-beta.15
|
||||||
version: 5.0.0-beta.13(next@14.2.0-canary.23)(react@18.2.0)
|
version: 5.0.0-beta.15(next@14.2.0-canary.31)(react@18.2.0)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.3.0
|
specifier: ^0.3.0
|
||||||
version: 0.3.0(react-dom@18.2.0)(react@18.2.0)
|
version: 0.3.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -156,8 +156,8 @@ packages:
|
|||||||
'@jridgewell/trace-mapping': 0.3.22
|
'@jridgewell/trace-mapping': 0.3.22
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@auth/core@0.27.0:
|
/@auth/core@0.28.0:
|
||||||
resolution: {integrity: sha512-3bydnRJIM/Al6mkYmb53MsC+6G8ojw3lLPzwgVnX4dCo6N2lrib6Wq6r0vxZIhuHGjLObqqtUfpeaEj5aeTHFg==}
|
resolution: {integrity: sha512-/fh/tb/L4NMSYcyPoo4Imn8vN6MskcVfgESF8/ndgtI4fhD/7u7i5fTVzWgNRZ4ebIEGHNDbWFRxaTu1NtQgvA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@simplewebauthn/browser': ^9.0.1
|
'@simplewebauthn/browser': ^9.0.1
|
||||||
'@simplewebauthn/server': ^9.0.2
|
'@simplewebauthn/server': ^9.0.2
|
||||||
@ -1551,8 +1551,8 @@ packages:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@next/env@14.2.0-canary.23:
|
/@next/env@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-cBlFB8Y/iE3K2NX/Km4tP4RZGLsv0D72KI9KxmZepKSkaQBSbtHM0YeHnZ51CFe9UQKzQ/1mPnCY89BjiyIQtQ==}
|
resolution: {integrity: sha512-xdUjSv8c5e1QPiB010TcyW1zPL3bK7FySHQDu6NjzZuUkYwm8W9c9NGIdJLB2UQv0rfpaFBKfWNlGbakicrE+g==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@next/eslint-plugin-next@14.1.3:
|
/@next/eslint-plugin-next@14.1.3:
|
||||||
@ -1561,8 +1561,8 @@ packages:
|
|||||||
glob: 10.3.10
|
glob: 10.3.10
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@next/swc-darwin-arm64@14.2.0-canary.23:
|
/@next/swc-darwin-arm64@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-K1f7A/0ljZO7IX+M+phguFP8lxdqMgEv1x1+gC+UmyZ1c8Na1PgirGgUwdLKxuNjyxlRqY5lkQ/FDk2FOYBSLA==}
|
resolution: {integrity: sha512-9NRPNOWxY/ecv1hxZej9nVvggIemdCqkwlgmkVv+M2TkAJzff5bwY4IzTuyPxQmioxHs43gPyKh/wpVsH4cuog==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
@ -1570,8 +1570,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@next/swc-darwin-x64@14.2.0-canary.23:
|
/@next/swc-darwin-x64@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-3Zg0aZZV9Z0+4QCVNMH/LLW1dRD2mVNtuFBoTI6/7rWUiGrm/9+58sKyjjg+cE9S/zAKvJoFZP7Ask9vnrk4tg==}
|
resolution: {integrity: sha512-8xbOircQJzJx39GZ4iNd/6PIyOI/qZ8TpjJ9qzA1FpVZUDAMXZIDnQIHIsnxvn0HkP85PByw3tcKZLLKcw5k4g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
@ -1579,8 +1579,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@next/swc-linux-arm64-gnu@14.2.0-canary.23:
|
/@next/swc-linux-arm64-gnu@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-OMt5uTXtEZNKaeSvviJQVXzARr3Jyk1BKUQVD10hKUa4edWBcmnrpdqiDVoDCGt9kMOdKdGqJHOJUk/jV/G15w==}
|
resolution: {integrity: sha512-zpA19+op2KxnIutpoZbvQiplmoJHsUUPIhynC+PpZtSqA1IWBZoem+T4IhnmHr+F0DQZRqO9lmZZkHdEpRAzbQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@ -1588,8 +1588,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@next/swc-linux-arm64-musl@14.2.0-canary.23:
|
/@next/swc-linux-arm64-musl@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-vllciUQ4U99LCOBnsFt9QGf9AyE8yhBtNdNxbij5QsdQ/F53SamxrbYOgG7RisvRqFmWSQMfHdpGZOE0EUUsvQ==}
|
resolution: {integrity: sha512-cZ1GuBi6YOHdEw8/lhuxZsObJVpry7irgf1PCsOLigpIqCAyMhbAcQ32FkTA3N5MKWpOkrmS9xQ2B6K1ZaQzCg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@ -1597,8 +1597,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@next/swc-linux-x64-gnu@14.2.0-canary.23:
|
/@next/swc-linux-x64-gnu@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-soKCxTCi0m0hOBSEchH2YTluvQzAEb8HIoQXxligU8C1fmFDX25pwQ4iSWmdvA6xDJn96PG2R64NyytTTMg9TQ==}
|
resolution: {integrity: sha512-YxWC/fipzs/3cTeGcsSSU3GXEAJ16TI3yo4jgwofWko2jSF2/kpCOZSnRYbqX3eNyBCD4K9/g/9v6rAx2zHiew==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@ -1606,8 +1606,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@next/swc-linux-x64-musl@14.2.0-canary.23:
|
/@next/swc-linux-x64-musl@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-Qr+4ySSYEh1hSSmUJ50oHtKopkqwo3RFb2CXpzcMqp+6U8WOMYBX+JTSCFdA3lSlUJqScIgoRoMDk9I3TZM71g==}
|
resolution: {integrity: sha512-CoZVZyx08myeDXmoSEuk+eXrwMYenevXP03Rz/+6+BT6zOrq+1s1rFKUZVd6/3AnD9Q7vNCuTGtaL5jaF5koGw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
@ -1615,8 +1615,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@next/swc-win32-arm64-msvc@14.2.0-canary.23:
|
/@next/swc-win32-arm64-msvc@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-94HGrBqz9s7z6d6hlukbkF4LiU4Bw+a+C3i+J7pBA+3BHtIyffuqmnrP3HY92trP3M328GBN01H7/zqahcvwPQ==}
|
resolution: {integrity: sha512-m1nuZmu8DOJKJvVrrz7KxMH1K3IU1UC7av1jD55cFf3ZM5ur06Mx2PvtbKSnSLCjK7Ga8LHMYXBXQWAbkD6Bcg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@ -1624,8 +1624,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@next/swc-win32-ia32-msvc@14.2.0-canary.23:
|
/@next/swc-win32-ia32-msvc@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-lrvn6ekxPyrBidQxA0kE3xyys8fCLlbhi1tl2FA0QUPHvgPbu2127Q7+UFnl/NGC9veZfCbw6b7TVFyvPseRsQ==}
|
resolution: {integrity: sha512-OFmjN8wK6eSViHUqsh7VLzI8H8d3G6esjn3zOHoSirg821MJLyWVGAXMBjykDz4kTP6VmGdCkohBP6nf/uy94Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@ -1633,8 +1633,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@next/swc-win32-x64-msvc@14.2.0-canary.23:
|
/@next/swc-win32-x64-msvc@14.2.0-canary.31:
|
||||||
resolution: {integrity: sha512-T8zZcK8M5GYWzIKOlxcothb0sixdFSowUztZTNb/kmUK8Vz1Z+TFBxU00WvBDx9ymrGP2f4EKL9YQqu2d+BmcA==}
|
resolution: {integrity: sha512-Dv+FC2zYh8aEKsFUpq6815grRS0dcRw9uJ9hxULAZ9EuFcw0iu5zKbEPWZ+klxDrAWA8bw7WYMZfkvEH7bSLOA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@ -3088,7 +3088,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@vercel/analytics@1.2.2(next@14.2.0-canary.23)(react@18.2.0):
|
/@vercel/analytics@1.2.2(next@14.2.0-canary.31)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==}
|
resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
next: '>= 13'
|
next: '>= 13'
|
||||||
@ -3099,7 +3099,7 @@ packages:
|
|||||||
react:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
next: 14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
next: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
server-only: 0.0.1
|
server-only: 0.0.1
|
||||||
dev: false
|
dev: false
|
||||||
@ -3124,7 +3124,7 @@ packages:
|
|||||||
ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)
|
ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@vercel/speed-insights@1.0.10(next@14.2.0-canary.23)(react@18.2.0):
|
/@vercel/speed-insights@1.0.10(next@14.2.0-canary.31)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-4uzdKB0RW6Ff2FkzshzjZ+RlJfLPxgm/00i0XXgxfMPhwnnsk92YgtqsxT9OcPLdJUyVU1DqFlSWWjIQMPkh0g==}
|
resolution: {integrity: sha512-4uzdKB0RW6Ff2FkzshzjZ+RlJfLPxgm/00i0XXgxfMPhwnnsk92YgtqsxT9OcPLdJUyVU1DqFlSWWjIQMPkh0g==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3148,7 +3148,7 @@ packages:
|
|||||||
vue-router:
|
vue-router:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
next: 14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
next: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@ -6057,8 +6057,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/next-auth@5.0.0-beta.13(next@14.2.0-canary.23)(react@18.2.0):
|
/next-auth@5.0.0-beta.15(next@14.2.0-canary.31)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-2m2Gq69WQ0YXcHCCpHn2y5z1bxSlqD/XOuAgrdtz49/VIAdTFFeYZz97RYqf6xMF8VGmoG32VUnJ6LzaHk6Fwg==}
|
resolution: {integrity: sha512-UQggNq8CDu3/w8CYkihKLLnRPNXel98K0j7mtjj9a6XTNYo4Hni8xg/2h1YhElW6vXE8mgtvmH11rU8NKw86jQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@simplewebauthn/browser': ^9.0.1
|
'@simplewebauthn/browser': ^9.0.1
|
||||||
'@simplewebauthn/server': ^9.0.2
|
'@simplewebauthn/server': ^9.0.2
|
||||||
@ -6073,8 +6073,8 @@ packages:
|
|||||||
nodemailer:
|
nodemailer:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@auth/core': 0.27.0
|
'@auth/core': 0.28.0
|
||||||
next: 14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
next: 14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@ -6088,8 +6088,8 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/next@14.2.0-canary.23(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0):
|
/next@14.2.0-canary.31(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-HuQYIeSlmBmTueVIxn0Zjxx5I4MD6vG3p6AsFySTF7/hMSF5qFUCHGIj3YJRWsDcUw/LQhuGPNLaGUg119YegQ==}
|
resolution: {integrity: sha512-aKls3+4raMbu8ex6YDuQFa8U4ajKojXpn8GvdlFtgNUyBHJ7IrVuaFJw6rU9s9OibfgpFnAbsmvKzYQmAAjmlg==}
|
||||||
engines: {node: '>=18.17.0'}
|
engines: {node: '>=18.17.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -6103,7 +6103,7 @@ packages:
|
|||||||
sass:
|
sass:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 14.2.0-canary.23
|
'@next/env': 14.2.0-canary.31
|
||||||
'@swc/helpers': 0.5.5
|
'@swc/helpers': 0.5.5
|
||||||
busboy: 1.6.0
|
busboy: 1.6.0
|
||||||
caniuse-lite: 1.0.30001591
|
caniuse-lite: 1.0.30001591
|
||||||
@ -6113,15 +6113,15 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
styled-jsx: 5.1.1(@babel/core@7.23.9)(react@18.2.0)
|
styled-jsx: 5.1.1(@babel/core@7.23.9)(react@18.2.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 14.2.0-canary.23
|
'@next/swc-darwin-arm64': 14.2.0-canary.31
|
||||||
'@next/swc-darwin-x64': 14.2.0-canary.23
|
'@next/swc-darwin-x64': 14.2.0-canary.31
|
||||||
'@next/swc-linux-arm64-gnu': 14.2.0-canary.23
|
'@next/swc-linux-arm64-gnu': 14.2.0-canary.31
|
||||||
'@next/swc-linux-arm64-musl': 14.2.0-canary.23
|
'@next/swc-linux-arm64-musl': 14.2.0-canary.31
|
||||||
'@next/swc-linux-x64-gnu': 14.2.0-canary.23
|
'@next/swc-linux-x64-gnu': 14.2.0-canary.31
|
||||||
'@next/swc-linux-x64-musl': 14.2.0-canary.23
|
'@next/swc-linux-x64-musl': 14.2.0-canary.31
|
||||||
'@next/swc-win32-arm64-msvc': 14.2.0-canary.23
|
'@next/swc-win32-arm64-msvc': 14.2.0-canary.31
|
||||||
'@next/swc-win32-ia32-msvc': 14.2.0-canary.23
|
'@next/swc-win32-ia32-msvc': 14.2.0-canary.31
|
||||||
'@next/swc-win32-x64-msvc': 14.2.0-canary.23
|
'@next/swc-win32-x64-msvc': 14.2.0-canary.31
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default async function GridPage() {
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>}
|
</div>}
|
||||||
contentSide={<div className="sticky top-4 space-y-4">
|
contentSide={<div className="sticky top-4 space-y-4 mt-[-4px]">
|
||||||
<PhotoGridSidebar {...{
|
<PhotoGridSidebar {...{
|
||||||
tags,
|
tags,
|
||||||
cameras,
|
cameras,
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
import { getPhotosCached } from '@/photo/cache';
|
|
||||||
import InfoBlock from '@/components/InfoBlock';
|
|
||||||
import RedirectOnDesktop from '@/components/RedirectOnDesktop';
|
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
|
||||||
import { generateOgImageMetaForPhotos } from '@/photo';
|
|
||||||
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
|
|
||||||
import { getPhotoSidebarDataCached } from '@/photo/data';
|
|
||||||
import { MAX_PHOTOS_TO_SHOW_OG } from '@/image-response';
|
|
||||||
import { PATH_GRID } from '@/site/paths';
|
|
||||||
import { Metadata } from 'next/types';
|
|
||||||
|
|
||||||
export const runtime = 'edge';
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
|
||||||
const photos = await getPhotosCached({ limit: MAX_PHOTOS_TO_SHOW_OG });
|
|
||||||
return generateOgImageMetaForPhotos(photos);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function SetsPage() {
|
|
||||||
const [
|
|
||||||
photosCount,
|
|
||||||
tags,
|
|
||||||
cameras,
|
|
||||||
simulations,
|
|
||||||
] = await Promise.all(getPhotoSidebarDataCached());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SiteGrid
|
|
||||||
contentMain={<InfoBlock
|
|
||||||
padding="tight"
|
|
||||||
centered={false}
|
|
||||||
>
|
|
||||||
<RedirectOnDesktop redirectPath={PATH_GRID} />
|
|
||||||
<div className="text-base space-y-4 p-2">
|
|
||||||
<PhotoGridSidebar {...{
|
|
||||||
tags,
|
|
||||||
cameras,
|
|
||||||
simulations,
|
|
||||||
photosCount,
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
</InfoBlock>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -44,6 +44,17 @@ export const {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const safelyRunServerAdminAction = async <T>(
|
||||||
|
callback: () => T,
|
||||||
|
): Promise<T> => {
|
||||||
|
const session = await auth();
|
||||||
|
if (session?.user) {
|
||||||
|
return callback();
|
||||||
|
} else {
|
||||||
|
throw new Error('Unauthorized server action request');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const generateAuthSecret = () => fetch(
|
export const generateAuthSecret = () => fetch(
|
||||||
'https://generate-secret.vercel.app/32',
|
'https://generate-secret.vercel.app/32',
|
||||||
{ cache: 'no-cache' },
|
{ cache: 'no-cache' },
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { pathForCamera } from '@/site/paths';
|
|||||||
import { IoMdCamera } from 'react-icons/io';
|
import { IoMdCamera } from 'react-icons/io';
|
||||||
import { Camera, formatCameraText } from '.';
|
import { Camera, formatCameraText } from '.';
|
||||||
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||||
import { clsx } from 'clsx/lite';
|
|
||||||
|
|
||||||
export default function PhotoCamera({
|
export default function PhotoCamera({
|
||||||
camera,
|
camera,
|
||||||
@ -28,18 +27,12 @@ export default function PhotoCamera({
|
|||||||
icon={showAppleIcon
|
icon={showAppleIcon
|
||||||
? <AiFillApple
|
? <AiFillApple
|
||||||
title="Apple"
|
title="Apple"
|
||||||
className={clsx(
|
className="translate-x-[-2.5px] translate-y-[2px]"
|
||||||
'text-icon',
|
|
||||||
'translate-x-[-2.5px] translate-y-[2px]',
|
|
||||||
)}
|
|
||||||
size={15}
|
size={15}
|
||||||
/>
|
/>
|
||||||
: <IoMdCamera
|
: <IoMdCamera
|
||||||
size={13}
|
size={12}
|
||||||
className={clsx(
|
className="translate-x-[-1px] translate-y-[3.5px]"
|
||||||
'text-icon',
|
|
||||||
'translate-x-[-1px] translate-y-[3.5px]',
|
|
||||||
)}
|
|
||||||
/>}
|
/>}
|
||||||
type={showAppleIcon && isCameraApple ? 'icon-first' : type}
|
type={showAppleIcon && isCameraApple ? 'icon-first' : type}
|
||||||
badged={badged}
|
badged={badged}
|
||||||
|
|||||||
@ -18,8 +18,8 @@ export default function EntityLink({
|
|||||||
title,
|
title,
|
||||||
type = 'icon-first',
|
type = 'icon-first',
|
||||||
badged,
|
badged,
|
||||||
contrast,
|
|
||||||
prefetch,
|
prefetch,
|
||||||
|
contrast = 'high',
|
||||||
hoverEntity,
|
hoverEntity,
|
||||||
}: {
|
}: {
|
||||||
label: ReactNode
|
label: ReactNode
|
||||||
@ -38,15 +38,26 @@ export default function EntityLink({
|
|||||||
</span>
|
</span>
|
||||||
</>;
|
</>;
|
||||||
|
|
||||||
|
const classForContrast = () => {
|
||||||
|
switch (contrast) {
|
||||||
|
case 'low':
|
||||||
|
return 'text-dim';
|
||||||
|
case 'high':
|
||||||
|
return 'text-main';
|
||||||
|
default:
|
||||||
|
return 'text-medium';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="group inline-flex items-center gap-2">
|
<span className="group inline-flex items-center gap-2 h-5">
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
title={title}
|
title={title}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'inline-flex gap-[0.23rem]',
|
'inline-flex gap-[0.23rem]',
|
||||||
!badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100',
|
!badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100',
|
||||||
contrast === 'low' && 'text-dim',
|
classForContrast(),
|
||||||
)}
|
)}
|
||||||
prefetch={prefetch}
|
prefetch={prefetch}
|
||||||
>
|
>
|
||||||
@ -70,7 +81,9 @@ export default function EntityLink({
|
|||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
'flex-shrink-0',
|
'flex-shrink-0',
|
||||||
'inline-flex min-w-[0.9rem]',
|
'inline-flex min-w-[0.9rem]',
|
||||||
contrast === 'low' ? 'text-dim' : 'text-main',
|
contrast === 'high'
|
||||||
|
? 'text-icon'
|
||||||
|
: classForContrast(),
|
||||||
type === 'icon-first' && 'order-first',
|
type === 'icon-first' && 'order-first',
|
||||||
badged && 'translate-y-[4px]',
|
badged && 'translate-y-[4px]',
|
||||||
hoverEntity !== undefined && 'group-hover:hidden',
|
hoverEntity !== undefined && 'group-hover:hidden',
|
||||||
|
|||||||
@ -15,7 +15,10 @@ export default function HeaderList({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<AnimateItems
|
<AnimateItems
|
||||||
className={className}
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'space-y-0.5',
|
||||||
|
)}
|
||||||
scaleOffset={0.95}
|
scaleOffset={0.95}
|
||||||
duration={0.5}
|
duration={0.5}
|
||||||
staggerDelay={0.05}
|
staggerDelay={0.05}
|
||||||
@ -25,7 +28,7 @@ export default function HeaderList({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'text-gray-900',
|
'text-gray-900',
|
||||||
'dark:text-gray-100',
|
'dark:text-gray-100',
|
||||||
'flex items-center mb-0.5 gap-1',
|
'flex items-center mb-1 gap-1',
|
||||||
'uppercase',
|
'uppercase',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export default function PhotoGridSidebar({
|
|||||||
countOnHover={count}
|
countOnHover={count}
|
||||||
type="icon-last"
|
type="icon-last"
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
|
contrast="low"
|
||||||
badged
|
badged
|
||||||
/>
|
/>
|
||||||
: <PhotoTag
|
: <PhotoTag
|
||||||
@ -45,6 +46,7 @@ export default function PhotoGridSidebar({
|
|||||||
type="text-only"
|
type="text-only"
|
||||||
countOnHover={count}
|
countOnHover={count}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
|
contrast="low"
|
||||||
badged
|
badged
|
||||||
/>)}
|
/>)}
|
||||||
/>}
|
/>}
|
||||||
@ -63,6 +65,7 @@ export default function PhotoGridSidebar({
|
|||||||
type="text-only"
|
type="text-only"
|
||||||
countOnHover={count}
|
countOnHover={count}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
|
contrast="low"
|
||||||
hideAppleIcon
|
hideAppleIcon
|
||||||
badged
|
badged
|
||||||
/>)}
|
/>)}
|
||||||
|
|||||||
@ -47,15 +47,9 @@ export default function PhotoLarge({
|
|||||||
|
|
||||||
const camera = cameraFromPhoto(photo);
|
const camera = cameraFromPhoto(photo);
|
||||||
|
|
||||||
const renderMiniGrid = (children: JSX.Element, rightPadding = true) =>
|
const showCameraContent = showCamera && shouldShowCameraDataForPhoto(photo);
|
||||||
<div className={clsx(
|
const showTagsContent = tags.length > 0;
|
||||||
'flex gap-y-4',
|
const showExifContent = shouldShowExifDataForPhoto(photo);
|
||||||
'flex-col sm:flex-row md:flex-col',
|
|
||||||
'[&>*]:sm:flex-grow',
|
|
||||||
rightPadding && 'pr-2',
|
|
||||||
)}>
|
|
||||||
{children}
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
@ -76,82 +70,81 @@ export default function PhotoLarge({
|
|||||||
</Link>}
|
</Link>}
|
||||||
contentSide={
|
contentSide={
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
|
'relative',
|
||||||
'leading-snug',
|
'leading-snug',
|
||||||
'sticky top-4 self-start',
|
'sticky top-4 self-start -translate-y-1',
|
||||||
'grid grid-cols-2 md:grid-cols-1',
|
'grid grid-cols-2 md:grid-cols-1',
|
||||||
'gap-x-0.5 sm:gap-x-1',
|
'gap-x-0.5 sm:gap-x-1 gap-y-4',
|
||||||
'gap-y-4',
|
'pb-6',
|
||||||
'-translate-y-1',
|
|
||||||
'mb-4',
|
|
||||||
)}>
|
)}>
|
||||||
{renderMiniGrid(<>
|
{/* Meta */}
|
||||||
<div className="-space-y-0.5">
|
<div className="pr-3 md:pr-0">
|
||||||
<div className="relative flex gap-2 items-start">
|
<div className="md:relative flex gap-2 items-start">
|
||||||
<div className="md:flex-grow">
|
<div className="flex-grow">
|
||||||
<Link
|
<Link
|
||||||
href={pathForPhoto(photo)}
|
href={pathForPhoto(photo)}
|
||||||
className="font-bold uppercase"
|
className="font-bold uppercase"
|
||||||
prefetch={prefetch}
|
>
|
||||||
>
|
{titleForPhoto(photo)}
|
||||||
{titleForPhoto(photo)}
|
</Link>
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<Suspense>
|
|
||||||
<div className="h-4 translate-y-[-3.5px] z-10">
|
|
||||||
<AdminPhotoMenu photo={photo} />
|
|
||||||
</div>
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
{tags.length > 0 &&
|
<Suspense>
|
||||||
<PhotoTags
|
<div className="absolute right-0 translate-y-[-4px] z-10">
|
||||||
tags={tags}
|
<AdminPhotoMenu photo={photo} />
|
||||||
prefetch={prefetchRelatedLinks}
|
</div>
|
||||||
/>}
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
{showCamera && shouldShowCameraDataForPhoto(photo) &&
|
<div className="space-y-4">
|
||||||
<div className="space-y-0.5">
|
{photo.caption &&
|
||||||
<PhotoCamera
|
<div className="uppercase">
|
||||||
camera={camera}
|
{photo.caption}
|
||||||
type="text-only"
|
</div>}
|
||||||
prefetch={prefetchRelatedLinks}
|
{(showCameraContent || showTagsContent) &&
|
||||||
/>
|
<div>
|
||||||
|
{showCameraContent &&
|
||||||
|
<PhotoCamera
|
||||||
|
camera={camera}
|
||||||
|
contrast="medium"
|
||||||
|
/>}
|
||||||
|
{showTagsContent &&
|
||||||
|
<PhotoTags tags={tags} contrast="medium" />}
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* EXIF Data */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{showExifContent &&
|
||||||
|
<>
|
||||||
|
<ul className="text-medium">
|
||||||
|
<li>
|
||||||
|
{photo.focalLengthFormatted}
|
||||||
|
{photo.focalLengthIn35MmFormatFormatted &&
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<span
|
||||||
|
title="35mm equivalent"
|
||||||
|
className="text-extra-dim"
|
||||||
|
>
|
||||||
|
{photo.focalLengthIn35MmFormatFormatted}
|
||||||
|
</span>
|
||||||
|
</>}
|
||||||
|
</li>
|
||||||
|
<li>{photo.fNumberFormatted}</li>
|
||||||
|
<li>{photo.exposureTimeFormatted}</li>
|
||||||
|
<li>{photo.isoFormatted}</li>
|
||||||
|
<li>{photo.exposureCompensationFormatted ?? '0ev'}</li>
|
||||||
|
</ul>
|
||||||
{showSimulation && photo.filmSimulation &&
|
{showSimulation && photo.filmSimulation &&
|
||||||
<div className="translate-x-[-0.3rem]">
|
<PhotoFilmSimulation
|
||||||
<PhotoFilmSimulation
|
simulation={photo.filmSimulation}
|
||||||
simulation={photo.filmSimulation}
|
/>}
|
||||||
prefetch={prefetchRelatedLinks}
|
</>}
|
||||||
/>
|
|
||||||
</div>}
|
|
||||||
</div>}
|
|
||||||
</>)}
|
|
||||||
{renderMiniGrid(<>
|
|
||||||
{shouldShowExifDataForPhoto(photo) &&
|
|
||||||
<ul className="text-medium">
|
|
||||||
<li>
|
|
||||||
{photo.focalLengthFormatted}
|
|
||||||
{photo.focalLengthIn35MmFormatFormatted &&
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<span
|
|
||||||
title="35mm equivalent"
|
|
||||||
className="text-extra-dim"
|
|
||||||
>
|
|
||||||
{photo.focalLengthIn35MmFormatFormatted}
|
|
||||||
</span>
|
|
||||||
</>}
|
|
||||||
</li>
|
|
||||||
<li>{photo.fNumberFormatted}</li>
|
|
||||||
<li>{photo.exposureTimeFormatted}</li>
|
|
||||||
<li>{photo.isoFormatted}</li>
|
|
||||||
<li>{photo.exposureCompensationFormatted ?? '—'}</li>
|
|
||||||
</ul>}
|
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'flex gap-y-4',
|
'flex gap-2',
|
||||||
'flex-col sm:flex-row md:flex-col',
|
'md:flex-col md:gap-4 md:justify-normal',
|
||||||
)}>
|
)}>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'grow uppercase',
|
'text-medium uppercase pr-1',
|
||||||
'text-medium',
|
|
||||||
)}>
|
)}>
|
||||||
{photo.takenAtNaiveFormatted}
|
{photo.takenAtNaiveFormatted}
|
||||||
</div>
|
</div>
|
||||||
@ -166,7 +159,7 @@ export default function PhotoLarge({
|
|||||||
shouldScroll={shouldScrollOnShare}
|
shouldScroll={shouldScrollOnShare}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>, false)}
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -34,51 +34,58 @@ import {
|
|||||||
} from '@/site/paths';
|
} from '@/site/paths';
|
||||||
import { extractExifDataFromBlobPath } from './server';
|
import { extractExifDataFromBlobPath } from './server';
|
||||||
import { TAG_FAVS, isTagFavs } from '@/tag';
|
import { TAG_FAVS, isTagFavs } from '@/tag';
|
||||||
import { convertPhotoToPhotoDbInsert, titleForPhoto } from '.';
|
|
||||||
import { TbPhoto } from 'react-icons/tb';
|
import { TbPhoto } from 'react-icons/tb';
|
||||||
import PhotoTiny from './PhotoTiny';
|
import PhotoTiny from './PhotoTiny';
|
||||||
import { formatDate } from '@/utility/date';
|
import { formatDate } from '@/utility/date';
|
||||||
|
import { convertPhotoToPhotoDbInsert, titleForPhoto } from '.';
|
||||||
|
import { safelyRunServerAdminAction } from '@/auth';
|
||||||
|
|
||||||
export async function createPhotoAction(formData: FormData) {
|
export async function createPhotoAction(formData: FormData) {
|
||||||
const photo = convertFormDataToPhotoDbInsert(formData, true);
|
return safelyRunServerAdminAction(async () => {
|
||||||
|
const photo = convertFormDataToPhotoDbInsert(formData, true);
|
||||||
|
|
||||||
const updatedUrl = await convertUploadToPhoto(photo.url, photo.id);
|
const updatedUrl = await convertUploadToPhoto(photo.url, photo.id);
|
||||||
|
|
||||||
if (updatedUrl) { photo.url = updatedUrl; }
|
if (updatedUrl) { photo.url = updatedUrl; }
|
||||||
|
|
||||||
await sqlInsertPhoto(photo);
|
await sqlInsertPhoto(photo);
|
||||||
|
|
||||||
revalidateAllKeysAndPaths();
|
revalidateAllKeysAndPaths();
|
||||||
|
|
||||||
redirect(PATH_ADMIN_PHOTOS);
|
redirect(PATH_ADMIN_PHOTOS);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updatePhotoAction(formData: FormData) {
|
export async function updatePhotoAction(formData: FormData) {
|
||||||
const photo = convertFormDataToPhotoDbInsert(formData);
|
return safelyRunServerAdminAction(async () => {
|
||||||
|
const photo = convertFormDataToPhotoDbInsert(formData);
|
||||||
|
|
||||||
await sqlUpdatePhoto(photo);
|
await sqlUpdatePhoto(photo);
|
||||||
|
|
||||||
revalidatePhoto(photo.id);
|
revalidatePhoto(photo.id);
|
||||||
|
|
||||||
redirect(PATH_ADMIN_PHOTOS);
|
redirect(PATH_ADMIN_PHOTOS);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleFavoritePhotoAction(
|
export async function toggleFavoritePhotoAction(
|
||||||
photoId: string,
|
photoId: string,
|
||||||
shouldRedirect?: boolean,
|
shouldRedirect?: boolean,
|
||||||
) {
|
) {
|
||||||
const photo = await getPhoto(photoId);
|
return safelyRunServerAdminAction(async () => {
|
||||||
if (photo) {
|
const photo = await getPhoto(photoId);
|
||||||
const { tags } = photo;
|
if (photo) {
|
||||||
photo.tags = tags.some(tag => tag === TAG_FAVS)
|
const { tags } = photo;
|
||||||
? tags.filter(tag => !isTagFavs(tag))
|
photo.tags = tags.some(tag => tag === TAG_FAVS)
|
||||||
: [...tags, TAG_FAVS];
|
? tags.filter(tag => !isTagFavs(tag))
|
||||||
await sqlUpdatePhoto(convertPhotoToPhotoDbInsert(photo));
|
: [...tags, TAG_FAVS];
|
||||||
revalidateAllKeysAndPaths();
|
await sqlUpdatePhoto(convertPhotoToPhotoDbInsert(photo));
|
||||||
if (shouldRedirect) {
|
revalidateAllKeysAndPaths();
|
||||||
redirect(pathForPhoto(photoId));
|
if (shouldRedirect) {
|
||||||
|
redirect(pathForPhoto(photoId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deletePhotoAction(
|
export async function deletePhotoAction(
|
||||||
@ -86,84 +93,98 @@ export async function deletePhotoAction(
|
|||||||
photoUrl: string,
|
photoUrl: string,
|
||||||
shouldRedirect?: boolean,
|
shouldRedirect?: boolean,
|
||||||
) {
|
) {
|
||||||
await sqlDeletePhoto(photoId).then(() => deleteStorageUrl(photoUrl));
|
return safelyRunServerAdminAction(async () => {
|
||||||
revalidateAllKeysAndPaths();
|
await sqlDeletePhoto(photoId).then(() => deleteStorageUrl(photoUrl));
|
||||||
if (shouldRedirect) {
|
revalidateAllKeysAndPaths();
|
||||||
redirect(PATH_ROOT);
|
if (shouldRedirect) {
|
||||||
}
|
redirect(PATH_ROOT);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function deletePhotoFormAction(formData: FormData) {
|
export async function deletePhotoFormAction(formData: FormData) {
|
||||||
return deletePhotoAction(
|
return safelyRunServerAdminAction(async () =>
|
||||||
formData.get('id') as string,
|
deletePhotoAction(
|
||||||
formData.get('url') as string,
|
formData.get('id') as string,
|
||||||
|
formData.get('url') as string,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function deletePhotoTagGloballyAction(formData: FormData) {
|
export async function deletePhotoTagGloballyAction(formData: FormData) {
|
||||||
const tag = formData.get('tag') as string;
|
return safelyRunServerAdminAction(async () => {
|
||||||
|
const tag = formData.get('tag') as string;
|
||||||
|
|
||||||
await sqlDeletePhotoTagGlobally(tag);
|
await sqlDeletePhotoTagGlobally(tag);
|
||||||
|
|
||||||
revalidatePhotosKey();
|
revalidatePhotosKey();
|
||||||
revalidateAdminPaths();
|
revalidateAdminPaths();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renamePhotoTagGloballyAction(formData: FormData) {
|
export async function renamePhotoTagGloballyAction(formData: FormData) {
|
||||||
const tag = formData.get('tag') as string;
|
return safelyRunServerAdminAction(async () => {
|
||||||
const updatedTag = formData.get('updatedTag') as string;
|
const tag = formData.get('tag') as string;
|
||||||
|
const updatedTag = formData.get('updatedTag') as string;
|
||||||
|
|
||||||
if (tag && updatedTag && tag !== updatedTag) {
|
if (tag && updatedTag && tag !== updatedTag) {
|
||||||
await sqlRenamePhotoTagGlobally(tag, updatedTag);
|
await sqlRenamePhotoTagGlobally(tag, updatedTag);
|
||||||
revalidatePhotosKey();
|
revalidatePhotosKey();
|
||||||
revalidateTagsKey();
|
revalidateTagsKey();
|
||||||
redirect(PATH_ADMIN_TAGS);
|
redirect(PATH_ADMIN_TAGS);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteBlobPhotoAction(formData: FormData) {
|
export async function deleteBlobPhotoAction(formData: FormData) {
|
||||||
await deleteStorageUrl(formData.get('url') as string);
|
return safelyRunServerAdminAction(async () => {
|
||||||
|
await deleteStorageUrl(formData.get('url') as string);
|
||||||
|
|
||||||
revalidateAdminPaths();
|
revalidateAdminPaths();
|
||||||
|
|
||||||
if (formData.get('redirectToPhotos') === 'true') {
|
if (formData.get('redirectToPhotos') === 'true') {
|
||||||
redirect(PATH_ADMIN_PHOTOS);
|
redirect(PATH_ADMIN_PHOTOS);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExifDataAction(
|
export async function getExifDataAction(
|
||||||
photoFormPrevious: Partial<PhotoFormData>,
|
photoFormPrevious: Partial<PhotoFormData>,
|
||||||
): Promise<Partial<PhotoFormData>> {
|
): Promise<Partial<PhotoFormData>> {
|
||||||
const { url } = photoFormPrevious;
|
return safelyRunServerAdminAction(async () => {
|
||||||
if (url) {
|
const { url } = photoFormPrevious;
|
||||||
const { photoFormExif } = await extractExifDataFromBlobPath(url);
|
if (url) {
|
||||||
if (photoFormExif) {
|
const { photoFormExif } = await extractExifDataFromBlobPath(url);
|
||||||
return photoFormExif;
|
if (photoFormExif) {
|
||||||
|
return photoFormExif;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return {};
|
||||||
return {};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncPhotoExifDataAction(formData: FormData) {
|
export async function syncPhotoExifDataAction(formData: FormData) {
|
||||||
const photoId = formData.get('id') as string;
|
return safelyRunServerAdminAction(async () => {
|
||||||
if (photoId) {
|
const photoId = formData.get('id') as string;
|
||||||
const photo = await getPhoto(photoId);
|
if (photoId) {
|
||||||
if (photo) {
|
const photo = await getPhoto(photoId);
|
||||||
const { photoFormExif } = await extractExifDataFromBlobPath(photo.url);
|
if (photo) {
|
||||||
if (photoFormExif) {
|
const { photoFormExif } = await extractExifDataFromBlobPath(photo.url);
|
||||||
const photoFormDbInsert = convertFormDataToPhotoDbInsert({
|
if (photoFormExif) {
|
||||||
...convertPhotoToFormData(photo),
|
const photoFormDbInsert = convertFormDataToPhotoDbInsert({
|
||||||
...photoFormExif,
|
...convertPhotoToFormData(photo),
|
||||||
});
|
...photoFormExif,
|
||||||
await sqlUpdatePhoto(photoFormDbInsert);
|
});
|
||||||
revalidatePhotosKey();
|
await sqlUpdatePhoto(photoFormDbInsert);
|
||||||
|
revalidatePhotosKey();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncCacheAction() {
|
export async function syncCacheAction() {
|
||||||
revalidateAllKeysAndPaths();
|
return safelyRunServerAdminAction(revalidateAllKeysAndPaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPhotoItemsAction(query: string) {
|
export async function getPhotoItemsAction(query: string) {
|
||||||
|
|||||||
@ -168,15 +168,16 @@ export default function PhotoForm({
|
|||||||
tagOptions,
|
tagOptions,
|
||||||
readOnly,
|
readOnly,
|
||||||
validate,
|
validate,
|
||||||
|
validateStringMaxLength,
|
||||||
capitalize,
|
capitalize,
|
||||||
hideIfEmpty,
|
hideIfEmpty,
|
||||||
hideBasedOnCamera,
|
shouldHide,
|
||||||
loadingMessage,
|
loadingMessage,
|
||||||
type,
|
type,
|
||||||
}]) =>
|
}]) =>
|
||||||
(
|
(
|
||||||
(!hideIfEmpty || formData[key]) &&
|
(!hideIfEmpty || formData[key]) &&
|
||||||
!hideBasedOnCamera?.(formData.make)
|
!shouldHide?.(formData)
|
||||||
) &&
|
) &&
|
||||||
<FieldSetWithStatus
|
<FieldSetWithStatus
|
||||||
key={key}
|
key={key}
|
||||||
@ -189,6 +190,13 @@ export default function PhotoForm({
|
|||||||
setFormData({ ...formData, [key]: value });
|
setFormData({ ...formData, [key]: value });
|
||||||
if (validate) {
|
if (validate) {
|
||||||
setFormErrors({ ...formErrors, [key]: validate(value) });
|
setFormErrors({ ...formErrors, [key]: validate(value) });
|
||||||
|
} else if (validateStringMaxLength !== undefined) {
|
||||||
|
setFormErrors({
|
||||||
|
...formErrors,
|
||||||
|
[key]: value.length > validateStringMaxLength
|
||||||
|
? `${validateStringMaxLength} characters or less`
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (key === 'title') {
|
if (key === 'title') {
|
||||||
onTitleChange?.(value.trim());
|
onTitleChange?.(value.trim());
|
||||||
|
|||||||
@ -39,10 +39,11 @@ type FormMeta = {
|
|||||||
virtual?: boolean
|
virtual?: boolean
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
validate?: (value?: string) => string | undefined
|
validate?: (value?: string) => string | undefined
|
||||||
|
validateStringMaxLength?: number
|
||||||
capitalize?: boolean
|
capitalize?: boolean
|
||||||
hide?: boolean
|
hide?: boolean
|
||||||
hideIfEmpty?: boolean
|
hideIfEmpty?: boolean
|
||||||
hideBasedOnCamera?: (make?: string, mode?: string) => boolean
|
shouldHide?: (formData: Partial<PhotoFormData>) => boolean
|
||||||
loadingMessage?: string
|
loadingMessage?: string
|
||||||
type?: FieldSetType
|
type?: FieldSetType
|
||||||
selectOptions?: { value: string, label: string }[]
|
selectOptions?: { value: string, label: string }[]
|
||||||
@ -50,10 +51,29 @@ type FormMeta = {
|
|||||||
tagOptions?: AnnotatedTag[]
|
tagOptions?: AnnotatedTag[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const STRING_MAX_LENGTH_SHORT = 255;
|
||||||
|
const STRING_MAX_LENGTH_LONG = 1000;
|
||||||
|
|
||||||
const FORM_METADATA = (
|
const FORM_METADATA = (
|
||||||
tagOptions?: AnnotatedTag[]
|
tagOptions?: AnnotatedTag[]
|
||||||
): Record<keyof PhotoFormData, FormMeta> => ({
|
): Record<keyof PhotoFormData, FormMeta> => ({
|
||||||
title: { label: 'title', capitalize: true },
|
title: {
|
||||||
|
label: 'title',
|
||||||
|
capitalize: true,
|
||||||
|
validateStringMaxLength: STRING_MAX_LENGTH_SHORT,
|
||||||
|
},
|
||||||
|
caption: {
|
||||||
|
label: 'caption',
|
||||||
|
capitalize: true,
|
||||||
|
validateStringMaxLength: STRING_MAX_LENGTH_LONG,
|
||||||
|
shouldHide: ({ title, caption }) => !title && !caption,
|
||||||
|
},
|
||||||
|
semanticDescription: {
|
||||||
|
label: 'semantic description',
|
||||||
|
capitalize: true,
|
||||||
|
validateStringMaxLength: STRING_MAX_LENGTH_LONG,
|
||||||
|
hide: true,
|
||||||
|
},
|
||||||
tags: {
|
tags: {
|
||||||
label: 'tags',
|
label: 'tags',
|
||||||
tagOptions,
|
tagOptions,
|
||||||
@ -78,7 +98,7 @@ const FORM_METADATA = (
|
|||||||
label: 'fujifilm simulation',
|
label: 'fujifilm simulation',
|
||||||
selectOptions: FILM_SIMULATION_FORM_INPUT_OPTIONS,
|
selectOptions: FILM_SIMULATION_FORM_INPUT_OPTIONS,
|
||||||
selectOptionsDefaultLabel: 'Unknown',
|
selectOptionsDefaultLabel: 'Unknown',
|
||||||
hideBasedOnCamera: make => make !== MAKE_FUJIFILM,
|
shouldHide: ({ make }) => make !== MAKE_FUJIFILM,
|
||||||
},
|
},
|
||||||
focalLength: { label: 'focal length' },
|
focalLength: { label: 'focal length' },
|
||||||
focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' },
|
focalLengthIn35MmFormat: { label: 'focal length 35mm-equivalent' },
|
||||||
@ -116,9 +136,12 @@ export const getFormErrors = (
|
|||||||
|
|
||||||
export const isFormValid = (formData: Partial<PhotoFormData>) =>
|
export const isFormValid = (formData: Partial<PhotoFormData>) =>
|
||||||
FORM_METADATA_ENTRIES().every(
|
FORM_METADATA_ENTRIES().every(
|
||||||
([key, { required, validate }]) =>
|
([key, { required, validate, validateStringMaxLength }]) =>
|
||||||
(!required || Boolean(formData[key])) &&
|
(!required || Boolean(formData[key])) &&
|
||||||
(validate?.(formData[key]) === undefined)
|
(validate?.(formData[key]) === undefined) &&
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
(!validateStringMaxLength || (formData[key]?.length ?? 0) <= validateStringMaxLength) &&
|
||||||
|
(key !== 'tags' || !doesTagsStringIncludeFavs(formData.tags ?? ''))
|
||||||
);
|
);
|
||||||
|
|
||||||
// CREATE FORM DATA: FROM PHOTO
|
// CREATE FORM DATA: FROM PHOTO
|
||||||
|
|||||||
@ -55,6 +55,8 @@ export interface PhotoDbInsert extends PhotoExif {
|
|||||||
extension: string
|
extension: string
|
||||||
blurData?: string
|
blurData?: string
|
||||||
title?: string
|
title?: string
|
||||||
|
caption?: string
|
||||||
|
semanticDescription?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
locationName?: string
|
locationName?: string
|
||||||
priorityOrder?: number
|
priorityOrder?: number
|
||||||
@ -237,16 +239,16 @@ export const dateRangeForPhotos = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const photoHasCameraData = (photo: Photo) =>
|
const photoHasCameraData = (photo: Photo) =>
|
||||||
photo.make &&
|
Boolean(photo.make) &&
|
||||||
photo.model;
|
Boolean(photo.model);
|
||||||
|
|
||||||
const photoHasExifData = (photo: Photo) =>
|
const photoHasExifData = (photo: Photo) =>
|
||||||
photo.focalLength ||
|
Boolean(photo.focalLength) ||
|
||||||
photo.focalLengthIn35MmFormat ||
|
Boolean(photo.focalLengthIn35MmFormat) ||
|
||||||
photo.fNumberFormatted ||
|
Boolean(photo.fNumberFormatted) ||
|
||||||
photo.isoFormatted ||
|
Boolean(photo.isoFormatted) ||
|
||||||
photo.exposureTimeFormatted ||
|
Boolean(photo.exposureTimeFormatted) ||
|
||||||
photo.exposureCompensationFormatted;
|
Boolean(photo.exposureCompensationFormatted);
|
||||||
|
|
||||||
export const shouldShowCameraDataForPhoto = (photo: Photo) =>
|
export const shouldShowCameraDataForPhoto = (photo: Photo) =>
|
||||||
SHOW_EXIF_DATA && photoHasCameraData(photo);
|
SHOW_EXIF_DATA && photoHasCameraData(photo);
|
||||||
|
|||||||
@ -29,6 +29,8 @@ const sqlCreatePhotosTable = () =>
|
|||||||
aspect_ratio REAL DEFAULT 1.5,
|
aspect_ratio REAL DEFAULT 1.5,
|
||||||
blur_data TEXT,
|
blur_data TEXT,
|
||||||
title VARCHAR(255),
|
title VARCHAR(255),
|
||||||
|
caption TEXT,
|
||||||
|
semantic_description TEXT,
|
||||||
tags VARCHAR(255)[],
|
tags VARCHAR(255)[],
|
||||||
make VARCHAR(255),
|
make VARCHAR(255),
|
||||||
model VARCHAR(255),
|
model VARCHAR(255),
|
||||||
@ -51,9 +53,18 @@ const sqlCreatePhotosTable = () =>
|
|||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Migration 01
|
||||||
|
const MIGRATION_FIELDS_01 = ['caption', 'semantic_description'];
|
||||||
|
const sqlRunMigration01 = () =>
|
||||||
|
sql`
|
||||||
|
ALTER TABLE photos
|
||||||
|
ADD COLUMN IF NOT EXISTS caption TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS semantic_description TEXT
|
||||||
|
`;
|
||||||
|
|
||||||
// Must provide id as 8-character nanoid
|
// Must provide id as 8-character nanoid
|
||||||
export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
|
export const sqlInsertPhoto = (photo: PhotoDbInsert) =>
|
||||||
return sql`
|
safelyQueryPhotos(() => sql`
|
||||||
INSERT INTO photos (
|
INSERT INTO photos (
|
||||||
id,
|
id,
|
||||||
url,
|
url,
|
||||||
@ -61,6 +72,8 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
|
|||||||
aspect_ratio,
|
aspect_ratio,
|
||||||
blur_data,
|
blur_data,
|
||||||
title,
|
title,
|
||||||
|
caption,
|
||||||
|
semantic_description,
|
||||||
tags,
|
tags,
|
||||||
make,
|
make,
|
||||||
model,
|
model,
|
||||||
@ -86,6 +99,8 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
|
|||||||
${photo.aspectRatio},
|
${photo.aspectRatio},
|
||||||
${photo.blurData},
|
${photo.blurData},
|
||||||
${photo.title},
|
${photo.title},
|
||||||
|
${photo.caption},
|
||||||
|
${photo.semanticDescription},
|
||||||
${convertArrayToPostgresString(photo.tags)},
|
${convertArrayToPostgresString(photo.tags)},
|
||||||
${photo.make},
|
${photo.make},
|
||||||
${photo.model},
|
${photo.model},
|
||||||
@ -104,17 +119,18 @@ export const sqlInsertPhoto = (photo: PhotoDbInsert) => {
|
|||||||
${photo.takenAt},
|
${photo.takenAt},
|
||||||
${photo.takenAtNaive}
|
${photo.takenAtNaive}
|
||||||
)
|
)
|
||||||
`;
|
`, 'sqlInsertPhoto');
|
||||||
};
|
|
||||||
|
|
||||||
export const sqlUpdatePhoto = (photo: PhotoDbInsert) =>
|
export const sqlUpdatePhoto = (photo: PhotoDbInsert) =>
|
||||||
sql`
|
safelyQueryPhotos(() => sql`
|
||||||
UPDATE photos SET
|
UPDATE photos SET
|
||||||
url=${photo.url},
|
url=${photo.url},
|
||||||
extension=${photo.extension},
|
extension=${photo.extension},
|
||||||
aspect_ratio=${photo.aspectRatio},
|
aspect_ratio=${photo.aspectRatio},
|
||||||
blur_data=${photo.blurData},
|
blur_data=${photo.blurData},
|
||||||
title=${photo.title},
|
title=${photo.title},
|
||||||
|
caption=${photo.caption},
|
||||||
|
semantic_description=${photo.semanticDescription},
|
||||||
tags=${convertArrayToPostgresString(photo.tags)},
|
tags=${convertArrayToPostgresString(photo.tags)},
|
||||||
make=${photo.make},
|
make=${photo.make},
|
||||||
model=${photo.model},
|
model=${photo.model},
|
||||||
@ -134,27 +150,33 @@ export const sqlUpdatePhoto = (photo: PhotoDbInsert) =>
|
|||||||
taken_at_naive=${photo.takenAtNaive},
|
taken_at_naive=${photo.takenAtNaive},
|
||||||
updated_at=${(new Date()).toISOString()}
|
updated_at=${(new Date()).toISOString()}
|
||||||
WHERE id=${photo.id}
|
WHERE id=${photo.id}
|
||||||
`;
|
`, 'sqlUpdatePhoto');
|
||||||
|
|
||||||
export const sqlDeletePhotoTagGlobally = (tag: string) =>
|
export const sqlDeletePhotoTagGlobally = (tag: string) =>
|
||||||
sql`
|
safelyQueryPhotos(() => sql`
|
||||||
UPDATE photos
|
UPDATE photos
|
||||||
SET tags=ARRAY_REMOVE(tags, ${tag})
|
SET tags=ARRAY_REMOVE(tags, ${tag})
|
||||||
WHERE ${tag}=ANY(tags)
|
WHERE ${tag}=ANY(tags)
|
||||||
`;
|
`, 'sqlDeletePhotoTagGlobally');
|
||||||
|
|
||||||
export const sqlRenamePhotoTagGlobally = (tag: string, updatedTag: string) =>
|
export const sqlRenamePhotoTagGlobally = (tag: string, updatedTag: string) =>
|
||||||
sql`
|
safelyQueryPhotos(() => sql`
|
||||||
UPDATE photos
|
UPDATE photos
|
||||||
SET tags=ARRAY_REPLACE(tags, ${tag}, ${updatedTag})
|
SET tags=ARRAY_REPLACE(tags, ${tag}, ${updatedTag})
|
||||||
WHERE ${tag}=ANY(tags)
|
WHERE ${tag}=ANY(tags)
|
||||||
`;
|
`, 'sqlRenamePhotoTagGlobally');
|
||||||
|
|
||||||
export const sqlDeletePhoto = (id: string) =>
|
export const sqlDeletePhoto = (id: string) =>
|
||||||
sql`DELETE FROM photos WHERE id=${id}`;
|
safelyQueryPhotos(
|
||||||
|
() => sql`DELETE FROM photos WHERE id=${id}`,
|
||||||
|
'sqlDeletePhoto',
|
||||||
|
);
|
||||||
|
|
||||||
const sqlGetPhoto = (id: string) =>
|
const sqlGetPhoto = (id: string) =>
|
||||||
sql<PhotoDb>`SELECT * FROM photos WHERE id=${id} LIMIT 1`;
|
safelyQueryPhotos(
|
||||||
|
() => sql<PhotoDb>`SELECT * FROM photos WHERE id=${id} LIMIT 1`,
|
||||||
|
'sqlGetPhoto',
|
||||||
|
);
|
||||||
|
|
||||||
const sqlGetPhotosCount = async () => sql`
|
const sqlGetPhotosCount = async () => sql`
|
||||||
SELECT COUNT(*) FROM photos
|
SELECT COUNT(*) FROM photos
|
||||||
@ -287,8 +309,16 @@ const safelyQueryPhotos = async <T>(
|
|||||||
result = await callback();
|
result = await callback();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
screenForPPR(e, undefined, 'neon postgres');
|
screenForPPR(e, undefined, 'neon postgres');
|
||||||
if (/relation "photos" does not exist/i.test(e.message)) {
|
if (MIGRATION_FIELDS_01.some(field => new RegExp(
|
||||||
console.log('Creating table "photos" because it did not exist');
|
`column "${field}" of relation "photos" does not exist`,
|
||||||
|
'i',
|
||||||
|
).test(e.message))) {
|
||||||
|
console.log('Running migration 01 ...');
|
||||||
|
await sqlRunMigration01();
|
||||||
|
result = await callback();
|
||||||
|
} else if (/relation "photos" does not exist/i.test(e.message)) {
|
||||||
|
// If the table does not exist, create it
|
||||||
|
console.log('Creating photos table ...');
|
||||||
await sqlCreatePhotosTable();
|
await sqlCreatePhotosTable();
|
||||||
result = await callback();
|
result = await callback();
|
||||||
} else if (/endpoint is in transition/i.test(e.message)) {
|
} else if (/endpoint is in transition/i.test(e.message)) {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export default function PhotoTag({
|
|||||||
href={pathForTag(tag)}
|
href={pathForTag(tag)}
|
||||||
icon={<FaTag
|
icon={<FaTag
|
||||||
size={11}
|
size={11}
|
||||||
className="text-icon translate-y-[5px]"
|
className="translate-y-[5px]"
|
||||||
/>}
|
/>}
|
||||||
type={type}
|
type={type}
|
||||||
badged={badged}
|
badged={badged}
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import PhotoTag from '@/tag/PhotoTag';
|
import PhotoTag from '@/tag/PhotoTag';
|
||||||
import { isTagFavs } from '.';
|
import { isTagFavs } from '.';
|
||||||
import FavsTag from './FavsTag';
|
import FavsTag from './FavsTag';
|
||||||
|
import { EntityLinkExternalProps } from '@/components/EntityLink';
|
||||||
|
|
||||||
export default function PhotoTags({
|
export default function PhotoTags({
|
||||||
tags,
|
tags,
|
||||||
prefetch,
|
contrast,
|
||||||
}: {
|
}: {
|
||||||
tags: string[]
|
tags: string[]
|
||||||
prefetch?: boolean
|
} & EntityLinkExternalProps) {
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="-space-y-0.5">
|
<div className="-space-y-0.5">
|
||||||
{tags.map(tag =>
|
{tags.map(tag =>
|
||||||
<div key={tag}>
|
<div key={tag}>
|
||||||
{isTagFavs(tag)
|
{isTagFavs(tag)
|
||||||
? <FavsTag {...{ prefetch }} />
|
? <FavsTag {...{ contrast }} />
|
||||||
: <PhotoTag {...{ tag, prefetch }} />}
|
: <PhotoTag {...{ tag, contrast }} />}
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user