feat: add screenshots carousel modal

This commit is contained in:
2024-10-21 03:52:48 +03:00
parent addcfe0942
commit 02be1530f1
10 changed files with 1044 additions and 132 deletions

View File

@@ -11,11 +11,13 @@
},
"dependencies": {
"@fontsource-variable/nunito": "^5.1.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-query": "^5.59.15",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.3.0",
"eslint-plugin-react": "^7.37.1",
"lucide-react": "^0.453.0",
"react": "^18.3.1",

421
frontend/pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@fontsource-variable/nunito':
specifier: ^5.1.0
version: 5.1.0
'@radix-ui/react-dialog':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-icons':
specifier: ^1.3.0
version: 1.3.0(react@18.3.1)
@@ -26,6 +29,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
embla-carousel-react:
specifier: ^8.3.0
version: 8.3.0(react@18.3.1)
eslint-plugin-react:
specifier: ^7.37.1
version: 7.37.1(eslint@9.12.0(jiti@2.3.3))
@@ -420,6 +426,9 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@radix-ui/primitive@1.1.0':
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
'@radix-ui/react-compose-refs@1.1.0':
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
@@ -429,11 +438,116 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.1':
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.2':
resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-dismissable-layer@1.1.1':
resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-focus-guards@1.1.1':
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-focus-scope@1.1.0':
resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-icons@1.3.0':
resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==}
peerDependencies:
react: ^16.x || ^17.x || ^18.x
'@radix-ui/react-id@1.1.0':
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-portal@1.1.2':
resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.1':
resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.0':
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.1.0':
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
peerDependencies:
@@ -443,6 +557,42 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.1.0':
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.1.0':
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.0':
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@remix-run/router@1.20.0':
resolution: {integrity: sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==}
engines: {node: '>=14.0.0'}
@@ -739,6 +889,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
aria-hidden@1.2.4:
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
engines: {node: '>=10'}
array-buffer-byte-length@1.0.1:
resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
engines: {node: '>= 0.4'}
@@ -908,6 +1062,9 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -924,6 +1081,19 @@ packages:
electron-to-chromium@1.5.40:
resolution: {integrity: sha512-LYm78o6if4zTasnYclgQzxEcgMoIcybWOhkATWepN95uwVVWV0/IW10v+2sIeHE+bIYWipLneTftVyQm45UY7g==}
embla-carousel-react@8.3.0:
resolution: {integrity: sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==}
peerDependencies:
react: ^16.8.0 || ^17.0.1 || ^18.0.0
embla-carousel-reactive-utils@8.3.0:
resolution: {integrity: sha512-EYdhhJ302SC4Lmkx8GRsp0sjUhEN4WyFXPOk0kGu9OXZSRMmcBlRgTvHcq8eKJE1bXWBsOi1T83B+BSSVZSmwQ==}
peerDependencies:
embla-carousel: 8.3.0
embla-carousel@8.3.0:
resolution: {integrity: sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -1100,6 +1270,10 @@ packages:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
get-symbol-description@1.0.2:
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
engines: {node: '>= 0.4'}
@@ -1190,6 +1364,9 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
is-array-buffer@3.0.4:
resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
engines: {node: '>= 0.4'}
@@ -1612,6 +1789,26 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
react-remove-scroll-bar@2.3.6:
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-remove-scroll@2.6.0:
resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react-router-dom@6.27.0:
resolution: {integrity: sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==}
engines: {node: '>=14.0.0'}
@@ -1625,6 +1822,16 @@ packages:
peerDependencies:
react: '>=16.8'
react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -1819,6 +2026,9 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tslib@2.8.0:
resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -1873,6 +2083,26 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
use-callback-ref@1.3.2:
resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
use-sidecar@1.1.2:
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -2252,16 +2482,112 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@radix-ui/primitive@1.1.0': {}
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-context@1.1.1(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1)
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-icons@1.3.0(react@18.3.1)':
dependencies:
react: 18.3.1
'@radix-ui/react-id@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-slot@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
@@ -2269,6 +2595,32 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@remix-run/router@1.20.0': {}
'@rollup/rollup-android-arm-eabi@4.24.0':
@@ -2539,6 +2891,10 @@ snapshots:
argparse@2.0.1: {}
aria-hidden@1.2.4:
dependencies:
tslib: 2.8.0
array-buffer-byte-length@1.0.1:
dependencies:
call-bind: 1.0.7
@@ -2744,6 +3100,8 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
detect-node-es@1.1.0: {}
didyoumean@1.2.2: {}
dlv@1.1.3: {}
@@ -2756,6 +3114,18 @@ snapshots:
electron-to-chromium@1.5.40: {}
embla-carousel-react@8.3.0(react@18.3.1):
dependencies:
embla-carousel: 8.3.0
embla-carousel-reactive-utils: 8.3.0(embla-carousel@8.3.0)
react: 18.3.1
embla-carousel-reactive-utils@8.3.0(embla-carousel@8.3.0):
dependencies:
embla-carousel: 8.3.0
embla-carousel@8.3.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -3052,6 +3422,8 @@ snapshots:
has-symbols: 1.0.3
hasown: 2.0.2
get-nonce@1.0.1: {}
get-symbol-description@1.0.2:
dependencies:
call-bind: 1.0.7
@@ -3145,6 +3517,10 @@ snapshots:
hasown: 2.0.2
side-channel: 1.0.6
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
is-array-buffer@3.0.4:
dependencies:
call-bind: 1.0.7
@@ -3521,6 +3897,25 @@ snapshots:
react-refresh@0.14.2: {}
react-remove-scroll-bar@2.3.6(@types/react@18.3.11)(react@18.3.1):
dependencies:
react: 18.3.1
react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1)
tslib: 2.8.0
optionalDependencies:
'@types/react': 18.3.11
react-remove-scroll@2.6.0(@types/react@18.3.11)(react@18.3.1):
dependencies:
react: 18.3.1
react-remove-scroll-bar: 2.3.6(@types/react@18.3.11)(react@18.3.1)
react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1)
tslib: 2.8.0
use-callback-ref: 1.3.2(@types/react@18.3.11)(react@18.3.1)
use-sidecar: 1.1.2(@types/react@18.3.11)(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
react-router-dom@6.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@remix-run/router': 1.20.0
@@ -3533,6 +3928,15 @@ snapshots:
'@remix-run/router': 1.20.0
react: 18.3.1
react-style-singleton@2.2.1(@types/react@18.3.11)(react@18.3.1):
dependencies:
get-nonce: 1.0.1
invariant: 2.2.4
react: 18.3.1
tslib: 2.8.0
optionalDependencies:
'@types/react': 18.3.11
react@18.3.1:
dependencies:
loose-envify: 1.4.0
@@ -3803,6 +4207,8 @@ snapshots:
ts-interface-checker@0.1.13: {}
tslib@2.8.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -3874,6 +4280,21 @@ snapshots:
dependencies:
punycode: 2.3.1
use-callback-ref@1.3.2(@types/react@18.3.11)(react@18.3.1):
dependencies:
react: 18.3.1
tslib: 2.8.0
optionalDependencies:
'@types/react': 18.3.11
use-sidecar@1.1.2(@types/react@18.3.11)(react@18.3.1):
dependencies:
detect-node-es: 1.1.0
react: 18.3.1
tslib: 2.8.0
optionalDependencies:
'@types/react': 18.3.11
util-deprecate@1.0.2: {}
vite@5.4.9(@types/node@22.7.7):

View File

@@ -3,7 +3,7 @@ import { Sidebar } from './components/Sidebar/Sidebar'
import React, { useEffect, useState } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { GetLibraryInfo, OnWindowResize } from '$app'
import { ScreenshotsPage } from './pages/Screenshots/ScreenshotsPage'
import { ScreenshotsHomePage } from './pages/Screenshots/ScreenshotsHomePage'
import { useApi } from './common/api'
import { AppContext } from './common/app_context'
import { LoadingContainer } from './components/Loader/LoadingContainer'
@@ -25,7 +25,7 @@ function App() {
<Routes>
<Route path="/" element={<GamesPage />} />
<Route path="/games" element={<GamesPage />} />
<Route path="/screenshots/*" element={<ScreenshotsPage />} />
<Route path="/screenshots/*" element={<ScreenshotsHomePage />} />
</Routes>
</div>
</div>

View File

@@ -0,0 +1,15 @@
import { HtmlProps } from '@/common/types'
import { screenshots } from '$models'
import { cn } from '@/common/utils'
export function ScreenshotImg({
className,
screenshot,
...rest
}: HtmlProps<'div'> & { screenshot: screenshots.ScreenshotEntry }) {
return (
<div className={cn('', rest.onClick && 'cursor-pointer', className)} {...rest}>
<img className="rounded-md" src={screenshot.base64} alt={screenshot.name} />
</div>
)
}

View File

@@ -0,0 +1,42 @@
import { HtmlProps } from '@/common/types'
import { cn } from '@/common/utils'
import { screenshots } from '$models'
import { DialogContent } from '../ui/dialog'
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from '../ui/carousel'
import { ScreenshotImg } from '../ScreenshotImg/ScreenshotImg'
export function ScreenshotsCarouselModal({
className,
screenshots,
activeIndex,
...rest
}: HtmlProps<'div'> & {
screenshots: screenshots.ScreenshotEntry[]
activeIndex?: number | null
}) {
return (
<div className={cn('', className)} {...rest}>
<DialogContent className="max-w-[calc(100%_-_128px)]">
<Carousel opts={{ startIndex: activeIndex ?? undefined }}>
<CarouselContent>
{screenshots.map((scr) => (
<CarouselItem key={scr.path}>
<div className="h-full flex items-center">
<ScreenshotImg screenshot={scr} />
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</DialogContent>
</div>
)
}

View File

@@ -0,0 +1,260 @@
import * as React from "react"
import { ArrowLeftIcon, ArrowRightIcon } from "@radix-ui/react-icons"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeftIcon className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRightIcon className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View File

@@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,90 @@
import { Link, useParams } from 'react-router-dom'
import { GetScreenshotsForGame, NativeOpen } from '$app'
import { useApi } from '@/common/api'
import { LoadingContainer } from '@/components/Loader/LoadingContainer'
import { Button } from '@/components/ui/button'
import { FaAngleLeft } from 'react-icons/fa6'
import { ScreenshotImg } from '@/components/ScreenshotImg/ScreenshotImg'
import { useCallback, useMemo, useState } from 'react'
import { ScreenshotsCarouselModal } from '@/components/ScreenshotsCarouselModal/ScreenshotsCarouselModal'
import { screenshots } from '$models'
import { Dialog } from '@/components/ui/dialog'
function useScreenshotsDir(gameId: string) {
const { data: screenshots, ...rest } = useApi(
() => GetScreenshotsForGame(gameId),
['screenshots', 'game', gameId],
{
debug: true,
initialData: {} as never,
},
)
return {
screenshots: screenshots ?? {},
...rest,
}
}
function ScreenshotsGamePage() {
const { gameId } = useParams()
const { screenshots, isFetching } = useScreenshotsDir(gameId!)
const [dir] = screenshots.screenshotCollections ?? [{ screenshots: [] }]
const [modalIndex, setModalIndex] = useState<number | null>(null)
const modalScreenshots = useMemo(() => dir.screenshots ?? [], [dir])
const openScreenshotsModal = useCallback(
(index: number) => () => {
setModalIndex(index)
},
[],
)
const closeScreenshotsModal = useCallback(() => {
setModalIndex(null)
}, [])
return (
<div className="relative">
<div className="sticky top-0 p-4 bg-background flex flex-col gap-2 z-10">
<div>
<Button variant="outline" size="sm" asChild>
<Link to="/screenshots">
<FaAngleLeft />
Back
</Link>
</Button>
</div>
<div className="flex items-center gap-2 justify-between">
<h1 className="text-2xl p-4 bg-background">
Screenshots for <span className="text-black dark:text-white">{dir.gameName}</span>
</h1>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => NativeOpen(dir.dir)}>
Browse Folder
</Button>
</div>
</div>
</div>
<div className="p-4 pt-0">
<Dialog
modal
open={modalIndex !== null}
onOpenChange={(open) => !open && closeScreenshotsModal()}
>
<LoadingContainer loading={isFetching}>
<div className="flex items-start gap-4 flex-wrap max-w-full">
{dir.screenshots.map((file, i) => (
<ScreenshotImg
className="max-w-64 rounded-md"
screenshot={file}
key={file.path}
onClick={openScreenshotsModal(i)}
/>
))}
</div>
</LoadingContainer>
<ScreenshotsCarouselModal screenshots={modalScreenshots} activeIndex={modalIndex} />
</Dialog>
</div>
</div>
)
}
export default ScreenshotsGamePage

View File

@@ -0,0 +1,92 @@
import { Link, Route, Routes } from 'react-router-dom'
import { GetScreenshots, NativeOpen } from '$app'
import { useApi } from '@/common/api'
import { LoadingContainer } from '@/components/Loader/LoadingContainer'
import { Button } from '@/components/ui/button'
import ScreenshotsGamePage from './ScreenshotsGamePage'
import { ScreenshotImg } from '@/components/ScreenshotImg/ScreenshotImg'
import { Dialog } from '@/components/ui/dialog'
import { useCallback, useState } from 'react'
import { screenshots } from '$models'
import { ScreenshotsCarouselModal } from '@/components/ScreenshotsCarouselModal/ScreenshotsCarouselModal'
function useScreenshotsDirs() {
const { data: screenshots, ...rest } = useApi(GetScreenshots, ['screenshots'], {
debug: true,
initialData: {} as never,
})
return {
screenshots: screenshots ?? {},
...rest,
}
}
export function ScreenshotsHomePage() {
return (
<Routes>
<Route path="/:gameId" element={<ScreenshotsGamePage />} />
<Route path="/" element={<ScreenshotsHome />} />
</Routes>
)
}
function ScreenshotsHome() {
const { screenshots, isFetching } = useScreenshotsDirs()
const [modalIndex, setModalIndex] = useState<number | null>(null)
const [modalScreenshots, setModalScreenshots] = useState<screenshots.ScreenshotEntry[]>([])
const openScreenshotsModal = useCallback(
(screenshots: screenshots.ScreenshotEntry[], index: number) => () => {
setModalIndex(index)
setModalScreenshots(screenshots)
},
[],
)
const closeScreenshotsModal = useCallback(() => {
setModalIndex(null)
setModalScreenshots([])
}, [])
return (
<div className="relative">
<h1 className="sticky top-0 p-4 bg-background text-2xl z-10">Screenshots</h1>
<div>
<Dialog
modal
open={modalIndex !== null}
onOpenChange={(open) => !open && closeScreenshotsModal()}
>
<LoadingContainer loading={isFetching}>
<div className="flex flex-col gap-2">
{screenshots.screenshotCollections?.map((dir) => (
<div key={dir.dir} className="flex flex-col gap-4 p-4">
<div className="sticky top-[32px] bg-background flex items-center gap-2 justify-between z-0">
<h2 className="text-xl">{dir.gameName}</h2>
<div className="flex items-center gap-2">
<Button variant="outline" asChild>
<Link to={`/screenshots/${dir.gameId}`}>View All ({dir.totalCount})</Link>
</Button>
<Button variant="outline" onClick={() => NativeOpen(dir.dir)}>
Browse Folder
</Button>
</div>
</div>
<div className="flex items-start gap-4 flex-nowrap overflow-x-hidden max-w-full">
{dir.screenshots.map((file, i) => (
<ScreenshotImg
className="max-w-64 rounded-md"
screenshot={file}
key={file.path}
onClick={openScreenshotsModal(dir.screenshots, i)}
/>
))}
</div>
</div>
))}
</div>
</LoadingContainer>
<ScreenshotsCarouselModal screenshots={modalScreenshots} activeIndex={modalIndex} />
</Dialog>
</div>
</div>
)
}

View File

@@ -1,130 +0,0 @@
import { Link, Route, Routes, useParams } from 'react-router-dom'
import { GetScreenshots, GetScreenshotsForGame, NativeOpen } from '$app'
import { useApi } from '@/common/api'
import { useAppContext } from '@/common/app_context'
import { LoadingContainer } from '@/components/Loader/LoadingContainer'
import { Button } from '@/components/ui/button'
import { FaAngleLeft } from 'react-icons/fa6'
function useScreenshotsDir(gameId: string) {
const { data: screenshots, ...rest } = useApi(
() => GetScreenshotsForGame(gameId),
['screenshots', 'game', gameId],
{
debug: true,
initialData: {} as never,
},
)
return {
screenshots: screenshots ?? {},
...rest,
}
}
function useScreenshotsDirs() {
const { data: screenshots, ...rest } = useApi(GetScreenshots, ['screenshots'], {
debug: true,
initialData: {} as never,
})
return {
screenshots: screenshots ?? {},
...rest,
}
}
export function ScreenshotsPage() {
const { meta } = useAppContext()
console.debug('ScreenshotsPage', meta)
return (
<Routes>
<Route path="/:gameId" element={<ScreenshotsGamePage />} />
<Route path="/" element={<ScreenshotsHome />} />
</Routes>
)
}
function ScreenshotsHome() {
const { screenshots, isFetching } = useScreenshotsDirs()
return (
<div className="relative">
<h1 className="sticky top-0 p-4 bg-background text-2xl z-10">Screenshots</h1>
<div>
<LoadingContainer loading={isFetching}>
<div className="flex flex-col gap-2">
{screenshots.screenshotCollections?.map((dir) => (
<div key={dir.dir} className="flex flex-col gap-4 p-4">
<div className="sticky top-[32px] bg-background flex items-center gap-2 justify-between z-0">
<h2 className="text-xl">{dir.gameName}</h2>
<div className="flex items-center gap-2">
<Button variant="outline" asChild>
<Link to={`/screenshots/${dir.gameId}`}>View All ({dir.totalCount})</Link>
</Button>
<Button variant="outline" onClick={() => NativeOpen(dir.dir)}>
Browse Folder
</Button>
</div>
</div>
<div className="flex items-start gap-4 flex-nowrap overflow-x-hidden max-w-full">
{dir.screenshots.map((file) => (
<img
className="max-w-64 rounded-md"
key={file.path}
src={file.base64}
alt={file.name}
/>
))}
</div>
</div>
))}
</div>
</LoadingContainer>
</div>
</div>
)
}
function ScreenshotsGamePage() {
const { gameId } = useParams()
const { screenshots, isFetching } = useScreenshotsDir(gameId!)
const [dir] = screenshots.screenshotCollections ?? [{ screenshots: [] }]
return (
<div className="relative">
<div className="sticky top-0 p-4 bg-background flex flex-col gap-2 z-10">
<div>
<Button variant="outline" size="sm" asChild>
<Link to="/screenshots">
<FaAngleLeft />
Back
</Link>
</Button>
</div>
<div className="flex items-center gap-2 justify-between">
<h1 className="text-2xl p-4 bg-background">
Screenshots for <span className="text-black dark:text-white">{dir.gameName}</span>
</h1>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => NativeOpen(dir.dir)}>
Browse Folder
</Button>
</div>
</div>
</div>
<div className="p-4 pt-0">
<LoadingContainer loading={isFetching}>
<div className="flex flex-col gap-8">
<div className="flex items-start gap-4 flex-wrap max-w-full">
{dir.screenshots.map((file) => (
<img
className="max-w-64 rounded-md"
key={file.path}
src={file.base64}
alt={file.name}
/>
))}
</div>
</div>
</LoadingContainer>
</div>
</div>
)
}