From c7d48ba33ed782b5a9c2feb607136d48b54eb3ed Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 24 Feb 2025 19:41:47 -0600 Subject: [PATCH 01/16] Bump dependencies --- package.json | 20 +- pnpm-lock.yaml | 484 ++++++++++++++++++++++++------------------------- 2 files changed, 248 insertions(+), 256 deletions(-) diff --git a/package.json b/package.json index 04620d50..3fbc2070 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "analyze": "ANALYZE=true next build" }, "dependencies": { - "@ai-sdk/openai": "^1.1.12", + "@ai-sdk/openai": "^1.1.14", "@aws-sdk/client-s3": "3.750.0", "@aws-sdk/s3-request-presigner": "3.750.0", "@radix-ui/react-dialog": "^1.1.6", @@ -21,14 +21,14 @@ "@vercel/analytics": "^1.5.0", "@vercel/blob": "^0.27.1", "@vercel/speed-insights": "^1.2.0", - "ai": "^4.1.41", + "ai": "^4.1.46", "camelcase-keys": "^9.1.3", "cmdk": "^1.0.4", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "exifr": "^7.1.3", - "framer-motion": "^12.4.4", - "nanoid": "^5.1.0", + "framer-motion": "^12.4.7", + "nanoid": "^5.1.2", "next": "15.1.7", "next-auth": "5.0.0-beta.25", "next-themes": "^0.4.4", @@ -45,30 +45,30 @@ "viewerjs": "^1.11.7" }, "devDependencies": { - "@eslint/eslintrc": "^3.2.0", + "@eslint/eslintrc": "^3.3.0", "@next/bundle-analyzer": "15.1.7", "@next/eslint-plugin-next": "^15.1.7", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/postcss": "^4.0.7", + "@tailwindcss/postcss": "^4.0.8", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "@types/pg": "^8.11.11", "@types/react": "19.0.10", "@types/react-dom": "19.0.4", "@types/sanitize-html": "^2.13.0", "clsx": "^2.1.1", "cross-fetch": "^4.1.0", - "eslint": "9.20.1", + "eslint": "9.21.0", "eslint-config-next": "15.1.7", "eslint-plugin-react-hooks": "^5.1.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "postcss": "8.5.2", - "tailwindcss": "4.0.7", + "postcss": "8.5.3", + "tailwindcss": "4.0.8", "ts-node": "^10.9.2", "typescript": "5.7.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38df3f62..10bb7ebc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@ai-sdk/openai': - specifier: ^1.1.12 - version: 1.1.12(zod@3.24.2) + specifier: ^1.1.14 + version: 1.1.14(zod@3.24.2) '@aws-sdk/client-s3': specifier: 3.750.0 version: 3.750.0 @@ -45,8 +45,8 @@ importers: specifier: ^1.2.0 version: 1.2.0(next@15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) ai: - specifier: ^4.1.41 - version: 4.1.41(react@19.0.0)(zod@3.24.2) + specifier: ^4.1.46 + version: 4.1.46(react@19.0.0)(zod@3.24.2) camelcase-keys: specifier: ^9.1.3 version: 9.1.3 @@ -63,11 +63,11 @@ importers: specifier: ^7.1.3 version: 7.1.3 framer-motion: - specifier: ^12.4.4 - version: 12.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^12.4.7 + version: 12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) nanoid: - specifier: ^5.1.0 - version: 5.1.0 + specifier: ^5.1.2 + version: 5.1.2 next: specifier: 15.1.7 version: 15.1.7(@babel/core@7.26.9)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -112,8 +112,8 @@ importers: version: 1.11.7 devDependencies: '@eslint/eslintrc': - specifier: ^3.2.0 - version: 3.2.0 + specifier: ^3.3.0 + version: 3.3.0 '@next/bundle-analyzer': specifier: 15.1.7 version: 15.1.7 @@ -122,13 +122,13 @@ importers: version: 15.1.7 '@tailwindcss/container-queries': specifier: ^0.1.1 - version: 0.1.1(tailwindcss@4.0.7) + version: 0.1.1(tailwindcss@4.0.8) '@tailwindcss/forms': specifier: ^0.5.10 - version: 0.5.10(tailwindcss@4.0.7) + version: 0.5.10(tailwindcss@4.0.8) '@tailwindcss/postcss': - specifier: ^4.0.7 - version: 4.0.7 + specifier: ^4.0.8 + version: 4.0.8 '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -142,8 +142,8 @@ importers: specifier: ^29.5.14 version: 29.5.14 '@types/node': - specifier: ^22.13.4 - version: 22.13.4 + specifier: ^22.13.5 + version: 22.13.5 '@types/pg': specifier: ^8.11.11 version: 8.11.11 @@ -163,29 +163,29 @@ importers: specifier: ^4.1.0 version: 4.1.0 eslint: - specifier: 9.20.1 - version: 9.20.1(jiti@2.4.2) + specifier: 9.21.0 + version: 9.21.0(jiti@2.4.2) eslint-config-next: specifier: 15.1.7 - version: 15.1.7(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + version: 15.1.7(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) eslint-plugin-react-hooks: specifier: ^5.1.0 - version: 5.1.0(eslint@9.20.1(jiti@2.4.2)) + version: 5.1.0(eslint@9.21.0(jiti@2.4.2)) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)) + version: 29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0 postcss: - specifier: 8.5.2 - version: 8.5.2 + specifier: 8.5.3 + version: 8.5.3 tailwindcss: - specifier: 4.0.7 - version: 4.0.7 + specifier: 4.0.8 + version: 4.0.8 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.13.4)(typescript@5.7.3) + version: 10.9.2(@types/node@22.13.5)(typescript@5.7.3) typescript: specifier: 5.7.3 version: 5.7.3 @@ -195,14 +195,14 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} - '@ai-sdk/openai@1.1.12': - resolution: {integrity: sha512-lh3zN4J/XEqkjpZAOBajSttF1Nl2qV/7WxRbORn+4jZmQkmzWQbsEUpgQ6ME+heyRvnsRCNq3fkMGehWWxWuXg==} + '@ai-sdk/openai@1.1.14': + resolution: {integrity: sha512-r5oD+Sz7z8kfxnXfqR53fYQ1xbT/BeUGhQ26FWzs5gO4j52pGUpzCt0SBm3SH1ZSjFY5O/zoKRnsbrPeBe1sNA==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 - '@ai-sdk/provider-utils@2.1.8': - resolution: {integrity: sha512-1j9niMUAFlCBdYRYJr1yoB5kwZcRFBVuBiL1hhrf0ONFNrDiJYA6F+gROOuP16NHhezMfTo60+GeeV1xprHFjg==} + '@ai-sdk/provider-utils@2.1.10': + resolution: {integrity: sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -210,12 +210,12 @@ packages: zod: optional: true - '@ai-sdk/provider@1.0.7': - resolution: {integrity: sha512-q1PJEZ0qD9rVR+8JFEd01/QM++csMT5UVwYXSN2u54BrVw/D8TZLTeg2FEfKK00DgAx0UtWd8XOhhwITP9BT5g==} + '@ai-sdk/provider@1.0.9': + resolution: {integrity: sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA==} engines: {node: '>=18'} - '@ai-sdk/react@1.1.16': - resolution: {integrity: sha512-4Jx1piCte2+YoDd6ZdwM0Mw29046edw7MMNICImCPv2s7sfwFwe4c1t8waA4PYRefuETmzheqjh80kafQYJf8g==} + '@ai-sdk/react@1.1.18': + resolution: {integrity: sha512-2wlWug6NVAc8zh3pgqtvwPkSNTdA6Q4x9CmrNXCeHcXfJkJ+MuHFQz/I7Wb7mLRajf0DAxsFLIhHyBCEuTkDNw==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -226,8 +226,8 @@ packages: zod: optional: true - '@ai-sdk/ui-utils@1.1.14': - resolution: {integrity: sha512-JQXcnPRnDfeH1l503s/8+SxJdmgyUKC3QvKjOpTV6Z/LyRWJZrruBoZnVB1OrL9o/WHEguC+rD+p9udv281KzQ==} + '@ai-sdk/ui-utils@1.1.16': + resolution: {integrity: sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -606,28 +606,24 @@ packages: resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.10.0': - resolution: {integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==} + '@eslint/core@0.12.0': + resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.11.0': - resolution: {integrity: sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==} + '@eslint/eslintrc@3.3.0': + resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.2.0': - resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.20.0': - resolution: {integrity: sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==} + '@eslint/js@9.21.0': + resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.5': - resolution: {integrity: sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==} + '@eslint/plugin-kit@0.2.7': + resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@fastify/busboy@2.1.1': @@ -665,8 +661,8 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.1': - resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + '@humanwhocodes/retry@0.4.2': + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} '@img/sharp-darwin-arm64@0.33.5': @@ -1491,81 +1487,81 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@tailwindcss/node@4.0.7': - resolution: {integrity: sha512-dkFXufkbRB2mu3FPsW5xLAUWJyexpJA+/VtQj18k3SUiJVLdpgzBd1v1gRRcIpEJj7K5KpxBKfOXlZxT3ZZRuA==} + '@tailwindcss/node@4.0.8': + resolution: {integrity: sha512-FKArQpbrbwv08TNT0k7ejYXpF+R8knZFAatNc0acOxbgeqLzwb86r+P3LGOjIeI3Idqe9CVkZrh4GlsJLJKkkw==} - '@tailwindcss/oxide-android-arm64@4.0.7': - resolution: {integrity: sha512-5iQXXcAeOHBZy8ASfHFm1k0O/9wR2E3tKh6+P+ilZZbQiMgu+qrnfpBWYPc3FPuQdWiWb73069WT5D+CAfx/tg==} + '@tailwindcss/oxide-android-arm64@4.0.8': + resolution: {integrity: sha512-We7K79+Sm4mwJHk26Yzu/GAj7C7myemm7PeXvpgMxyxO70SSFSL3uCcqFbz9JA5M5UPkrl7N9fkBe/Y0iazqpA==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.0.7': - resolution: {integrity: sha512-7yGZtEc5IgVYylqK/2B0yVqoofk4UAbkn1ygNpIJZyrOhbymsfr8uUFCueTu2fUxmAYIfMZ8waWo2dLg/NgLgg==} + '@tailwindcss/oxide-darwin-arm64@4.0.8': + resolution: {integrity: sha512-Lv9Isi2EwkCTG1sRHNDi0uRNN1UGFdEThUAGFrydRmQZnraGLMjN8gahzg2FFnOizDl7LB2TykLUuiw833DSNg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.0.7': - resolution: {integrity: sha512-tPQDV20fBjb26yWbPqT1ZSoDChomMCiXTKn4jupMSoMCFyU7+OJvIY1ryjqBuY622dEBJ8LnCDDWsnj1lX9nNQ==} + '@tailwindcss/oxide-darwin-x64@4.0.8': + resolution: {integrity: sha512-fWfywfYIlSWtKoqWTjukTHLWV3ARaBRjXCC2Eo0l6KVpaqGY4c2y8snUjp1xpxUtpqwMvCvFWFaleMoz1Vhzlw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.0.7': - resolution: {integrity: sha512-sZqJpTyTZiknU9LLHuByg5GKTW+u3FqM7q7myequAXxKOpAFiOfXpY710FuMY+gjzSapyRbDXJlsTQtCyiTo5w==} + '@tailwindcss/oxide-freebsd-x64@4.0.8': + resolution: {integrity: sha512-SO+dyvjJV9G94bnmq2288Ke0BIdvrbSbvtPLaQdqjqHR83v5L2fWADyFO+1oecHo9Owsk8MxcXh1agGVPIKIqw==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.7': - resolution: {integrity: sha512-PBgvULgeSswjd8cbZ91gdIcIDMdc3TUHV5XemEpxlqt9M8KoydJzkuB/Dt910jYdofOIaTWRL6adG9nJICvU4A==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.8': + resolution: {integrity: sha512-ZSHggWiEblQNV69V0qUK5vuAtHP+I+S2eGrKGJ5lPgwgJeAd6GjLsVBN+Mqn2SPVfYM3BOpS9jX/zVg9RWQVDQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.0.7': - resolution: {integrity: sha512-By/a2yeh+e9b+C67F88ndSwVJl2A3tcUDb29FbedDi+DZ4Mr07Oqw9Y1DrDrtHIDhIZ3bmmiL1dkH2YxrtV+zw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.0.8': + resolution: {integrity: sha512-xWpr6M0OZLDNsr7+bQz+3X7zcnDJZJ1N9gtBWCtfhkEtDjjxYEp+Lr5L5nc/yXlL4MyCHnn0uonGVXy3fhxaVA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.0.7': - resolution: {integrity: sha512-WHYs3cpPEJb/ccyT20NOzopYQkl7JKncNBUbb77YFlwlXMVJLLV3nrXQKhr7DmZxz2ZXqjyUwsj2rdzd9stYdw==} + '@tailwindcss/oxide-linux-arm64-musl@4.0.8': + resolution: {integrity: sha512-5tz2IL7LN58ssGEq7h/staD7pu/izF/KeMWdlJ86WDe2Ah46LF3ET6ZGKTr5eZMrnEA0M9cVFuSPprKRHNgjeg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.0.7': - resolution: {integrity: sha512-7bP1UyuX9kFxbOwkeIJhBZNevKYPXB6xZI37v09fqi6rqRJR8elybwjMUHm54GVP+UTtJ14ueB1K54Dy1tIO6w==} + '@tailwindcss/oxide-linux-x64-gnu@4.0.8': + resolution: {integrity: sha512-KSzMkhyrxAQyY2o194NKVKU9j/c+NFSoMvnHWFaNHKi3P1lb+Vq1UC19tLHrmxSkKapcMMu69D7+G1+FVGNDXQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.0.7': - resolution: {integrity: sha512-gBQIV8nL/LuhARNGeroqzXymMzzW5wQzqlteVqOVoqwEfpHOP3GMird5pGFbnpY+NP0fOlsZGrxxOPQ4W/84bQ==} + '@tailwindcss/oxide-linux-x64-musl@4.0.8': + resolution: {integrity: sha512-yFYKG5UtHTRimjtqxUWXBgI4Tc6NJe3USjRIVdlTczpLRxq/SFwgzGl5JbatCxgSRDPBFwRrNPxq+ukfQFGdrw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-win32-arm64-msvc@4.0.7': - resolution: {integrity: sha512-aH530NFfx0kpQpvYMfWoeG03zGnRCMVlQG8do/5XeahYydz+6SIBxA1tl/cyITSJyWZHyVt6GVNkXeAD30v0Xg==} + '@tailwindcss/oxide-win32-arm64-msvc@4.0.8': + resolution: {integrity: sha512-tndGujmCSba85cRCnQzXgpA2jx5gXimyspsUYae5jlPyLRG0RjXbDshFKOheVXU4TLflo7FSG8EHCBJ0EHTKdQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.0.7': - resolution: {integrity: sha512-8Cva6bbJN7ZJx320k7vxGGdU0ewmpfS5A4PudyzUuofdi8MgeINuiiWiPQ0VZCda/GX88K6qp+6UpDZNVr8HMQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.0.8': + resolution: {integrity: sha512-T77jroAc0p4EHVVgTUiNeFn6Nj3jtD3IeNId2X+0k+N1XxfNipy81BEkYErpKLiOkNhpNFjPee8/ZVas29b2OQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.0.7': - resolution: {integrity: sha512-yr6w5YMgjy+B+zkJiJtIYGXW+HNYOPfRPtSs+aqLnKwdEzNrGv4ZuJh9hYJ3mcA+HMq/K1rtFV+KsEr65S558g==} + '@tailwindcss/oxide@4.0.8': + resolution: {integrity: sha512-KfMcuAu/Iw+DcV1e8twrFyr2yN8/ZDC/odIGta4wuuJOGkrkHZbvJvRNIbQNhGh7erZTYV6Ie0IeD6WC9Y8Hcw==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.0.7': - resolution: {integrity: sha512-zXcKs1uGssVDlnsQ+iwrkul5GPKvsXPynGCuk/eXLx3DVhHlQKMpA6tXN2oO28x2ki1xRBTfadKiHy2taVvp7g==} + '@tailwindcss/postcss@4.0.8': + resolution: {integrity: sha512-SUwlrXjn1ycmUbA0o0n3Y0LqlXqxN5R8HR+ti+OBbRS79wl2seDmiypEs3xJCuQXe07ol81s1AmRMitBmPveJA==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -1654,8 +1650,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@22.13.4': - resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==} + '@types/node@22.13.5': + resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==} '@types/pg@8.11.11': resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==} @@ -1820,8 +1816,8 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - ai@4.1.41: - resolution: {integrity: sha512-qQ5eVm5ivTij0/auLaoggfW3Y+IgWL0uNCCH79P8eUODeJTTqCRvB0B3iz9xl9b4uqOPcgZCVELgLfVODnCJ9g==} + ai@4.1.46: + resolution: {integrity: sha512-VTvAktT69IN1qcNAv7OlcOuR0q4HqUlhkVacrWmMlEoprYykF9EL5RY8IECD5d036Wqg0walwbSKZlA2noHm1A==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -2438,8 +2434,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.20.1: - resolution: {integrity: sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==} + eslint@9.21.0: + resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2558,8 +2554,8 @@ packages: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} - framer-motion@12.4.4: - resolution: {integrity: sha512-JWkVwbJBgVkeZHNcnk8ififgwTF+5de9wbJnTLI+g9YqaGo75Xd5uRVDm9FR8chqRDOKcXv/71f40CGescYVmg==} + framer-motion@12.4.7: + resolution: {integrity: sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -3289,8 +3285,8 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - motion-dom@12.4.4: - resolution: {integrity: sha512-D8Kjp8oqUNqxoAVmIlOH+YCMov/4koBAmG4OJs0VWfh18xkQEIsx9+S7yrXyx0XaMBEPtre6e9LiSW2Zs7vIhA==} + motion-dom@12.4.5: + resolution: {integrity: sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==} motion-utils@12.0.0: resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==} @@ -3307,8 +3303,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.0: - resolution: {integrity: sha512-zDAl/llz8Ue/EblwSYwdxGBYfj46IM1dhjVi8dyp9LQffoIGxJEAHj2oeZ4uNcgycSRcQ83CnfcZqEJzVDLcDw==} + nanoid@5.1.2: + resolution: {integrity: sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==} engines: {node: ^18 || >=20} hasBin: true @@ -3559,8 +3555,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.2: - resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -3985,8 +3981,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tailwindcss@4.0.7: - resolution: {integrity: sha512-yH5bPPyapavo7L+547h3c4jcBXcrKwybQRjwdEIVAd9iXRvy/3T1CC6XSQEgZtRySjKfqvo3Cc0ZF1DTheuIdA==} + tailwindcss@4.0.8: + resolution: {integrity: sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==} tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} @@ -4304,39 +4300,39 @@ snapshots: '@adobe/css-tools@4.4.2': {} - '@ai-sdk/openai@1.1.12(zod@3.24.2)': + '@ai-sdk/openai@1.1.14(zod@3.24.2)': dependencies: - '@ai-sdk/provider': 1.0.7 - '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + '@ai-sdk/provider': 1.0.9 + '@ai-sdk/provider-utils': 2.1.10(zod@3.24.2) zod: 3.24.2 - '@ai-sdk/provider-utils@2.1.8(zod@3.24.2)': + '@ai-sdk/provider-utils@2.1.10(zod@3.24.2)': dependencies: - '@ai-sdk/provider': 1.0.7 + '@ai-sdk/provider': 1.0.9 eventsource-parser: 3.0.0 nanoid: 3.3.8 secure-json-parse: 2.7.0 optionalDependencies: zod: 3.24.2 - '@ai-sdk/provider@1.0.7': + '@ai-sdk/provider@1.0.9': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@1.1.16(react@19.0.0)(zod@3.24.2)': + '@ai-sdk/react@1.1.18(react@19.0.0)(zod@3.24.2)': dependencies: - '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) - '@ai-sdk/ui-utils': 1.1.14(zod@3.24.2) + '@ai-sdk/provider-utils': 2.1.10(zod@3.24.2) + '@ai-sdk/ui-utils': 1.1.16(zod@3.24.2) swr: 2.3.2(react@19.0.0) throttleit: 2.1.0 optionalDependencies: react: 19.0.0 zod: 3.24.2 - '@ai-sdk/ui-utils@1.1.14(zod@3.24.2)': + '@ai-sdk/ui-utils@1.1.16(zod@3.24.2)': dependencies: - '@ai-sdk/provider': 1.0.7 - '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) + '@ai-sdk/provider': 1.0.9 + '@ai-sdk/provider-utils': 2.1.10(zod@3.24.2) zod-to-json-schema: 3.24.1(zod@3.24.2) optionalDependencies: zod: 3.24.2 @@ -5036,9 +5032,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0(jiti@2.4.2))': dependencies: - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -5051,15 +5047,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/core@0.10.0': + '@eslint/core@0.12.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@0.11.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.2.0': + '@eslint/eslintrc@3.3.0': dependencies: ajv: 6.12.6 debug: 4.4.0 @@ -5073,13 +5065,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.20.0': {} + '@eslint/js@9.21.0': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.2.5': + '@eslint/plugin-kit@0.2.7': dependencies: - '@eslint/core': 0.10.0 + '@eslint/core': 0.12.0 levn: 0.4.1 '@fastify/busboy@2.1.1': {} @@ -5112,7 +5104,7 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.1': {} + '@humanwhocodes/retry@0.4.2': {} '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: @@ -5202,27 +5194,27 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)) + jest-config: 29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -5247,7 +5239,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -5265,7 +5257,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.13.4 + '@types/node': 22.13.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -5287,7 +5279,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 22.13.4 + '@types/node': 22.13.5 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -5357,7 +5349,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.13.4 + '@types/node': 22.13.5 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -6074,76 +6066,76 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/container-queries@0.1.1(tailwindcss@4.0.7)': + '@tailwindcss/container-queries@0.1.1(tailwindcss@4.0.8)': dependencies: - tailwindcss: 4.0.7 + tailwindcss: 4.0.8 - '@tailwindcss/forms@0.5.10(tailwindcss@4.0.7)': + '@tailwindcss/forms@0.5.10(tailwindcss@4.0.8)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 4.0.7 + tailwindcss: 4.0.8 - '@tailwindcss/node@4.0.7': + '@tailwindcss/node@4.0.8': dependencies: enhanced-resolve: 5.18.1 jiti: 2.4.2 - tailwindcss: 4.0.7 + tailwindcss: 4.0.8 - '@tailwindcss/oxide-android-arm64@4.0.7': + '@tailwindcss/oxide-android-arm64@4.0.8': optional: true - '@tailwindcss/oxide-darwin-arm64@4.0.7': + '@tailwindcss/oxide-darwin-arm64@4.0.8': optional: true - '@tailwindcss/oxide-darwin-x64@4.0.7': + '@tailwindcss/oxide-darwin-x64@4.0.8': optional: true - '@tailwindcss/oxide-freebsd-x64@4.0.7': + '@tailwindcss/oxide-freebsd-x64@4.0.8': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.7': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.8': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.0.7': + '@tailwindcss/oxide-linux-arm64-gnu@4.0.8': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.0.7': + '@tailwindcss/oxide-linux-arm64-musl@4.0.8': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.0.7': + '@tailwindcss/oxide-linux-x64-gnu@4.0.8': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.0.7': + '@tailwindcss/oxide-linux-x64-musl@4.0.8': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.0.7': + '@tailwindcss/oxide-win32-arm64-msvc@4.0.8': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.0.7': + '@tailwindcss/oxide-win32-x64-msvc@4.0.8': optional: true - '@tailwindcss/oxide@4.0.7': + '@tailwindcss/oxide@4.0.8': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.0.7 - '@tailwindcss/oxide-darwin-arm64': 4.0.7 - '@tailwindcss/oxide-darwin-x64': 4.0.7 - '@tailwindcss/oxide-freebsd-x64': 4.0.7 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.7 - '@tailwindcss/oxide-linux-arm64-gnu': 4.0.7 - '@tailwindcss/oxide-linux-arm64-musl': 4.0.7 - '@tailwindcss/oxide-linux-x64-gnu': 4.0.7 - '@tailwindcss/oxide-linux-x64-musl': 4.0.7 - '@tailwindcss/oxide-win32-arm64-msvc': 4.0.7 - '@tailwindcss/oxide-win32-x64-msvc': 4.0.7 + '@tailwindcss/oxide-android-arm64': 4.0.8 + '@tailwindcss/oxide-darwin-arm64': 4.0.8 + '@tailwindcss/oxide-darwin-x64': 4.0.8 + '@tailwindcss/oxide-freebsd-x64': 4.0.8 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.8 + '@tailwindcss/oxide-linux-arm64-gnu': 4.0.8 + '@tailwindcss/oxide-linux-arm64-musl': 4.0.8 + '@tailwindcss/oxide-linux-x64-gnu': 4.0.8 + '@tailwindcss/oxide-linux-x64-musl': 4.0.8 + '@tailwindcss/oxide-win32-arm64-msvc': 4.0.8 + '@tailwindcss/oxide-win32-x64-msvc': 4.0.8 - '@tailwindcss/postcss@4.0.7': + '@tailwindcss/postcss@4.0.8': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.0.7 - '@tailwindcss/oxide': 4.0.7 + '@tailwindcss/node': 4.0.8 + '@tailwindcss/oxide': 4.0.8 lightningcss: 1.29.1 - postcss: 8.5.2 - tailwindcss: 4.0.7 + postcss: 8.5.3 + tailwindcss: 4.0.8 '@testing-library/dom@10.4.0': dependencies: @@ -6217,7 +6209,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.13.4 + '@types/node': 22.13.5 '@types/istanbul-lib-coverage@2.0.6': {} @@ -6236,7 +6228,7 @@ snapshots: '@types/jsdom@20.0.1': dependencies: - '@types/node': 22.13.4 + '@types/node': 22.13.5 '@types/tough-cookie': 4.0.5 parse5: 7.2.1 @@ -6244,13 +6236,13 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@22.13.4': + '@types/node@22.13.5': dependencies: undici-types: 6.20.0 '@types/pg@8.11.11': dependencies: - '@types/node': 22.13.4 + '@types/node': 22.13.5 pg-protocol: 1.7.1 pg-types: 4.0.2 @@ -6276,15 +6268,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.24.1 - '@typescript-eslint/type-utils': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/type-utils': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.24.1 - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -6293,14 +6285,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)': + '@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.24.1 '@typescript-eslint/types': 8.24.1 '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.24.1 debug: 4.4.0 - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -6310,12 +6302,12 @@ snapshots: '@typescript-eslint/types': 8.24.1 '@typescript-eslint/visitor-keys': 8.24.1 - '@typescript-eslint/type-utils@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) debug: 4.4.0 - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) ts-api-utils: 2.0.1(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: @@ -6337,13 +6329,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3)': + '@typescript-eslint/utils@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.24.1 '@typescript-eslint/types': 8.24.1 '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -6407,12 +6399,12 @@ snapshots: transitivePeerDependencies: - supports-color - ai@4.1.41(react@19.0.0)(zod@3.24.2): + ai@4.1.46(react@19.0.0)(zod@3.24.2): dependencies: - '@ai-sdk/provider': 1.0.7 - '@ai-sdk/provider-utils': 2.1.8(zod@3.24.2) - '@ai-sdk/react': 1.1.16(react@19.0.0)(zod@3.24.2) - '@ai-sdk/ui-utils': 1.1.14(zod@3.24.2) + '@ai-sdk/provider': 1.0.9 + '@ai-sdk/provider-utils': 2.1.10(zod@3.24.2) + '@ai-sdk/react': 1.1.18(react@19.0.0)(zod@3.24.2) + '@ai-sdk/ui-utils': 1.1.16(zod@3.24.2) '@opentelemetry/api': 1.9.0 jsondiffpatch: 0.6.0 optionalDependencies: @@ -6738,13 +6730,13 @@ snapshots: cookie@0.7.1: {} - create-jest@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)): + create-jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)) + jest-config: 29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -7030,19 +7022,19 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@15.1.7(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3): + eslint-config-next@15.1.7(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3): dependencies: '@next/eslint-plugin-next': 15.1.7 '@rushstack/eslint-patch': 1.10.5 - '@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) - '@typescript-eslint/parser': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) - eslint: 9.20.1(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.21.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.20.1(jiti@2.4.2)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.20.1(jiti@2.4.2)) - eslint-plugin-react: 7.37.4(eslint@9.20.1(jiti@2.4.2)) - eslint-plugin-react-hooks: 5.1.0(eslint@9.20.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@2.4.2)) + eslint-plugin-react: 7.37.4(eslint@9.21.0(jiti@2.4.2)) + eslint-plugin-react-hooks: 5.1.0(eslint@9.21.0(jiti@2.4.2)) optionalDependencies: typescript: 5.7.3 transitivePeerDependencies: @@ -7058,33 +7050,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)): + eslint-import-resolver-typescript@3.8.1(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 enhanced-resolve: 5.18.1 - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) get-tsconfig: 4.10.0 is-bun-module: 1.3.0 stable-hash: 0.0.4 tinyglobby: 0.2.11 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.20.1(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.20.1(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) - eslint: 9.20.1(jiti@2.4.2) + '@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.21.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.1(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.20.1(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7093,9 +7085,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.20.1(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.1)(eslint@9.21.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -7107,13 +7099,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.24.1(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.20.1(jiti@2.4.2)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.21.0(jiti@2.4.2)): dependencies: aria-query: 5.3.2 array-includes: 3.1.8 @@ -7123,7 +7115,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -7132,11 +7124,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.1.0(eslint@9.20.1(jiti@2.4.2)): + eslint-plugin-react-hooks@5.1.0(eslint@9.21.0(jiti@2.4.2)): dependencies: - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) - eslint-plugin-react@7.37.4(eslint@9.20.1(jiti@2.4.2)): + eslint-plugin-react@7.37.4(eslint@9.21.0(jiti@2.4.2)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -7144,7 +7136,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.20.1(jiti@2.4.2) + eslint: 9.21.0(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -7167,18 +7159,18 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.20.1(jiti@2.4.2): + eslint@9.21.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.2 - '@eslint/core': 0.11.0 - '@eslint/eslintrc': 3.2.0 - '@eslint/js': 9.20.0 - '@eslint/plugin-kit': 0.2.5 + '@eslint/core': 0.12.0 + '@eslint/eslintrc': 3.3.0 + '@eslint/js': 9.21.0 + '@eslint/plugin-kit': 0.2.7 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.1 + '@humanwhocodes/retry': 0.4.2 '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 ajv: 6.12.6 @@ -7328,9 +7320,9 @@ snapshots: es-set-tostringtag: 2.1.0 mime-types: 2.1.35 - framer-motion@12.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - motion-dom: 12.4.4 + motion-dom: 12.4.5 motion-utils: 12.0.0 tslib: 2.8.1 optionalDependencies: @@ -7707,7 +7699,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -7727,16 +7719,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)): + jest-cli@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)) + create-jest: 29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)) + jest-config: 29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -7746,7 +7738,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)): + jest-config@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)): dependencies: '@babel/core': 7.26.9 '@jest/test-sequencer': 29.7.0 @@ -7771,8 +7763,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.13.4 - ts-node: 10.9.2(@types/node@22.13.4)(typescript@5.7.3) + '@types/node': 22.13.5 + ts-node: 10.9.2(@types/node@22.13.5)(typescript@5.7.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -7802,7 +7794,7 @@ snapshots: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 22.13.4 + '@types/node': 22.13.5 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -7816,7 +7808,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -7826,7 +7818,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.13.4 + '@types/node': 22.13.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -7865,7 +7857,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -7900,7 +7892,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -7928,7 +7920,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.2 @@ -7974,7 +7966,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -7993,7 +7985,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.13.4 + '@types/node': 22.13.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -8002,17 +7994,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.13.4 + '@types/node': 22.13.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)): + jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.13.4)(ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3)) + jest-cli: 29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -8231,7 +8223,7 @@ snapshots: minimist@1.2.8: {} - motion-dom@12.4.4: + motion-dom@12.4.5: dependencies: motion-utils: 12.0.0 @@ -8243,7 +8235,7 @@ snapshots: nanoid@3.3.8: {} - nanoid@5.1.0: {} + nanoid@5.1.2: {} natural-compare@1.4.0: {} @@ -8480,7 +8472,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.2: + postcss@8.5.3: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 @@ -8691,7 +8683,7 @@ snapshots: htmlparser2: 8.0.2 is-plain-object: 5.0.0 parse-srcset: 1.0.2 - postcss: 8.5.2 + postcss: 8.5.3 sax@1.2.4: {} @@ -8935,7 +8927,7 @@ snapshots: symbol-tree@3.2.4: {} - tailwindcss@4.0.7: {} + tailwindcss@4.0.8: {} tapable@2.2.1: {} @@ -8981,14 +8973,14 @@ snapshots: dependencies: sax: 1.2.4 - ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3): + ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.13.4 + '@types/node': 22.13.5 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 From bc24d42864078d1dfe38c6a31227a1eec32877c6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 24 Feb 2025 19:47:59 -0600 Subject: [PATCH 02/16] Refine more menu --- src/admin/AdminPhotoMenuClient.tsx | 2 +- src/components/more/MoreMenu.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx index 3209b68c..78f14b0b 100644 --- a/src/admin/AdminPhotoMenuClient.tsx +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -96,7 +96,7 @@ export default function AdminPhotoMenuClient({ size={15} className="translate-x-[-1px]" />, - className: 'text-error', + className: 'text-error *:hover:text-error', action: () => { if (confirm(deleteConfirmationTextForPhoto(photo))) { return deletePhotoAction( diff --git a/src/components/more/MoreMenu.tsx b/src/components/more/MoreMenu.tsx index 4045ded4..81f8b8dc 100644 --- a/src/components/more/MoreMenu.tsx +++ b/src/components/more/MoreMenu.tsx @@ -31,7 +31,6 @@ export default function MoreMenu({ - From 9f483bcf213228662b4983219f621fc3608071a1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 25 Feb 2025 09:13:43 -0600 Subject: [PATCH 03/16] Create multi-item admin menu --- src/admin/AdminAppMenu.tsx | 51 +++++++++++++++++---- src/admin/AdminPhotoMenuClient.tsx | 5 +- src/admin/insights/AdminAppInsightsIcon.tsx | 11 ++++- src/app/Nav.tsx | 15 ------ src/app/ViewSwitcher.tsx | 24 ++++++---- src/components/Switcher.tsx | 4 +- src/components/more/MoreMenu.tsx | 38 +++++++++++---- src/components/more/MoreMenuItem.tsx | 26 +++++++++-- 8 files changed, 124 insertions(+), 50 deletions(-) diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index d7c1cf3c..ea2b63c2 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -4,15 +4,27 @@ import MoreMenu from '@/components/more/MoreMenu'; import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS, + PATH_ADMIN_PHOTOS, + PATH_ADMIN_TAGS, PATH_GRID_INFERRED, } from '@/app/paths'; import { useAppState } from '@/state/AppState'; import { ImCheckboxUnchecked } from 'react-icons/im'; import { IoCloseSharp } from 'react-icons/io5'; -import AdminAppInsightsIcon from './insights/AdminAppInsightsIcon'; import { LuCog } from 'react-icons/lu'; +import { clsx } from 'clsx/lite'; +import { TbPhoto } from 'react-icons/tb'; +import { FiTag } from 'react-icons/fi'; +import { BiLockAlt } from 'react-icons/bi'; +import AdminAppInsightsIcon from './insights/AdminAppInsightsIcon'; -export default function AdminAppMenu() { +export default function AdminAppMenu({ + className, + buttonClassName, +}: { + className?: string + buttonClassName?: string +}) { const { selectedPhotoIds, setSelectedPhotoIds, @@ -22,28 +34,51 @@ export default function AdminAppMenu() { return ( } + align="start" + className={clsx( + 'border-medium', + className, + )} + buttonClassName={clsx( + 'rounded-none focus:outline-none', + buttonClassName, + )} items={[{ + label: 'Manage Photos', + icon: , + href: PATH_ADMIN_PHOTOS, + }, { + label: 'Manage Tags', + icon: , + href: PATH_ADMIN_TAGS, + }, { label: 'Insights', - icon: - - , + icon: , href: PATH_ADMIN_INSIGHTS, }, { label: 'Configuration', icon: , href: PATH_ADMIN_CONFIGURATION, }, { label: isSelecting ? 'Exit Select' - : 'Select', + : 'Select Photos', icon: isSelecting ? : , href: PATH_GRID_INFERRED, action: () => { diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx index 78f14b0b..0cd75923 100644 --- a/src/admin/AdminPhotoMenuClient.tsx +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -47,7 +47,10 @@ export default function AdminPhotoMenuClient({ const items = useMemo(() => { const items: ComponentProps[] = [{ label: 'Edit', - icon: , + icon: , href: pathForAdminPhotoEdit(photo.id), }]; if (includeFavorite) { diff --git a/src/admin/insights/AdminAppInsightsIcon.tsx b/src/admin/insights/AdminAppInsightsIcon.tsx index b2b9578a..19d91380 100644 --- a/src/admin/insights/AdminAppInsightsIcon.tsx +++ b/src/admin/insights/AdminAppInsightsIcon.tsx @@ -2,13 +2,20 @@ import { useAppState } from '@/state/AppState'; import clsx from 'clsx/lite'; import { LuLightbulb } from 'react-icons/lu'; import { FaCircle } from 'react-icons/fa6'; -export default function AdminAppInsightsIcon() { +export default function AdminAppInsightsIcon({ + className, +}: { + className?: string +}) { const { insightIndicatorStatus, } = useAppState(); return ( - +
} - contentSide={isUserSignedIn && !isPathAdmin(pathname) - ?
- -
- : undefined} sideHiddenOnMobile /> ); diff --git a/src/app/ViewSwitcher.tsx b/src/app/ViewSwitcher.tsx index 2ff538ed..407ade46 100644 --- a/src/app/ViewSwitcher.tsx +++ b/src/app/ViewSwitcher.tsx @@ -3,25 +3,23 @@ import SwitcherItem from '@/components/SwitcherItem'; import IconFeed from '@/app/IconFeed'; import IconGrid from '@/app/IconGrid'; import { - PATH_ADMIN_PHOTOS, PATH_FEED_INFERRED, PATH_GRID_INFERRED, } from '@/app/paths'; -import { BiLockAlt } from 'react-icons/bi'; import IconSearch from './IconSearch'; import { useAppState } from '@/state/AppState'; import { GRID_HOMEPAGE_ENABLED } from './config'; +import AdminAppMenu from '@/admin/AdminAppMenu'; +import { clsx } from 'clsx/lite'; export type SwitcherSelection = 'feed' | 'grid' | 'admin'; export default function ViewSwitcher({ currentSelection, - showAdmin, }: { currentSelection?: SwitcherSelection - showAdmin?: boolean }) { - const { setIsCommandKOpen } = useAppState(); + const { setIsCommandKOpen, isUserSignedIn } = useAppState(); const renderItemFeed = () => {GRID_HOMEPAGE_ENABLED ? renderItemGrid() : renderItemFeed()} {GRID_HOMEPAGE_ENABLED ? renderItemFeed() : renderItemGrid()} - {showAdmin && - } - href={PATH_ADMIN_PHOTOS} - active={currentSelection === 'admin'} + {isUserSignedIn && + } diff --git a/src/components/Switcher.tsx b/src/components/Switcher.tsx index a5225b89..40fd7b06 100644 --- a/src/components/Switcher.tsx +++ b/src/components/Switcher.tsx @@ -11,10 +11,10 @@ export default function Switcher({ return (
diff --git a/src/components/more/MoreMenu.tsx b/src/components/more/MoreMenu.tsx index 81f8b8dc..968c26d3 100644 --- a/src/components/more/MoreMenu.tsx +++ b/src/components/more/MoreMenu.tsx @@ -1,4 +1,4 @@ -import { ComponentProps } from 'react'; +import { ComponentProps, ReactNode, useCallback, useState } from 'react'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { clsx } from 'clsx/lite'; import { FiMoreHorizontal } from 'react-icons/fi'; @@ -6,38 +6,50 @@ import MoreMenuItem from './MoreMenuItem'; export default function MoreMenu({ items, + icon, + header, className, buttonClassName, ariaLabel, + align = 'end', + ...props }: { items: ComponentProps [] + icon?: ReactNode + header?: ReactNode className?: string buttonClassName?: string ariaLabel: string -}){ +} & ComponentProps){ + const [isOpen, setIsOpen] = useState(false); + + const dismissMenu = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + return ( - + + {header &&
+ {header} +
} {items.map(props => - , + , )}
diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index cc2c904d..122549f9 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -2,7 +2,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { clsx } from 'clsx/lite'; -import { ReactNode, useState, useTransition } from 'react'; +import { ReactNode, useEffect, useState, useTransition } from 'react'; import LoaderButton from '../primitives/LoaderButton'; import { usePathname, useRouter } from 'next/navigation'; import { downloadFileFromBrowser } from '@/utility/url'; @@ -14,6 +14,7 @@ export default function MoreMenuItem({ hrefDownloadName, className, action, + dismissMenu, shouldPreventDefault = true, }: { label: ReactNode @@ -22,6 +23,7 @@ export default function MoreMenuItem({ hrefDownloadName?: string className?: string action?: () => Promise | void + dismissMenu?: () => void shouldPreventDefault?: boolean }) { const router = useRouter(); @@ -30,8 +32,17 @@ export default function MoreMenuItem({ const [isPending, startTransition] = useTransition(); + const [transitionDidStart, setTransitionDidStart] = useState(false); + const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + if (transitionDidStart && !isPending) { + dismissMenu?.(); + setTransitionDidStart(false); + } + }, [isPending, dismissMenu, transitionDidStart]); + return ( { + onSelect={async e => { if (shouldPreventDefault) { e.preventDefault(); } if (action) { const result = action(); if (result instanceof Promise) { setIsLoading(true); - await result.finally(() => setIsLoading(false)); + await result.finally(() => { + setIsLoading(false); + dismissMenu?.(); + }); } } if (href && href !== pathname) { if (hrefDownloadName) { setIsLoading(true); downloadFileFromBrowser(href, hrefDownloadName) - .finally(() => setIsLoading(false)); + .finally(() => { + setIsLoading(false); + dismissMenu?.(); + }); } else { + setTransitionDidStart(true); startTransition(() => router.push(href)); } } From bd7cf64f2a3883c542b64936c5703e6b2d1164f1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 25 Feb 2025 20:58:01 -0600 Subject: [PATCH 04/16] Refactor admin sub-nav --- app/admin/configuration/page.tsx | 6 +---- app/admin/insights/page.tsx | 2 +- src/admin/AdminAppInfoIcon.tsx | 36 +++++++++++++++++++++++++ src/admin/AdminInfoPage.tsx | 45 ++++++++++++++++++++++++++----- src/admin/AdminNavClient.tsx | 45 +++++++++---------------------- src/app/paths.ts | 9 +++++-- src/components/LinkWithStatus.tsx | 5 +++- tailwind.css | 1 + 8 files changed, 101 insertions(+), 48 deletions(-) create mode 100644 src/admin/AdminAppInfoIcon.tsx diff --git a/app/admin/configuration/page.tsx b/app/admin/configuration/page.tsx index 59d27f49..6e6435c7 100644 --- a/app/admin/configuration/page.tsx +++ b/app/admin/configuration/page.tsx @@ -1,13 +1,9 @@ -import ClearCacheButton from '@/admin/ClearCacheButton'; import AdminAppConfiguration from '@/admin/AdminAppConfiguration'; import AdminInfoPage from '@/admin/AdminInfoPage'; export default function AdminAppConfigurationPage() { return ( - } - > + ); diff --git a/app/admin/insights/page.tsx b/app/admin/insights/page.tsx index 0bf5efa4..eed08769 100644 --- a/app/admin/insights/page.tsx +++ b/app/admin/insights/page.tsx @@ -2,7 +2,7 @@ import AdminAppInsights from '@/admin/insights/AdminAppInsights'; import AdminInfoPage from '@/admin/AdminInfoPage'; export default async function AdminInsightsPage() { - return + return ; } diff --git a/src/admin/AdminAppInfoIcon.tsx b/src/admin/AdminAppInfoIcon.tsx new file mode 100644 index 00000000..73142c3c --- /dev/null +++ b/src/admin/AdminAppInfoIcon.tsx @@ -0,0 +1,36 @@ +import { useAppState } from '@/state/AppState'; +import clsx from 'clsx/lite'; +import { FaCircle } from 'react-icons/fa6'; +import { LuCog } from 'react-icons/lu'; + +export default function AdminAppInfoIcon({ + className, +}: { + className?: string +}) { + const { insightIndicatorStatus } = useAppState(); + + return ( + + + {insightIndicatorStatus && + } + + ); +} diff --git a/src/admin/AdminInfoPage.tsx b/src/admin/AdminInfoPage.tsx index 0af159ee..cb62b1ed 100644 --- a/src/admin/AdminInfoPage.tsx +++ b/src/admin/AdminInfoPage.tsx @@ -1,14 +1,27 @@ +import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/paths'; import Container from '@/components/Container'; +import LinkWithStatus from '@/components/LinkWithStatus'; +import ResponsiveText from '@/components/primitives/ResponsiveText'; import SiteGrid from '@/components/SiteGrid'; +import clsx from 'clsx/lite'; import { ReactNode } from 'react'; +import ClearCacheButton from '@/admin/ClearCacheButton'; + +const ADMIN_INFO_PAGES = [{ + titleShort: 'Insights', + path: PATH_ADMIN_INSIGHTS, +}, +{ + title: 'Configuration', + titleShort: 'Config', + path: PATH_ADMIN_CONFIGURATION, +}]; export default function AdminInfoPage({ - title, - accessory, + page, children, }: { - title: string - accessory?: ReactNode + page: (typeof ADMIN_INFO_PAGES)[number]['titleShort'] children: ReactNode }) { return ( @@ -16,10 +29,28 @@ export default function AdminInfoPage({ contentMain={
-
- {title} +
+ {ADMIN_INFO_PAGES.map(({ title, titleShort, path }) => + + + {title ?? titleShort} + + )}
- {accessory} +
{children} diff --git a/src/admin/AdminNavClient.tsx b/src/admin/AdminNavClient.tsx index c7aea84a..7255cb10 100644 --- a/src/admin/AdminNavClient.tsx +++ b/src/admin/AdminNavClient.tsx @@ -6,11 +6,9 @@ import Note from '@/components/Note'; import SiteGrid from '@/components/SiteGrid'; import Spinner from '@/components/Spinner'; import { - PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS, checkPathPrefix, - isPathAdminConfiguration, - isPathAdminInsights, + isPathAdminInfo, isPathTopLevelAdmin, } from '@/app/paths'; import { useAppState } from '@/state/AppState'; @@ -19,8 +17,7 @@ import { differenceInMinutes } from 'date-fns'; import { usePathname } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { FaRegClock } from 'react-icons/fa'; -import AdminAppInsightsIcon from './insights/AdminAppInsightsIcon'; -import { LuCog } from 'react-icons/lu'; +import AdminAppInfoIcon from './AdminAppInfoIcon'; // Updates considered recent if they occurred in past 5 minutes const areTimesRecent = (dates: Date[]) => dates @@ -29,6 +26,8 @@ const areTimesRecent = (dates: Date[]) => dates export default function AdminNavClient({ items, mostRecentPhotoUpdateTime, + // TODO: use this with new component + // eslint-disable-next-line @typescript-eslint/no-unused-vars includeInsights, }: { items: { @@ -91,33 +90,15 @@ export default function AdminNavClient({ ({count})} )}
-
- {includeInsights && - } - > - - } - } - > - - -
+ } + > + +
{shouldShowBanner && }> diff --git a/src/app/paths.ts b/src/app/paths.ts index 302ead59..e03a7ac1 100644 --- a/src/app/paths.ts +++ b/src/app/paths.ts @@ -64,6 +64,7 @@ export const PATHS_ADMIN = [ PATH_ADMIN_PHOTOS, PATH_ADMIN_UPLOADS, PATH_ADMIN_TAGS, + PATH_ADMIN_INSIGHTS, PATH_ADMIN_CONFIGURATION, PATH_ADMIN_BASELINE, PATH_ADMIN_COMPONENTS, @@ -225,11 +226,15 @@ export const isPathAdmin = (pathname?: string) => export const isPathTopLevelAdmin = (pathname?: string) => PATHS_ADMIN.some(path => path === pathname); +export const isPathAdminInsights = (pathname?: string) => + checkPathPrefix(pathname, PATH_ADMIN_INSIGHTS); + export const isPathAdminConfiguration = (pathname?: string) => checkPathPrefix(pathname, PATH_ADMIN_CONFIGURATION); -export const isPathAdminInsights = (pathname?: string) => - checkPathPrefix(pathname, PATH_ADMIN_INSIGHTS); +export const isPathAdminInfo = (pathname?: string) => + isPathAdminInsights(pathname) || + isPathAdminConfiguration(pathname); export const isPathProtected = (pathname?: string) => checkPathPrefix(pathname, PATH_ADMIN) || diff --git a/src/components/LinkWithStatus.tsx b/src/components/LinkWithStatus.tsx index fa5a2803..b64852b5 100644 --- a/src/components/LinkWithStatus.tsx +++ b/src/components/LinkWithStatus.tsx @@ -24,6 +24,7 @@ export type LinkWithStatusProps = Omit< children: ReactNode | ((props: { isLoading: boolean }) => ReactNode) + debugLoading?: boolean } export default function LinkWithStatus({ @@ -32,12 +33,14 @@ export default function LinkWithStatus({ className, onClick, children, + debugLoading = false, ...props }: LinkWithStatusProps) { const path = usePathname(); const [pathWhenClicked, setPathWhenClicked] = useState(); - const [isLoading, setIsLoading] = useState(false); + const [_isLoading, setIsLoading] = useState(false); + const isLoading = _isLoading || debugLoading; const isLoadingStartTime = useRef(undefined); diff --git a/tailwind.css b/tailwind.css index 6aa846fe..f1368499 100644 --- a/tailwind.css +++ b/tailwind.css @@ -301,6 +301,7 @@ disabled:bg-gray-100 dark:disabled:bg-gray-900 disabled:border-gray-200 disabled:dark:border-gray-700 border-gray-900 dark:border-gray-100 + hover:border-gray-900 dark:hover:border-gray-100 active:bg-gray-700 active:border-gray-700 active:dark:bg-gray-300 active:dark:border-gray-300 shadow-none From 13867f708988cb051be0e3e27447f380240d9165 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 25 Feb 2025 21:07:07 -0600 Subject: [PATCH 05/16] Standardize app info icon --- src/admin/AdminAppInfoIcon.tsx | 8 +++-- src/admin/AdminAppMenu.tsx | 17 ++++------ src/admin/insights/AdminAppInsightsIcon.tsx | 36 --------------------- 3 files changed, 11 insertions(+), 50 deletions(-) delete mode 100644 src/admin/insights/AdminAppInsightsIcon.tsx diff --git a/src/admin/AdminAppInfoIcon.tsx b/src/admin/AdminAppInfoIcon.tsx index 73142c3c..4cd70191 100644 --- a/src/admin/AdminAppInfoIcon.tsx +++ b/src/admin/AdminAppInfoIcon.tsx @@ -4,8 +4,10 @@ import { FaCircle } from 'react-icons/fa6'; import { LuCog } from 'react-icons/lu'; export default function AdminAppInfoIcon({ + size = 'large', className, }: { + size?: 'small' | 'large' className?: string }) { const { insightIndicatorStatus } = useAppState(); @@ -16,16 +18,16 @@ export default function AdminAppInfoIcon({ className, )}> {insightIndicatorStatus && , href: PATH_ADMIN_TAGS, }, { - label: 'Insights', - icon: , - href: PATH_ADMIN_INSIGHTS, - }, { - label: 'Configuration', - icon: , - href: PATH_ADMIN_CONFIGURATION, + href: PATH_ADMIN_INSIGHTS, }, { label: isSelecting ? 'Exit Select' diff --git a/src/admin/insights/AdminAppInsightsIcon.tsx b/src/admin/insights/AdminAppInsightsIcon.tsx deleted file mode 100644 index 19d91380..00000000 --- a/src/admin/insights/AdminAppInsightsIcon.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useAppState } from '@/state/AppState'; -import clsx from 'clsx/lite'; -import { LuLightbulb } from 'react-icons/lu'; -import { FaCircle } from 'react-icons/fa6'; -export default function AdminAppInsightsIcon({ - className, -}: { - className?: string -}) { - const { - insightIndicatorStatus, - } = useAppState(); - - return ( - - - {insightIndicatorStatus && - } - - ); -} From 783a4f19880f88d8b4d9fccf5303af06e169730f Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 25 Feb 2025 23:15:17 -0600 Subject: [PATCH 06/16] Refine admin UI --- src/admin/AdminAppInfoIcon.tsx | 4 +++- src/admin/AdminInfoPage.tsx | 2 +- src/admin/AdminOutdatedClient.tsx | 10 +++++++--- src/components/Container.tsx | 18 +++++++++--------- src/components/more/MoreMenuItem.tsx | 6 +++--- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/admin/AdminAppInfoIcon.tsx b/src/admin/AdminAppInfoIcon.tsx index 4cd70191..8cd3c4b3 100644 --- a/src/admin/AdminAppInfoIcon.tsx +++ b/src/admin/AdminAppInfoIcon.tsx @@ -27,7 +27,9 @@ export default function AdminAppInfoIcon({ size={size === 'large' ? 8 : 7} className={clsx( 'absolute', - 'top-[1.5px] right-[0.5px]', + size === 'large' + ? 'top-[1.5px] right-[0.5px]' + : 'top-[1px] right-[-0.5px]', insightIndicatorStatus === 'blue' ? 'text-blue-500' : 'text-amber-500', diff --git a/src/admin/AdminInfoPage.tsx b/src/admin/AdminInfoPage.tsx index cb62b1ed..94ef0538 100644 --- a/src/admin/AdminInfoPage.tsx +++ b/src/admin/AdminInfoPage.tsx @@ -39,7 +39,7 @@ export default function AdminInfoPage({ href={path} className={clsx( page === titleShort - ? 'underline underline-offset-10 decoration-[1.5px]' + ? 'underline underline-offset-10 decoration-2' : 'text-dim', 'px-1 py-0.5 rounded-md', )} diff --git a/src/admin/AdminOutdatedClient.tsx b/src/admin/AdminOutdatedClient.tsx index c108c0ef..d171c7c0 100644 --- a/src/admin/AdminOutdatedClient.tsx +++ b/src/admin/AdminOutdatedClient.tsx @@ -11,6 +11,7 @@ import { useState } from 'react'; import { syncPhotosAction } from '@/photo/actions'; import { useRouter } from 'next/navigation'; import ResponsiveText from '@/components/primitives/ResponsiveText'; +import { LiaBroomSolid } from 'react-icons/lia'; const UPDATE_BATCH_SIZE_MAX = 4; @@ -77,16 +78,19 @@ export default function AdminOutdatedClient({ } >
- + } + >
{photos.length} outdated {' '} {photos.length === 1 ? 'photo' : 'photos'} found
- They may have missing EXIF fields, inaccurate blur data, + Sync photos to import newer EXIF fields, improve blur data, {' '} - undesired privacy settings, or text that can be AI-generated + and leverage AI-generated text where possible
diff --git a/src/components/Container.tsx b/src/components/Container.tsx index 5d0c43fd..b2db8062 100644 --- a/src/components/Container.tsx +++ b/src/components/Container.tsx @@ -26,22 +26,22 @@ export default function Container({ case 'gray': return [ 'text-medium', 'bg-gray-50 dark:bg-gray-900/40', - 'border-gray-200 dark:border-gray-800', + 'border-medium', ]; case 'blue': return [ 'text-blue-900 dark:text-blue-300', - 'bg-blue-50/50 dark:bg-blue-950/30', - 'border-blue-200 dark:border-blue-500/40', + 'bg-blue-100/35 dark:bg-blue-950/60', + 'border-transparent', ]; case 'red': return [ - 'text-red-600 dark:text-red-500/90', - 'bg-red-50/50 dark:bg-red-950/50', - 'border-red-100 dark:border-red-950', + 'text-red-700 dark:text-red-400', + 'bg-red-100/50 dark:bg-red-950/55', + 'border-transparent', ]; case 'yellow': return [ - 'text-amber-700 dark:text-amber-500/90', - 'bg-amber-50/50 dark:bg-amber-950/30', - 'border-amber-600/30 dark:border-amber-800/30', + 'text-amber-700 dark:text-amber-500', + 'bg-amber-100/55 dark:bg-amber-950/55', + 'border-transparent', ]; } }; diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index 122549f9..eea7f47d 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -48,10 +48,10 @@ export default function MoreMenuItem({ disabled={isLoading} className={clsx( 'flex items-center h-8', - 'px-2 py-1.5 rounded-[3px]', + 'pl-2 pr-4 py-1.5 rounded-[3px]', 'select-none hover:outline-hidden', - 'hover:bg-gray-50 active:bg-gray-100', - 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900', + 'hover:bg-gray-100/90 active:bg-gray-200/75', + 'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80', 'whitespace-nowrap', isLoading ? 'cursor-not-allowed opacity-50' From d2494e66d575fcd8ccd2da5306104d71897f8f07 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 25 Feb 2025 23:31:19 -0600 Subject: [PATCH 07/16] Refactor admin subnav --- app/admin/configuration/page.tsx | 2 +- app/admin/insights/page.tsx | 2 +- src/admin/AdminInfoNav.tsx | 64 ++++++++++++++++++++++++++++++++ src/admin/AdminInfoPage.tsx | 49 ++---------------------- src/admin/AdminNavClient.tsx | 12 ++++-- 5 files changed, 77 insertions(+), 52 deletions(-) create mode 100644 src/admin/AdminInfoNav.tsx diff --git a/app/admin/configuration/page.tsx b/app/admin/configuration/page.tsx index 6e6435c7..c3eb3ea7 100644 --- a/app/admin/configuration/page.tsx +++ b/app/admin/configuration/page.tsx @@ -3,7 +3,7 @@ import AdminInfoPage from '@/admin/AdminInfoPage'; export default function AdminAppConfigurationPage() { return ( - + ); diff --git a/app/admin/insights/page.tsx b/app/admin/insights/page.tsx index eed08769..93b624da 100644 --- a/app/admin/insights/page.tsx +++ b/app/admin/insights/page.tsx @@ -2,7 +2,7 @@ import AdminAppInsights from '@/admin/insights/AdminAppInsights'; import AdminInfoPage from '@/admin/AdminInfoPage'; export default async function AdminInsightsPage() { - return + return ; } diff --git a/src/admin/AdminInfoNav.tsx b/src/admin/AdminInfoNav.tsx new file mode 100644 index 00000000..b1718eb6 --- /dev/null +++ b/src/admin/AdminInfoNav.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/paths'; +import LinkWithStatus from '@/components/LinkWithStatus'; +import ResponsiveText from '@/components/primitives/ResponsiveText'; +import clsx from 'clsx/lite'; +import ClearCacheButton from '@/admin/ClearCacheButton'; +import { usePathname } from 'next/navigation'; + +const ADMIN_INFO_PAGES = [{ + titleShort: 'Insights', + path: PATH_ADMIN_INSIGHTS, +}, { + title: 'Configuration', + titleShort: 'Config', + path: PATH_ADMIN_CONFIGURATION, +}]; + +export default function AdminInfoPage({ + includeInsights, +}: { + includeInsights: boolean +}) { + const pathname = usePathname(); + + const pages = ADMIN_INFO_PAGES + .filter(({ titleShort }) => ( + titleShort !== 'Insights' || + includeInsights + )); + + const hasMultiplePages = pages.length > 1; + + return ( +
+
+ {pages + .map(({ title, titleShort, path }) => + + + {title ?? titleShort} + + )} +
+ +
+ ); +} diff --git a/src/admin/AdminInfoPage.tsx b/src/admin/AdminInfoPage.tsx index 94ef0538..dc6d9091 100644 --- a/src/admin/AdminInfoPage.tsx +++ b/src/admin/AdminInfoPage.tsx @@ -1,61 +1,18 @@ -import { PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS } from '@/app/paths'; import Container from '@/components/Container'; -import LinkWithStatus from '@/components/LinkWithStatus'; -import ResponsiveText from '@/components/primitives/ResponsiveText'; import SiteGrid from '@/components/SiteGrid'; -import clsx from 'clsx/lite'; import { ReactNode } from 'react'; -import ClearCacheButton from '@/admin/ClearCacheButton'; - -const ADMIN_INFO_PAGES = [{ - titleShort: 'Insights', - path: PATH_ADMIN_INSIGHTS, -}, -{ - title: 'Configuration', - titleShort: 'Config', - path: PATH_ADMIN_CONFIGURATION, -}]; export default function AdminInfoPage({ - page, children, }: { - page: (typeof ADMIN_INFO_PAGES)[number]['titleShort'] children: ReactNode }) { return ( -
-
- {ADMIN_INFO_PAGES.map(({ title, titleShort, path }) => - - - {title ?? titleShort} - - )} -
- -
- - {children} - -
} + + {children} + } /> ); } diff --git a/src/admin/AdminNavClient.tsx b/src/admin/AdminNavClient.tsx index 7255cb10..9dd61e66 100644 --- a/src/admin/AdminNavClient.tsx +++ b/src/admin/AdminNavClient.tsx @@ -6,6 +6,7 @@ import Note from '@/components/Note'; import SiteGrid from '@/components/SiteGrid'; import Spinner from '@/components/Spinner'; import { + PATH_ADMIN_CONFIGURATION, PATH_ADMIN_INSIGHTS, checkPathPrefix, isPathAdminInfo, @@ -18,6 +19,7 @@ import { usePathname } from 'next/navigation'; import { useEffect, useMemo, useState } from 'react'; import { FaRegClock } from 'react-icons/fa'; import AdminAppInfoIcon from './AdminAppInfoIcon'; +import AdminInfoNav from './AdminInfoNav'; // Updates considered recent if they occurred in past 5 minutes const areTimesRecent = (dates: Date[]) => dates @@ -26,9 +28,7 @@ const areTimesRecent = (dates: Date[]) => dates export default function AdminNavClient({ items, mostRecentPhotoUpdateTime, - // TODO: use this with new component - // eslint-disable-next-line @typescript-eslint/no-unused-vars - includeInsights, + includeInsights = true, }: { items: { label: string, @@ -91,7 +91,9 @@ export default function AdminNavClient({ )}
} + {isPathAdminInfo(pathname) && + }
} /> From 87bd9ff5b1ff4a8de0098938093593291fadcddc Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 25 Feb 2025 23:52:59 -0600 Subject: [PATCH 08/16] Refine admin info/insights relationships --- src/admin/AdminInfoNav.tsx | 38 ++++++++++++++++++++-------- src/admin/AdminNavClient.tsx | 3 ++- src/components/more/MoreMenuItem.tsx | 4 +-- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/admin/AdminInfoNav.tsx b/src/admin/AdminInfoNav.tsx index b1718eb6..3495440a 100644 --- a/src/admin/AdminInfoNav.tsx +++ b/src/admin/AdminInfoNav.tsx @@ -6,9 +6,11 @@ import ResponsiveText from '@/components/primitives/ResponsiveText'; import clsx from 'clsx/lite'; import ClearCacheButton from '@/admin/ClearCacheButton'; import { usePathname } from 'next/navigation'; +import { useAppState } from '@/state/AppState'; +import { FaCircle } from 'react-icons/fa6'; const ADMIN_INFO_PAGES = [{ - titleShort: 'Insights', + title: 'Insights', path: PATH_ADMIN_INSIGHTS, }, { title: 'Configuration', @@ -16,6 +18,11 @@ const ADMIN_INFO_PAGES = [{ path: PATH_ADMIN_CONFIGURATION, }]; +const ADMIN_INFO_PAGE_WITHOUT_INSIGHTS = [{ + title: 'App Configuration', + path: PATH_ADMIN_CONFIGURATION, +}] as typeof ADMIN_INFO_PAGES; + export default function AdminInfoPage({ includeInsights, }: { @@ -23,20 +30,19 @@ export default function AdminInfoPage({ }) { const pathname = usePathname(); - const pages = ADMIN_INFO_PAGES - .filter(({ titleShort }) => ( - titleShort !== 'Insights' || - includeInsights - )); + const pages = includeInsights + ? ADMIN_INFO_PAGES + : ADMIN_INFO_PAGE_WITHOUT_INSIGHTS; const hasMultiplePages = pages.length > 1; + const { insightIndicatorStatus } = useAppState(); + return (
{pages .map(({ title, titleShort, path }) => @@ -44,18 +50,30 @@ export default function AdminInfoPage({ key={path} href={path} className={clsx( + 'relative', hasMultiplePages ? pathname === path - ? 'underline underline-offset-10 decoration-2' + ? 'font-medium' : 'text-dim' : undefined, 'px-1 py-0.5 rounded-md', + 'hover:text-main', )} - loadingClassName="bg-dim" + loadingClassName="bg-gray-200/50 dark:bg-gray-700/50" > - {title ?? titleShort} + {title} + {title === 'Insights' && insightIndicatorStatus && + } )}
diff --git a/src/admin/AdminNavClient.tsx b/src/admin/AdminNavClient.tsx index 9dd61e66..fd3458b9 100644 --- a/src/admin/AdminNavClient.tsx +++ b/src/admin/AdminNavClient.tsx @@ -81,8 +81,9 @@ export default function AdminNavClient({ 'flex gap-0.5', checkPathPrefix(pathname, href) ? 'font-bold' : 'text-dim', 'px-1 py-0.5 rounded-md', + 'hover:text-main', )} - loadingClassName="bg-dim" + loadingClassName="bg-gray-200/50 dark:bg-gray-700/50" prefetch={false} > {label} diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index eea7f47d..d623b972 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -47,8 +47,8 @@ export default function MoreMenuItem({ Date: Wed, 26 Feb 2025 00:08:22 -0600 Subject: [PATCH 09/16] Add sign out to admin menu --- app/sign-in/page.tsx | 14 +++++++++++++- src/admin/AdminAppMenu.tsx | 6 ++++++ src/auth/actions.ts | 6 +++--- src/components/more/MoreMenu.tsx | 2 +- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/sign-in/page.tsx b/app/sign-in/page.tsx index 7c3ee0a1..0bccd823 100644 --- a/app/sign-in/page.tsx +++ b/app/sign-in/page.tsx @@ -1,8 +1,10 @@ import { auth } from '@/auth'; import SignInForm from '@/auth/SignInForm'; -import { PATH_ADMIN } from '@/app/paths'; +import { PATH_ADMIN, PATH_ROOT } from '@/app/paths'; import { clsx } from 'clsx/lite'; import { redirect } from 'next/navigation'; +import LinkWithStatus from '@/components/LinkWithStatus'; +import { IoArrowBack } from 'react-icons/io5'; export default async function SignInPage() { const session = await auth(); @@ -17,6 +19,16 @@ export default async function SignInPage() { 'flex items-center justify-center flex-col gap-8', )}> + + + Home +
); } diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index b11e44b3..499242b5 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -15,6 +15,8 @@ import { TbPhoto } from 'react-icons/tb'; import { FiTag } from 'react-icons/fi'; import { BiLockAlt } from 'react-icons/bi'; import AdminAppInfoIcon from './AdminAppInfoIcon'; +import { PiSignOutBold } from 'react-icons/pi'; +import { signOutAndRedirectAction } from '@/auth/actions'; export default function AdminAppMenu({ className, @@ -87,6 +89,10 @@ export default function AdminAppMenu({ } }, shouldPreventDefault: false, + }, { + label: 'Sign Out', + icon: , + action: signOutAndRedirectAction, }]} ariaLabel="Admin Menu" /> diff --git a/src/auth/actions.ts b/src/auth/actions.ts index 894fb1d6..28d4e0ba 100644 --- a/src/auth/actions.ts +++ b/src/auth/actions.ts @@ -10,7 +10,7 @@ import { signIn, signOut, } from '@/auth'; -import { PATH_ADMIN_PHOTOS, PATH_ROOT } from '@/app/paths'; +import { PATH_ADMIN_PHOTOS, PATH_SIGN_IN } from '@/app/paths'; import type { Session } from 'next-auth'; import { redirect } from 'next/navigation'; @@ -41,8 +41,8 @@ export const signInAction = async ( redirect(formData.get(KEY_CALLBACK_URL) as string || PATH_ADMIN_PHOTOS); }; -export const signOutAndRedirectAction = async () => - signOut({ redirectTo: PATH_ROOT }); +export const signOutAndRedirectAction = async (redirectTo = PATH_SIGN_IN) => + signOut({ redirectTo }); export const getAuthAction = async () => auth(); diff --git a/src/components/more/MoreMenu.tsx b/src/components/more/MoreMenu.tsx index 968c26d3..b923face 100644 --- a/src/components/more/MoreMenu.tsx +++ b/src/components/more/MoreMenu.tsx @@ -59,7 +59,7 @@ export default function MoreMenu({ )} > {header &&
{header} From ac19ed2215b1fb785bd95ee4c0dbc67da5b8136f Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 26 Feb 2025 17:41:17 -0600 Subject: [PATCH 10/16] Integrate dynamic data into admin menu, update cmdk-menu --- src/admin/AdminAppInfoIcon.tsx | 19 +-- src/admin/AdminAppMenu.tsx | 128 ++++++++++++-------- src/admin/AdminInfoNav.tsx | 14 +-- src/admin/AdminNav.tsx | 5 +- src/admin/AdminPhotoMenuClient.tsx | 11 +- src/admin/actions.ts | 39 ++++++ src/admin/insights/InsightsIndicatorDot.tsx | 50 ++++++++ src/admin/insights/actions.ts | 36 ------ src/admin/insights/server.ts | 28 +++++ src/components/cmdk/CommandKClient.tsx | 128 +++++++++++--------- src/components/more/MoreMenu.tsx | 2 +- src/components/more/MoreMenuItem.tsx | 14 ++- src/photo/PhotoGridSidebar.tsx | 6 +- src/photo/actions.ts | 5 - src/state/AppState.ts | 5 +- src/state/AppStateProvider.tsx | 55 ++++++--- src/tag/index.ts | 6 +- 17 files changed, 344 insertions(+), 207 deletions(-) create mode 100644 src/admin/insights/InsightsIndicatorDot.tsx delete mode 100644 src/admin/insights/actions.ts create mode 100644 src/admin/insights/server.ts diff --git a/src/admin/AdminAppInfoIcon.tsx b/src/admin/AdminAppInfoIcon.tsx index 8cd3c4b3..dac461da 100644 --- a/src/admin/AdminAppInfoIcon.tsx +++ b/src/admin/AdminAppInfoIcon.tsx @@ -1,7 +1,7 @@ import { useAppState } from '@/state/AppState'; import clsx from 'clsx/lite'; -import { FaCircle } from 'react-icons/fa6'; import { LuCog } from 'react-icons/lu'; +import InsightsIndicatorDot from './insights/InsightsIndicatorDot'; export default function AdminAppInfoIcon({ size = 'large', @@ -18,22 +18,15 @@ export default function AdminAppInfoIcon({ className, )}> {insightIndicatorStatus && - } ); diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index 499242b5..a0b62c91 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -5,6 +5,7 @@ import { PATH_ADMIN_INSIGHTS, PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS, + PATH_ADMIN_UPLOADS, PATH_GRID_INFERRED, } from '@/app/paths'; import { useAppState } from '@/state/AppState'; @@ -17,6 +18,8 @@ import { BiLockAlt } from 'react-icons/bi'; import AdminAppInfoIcon from './AdminAppInfoIcon'; import { PiSignOutBold } from 'react-icons/pi'; import { signOutAndRedirectAction } from '@/auth/actions'; +import { ComponentProps } from 'react'; +import { FaRegFolderOpen } from 'react-icons/fa'; export default function AdminAppMenu({ className, @@ -26,12 +29,87 @@ export default function AdminAppMenu({ buttonClassName?: string }) { const { + photosCount, + uploadsCount, + tagsCount, selectedPhotoIds, setSelectedPhotoIds, } = useAppState(); const isSelecting = selectedPhotoIds !== undefined; + const items: ComponentProps['items'] = [{ + label: 'Manage Photos', + ...photosCount !== undefined && { + annotation: `${photosCount}`, + }, + icon: , + href: PATH_ADMIN_PHOTOS, + }]; + + if (uploadsCount) { + items.push({ + label: 'Uploads', + annotation: `${uploadsCount}`, + icon: , + href: PATH_ADMIN_UPLOADS, + }); + } + + if (tagsCount) { + items.push({ + label: 'Manage Tags', + annotation: `${tagsCount}`, + icon: , + href: PATH_ADMIN_TAGS, + }); + } + + items.push({ + label: 'App Info', + icon: , + href: PATH_ADMIN_INSIGHTS, + }, { + label: isSelecting + ? 'Exit Select' + : 'Edit Multiple …', + icon: isSelecting + ? + : , + href: PATH_GRID_INFERRED, + action: () => { + if (isSelecting) { + setSelectedPhotoIds?.(undefined); + } else { + setSelectedPhotoIds?.([]); + } + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + }, + shouldPreventDefault: false, + }, { + label: 'Sign Out', + icon: , + action: signOutAndRedirectAction, + }); + return ( , - href: PATH_ADMIN_PHOTOS, - }, { - label: 'Manage Tags', - icon: , - href: PATH_ADMIN_TAGS, - }, { - label: 'App Info', - icon: , - href: PATH_ADMIN_INSIGHTS, - }, { - label: isSelecting - ? 'Exit Select' - : 'Select Photos', - icon: isSelecting - ? - : , - href: PATH_GRID_INFERRED, - action: () => { - if (isSelecting) { - setSelectedPhotoIds?.(undefined); - } else { - setSelectedPhotoIds?.([]); - } - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - }, - shouldPreventDefault: false, - }, { - label: 'Sign Out', - icon: , - action: signOutAndRedirectAction, - }]} + items={items} ariaLabel="Admin Menu" /> ); diff --git a/src/admin/AdminInfoNav.tsx b/src/admin/AdminInfoNav.tsx index 3495440a..70e9cd81 100644 --- a/src/admin/AdminInfoNav.tsx +++ b/src/admin/AdminInfoNav.tsx @@ -7,7 +7,7 @@ import clsx from 'clsx/lite'; import ClearCacheButton from '@/admin/ClearCacheButton'; import { usePathname } from 'next/navigation'; import { useAppState } from '@/state/AppState'; -import { FaCircle } from 'react-icons/fa6'; +import InsightsIndicatorDot from './insights/InsightsIndicatorDot'; const ADMIN_INFO_PAGES = [{ title: 'Insights', @@ -65,14 +65,10 @@ export default function AdminInfoPage({ {title} {title === 'Insights' && insightIndicatorStatus && - } )}
diff --git a/src/admin/AdminNav.tsx b/src/admin/AdminNav.tsx index e6f10e30..eecda0aa 100644 --- a/src/admin/AdminNav.tsx +++ b/src/admin/AdminNav.tsx @@ -14,20 +14,21 @@ import AdminNavClient from './AdminNavClient'; export default async function AdminNav() { const [ countPhotos, - countUploads, countTags, + countUploads, mostRecentPhotoUpdateTime, ] = await Promise.all([ getPhotosMetaCached({ hidden: 'include' }) .then(({ count }) => count) .catch(() => 0), + getUniqueTagsCached().then(tags => tags.length) + .catch(() => 0), getStorageUploadUrlsNoStore() .then(urls => urls.length) .catch(e => { console.error(`Error getting blob upload urls: ${e}`); return 0; }), - getUniqueTagsCached().then(tags => tags.length).catch(() => 0), getPhotosMostRecentUpdateCached().catch(() => undefined), ]); diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx index 0cd75923..9e7dd48b 100644 --- a/src/admin/AdminPhotoMenuClient.tsx +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -23,7 +23,7 @@ import { MdOutlineFileDownload } from 'react-icons/md'; import MoreMenuItem from '@/components/more/MoreMenuItem'; import IconGrSync from '@/app/IconGrSync'; import { isPhotoOutdated } from '@/photo/outdated'; -import { FaCircle } from 'react-icons/fa6'; +import InsightsIndicatorDot from './insights/InsightsIndicatorDot'; export default function AdminPhotoMenuClient({ photo, @@ -81,12 +81,13 @@ export default function AdminPhotoMenuClient({ hrefDownloadName: downloadFileNameForPhoto(photo), }); items.push({ - label: + label: 'Sync', + labelComplex: Sync {isPhotoOutdated(photo) && - } , icon: , diff --git a/src/admin/actions.ts b/src/admin/actions.ts index de215f9c..a79b27cf 100644 --- a/src/admin/actions.ts +++ b/src/admin/actions.ts @@ -6,6 +6,45 @@ import { testOpenAiConnection } from '@/platforms/openai'; import { testDatabaseConnection } from '@/platforms/postgres'; import { testStorageConnection } from '@/platforms/storage'; import { APP_CONFIGURATION } from '@/app/config'; +import { getStorageUploadUrlsNoStore } from '@/platforms/storage/cache'; +import { getPhotosMetaCached, getUniqueTagsCached } from '@/photo/cache'; +import { getShouldShowInsightsIndicator } from '@/admin/insights/server'; + +export const getAdminDataAction = async () => + runAuthenticatedAdminServerAction(async () => { + const [ + countPhotos, + countHiddenPhotos, + countTags, + countUploads, + shouldShowInsightsIndicator, + ] = await Promise.all([ + getPhotosMetaCached() + .then(({ count }) => count) + .catch(() => 0), + getPhotosMetaCached({ hidden: 'only' }) + .then(({ count }) => count) + .catch(() => 0), + getUniqueTagsCached() + .then(tags => tags.length) + .catch(() => 0), + getStorageUploadUrlsNoStore() + .then(urls => urls.length) + .catch(e => { + console.error(`Error getting blob upload urls: ${e}`); + return 0; + }), + getShouldShowInsightsIndicator(), + ]); + + return { + countPhotos, + countHiddenPhotos, + countTags, + countUploads, + shouldShowInsightsIndicator, + }; + }); const scanForError = ( shouldCheck: boolean, diff --git a/src/admin/insights/InsightsIndicatorDot.tsx b/src/admin/insights/InsightsIndicatorDot.tsx new file mode 100644 index 00000000..4d8d6377 --- /dev/null +++ b/src/admin/insights/InsightsIndicatorDot.tsx @@ -0,0 +1,50 @@ +import { useAppState } from '@/state/AppState'; +import clsx from 'clsx/lite'; +import { FaCircle } from 'react-icons/fa6'; + +export default function InsightsIndicatorDot({ + className, + size = 'medium', + colorOverride, + top, + right, + bottom, + left, +}: { + className?: string + size?: 'small' | 'medium' | 'large' + colorOverride?: 'blue' | 'yellow' + top?: number + right?: number + bottom?: number + left?: number +}) { + const { insightIndicatorStatus } = useAppState(); + + const getSize = () => { + switch (size) { + case 'small': return 6; + case 'medium': return 7; + case 'large': return 8; + } + }; + + return ( + + ); +} diff --git a/src/admin/insights/actions.ts b/src/admin/insights/actions.ts deleted file mode 100644 index ed6c03f9..00000000 --- a/src/admin/insights/actions.ts +++ /dev/null @@ -1,36 +0,0 @@ -'use server'; - -import { runAuthenticatedAdminServerAction } from '@/auth'; -import { - getGitHubMetaForCurrentApp, - getSignificantInsights, - InsightIndicatorStatus, -} from '.'; -import { getOutdatedPhotosCount } from '@/photo/db/query'; - -export const getShouldShowInsightsIndicatorAction = - async (): Promise => - runAuthenticatedAdminServerAction(async () => { - const [ - codeMeta, - photosCountOutdated, - ] = await Promise.all([ - getGitHubMetaForCurrentApp(), - getOutdatedPhotosCount(), - ]); - - const { - forkBehind, - noAiRateLimiting, - outdatedPhotos, - } = getSignificantInsights({ - codeMeta, - photosCountOutdated, - }); - - if (noAiRateLimiting || outdatedPhotos) { - return 'yellow'; - } else if (forkBehind) { - return 'blue'; - } - }); diff --git a/src/admin/insights/server.ts b/src/admin/insights/server.ts new file mode 100644 index 00000000..b721d3b6 --- /dev/null +++ b/src/admin/insights/server.ts @@ -0,0 +1,28 @@ +import { getOutdatedPhotosCount } from '@/photo/db/query'; +import { getSignificantInsights } from '.'; +import { getGitHubMetaForCurrentApp } from '.'; + +export const getShouldShowInsightsIndicator = async () => { + const [ + codeMeta, + photosCountOutdated, + ] = await Promise.all([ + getGitHubMetaForCurrentApp(), + getOutdatedPhotosCount(), + ]); + + const { + forkBehind, + noAiRateLimiting, + outdatedPhotos, + } = getSignificantInsights({ + codeMeta, + photosCountOutdated, + }); + + if (noAiRateLimiting || outdatedPhotos) { + return 'yellow'; + } else if (forkBehind) { + return 'blue'; + } +}; diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index 2f5682ea..1c8e6a56 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -44,7 +44,7 @@ import { TbPhoto } from 'react-icons/tb'; import { getKeywordsForPhoto, titleForPhoto } from '@/photo'; import PhotoDate from '@/photo/PhotoDate'; import PhotoSmall from '@/photo/PhotoSmall'; -import { FaCheck, FaCircle } from 'react-icons/fa6'; +import { FaCheck } from 'react-icons/fa6'; import { Tags, addHiddenToTags, formatTag } from '@/tag'; import { FaTag } from 'react-icons/fa'; import { formatCount, formatCountDescriptive } from '@/utility/string'; @@ -52,6 +52,7 @@ import CommandKItem from './CommandKItem'; import { GRID_HOMEPAGE_ENABLED } from '@/app/config'; import { DialogDescription, DialogTitle } from '@radix-ui/react-dialog'; import * as VisuallyHidden from '@radix-ui/react-visually-hidden'; +import InsightsIndicatorDot from '@/admin/insights/InsightsIndicatorDot'; const DIALOG_TITLE = 'Global Command-K Menu'; const DIALOG_DESCRIPTION = 'For searching photos, views, and settings'; @@ -102,7 +103,9 @@ export default function CommandKClient({ isUserSignedIn, setUserEmail, isCommandKOpen: isOpen, - hiddenPhotosCount, + photosCountHidden, + uploadsCount, + tagsCount, selectedPhotoIds, setSelectedPhotoIds, insightIndicatorStatus, @@ -228,8 +231,8 @@ export default function CommandKClient({ }, [isOpen, setShouldRespondToKeyboardCommands]); const tagsIncludingHidden = useMemo(() => - addHiddenToTags(tags, hiddenPhotosCount) - , [tags, hiddenPhotosCount]); + addHiddenToTags(tags, photosCountHidden) + , [tags, photosCountHidden]); const SECTION_TAGS: CommandKSection = { heading: 'Tags', @@ -336,70 +339,77 @@ export default function CommandKClient({ const adminSection: CommandKSection = { heading: 'Admin', accessory: , - items: isUserSignedIn - ? ([{ - label: 'Manage Photos', - annotation: , - path: PATH_ADMIN_PHOTOS, - }, { + items: [], + }; + + if (isUserSignedIn) { + adminSection.items.push({ + label: 'Manage Photos', + annotation: , + path: PATH_ADMIN_PHOTOS, + }); + if (uploadsCount) { + adminSection.items.push({ label: 'Manage Uploads', annotation: , path: PATH_ADMIN_UPLOADS, - }, { + }); + } + if (tagsCount) { + adminSection.items.push({ label: 'Manage Tags', annotation: , path: PATH_ADMIN_TAGS, - }, { - label: 'App Config', + }); + } + adminSection.items.push({ + label: + App Insights + {insightIndicatorStatus && + } + , + keywords: ['app insights'], + annotation: , + path: PATH_ADMIN_INSIGHTS, + }, { + label: 'App Config', + annotation: , + path: PATH_ADMIN_CONFIGURATION, + }, { + label: selectedPhotoIds === undefined + ? 'Select Multiple Photos' + : 'Exit Select Multiple Photos', + annotation: , + path: selectedPhotoIds === undefined + ? PATH_GRID_INFERRED + : undefined, + action: selectedPhotoIds === undefined + ? () => setSelectedPhotoIds?.([]) + : () => setSelectedPhotoIds?.(undefined), + }); + if (showDebugTools) { + adminSection.items.push({ + label: 'Baseline Overview', annotation: , - path: PATH_ADMIN_CONFIGURATION, + path: PATH_ADMIN_BASELINE, }, { - label: - App Insights - {insightIndicatorStatus && } - , - keywords: ['app insights'], + label: 'Components Overview', annotation: , - path: PATH_ADMIN_INSIGHTS, - }, { - label: selectedPhotoIds === undefined - ? 'Select Multiple Photos' - : 'Exit Select Multiple Photos', - annotation: , - path: selectedPhotoIds === undefined - ? PATH_GRID_INFERRED - : undefined, - action: selectedPhotoIds === undefined - ? () => setSelectedPhotoIds?.([]) - : () => setSelectedPhotoIds?.(undefined), - }] as CommandKItem[]) - .concat(showDebugTools - ? [{ - label: 'Baseline Overview', - path: PATH_ADMIN_BASELINE, - }, { - label: 'Components Overview', - path: PATH_ADMIN_COMPONENTS, - }] - : []) - .concat({ - label: 'Sign Out', - action: () => { - signOutAndRedirectAction().then(() => setUserEmail?.(undefined)); - }, - }) - : [{ - label: 'Sign In', - path: PATH_SIGN_IN, - }], - }; + path: PATH_ADMIN_COMPONENTS, + }); + } + adminSection.items.push({ + label: 'Sign Out', + action: () => { + signOutAndRedirectAction().then(() => setUserEmail?.(undefined)); + }, + }); + } else { + adminSection.items.push({ + label: 'Sign In', + path: PATH_SIGN_IN, + }); + } return ( [] + items: ComponentProps[] icon?: ReactNode header?: ReactNode className?: string diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index d623b972..fe54671d 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -9,6 +9,8 @@ import { downloadFileFromBrowser } from '@/utility/url'; export default function MoreMenuItem({ label, + labelComplex, + annotation, icon, href, hrefDownloadName, @@ -17,7 +19,9 @@ export default function MoreMenuItem({ dismissMenu, shouldPreventDefault = true, }: { - label: ReactNode + label: string + labelComplex?: ReactNode + annotation?: string icon?: ReactNode href?: string hrefDownloadName?: string @@ -48,7 +52,7 @@ export default function MoreMenuItem({ disabled={isLoading} className={clsx( 'flex items-center h-9', - 'pl-2 pr-4 py-2 rounded-sm', + 'pl-2 pr-3 py-2 rounded-sm', 'select-none hover:outline-hidden', 'hover:bg-gray-100/90 active:bg-gray-200/75', 'dark:hover:bg-gray-800/60 dark:active:bg-gray-900/80', @@ -92,7 +96,11 @@ export default function MoreMenuItem({ styleAs="link-without-hover" className="translate-y-[1px]" > - {label} + {labelComplex ?? label} + {annotation && + + {annotation} + } ); diff --git a/src/photo/PhotoGridSidebar.tsx b/src/photo/PhotoGridSidebar.tsx index 21c53dae..2288d94a 100644 --- a/src/photo/PhotoGridSidebar.tsx +++ b/src/photo/PhotoGridSidebar.tsx @@ -37,11 +37,11 @@ export default function PhotoGridSidebar({ }) { const { start, end } = dateRangeForPhotos(undefined, photosDateRange); - const { hiddenPhotosCount } = useAppState(); + const { photosCountHidden } = useAppState(); const tagsIncludingHidden = useMemo(() => - addHiddenToTags(tags, hiddenPhotosCount) - , [tags, hiddenPhotosCount]); + addHiddenToTags(tags, photosCountHidden) + , [tags, photosCountHidden]); return (
diff --git a/src/photo/actions.ts b/src/photo/actions.ts index b7e71a0f..9b741559 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -22,7 +22,6 @@ import { redirect } from 'next/navigation'; import { deleteFile } from '@/platforms/storage'; import { getPhotosCached, - getPhotosMetaCached, revalidateAdminPaths, revalidateAllKeysAndPaths, revalidatePhoto, @@ -416,10 +415,6 @@ export const streamAiImageQueryAction = async ( export const getImageBlurAction = async (url: string) => runAuthenticatedAdminServerAction(() => blurImageFromUrl(url)); -export const getPhotosHiddenMetaCachedAction = async () => - runAuthenticatedAdminServerAction(() => - getPhotosMetaCached({ hidden: 'only' })); - // Public/Private actions export const getPhotosAction = async ( diff --git a/src/state/AppState.ts b/src/state/AppState.ts index ebd1f451..8deaa844 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -26,7 +26,10 @@ export interface AppStateContext { isUserSignedIn?: boolean adminUpdateTimes?: Date[] registerAdminUpdate?: () => void - hiddenPhotosCount?: number + photosCount?: number + photosCountHidden?: number + uploadsCount?: number + tagsCount?: number selectedPhotoIds?: string[] setSelectedPhotoIds?: Dispatch> isPerformingSelectEdit?: boolean diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 28dc7455..a961194e 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -12,11 +12,10 @@ import { MATTE_PHOTOS, SHOW_ZOOM_CONTROLS, } from '@/app/config'; -import { getPhotosHiddenMetaCachedAction } from '@/photo/actions'; import { ShareModalProps } from '@/share'; import { storeTimezoneCookie } from '@/utility/timezone'; -import { getShouldShowInsightsIndicatorAction } from '@/admin/insights/actions'; import { InsightIndicatorStatus } from '@/admin/insights'; +import { getAdminDataAction } from '@/admin/actions'; export default function AppStateProvider({ children, @@ -44,8 +43,14 @@ export default function AppStateProvider({ useState(); const [adminUpdateTimes, setAdminUpdateTimes] = useState([]); - const [hiddenPhotosCount, setHiddenPhotosCount] = - useState(0); + const [photosCount, setPhotosCount] = + useState(); + const [photosCountHidden, setPhotosCountHidden] = + useState(); + const [uploadsCount, setUploadsCount] = + useState(); + const [tagsCount, setTagsCount] = + useState(); const [selectedPhotoIds, setSelectedPhotoIds] = useState(); const [isPerformingSelectEdit, setIsPerformingSelectEdit] = @@ -70,26 +75,37 @@ export default function AppStateProvider({ const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []); - const { data, error } = useSWR('getAuth', getAuthAction); + const { data: auth, error: authError } = useSWR('getAuth', getAuthAction); useEffect(() => { - if (!error) { - setUserEmail(data?.user?.email ?? undefined); + if (!authError) { + setUserEmail(auth?.user?.email ?? undefined); } - }, [data, error]); + }, [auth, authError]); const isUserSignedIn = Boolean(userEmail); + + const { data: adminData, error: adminError } = useSWR( + isUserSignedIn ? 'getAdminData' : null, + getAdminDataAction, { + refreshInterval: 1000 * 60 * 5, + }, + ); + useEffect(() => { if (isUserSignedIn) { - const timeout = setTimeout(() =>{ - getPhotosHiddenMetaCachedAction() - .then(({ count }) => setHiddenPhotosCount(count)); - getShouldShowInsightsIndicatorAction() - .then(setInsightIndicatorStatus); - }, 100); - return () => clearTimeout(timeout); + if (adminData) { + const timeout = setTimeout(() => { + setPhotosCount(adminData.countPhotos); + setPhotosCountHidden(adminData.countHiddenPhotos); + setUploadsCount(adminData.countUploads); + setTagsCount(adminData.countTags); + setInsightIndicatorStatus(adminData.shouldShowInsightsIndicator); + }, 100); + return () => clearTimeout(timeout); + } } else { - setHiddenPhotosCount(0); + setPhotosCountHidden(0); } - }, [isUserSignedIn]); + }, [adminData, adminError, isUserSignedIn]); const registerAdminUpdate = useCallback(() => setAdminUpdateTimes(updates => [...updates, new Date()]) @@ -125,7 +141,10 @@ export default function AppStateProvider({ isUserSignedIn, adminUpdateTimes, registerAdminUpdate, - hiddenPhotosCount, + photosCount, + photosCountHidden, + uploadsCount, + tagsCount, selectedPhotoIds, setSelectedPhotoIds, isPerformingSelectEdit, diff --git a/src/tag/index.ts b/src/tag/index.ts index 0bd2fb77..29cc8805 100644 --- a/src/tag/index.ts +++ b/src/tag/index.ts @@ -105,11 +105,11 @@ export const isPathFavs = (pathname?: string) => export const isTagHidden = (tag: string) => tag.toLowerCase() === TAG_HIDDEN; -export const addHiddenToTags = (tags: Tags, hiddenPhotosCount = 0) => { - if (hiddenPhotosCount > 0) { +export const addHiddenToTags = (tags: Tags, photosCountHidden = 0) => { + if (photosCountHidden > 0) { return tags .filter(({ tag }) => tag === TAG_FAVS) - .concat({ tag: TAG_HIDDEN, count: hiddenPhotosCount }) + .concat({ tag: TAG_HIDDEN, count: photosCountHidden }) .concat(tags.filter(({ tag }) => tag !== TAG_FAVS)); } else { return tags; From 2a0e898ba6e078aa0f6ce135eb3c67fa4ab449c9 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 26 Feb 2025 19:45:18 -0600 Subject: [PATCH 11/16] Eagerly load admin nav with client-side cookie strategy --- .vscode/settings.json | 1 + src/admin/AdminAppMenu.tsx | 5 +-- src/app/Footer.tsx | 8 ++--- src/app/ViewSwitcher.tsx | 45 +++++++++++++++++--------- src/auth/actions.ts | 3 ++ src/auth/client.ts | 12 +++++++ src/components/SwitcherItem.tsx | 16 ++++++--- src/components/cmdk/CommandKClient.tsx | 8 ++--- src/photo/form/index.ts | 1 - src/platforms/github.ts | 2 -- src/state/AppState.ts | 5 ++- src/state/AppStateProvider.tsx | 29 +++++++++++++++-- src/utility/cookie.ts | 24 ++++++++------ 13 files changed, 112 insertions(+), 47 deletions(-) create mode 100644 src/auth/client.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index a707b354..df4f6cbe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "ARROWLEFT", "ARROWRIGHT", "Astia", + "authjs", "camelcase", "cloudflarestorage", "cmdk", diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index a0b62c91..5147eecc 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -17,7 +17,7 @@ import { FiTag } from 'react-icons/fi'; import { BiLockAlt } from 'react-icons/bi'; import AdminAppInfoIcon from './AdminAppInfoIcon'; import { PiSignOutBold } from 'react-icons/pi'; -import { signOutAndRedirectAction } from '@/auth/actions'; +import { signOutAction } from '@/auth/actions'; import { ComponentProps } from 'react'; import { FaRegFolderOpen } from 'react-icons/fa'; @@ -34,6 +34,7 @@ export default function AdminAppMenu({ tagsCount, selectedPhotoIds, setSelectedPhotoIds, + clearAuthStateAndRedirect, } = useAppState(); const isSelecting = selectedPhotoIds !== undefined; @@ -107,7 +108,7 @@ export default function AdminAppMenu({ }, { label: 'Sign Out', icon: , - action: signOutAndRedirectAction, + action: () => signOutAction().then(clearAuthStateAndRedirect), }); return ( diff --git a/src/app/Footer.tsx b/src/app/Footer.tsx index 3579b55c..d639b014 100644 --- a/src/app/Footer.tsx +++ b/src/app/Footer.tsx @@ -9,7 +9,7 @@ import RepoLink from '../components/RepoLink'; import { usePathname } from 'next/navigation'; import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; -import { signOutAndRedirectAction } from '@/auth/actions'; +import { signOutAction } from '@/auth/actions'; import Spinner from '@/components/Spinner'; import AnimateItems from '@/components/AnimateItems'; import { useAppState } from '@/state/AppState'; @@ -17,7 +17,7 @@ import { useAppState } from '@/state/AppState'; export default function Footer() { const pathname = usePathname(); - const { userEmail, setUserEmail } = useAppState(); + const { userEmail, clearAuthStateAndRedirect } = useAppState(); const showFooter = !isPathSignIn(pathname); @@ -48,8 +48,8 @@ export default function Footer() { )}> {userEmail}
-
signOutAndRedirectAction() - .then(() => setUserEmail?.(undefined))}> + signOutAction() + .then(clearAuthStateAndRedirect)}> Sign out diff --git a/src/app/ViewSwitcher.tsx b/src/app/ViewSwitcher.tsx index 407ade46..d0cf9222 100644 --- a/src/app/ViewSwitcher.tsx +++ b/src/app/ViewSwitcher.tsx @@ -11,6 +11,7 @@ import { useAppState } from '@/state/AppState'; import { GRID_HOMEPAGE_ENABLED } from './config'; import AdminAppMenu from '@/admin/AdminAppMenu'; import { clsx } from 'clsx/lite'; +import Spinner from '@/components/Spinner'; export type SwitcherSelection = 'feed' | 'grid' | 'admin'; @@ -19,9 +20,13 @@ export default function ViewSwitcher({ }: { currentSelection?: SwitcherSelection }) { - const { setIsCommandKOpen, isUserSignedIn } = useAppState(); + const { + isUserSignedIn, + isUserSignedInEager, + setIsCommandKOpen, + } = useAppState(); - const renderItemFeed = () => + const renderItemFeed = } href={PATH_FEED_INFERRED} @@ -29,7 +34,7 @@ export default function ViewSwitcher({ noPadding />; - const renderItemGrid = () => + const renderItemGrid = } href={PATH_GRID_INFERRED} @@ -40,19 +45,29 @@ export default function ViewSwitcher({ return (
- {GRID_HOMEPAGE_ENABLED ? renderItemGrid() : renderItemFeed()} - {GRID_HOMEPAGE_ENABLED ? renderItemFeed() : renderItemGrid()} + {GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed} + {GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid} + {/* Show spinner if admin is suspected to be logged in */} + {(isUserSignedInEager && !isUserSignedIn) && + } + isInteractive={false} + noPadding + />} {isUserSignedIn && - } + noPadding />} diff --git a/src/auth/actions.ts b/src/auth/actions.ts index 28d4e0ba..1f0ba968 100644 --- a/src/auth/actions.ts +++ b/src/auth/actions.ts @@ -41,6 +41,9 @@ export const signInAction = async ( redirect(formData.get(KEY_CALLBACK_URL) as string || PATH_ADMIN_PHOTOS); }; +export const signOutAction = async () => + signOut({ redirect: false }); + export const signOutAndRedirectAction = async (redirectTo = PATH_SIGN_IN) => signOut({ redirectTo }); diff --git a/src/auth/client.ts b/src/auth/client.ts new file mode 100644 index 00000000..49645be4 --- /dev/null +++ b/src/auth/client.ts @@ -0,0 +1,12 @@ +import { deleteCookie, getCookie, storeCookie } from '@/utility/cookie'; + +const KEY_AUTH_EMAIL = 'authjs.email'; + +export const storeAuthEmailCookie = (email: string) => + storeCookie(KEY_AUTH_EMAIL, email); + +export const clearAuthEmailCookie = () => + deleteCookie(KEY_AUTH_EMAIL); + +export const hasAuthEmailCookie = () => + Boolean(getCookie(KEY_AUTH_EMAIL)); diff --git a/src/components/SwitcherItem.tsx b/src/components/SwitcherItem.tsx index b69b00fc..38eabe07 100644 --- a/src/components/SwitcherItem.tsx +++ b/src/components/SwitcherItem.tsx @@ -11,6 +11,7 @@ export default function SwitcherItem({ className: classNameProp, onClick, active, + isInteractive = true, noPadding, prefetch = SHOULD_PREFETCH_ALL_LINKS, }: { @@ -20,21 +21,24 @@ export default function SwitcherItem({ className?: string onClick?: () => void active?: boolean + isInteractive?: boolean noPadding?: boolean prefetch?: boolean }) { const className = clsx( - classNameProp, + 'flex items-center justify-center', + 'w-[42px] h-full', 'py-0.5 px-1.5', - 'cursor-pointer', - 'hover:bg-gray-100/60 active:bg-gray-100', - 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900', + isInteractive && 'cursor-pointer', + isInteractive && 'hover:bg-gray-100/60 active:bg-gray-100', + isInteractive && 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900', active ? 'text-black dark:text-white' : 'text-gray-400 dark:text-gray-600', active ? 'hover:text-black dark:hover:text-white' : 'hover:text-gray-700 dark:hover:text-gray-400', + classNameProp, ); const renderIcon = () => noPadding @@ -54,6 +58,8 @@ export default function SwitcherItem({ }}> {renderIcon()} - :
{renderIcon()}
+ :
+ {renderIcon()} +
); }; diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index 1c8e6a56..3bf4bd5d 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -39,7 +39,7 @@ import { searchPhotosAction } from '@/photo/actions'; import { RiToolsFill } from 'react-icons/ri'; import { BiLockAlt, BiSolidUser } from 'react-icons/bi'; import { HiDocumentText } from 'react-icons/hi'; -import { signOutAndRedirectAction } from '@/auth/actions'; +import { signOutAction } from '@/auth/actions'; import { TbPhoto } from 'react-icons/tb'; import { getKeywordsForPhoto, titleForPhoto } from '@/photo'; import PhotoDate from '@/photo/PhotoDate'; @@ -101,7 +101,7 @@ export default function CommandKClient({ const { isUserSignedIn, - setUserEmail, + clearAuthStateAndRedirect, isCommandKOpen: isOpen, photosCountHidden, uploadsCount, @@ -400,9 +400,7 @@ export default function CommandKClient({ } adminSection.items.push({ label: 'Sign Out', - action: () => { - signOutAndRedirectAction().then(() => setUserEmail?.(undefined)); - }, + action: () => signOutAction().then(clearAuthStateAndRedirect), }); } else { adminSection.items.push({ diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 62ed6e0b..515c0787 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -197,7 +197,6 @@ export const formHasTextContent = ({ // CREATE FORM DATA: FROM PHOTO export const convertPhotoToFormData = (photo: Photo): PhotoFormData => { - console.log('convertPhotoToFormData', photo); const valueForKey = (key: keyof Photo, value: any) => { switch (key) { case 'tags': diff --git a/src/platforms/github.ts b/src/platforms/github.ts index 8bb89823..afb14efb 100644 --- a/src/platforms/github.ts +++ b/src/platforms/github.ts @@ -133,8 +133,6 @@ export const getGitHubPublicFork = async (): Promise => { }; export const getGitHubMeta = async (params: RepoParams) => { - console.log('getGitHubMeta', params); - const urlOwner = getGitHubUrlOwner(params); const urlRepo = getGitHubUrlRepo(params); const urlBranch = getGitHubUrlBranch(params); diff --git a/src/state/AppState.ts b/src/state/AppState.ts index 8deaa844..6a24e41c 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -20,10 +20,13 @@ export interface AppStateContext { setIsCommandKOpen?: Dispatch> shareModalProps?: ShareModalProps setShareModalProps?: Dispatch> - // ADMIN + // AUTH userEmail?: string setUserEmail?: Dispatch> isUserSignedIn?: boolean + isUserSignedInEager?: boolean + clearAuthStateAndRedirect?: () => void + // ADMIN adminUpdateTimes?: Date[] registerAdminUpdate?: () => void photosCount?: number diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index a961194e..83452742 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -16,6 +16,13 @@ import { ShareModalProps } from '@/share'; import { storeTimezoneCookie } from '@/utility/timezone'; import { InsightIndicatorStatus } from '@/admin/insights'; import { getAdminDataAction } from '@/admin/actions'; +import { + storeAuthEmailCookie, + clearAuthEmailCookie, + hasAuthEmailCookie, +} from '@/auth/client'; +import { useRouter } from 'next/navigation'; +import { PATH_SIGN_IN } from '@/app/paths'; export default function AppStateProvider({ children, @@ -24,6 +31,8 @@ export default function AppStateProvider({ }) { const { previousPathname } = usePathnames(); + const router = useRouter(); + // CORE const [hasLoaded, setHasLoaded] = useState(false); @@ -41,6 +50,9 @@ export default function AppStateProvider({ // ADMIN const [userEmail, setUserEmail] = useState(); + const [isUserSignedInEager, setIsUserSignedInEager] = + useState(false); + // ADMIN const [adminUpdateTimes, setAdminUpdateTimes] = useState([]); const [photosCount, setPhotosCount] = @@ -77,6 +89,7 @@ export default function AppStateProvider({ const { data: auth, error: authError } = useSWR('getAuth', getAuthAction); useEffect(() => { + setIsUserSignedInEager(hasAuthEmailCookie()); if (!authError) { setUserEmail(auth?.user?.email ?? undefined); } @@ -91,7 +104,8 @@ export default function AppStateProvider({ ); useEffect(() => { - if (isUserSignedIn) { + if (userEmail) { + storeAuthEmailCookie(userEmail); if (adminData) { const timeout = setTimeout(() => { setPhotosCount(adminData.countPhotos); @@ -105,7 +119,7 @@ export default function AppStateProvider({ } else { setPhotosCountHidden(0); } - }, [adminData, adminError, isUserSignedIn]); + }, [adminData, adminError, userEmail]); const registerAdminUpdate = useCallback(() => setAdminUpdateTimes(updates => [...updates, new Date()]) @@ -116,6 +130,12 @@ export default function AppStateProvider({ storeTimezoneCookie(); }, []); + const clearAuthStateAndRedirect = useCallback((shouldRedirect = true) => { + setUserEmail(undefined); + clearAuthEmailCookie(); + if (shouldRedirect) { router.push(PATH_SIGN_IN); } + }, [router]); + return ( { - document.cookie = - `${name}=${value};Path=${path};Max-Age=${maxAge};SameSite=${sameSite}`; + if (typeof document !== 'undefined') { + document.cookie = + `${name}=${value};Path=${path};Max-Age=${maxAge};SameSite=${sameSite}`; + } }; export const getCookie = (name: string) => { - const cookie: Record = {}; - document.cookie.split(';').forEach(function(el) { - const split = el.split('='); - cookie[split[0].trim()] = split.slice(1).join('='); - }); - return cookie[name]; + if (typeof document !== 'undefined') { + const cookie: Record = {}; + document.cookie.split(';').forEach(function(el) { + const split = el.split('='); + cookie[split[0].trim()] = split.slice(1).join('='); + }); + return cookie[name]; + } }; export const deleteCookie = (name: string) => { - document.cookie = `${name}=;Max-Age=0`; + if (typeof document !== 'undefined') { + document.cookie = `${name}=;Max-Age=0`; + } }; From fe0e36c894c14ed2ad900ee23b2b3d9995dc46f6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 26 Feb 2025 21:15:23 -0600 Subject: [PATCH 12/16] Increase menu shadows in dark mode --- src/components/more/MoreMenu.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/more/MoreMenu.tsx b/src/components/more/MoreMenu.tsx index 04cf47f0..6b134d97 100644 --- a/src/components/more/MoreMenu.tsx +++ b/src/components/more/MoreMenu.tsx @@ -52,7 +52,9 @@ export default function MoreMenu({ 'min-w-[8rem]', 'component-surface', 'p-1', - 'shadow-lg dark:shadow-xl', + 'shadow-lg', + 'data-[side=top]:dark:shadow-[0_0px_40px_rgba(0,0,0,0.6)]', + 'data-[side=bottom]:dark:shadow-[0_10px_40px_rgba(0,0,0,0.6)]', 'data-[side=top]:animate-fade-in-from-bottom', 'data-[side=bottom]:animate-fade-in-from-top', className, From a7435852c4262cc3f19788f80c988a5f4c2e1794 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 26 Feb 2025 22:57:26 -0600 Subject: [PATCH 13/16] Prepare for 1-click uploads --- app/layout.tsx | 5 +++-- src/admin/AdminBatchEditPanelClient.tsx | 2 +- src/components/Container.tsx | 8 ++------ src/state/AppStateProvider.tsx | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index ae26b1c7..df6a17b5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -82,12 +82,13 @@ export default function RootLayout({ '3xl:mx-auto 3xl:w-[1280px]', )}>