Merge pull request #2509 from homarr-labs/feat/improve-community

This commit is contained in:
Thomas Camlong
2025-11-20 17:09:34 +01:00
committed by GitHub
10 changed files with 908 additions and 176 deletions

View File

@@ -1,10 +1,17 @@
import type { NextConfig } from "next"
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
unoptimized: true,
},
cacheComponents: false,
images: {
remotePatterns: [
new URL(
"https://pb.dashboardicons.com/**",
),
new URL(
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/**",
),
],
},
};
export default nextConfig
export default nextConfig

View File

@@ -17,6 +17,7 @@
},
"dependencies": {
"@hookform/resolvers": "^5.2.1",
"@posthog/nextjs-config": "^1.4.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.7",
@@ -95,6 +96,7 @@
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome",
"@posthog/cli",
"@tailwindcss/oxide",
"core-js",
"esbuild",

482
web/pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@hookform/resolvers':
specifier: ^5.2.1
version: 5.2.1(react-hook-form@7.66.0(react@19.2.0))
'@posthog/nextjs-config':
specifier: ^1.4.0
version: 1.4.0(next@16.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))
'@radix-ui/react-accordion':
specifier: ^1.2.12
version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.5))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -750,6 +753,18 @@ packages:
cpu: [x64]
os: [win32]
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
'@isaacs/brace-expansion@5.0.0':
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
engines: {node: 20 || >=22}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@jridgewell/gen-mapping@0.3.12':
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
@@ -829,9 +844,20 @@ packages:
'@poppinss/exception@1.2.2':
resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==}
'@posthog/cli@0.5.12':
resolution: {integrity: sha512-woMmA4ChCzpJUa939SKfc11bYDNSkqHvhcvPn+EarV1ivIkmBu9gBOAZW1S/MfH76LCJE7tesuR0EOxo0zYjvw==}
engines: {node: '>=14', npm: '>=6'}
hasBin: true
'@posthog/core@1.5.2':
resolution: {integrity: sha512-iedUP3EnOPPxTA2VaIrsrd29lSZnUV+ZrMnvY56timRVeZAXoYCkmjfIs3KBAsF8OUT5h1GXLSkoQdrV0r31OQ==}
'@posthog/nextjs-config@1.4.0':
resolution: {integrity: sha512-Ar2M6IXv+5QZ8uaeVHHxJyghvapRSnHzZHpz5TKAhysASikMMwg2L9hGmeNhCxgzgljyopyyda96DdL0j742xA==}
engines: {node: '>=20'}
peerDependencies:
next: '>12.1.0'
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -1778,17 +1804,46 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
aria-hidden@1.2.6:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
attr-accept@2.2.5:
resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==}
engines: {node: '>=4'}
axios-proxy-builder@0.1.2:
resolution: {integrity: sha512-6uBVsBZzkB3tCC8iyx59mCjQckhB8+GQrI9Cop8eC7ybIsvs/KtnNgEBfRMSEa7GqK2VBGUzgjNYMdPIfotyPA==}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
blake3-wasm@2.1.5:
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
caniuse-lite@1.0.30001726:
resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==}
@@ -1801,6 +1856,10 @@ packages:
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -1825,6 +1884,14 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
console.table@0.10.0:
resolution: {integrity: sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==}
engines: {node: '> 0.10'}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
@@ -1895,6 +1962,13 @@ packages:
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1902,6 +1976,16 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
easy-table@1.1.0:
resolution: {integrity: sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==}
embla-carousel-react@8.6.0:
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
peerDependencies:
@@ -1915,6 +1999,12 @@ packages:
embla-carousel@8.6.0:
resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
enhanced-resolve@5.18.3:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
@@ -1922,6 +2012,22 @@ packages:
error-stack-parser-es@1.0.5:
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
es-toolkit@1.42.0:
resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==}
@@ -1944,6 +2050,23 @@ packages:
resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
engines: {node: '>= 12'}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
framer-motion@12.23.24:
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
peerDependencies:
@@ -1963,16 +2086,48 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22}
hasBin: true
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
immer@10.2.0:
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
@@ -1989,9 +2144,17 @@ packages:
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
@@ -2077,6 +2240,10 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lru-cache@11.2.2:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
engines: {node: 20 || >=22}
lucide-react@0.553.0:
resolution: {integrity: sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw==}
peerDependencies:
@@ -2085,6 +2252,18 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime@3.0.0:
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
engines: {node: '>=10.0.0'}
@@ -2095,6 +2274,14 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
minimatch@10.1.1:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
motion-dom@12.23.23:
resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
@@ -2151,10 +2338,17 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-scurry@2.0.1:
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
engines: {node: 20 || >=22}
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
@@ -2188,6 +2382,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
radix-ui@1.4.3:
resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==}
peerDependencies:
@@ -2301,6 +2498,11 @@ packages:
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
rimraf@6.1.0:
resolution: {integrity: sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==}
engines: {node: 20 || >=22}
hasBin: true
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -2325,6 +2527,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
@@ -2342,6 +2548,22 @@ packages:
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
engines: {node: '>=4', npm: '>=6'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.2:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -2380,6 +2602,10 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tunnel@0.0.6:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
@@ -2432,6 +2658,9 @@ packages:
victory-vendor@37.3.6:
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
@@ -2455,6 +2684,14 @@ packages:
'@cloudflare/workers-types':
optional: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
@@ -2823,6 +3060,21 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@jridgewell/gen-mapping@0.3.12':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -2885,10 +3137,29 @@ snapshots:
'@poppinss/exception@1.2.2': {}
'@posthog/cli@0.5.12':
dependencies:
axios: 1.13.2
axios-proxy-builder: 0.1.2
console.table: 0.10.0
detect-libc: 2.1.2
rimraf: 6.1.0
transitivePeerDependencies:
- debug
'@posthog/core@1.5.2':
dependencies:
cross-spawn: 7.0.6
'@posthog/nextjs-config@1.4.0(next@16.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))':
dependencies:
'@posthog/cli': 0.5.12
'@posthog/core': 1.5.2
next: 16.0.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
semver: 7.7.3
transitivePeerDependencies:
- debug
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
@@ -3850,14 +4121,43 @@ snapshots:
acorn@8.14.0: {}
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.3: {}
aria-hidden@1.2.6:
dependencies:
tslib: 2.8.1
asynckit@0.4.0: {}
attr-accept@2.2.5: {}
axios-proxy-builder@0.1.2:
dependencies:
tunnel: 0.0.6
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
blake3-wasm@2.1.5: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
caniuse-lite@1.0.30001726: {}
canvas-confetti@1.9.3: {}
@@ -3868,6 +4168,9 @@ snapshots:
client-only@0.0.1: {}
clone@1.0.4:
optional: true
clsx@2.1.1: {}
cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.5))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
@@ -3898,6 +4201,14 @@ snapshots:
color-convert: 2.0.1
color-string: 1.9.1
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
console.table@0.10.0:
dependencies:
easy-table: 1.1.0
cookie@1.0.2: {}
core-js@3.41.0: {}
@@ -3956,10 +4267,29 @@ snapshots:
decimal.js-light@2.5.1: {}
defaults@1.0.4:
dependencies:
clone: 1.0.4
optional: true
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
eastasianwidth@0.2.0: {}
easy-table@1.1.0:
optionalDependencies:
wcwidth: 1.0.1
embla-carousel-react@8.6.0(react@19.2.0):
dependencies:
embla-carousel: 8.6.0
@@ -3972,6 +4302,10 @@ snapshots:
embla-carousel@8.6.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
@@ -3979,6 +4313,21 @@ snapshots:
error-stack-parser-es@1.0.5: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
es-toolkit@1.42.0: {}
esbuild@0.25.4:
@@ -4019,6 +4368,21 @@ snapshots:
dependencies:
tslib: 2.8.1
follow-redirects@1.15.11: {}
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
motion-dom: 12.23.23
@@ -4031,12 +4395,53 @@ snapshots:
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-nonce@1.0.1: {}
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
glob-to-regexp@0.4.1: {}
glob@11.1.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.1.1
minimatch: 10.1.1
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.1
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
immer@10.2.0: {}
input-otp@1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
@@ -4048,8 +4453,14 @@ snapshots:
is-arrayish@0.3.2: {}
is-fullwidth-code-point@3.0.0: {}
isexe@2.0.0: {}
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
jiti@2.6.1: {}
js-tokens@4.0.0: {}
@@ -4109,6 +4520,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
lru-cache@11.2.2: {}
lucide-react@0.553.0(react@19.2.0):
dependencies:
react: 19.2.0
@@ -4117,6 +4530,14 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime@3.0.0: {}
miniflare@4.20251109.0:
@@ -4137,6 +4558,12 @@ snapshots:
- bufferutil
- utf-8-validate
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
minipass@7.1.2: {}
motion-dom@12.23.23:
dependencies:
motion-utils: 12.23.6
@@ -4183,8 +4610,15 @@ snapshots:
object-assign@4.1.1: {}
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
path-scurry@2.0.1:
dependencies:
lru-cache: 11.2.2
minipass: 7.1.2
path-to-regexp@6.3.0: {}
pathe@2.0.3: {}
@@ -4225,6 +4659,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
proxy-from-env@1.1.0: {}
radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.5))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -4386,6 +4822,11 @@ snapshots:
reselect@5.1.1: {}
rimraf@6.1.0:
dependencies:
glob: 11.1.0
package-json-from-dist: 1.0.1
scheduler@0.27.0: {}
semver@7.7.3: {}
@@ -4454,6 +4895,8 @@ snapshots:
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
@@ -4467,6 +4910,26 @@ snapshots:
stoppable@1.1.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.2
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.2:
dependencies:
ansi-regex: 6.2.2
styled-jsx@5.1.6(react@19.2.0):
dependencies:
client-only: 0.0.1
@@ -4488,6 +4951,8 @@ snapshots:
tslib@2.8.1: {}
tunnel@0.0.6: {}
tw-animate-css@1.4.0: {}
typescript@5.9.3: {}
@@ -4545,6 +5010,11 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
wcwidth@1.0.1:
dependencies:
defaults: 1.0.4
optional: true
web-vitals@4.2.4: {}
which@2.0.2:
@@ -4575,6 +5045,18 @@ snapshots:
- bufferutil
- utf-8-validate
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.3
string-width: 5.1.2
strip-ansi: 7.1.2
ws@8.18.0: {}
youch-core@0.3.3:

View File

@@ -8,15 +8,6 @@ import { getCommunityGalleryRecord, getCommunitySubmissionByName, getCommunitySu
export const dynamicParams = false
export const revalidate = 21600 // 6 hours
export async function generateStaticParams() {
const icons = await getCommunitySubmissions()
return icons
.filter((icon) => icon.name)
.map((icon) => ({
icon: icon.name,
}))
}
type Props = {
params: Promise<{ icon: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
@@ -49,8 +40,10 @@ export async function generateMetadata({ params }: Props, _parent: ResolvingMeta
.join(" ")
const mainIconUrl =
typeof iconData.data.base === "string" && iconData.data.base.startsWith("http") ? iconData.data.base : `${BASE_URL}/svg/${icon}.svg`
typeof iconData.data.base === "string" &&
iconData.data.base.startsWith("http")
? iconData.data.base
: (iconData.data as any).mainIconUrl || `${BASE_URL}/svg/${icon}.svg`;
return {
title: `${formattedIconName} Icon (Community) | Dashboard Icons`,
description: `Download the ${formattedIconName} community-submitted icon. Part of a collection of ${totalIcons} community icons awaiting review and addition to the Dashboard Icons collection.`,
@@ -86,26 +79,16 @@ export async function generateMetadata({ params }: Props, _parent: ResolvingMeta
type: "website",
url: pageUrl,
siteName: "Dashboard Icons",
images: [
{
url: mainIconUrl,
width: 512,
height: 512,
alt: `${formattedIconName} icon`,
type: typeof mainIconUrl === "string" && mainIconUrl.endsWith(".png") ? "image/png" : typeof mainIconUrl === "string" && mainIconUrl.endsWith(".svg") ? "image/svg+xml" : "image/png",
},
],
},
twitter: {
card: "summary_large_image",
title: `${formattedIconName} Icon (Community) | Dashboard Icons`,
description: `Download the ${formattedIconName} community-submitted icon. Part of a collection of ${totalIcons} community icons awaiting review and addition to the Dashboard Icons collection.`,
images: [mainIconUrl],
},
alternates: {
canonical: `${WEB_URL}/community/${icon}`,
},
}
};
}
export default async function CommunityIconPage({ params }: { params: Promise<{ icon: string }> }) {

View File

@@ -38,8 +38,7 @@ export async function generateMetadata({ params, searchParams }: Props, _parent:
const formattedIconName = icon
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
.join(" ");
return {
title: `${formattedIconName} Icon | Dashboard Icons`,
description: `Download the ${formattedIconName} icon in SVG, PNG, and WEBP formats for FREE. Part of a collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`,

View File

@@ -187,7 +187,41 @@ export function AdvancedIconSubmissionFormTanStack() {
extras: extras,
}
await pb.collection("submissions").create(submissionData)
const record = await pb.collection("submissions").create(submissionData)
// Update extras with real filenames from PocketBase response
// PocketBase sanitizes and renames files, so we need to update our references
if (record.assets && record.assets.length > 0) {
const updatedExtras = JSON.parse(JSON.stringify(extras))
let assetIndex = 0
// Skip base icon (first asset) as we track it by 'base' format string only
assetIndex++
if (value.files.dark?.[0] && updatedExtras.colors) {
updatedExtras.colors.dark = record.assets[assetIndex]
assetIndex++
}
if (value.files.light?.[0] && updatedExtras.colors) {
updatedExtras.colors.light = record.assets[assetIndex]
assetIndex++
}
if (value.files.wordmark?.[0] && updatedExtras.wordmark) {
updatedExtras.wordmark.light = record.assets[assetIndex]
assetIndex++
}
if (value.files.wordmark_dark?.[0] && updatedExtras.wordmark) {
updatedExtras.wordmark.dark = record.assets[assetIndex]
assetIndex++
}
await pb.collection("submissions").update(record.id, {
extras: updatedExtras,
})
}
// Revalidate Next.js cache for community pages
await revalidateAllSubmissions()

View File

@@ -20,6 +20,7 @@ export function IconCard({ name, data: iconData, matchedAlias }: { name: string;
src={imageUrl}
alt={`${name} icon`}
fill
sizes="32px 32px"
className="object-contain p-1 group-hover:scale-110 transition-transform duration-300"
/>
</div>

View File

@@ -140,29 +140,67 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
const mainIconUrl = (iconData as any).mainIconUrl || (isCommunityIcon ? iconData.base : null)
const assetUrls = (iconData as any).assetUrls || []
const getAvailableFormats = () => {
const shouldShowBaseIcon = () => {
if (!iconData.colors) return true;
// For regular icons, check if base icon name matches any variant name
if (!isCommunityIcon) {
const darkIconName = iconData.colors.dark;
const lightIconName = iconData.colors.light;
// Don't show base icon if it's the same as dark or light variant
if (icon === darkIconName || icon === lightIconName) {
return false;
}
}
// For community icons, check if base icon matches any variant
if (isCommunityIcon && mainIconUrl && assetUrls.length > 0) {
// Find the actual URLs for dark and light variants
const darkFilename = iconData.colors.dark;
const lightFilename = iconData.colors.light;
const darkUrl = darkFilename
? assetUrls.find((url: string) => url.includes(darkFilename))
: null;
const lightUrl = lightFilename
? assetUrls.find((url: string) => url.includes(lightFilename))
: null;
// Don't show base icon if it's the same as dark or light variant
if (mainIconUrl === darkUrl || mainIconUrl === lightUrl) {
return false;
}
}
return true;
};
const getAvailableFormats = (): string[] => {
if (isCommunityIcon) {
if (assetUrls.length > 0) {
return assetUrls.map((url: string) => {
const ext = url.split(".").pop()?.toLowerCase() || "svg"
return ext === "svg" ? "svg" : ext === "png" ? "png" : "webp"
})
const formats = assetUrls.map((url: string) => {
const ext = url.split(".").pop()?.toLowerCase() || "svg";
return ext === "svg" ? "svg" : ext === "png" ? "png" : "webp";
});
// Deduplicate formats to avoid duplicate keys
return Array.from(new Set(formats));
}
if (mainIconUrl) {
const ext = mainIconUrl.split(".").pop()?.toLowerCase() || "svg"
return [ext === "svg" ? "svg" : ext === "png" ? "png" : "webp"]
const ext = mainIconUrl.split(".").pop()?.toLowerCase() || "svg";
return [ext === "svg" ? "svg" : ext === "png" ? "png" : "webp"];
}
return ["svg"]
return ["svg"];
}
switch (iconData.base) {
case "svg":
return ["svg", "png", "webp"]
return ["svg", "png", "webp"];
case "png":
return ["png", "webp"]
return ["png", "webp"];
default:
return [iconData.base]
return [String(iconData.base)];
}
}
};
const availableFormats = getAvailableFormats()
const [copiedVariants, _setCopiedVariants] = useState<Record<string, boolean>>({})
@@ -324,7 +362,29 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
if (isCommunityIcon && mainIconUrl) {
const formatExt = format === "svg" ? "svg" : format === "png" ? "png" : "webp"
const matchingUrl = assetUrls.find((url: string) => url.toLowerCase().endsWith(`.${formatExt}`))
// Try to find a specific asset URL that matches the requested variant filename
// This is important because assetUrls contains all variants (base, dark, light)
let matchingUrl: string | undefined;
if (theme && iconName && iconName !== icon) {
// If a theme is specified, iconName holds the specific filename for that variant
matchingUrl = assetUrls.find(
(url: string) =>
url.includes(iconName) &&
url.toLowerCase().endsWith(`.${formatExt}`),
);
}
if (!matchingUrl) {
// Fallback: find any asset with the matching extension
matchingUrl = assetUrls.find((url: string) =>
url.toLowerCase().endsWith(`.${formatExt}`),
);
}
const variantKey = `${format}-${theme || "default"}`;
imageUrl = matchingUrl || mainIconUrl
githubUrl = ""
} else {
@@ -376,6 +436,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
src={imageUrl}
alt={`${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading="eager"
priority
className="object-contain p-4"
@@ -404,7 +465,7 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
</div>
</MagicCard>
</TooltipProvider>
)
);
}
const formatedIconName = formatIconName(icon)
@@ -442,15 +503,25 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
<div className="space-y-2">
<div className="flex items-center gap-2">
<p className="text-sm">
<span className="font-medium">Updated on:</span> <time dateTime={iconData.update.timestamp}>{formattedDate}</time>
<span className="font-medium">Updated on:</span>{" "}
<time dateTime={iconData.update.timestamp}>
{formattedDate}
</time>
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">By:</p>
<Avatar className="h-5 w-5 border">
<AvatarImage src={authorData.avatar_url} alt={`${authorName}'s avatar`} />
<AvatarFallback>{authorName ? authorName.slice(0, 2).toUpperCase() : "??"}</AvatarFallback>
<AvatarImage
src={authorData.avatar_url}
alt={`${authorName}'s avatar`}
/>
<AvatarFallback>
{authorName
? authorName.slice(0, 2).toUpperCase()
: "??"}
</AvatarFallback>
</Avatar>
{authorData.html_url && (
<Link
@@ -462,7 +533,9 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
{authorName}
</Link>
)}
{!authorData.html_url && <span className="text-sm">{authorName}</span>}
{!authorData.html_url && (
<span className="text-sm">{authorName}</span>
)}
</div>
</div>
</div>
@@ -470,17 +543,26 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
{iconData.categories && iconData.categories.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Categories</h3>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
Categories
</h3>
<div className="flex flex-wrap gap-2">
{iconData.categories.map((category) => (
<Link key={category} href={`/icons?category=${encodeURIComponent(category)}`} className="cursor-pointer">
<Link
key={category}
href={`/icons?category=${encodeURIComponent(category)}`}
className="cursor-pointer"
>
<Badge
variant="outline"
className="inline-flex items-center border border-primary/20 hover:border-primary px-2.5 py-0.5 text-sm"
>
{category
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.map(
(word) =>
word.charAt(0).toUpperCase() + word.slice(1),
)
.join(" ")}
</Badge>
</Link>
@@ -491,7 +573,9 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
{iconData.aliases && iconData.aliases.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Aliases</h3>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
Aliases
</h3>
<div className="flex flex-wrap gap-2">
{iconData.aliases.map((alias) => (
<Badge
@@ -508,20 +592,25 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
)}
<div>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">About this icon</h3>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
About this icon
</h3>
<div className="text-xs text-muted-foreground space-y-2">
<p>
Available in{" "}
{availableFormats.length > 1
? `${availableFormats.length} formats (${availableFormats.map((f: string) => f.toUpperCase()).join(", ")}) `
? `${availableFormats.length} formats (${availableFormats.map((f) => f.toUpperCase()).join(", ")}) `
: `${availableFormats[0].toUpperCase()} format `}
with a base format of {iconData.base.toUpperCase()}.
{iconData.colors && " Includes both light and dark theme variants for better integration with different UI designs."}
{iconData.wordmark && " Wordmark variants are also available for enhanced branding options."}
{iconData.colors &&
" Includes both light and dark theme variants for better integration with different UI designs."}
{iconData.wordmark &&
" Wordmark variants are also available for enhanced branding options."}
</p>
<p>
Perfect for adding to dashboards, app directories, documentation, or anywhere you need the {formatIconName(icon)}{" "}
logo.
Perfect for adding to dashboards, app directories,
documentation, or anywhere you need the{" "}
{formatIconName(icon)} logo.
</p>
</div>
</div>
@@ -536,14 +625,16 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
<CardTitle>
<h2>Icon variants</h2>
</CardTitle>
<CardDescription>Click on any icon to copy its URL to your clipboard</CardDescription>
<CardDescription>
Click on any icon to copy its URL to your clipboard
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-10">
{!iconData.colors && (
{shouldShowBaseIcon() && (
<IconVariantsSection
title="Default"
description="Standard icon versions. Click to copy URL."
title="Base Icon"
description="Original icon version. Click to copy URL."
iconElement={<FileType className="w-4 h-4 text-blue-500" />}
aavailableFormats={availableFormats}
icon={icon}
@@ -555,36 +646,36 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
/>
)}
{iconData.colors && (
<>
<IconVariantsSection
title="Light theme"
description="Icon variants optimized for light backgrounds (typically lighter icon colors). Click to copy URL."
iconElement={<Sun className="w-4 h-4 text-amber-500" />}
aavailableFormats={availableFormats}
icon={icon}
theme="light"
iconData={iconData}
handleCopy={handleCopyUrl}
handleDownload={handleDownload}
copiedVariants={copiedVariants}
renderVariant={renderVariant}
/>
{iconData.colors?.light && (
<IconVariantsSection
title="Light theme"
description="Icon variants optimized for light backgrounds (typically lighter icon colors). Click to copy URL."
iconElement={<Sun className="w-4 h-4 text-amber-500" />}
aavailableFormats={availableFormats}
icon={icon}
theme="light"
iconData={iconData}
handleCopy={handleCopyUrl}
handleDownload={handleDownload}
copiedVariants={copiedVariants}
renderVariant={renderVariant}
/>
)}
<IconVariantsSection
title="Dark theme"
description="Icon variants optimized for dark backgrounds (typically darker icon colors). Click to copy URL."
iconElement={<Moon className="w-4 h-4 text-indigo-500" />}
aavailableFormats={availableFormats}
icon={icon}
theme="dark"
iconData={iconData}
handleCopy={handleCopyUrl}
handleDownload={handleDownload}
copiedVariants={copiedVariants}
renderVariant={renderVariant}
/>
</>
{iconData.colors?.dark && (
<IconVariantsSection
title="Dark theme"
description="Icon variants optimized for dark backgrounds (typically darker icon colors). Click to copy URL."
iconElement={<Moon className="w-4 h-4 text-indigo-500" />}
aavailableFormats={availableFormats}
icon={icon}
theme="dark"
iconData={iconData}
handleCopy={handleCopyUrl}
handleDownload={handleDownload}
copiedVariants={copiedVariants}
renderVariant={renderVariant}
/>
)}
{iconData.wordmark && (
@@ -612,25 +703,36 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
<div className="space-y-6">
{status && statusDisplayName && statusColor && (
<div className="">
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Status</h3>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
Status
</h3>
<Badge variant="outline" className={statusColor}>
{statusDisplayName}
</Badge>
</div>
)}
<div className="">
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Base format</h3>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
Base format
</h3>
<div className="flex items-center gap-2">
<FileType className="w-4 h-4 text-blue-500" />
<div className="px-3 py-1.5 border border-border rounded-lg text-sm font-medium">{iconData.base.toUpperCase()}</div>
<div className="px-3 py-1.5 border border-border rounded-lg text-sm font-medium">
{iconData.base.toUpperCase()}
</div>
</div>
</div>
<div className="">
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Available formats</h3>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
Available formats
</h3>
<div className="flex flex-wrap gap-2">
{availableFormats.map((format: string) => (
<div key={format} className="px-3 py-1.5 border border-border rounded-lg text-xs font-medium">
<div
key={format}
className="px-3 py-1.5 border border-border rounded-lg text-xs font-medium"
>
{format.toUpperCase()}
</div>
))}
@@ -639,35 +741,53 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
{iconData.colors && (
<div className="">
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Color variants</h3>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
Color variants
</h3>
<div className="space-y-2">
{Object.entries(iconData.colors).map(([theme, variant]) => (
<div key={theme} className="flex items-center gap-2">
<PaletteIcon className="w-4 h-4 text-purple-500" />
<span className="capitalize font-medium text-sm">{theme}:</span>
<code className=" border border-border px-2 py-0.5 rounded-lg text-xs">{variant}</code>
</div>
))}
{Object.entries(iconData.colors).map(
([theme, variant]) => (
<div key={theme} className="flex items-center gap-2">
<PaletteIcon className="w-4 h-4 text-purple-500" />
<span className="capitalize font-medium text-sm">
{theme}:
</span>
<code className=" border border-border px-2 py-0.5 rounded-lg text-xs">
{variant}
</code>
</div>
),
)}
</div>
</div>
)}
{iconData.wordmark && (
<div className="">
<h3 className="text-sm font-semibold text-muted-foreground">Wordmark variants</h3>
<h3 className="text-sm font-semibold text-muted-foreground">
Wordmark variants
</h3>
<div className="space-y-2">
{iconData.wordmark.light && (
<div className="flex items-center gap-2">
<Type className="w-4 h-4 text-green-500" />
<span className="capitalize font-medium text-sm">Light:</span>
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">{iconData.wordmark.light}</code>
<span className="capitalize font-medium text-sm">
Light:
</span>
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">
{iconData.wordmark.light}
</code>
</div>
)}
{iconData.wordmark.dark && (
<div className="flex items-center gap-2">
<Type className="w-4 h-4 text-green-500" />
<span className="capitalize font-medium text-sm">Dark:</span>
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">{iconData.wordmark.dark}</code>
<span className="capitalize font-medium text-sm">
Dark:
</span>
<code className="border border-border px-2 py-0.5 rounded-lg text-xs">
{iconData.wordmark.dark}
</code>
</div>
)}
</div>
@@ -676,9 +796,15 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
{!isCommunityIcon && (
<div className="">
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Source</h3>
<h3 className="text-sm font-semibold text-muted-foreground mb-2">
Source
</h3>
<Button variant="outline" className="w-full" asChild>
<Link href={`${REPO_PATH}/blob/main/meta/${icon}.json`} target="_blank" rel="noopener noreferrer">
<Link
href={`${REPO_PATH}/blob/main/meta/${icon}.json`}
target="_blank"
rel="noopener noreferrer"
>
<Github className="w-4 h-4 mr-2" />
View on GitHub
</Link>
@@ -694,30 +820,41 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
{iconData.categories &&
iconData.categories.length > 0 &&
(() => {
const MAX_RELATED_ICONS = 16
const currentCategories = iconData.categories || []
const MAX_RELATED_ICONS = 16;
const currentCategories = iconData.categories || [];
const relatedIconsWithScore = Object.entries(allIcons)
.map(([name, data]) => {
if (name === icon) return null // Exclude the current icon
if (name === icon) return null; // Exclude the current icon
const otherCategories = data.categories || []
const commonCategories = currentCategories.filter((cat) => otherCategories.includes(cat))
const score = commonCategories.length
const otherCategories = data.categories || [];
const commonCategories = currentCategories.filter((cat) =>
otherCategories.includes(cat),
);
const score = commonCategories.length;
return score > 0 ? { name, data, score } : null
return score > 0 ? { name, data, score } : null;
})
.filter((item): item is { name: string; data: Icon; score: number } => item !== null) // Type guard
.sort((a, b) => b.score - a.score) // Sort by score DESC
.filter(
(item): item is { name: string; data: Icon; score: number } =>
item !== null,
) // Type guard
.sort((a, b) => b.score - a.score); // Sort by score DESC
const topRelatedIcons = relatedIconsWithScore.slice(0, MAX_RELATED_ICONS)
const topRelatedIcons = relatedIconsWithScore.slice(
0,
MAX_RELATED_ICONS,
);
const viewMoreUrl = `/icons?${currentCategories.map((cat) => `category=${encodeURIComponent(cat)}`).join("&")}`
const viewMoreUrl = `/icons?${currentCategories.map((cat) => `category=${encodeURIComponent(cat)}`).join("&")}`;
if (topRelatedIcons.length === 0) return null
if (topRelatedIcons.length === 0) return null;
return (
<section className="container mx-auto mt-12" aria-labelledby="related-icons-title">
<section
className="container mx-auto mt-12"
aria-labelledby="related-icons-title"
>
<Card className="bg-background/50 border shadow-lg">
<CardHeader>
<CardTitle>
@@ -725,11 +862,18 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
<h2 id="related-icons-title">Related Icons</h2>
</CardTitle>
<CardDescription>
Other icons from {currentCategories.map((cat) => cat.replace(/-/g, " ")).join(", ")} categories
Other icons from{" "}
{currentCategories
.map((cat) => cat.replace(/-/g, " "))
.join(", ")}{" "}
categories
</CardDescription>
</CardHeader>
<CardContent>
<IconsGrid filteredIcons={topRelatedIcons} matchedAliases={{}} />
<IconsGrid
filteredIcons={topRelatedIcons}
matchedAliases={{}}
/>
{relatedIconsWithScore.length > MAX_RELATED_ICONS && (
<div className="mt-6 text-center">
<Button
@@ -747,8 +891,8 @@ export function IconDetails({ icon, iconData, authorData, allIcons, status, stat
</CardContent>
</Card>
</section>
)
);
})()}
</main>
)
);
}

View File

@@ -1,6 +1,6 @@
"use client"
import { Calendar, Check, Download, ExternalLink, FileType, FolderOpen, Palette, Tag, User as UserIcon, X } from "lucide-react"
import { Calendar, Check, Download, ExternalLink, FileType, FolderOpen, Palette, Tag, User as UserIcon, X, Eye } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { IconCard } from "@/components/icon-card"
@@ -193,40 +193,48 @@ export function SubmissionDetails({
<Tag className="w-5 h-5" />
Submission Details
</CardTitle>
{(onApprove || onReject) && (
<div className="flex gap-2">
{onApprove && (
<Button
size="sm"
color="green"
variant="outline"
onClick={(e) => {
e.stopPropagation()
onApprove()
}}
disabled={isApproving || isRejecting}
>
<Check className="w-4 h-4 mr-2" />
{isApproving ? "Approving..." : "Approve"}
</Button>
)}
{onReject && (
<Button
size="sm"
color="red"
variant="destructive"
onClick={(e) => {
e.stopPropagation()
onReject()
}}
disabled={isApproving || isRejecting}
>
<X className="w-4 h-4 mr-2" />
{isRejecting ? "Rejecting..." : "Reject"}
</Button>
)}
</div>
)}
<div className="flex gap-2">
<Button asChild size="sm" variant="outline">
<Link href={`/community/${submission.name}`} target="_blank">
<Eye className="w-4 h-4 mr-2" />
Preview
</Link>
</Button>
{(onApprove || onReject) && (
<>
{onApprove && (
<Button
size="sm"
color="green"
variant="outline"
onClick={(e) => {
e.stopPropagation()
onApprove()
}}
disabled={isApproving || isRejecting}
>
<Check className="w-4 h-4 mr-2" />
{isApproving ? "Approving..." : "Approve"}
</Button>
)}
{onReject && (
<Button
size="sm"
color="red"
variant="destructive"
onClick={(e) => {
e.stopPropagation()
onReject()
}}
disabled={isApproving || isRejecting}
>
<X className="w-4 h-4 mr-2" />
{isRejecting ? "Rejecting..." : "Reject"}
</Button>
)}
</>
)}
</div>
</div>
</CardHeader>
<CardContent className="pt-0">

View File

@@ -16,6 +16,42 @@ function createServerPB() {
return new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090")
}
/**
* Helper to find the best matching asset filename for a given original filename
* PocketBase sanitizes filenames and appends a random suffix.
* This function attempts to map the stored original filename (in extras) to the actual sanitized filename (in assets).
*/
function findBestMatchingAsset(originalName: string, assets: string[]): string {
if (!originalName || !assets || assets.length === 0) return originalName
// 1. Exact match
if (assets.includes(originalName)) return originalName
// 2. Normalized match
// Normalize: remove non-alphanumeric, lowercase
const normalize = (s: string) => s.replace(/[^a-z0-9]/gi, "").toLowerCase()
// Remove extension for comparison
const originalBase = originalName.substring(0, originalName.lastIndexOf(".")) || originalName
const normalizedOriginal = normalize(originalBase)
// Check against assets
for (const asset of assets) {
const assetBase = asset.substring(0, asset.lastIndexOf(".")) || asset
const normalizedAsset = normalize(assetBase)
// Check if normalized asset STARTS with normalized original
// PocketBase usually appends `_` + random chars, which normalize removes or appends to end
// "langsmith (2)" -> "langsmith2"
// "langsmith_2_8tf..." -> "langsmith28tf..."
if (normalizedAsset.startsWith(normalizedOriginal)) {
return asset
}
}
return originalName // Fallback to original if no match found
}
/**
* Transform a CommunityGallery item to IconWithName format for use with IconSearch
* For community icons, base is the full HTTP URL to the main icon asset
@@ -29,6 +65,27 @@ function transformGalleryToIcon(item: CommunityGallery): any {
const mainAssetExt = item.assets?.[0]?.split(".").pop()?.toLowerCase() || "svg"
const baseFormat = mainAssetExt === "svg" ? "svg" : mainAssetExt === "png" ? "png" : "webp"
// Process and fix file mappings in extras
const colors = item.extras?.colors ? { ...item.extras.colors } : undefined
if (colors && item.assets) {
Object.keys(colors).forEach((key) => {
const k = key as keyof typeof colors
if (colors[k]) {
colors[k] = findBestMatchingAsset(colors[k]!, item.assets || [])
}
})
}
const wordmark = item.extras?.wordmark ? { ...item.extras.wordmark } : undefined
if (wordmark && item.assets) {
Object.keys(wordmark).forEach((key) => {
const k = key as keyof typeof wordmark
if (wordmark[k]) {
wordmark[k] = findBestMatchingAsset(wordmark[k]!, item.assets || [])
}
})
}
const transformed = {
name: item.name,
status: item.status,
@@ -46,8 +103,8 @@ function transformGalleryToIcon(item: CommunityGallery): any {
name: item.created_by || "Community",
},
},
colors: item.extras?.colors,
wordmark: item.extras?.wordmark,
colors: colors,
wordmark: wordmark,
},
}
@@ -81,10 +138,14 @@ async function fetchCommunitySubmissions(): Promise<IconWithName[]> {
* Revalidates every 21600 seconds (6 hours) to match page revalidate time
* Can be invalidated on-demand using revalidateTag("community-gallery")
*/
export const getCommunitySubmissions = unstable_cache(fetchCommunitySubmissions, ["community-submissions-list"], {
revalidate: 21600,
tags: ["community-gallery"],
})
export const getCommunitySubmissions = unstable_cache(
fetchCommunitySubmissions,
["community-submissions-list-v2"],
{
revalidate: 21600,
tags: ["community-gallery"],
},
);
/**
* Fetch a single community submission by name (raw function)
@@ -92,10 +153,17 @@ export const getCommunitySubmissions = unstable_cache(fetchCommunitySubmissions,
*/
async function fetchCommunitySubmissionByName(name: string): Promise<IconWithName | null> {
try {
const pb = createServerPB()
const pb = createServerPB();
const record = await pb.collection("community_gallery").getFirstListItem<CommunityGallery>(`name="${name}"`)
return transformGalleryToIcon(record)
const record = await pb
.collection("community_gallery")
.getFirstListItem<CommunityGallery>(`name="${name}"`);
const transformed = transformGalleryToIcon(record);
console.log(
`[Community] Fetched ${name}, colors:`,
transformed.data.colors,
);
return transformed;
} catch (error) {
console.error(`Error fetching community submission ${name}:`, error)
return null
@@ -109,10 +177,14 @@ async function fetchCommunitySubmissionByName(name: string): Promise<IconWithNam
* Cache key: community-submission-{name}
*/
export function getCommunitySubmissionByName(name: string): Promise<IconWithName | null> {
return unstable_cache(async () => fetchCommunitySubmissionByName(name), [`community-submission-${name}`], {
revalidate: 21600,
tags: ["community-gallery", "community-submission"],
})()
return unstable_cache(
async () => fetchCommunitySubmissionByName(name),
[`community-submission-${name}-v2`],
{
revalidate: 21600,
tags: ["community-gallery", "community-submission"],
},
)();
}
/**