mirror of
https://github.com/walkxcode/dashboard-icons.git
synced 2025-10-26 21:19:04 +08:00
Compare commits
9 Commits
refactor/c
...
feat/struc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee297afbf4 | ||
|
|
038e4dc73d | ||
|
|
6001d195a6 | ||
|
|
40482771fa | ||
|
|
34fef44222 | ||
|
|
3e2709e7a8 | ||
|
|
245033befc | ||
|
|
9949f663eb | ||
|
|
a579d41f45 |
@@ -1,5 +1,5 @@
|
|||||||
name: "Add light/dark icon"
|
name: "Add light & dark icon"
|
||||||
description: Submit a new icon with light and dark versions.
|
description: Use this template to add a new icon to the project. Monochrome icons need both light and dark versions.
|
||||||
title: "feat(icons): add [NAME]"
|
title: "feat(icons): add [NAME]"
|
||||||
labels: ["monochrome-icon"]
|
labels: ["monochrome-icon"]
|
||||||
body:
|
body:
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/add_normal_icon.yml
vendored
7
.github/ISSUE_TEMPLATE/add_normal_icon.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: "Add standard icon"
|
name: "Add normal icon"
|
||||||
description: Submit a new icon for both light and dark themes.
|
description: Use this template to add a new icon to the project. Normal icons work for both light and dark themes.
|
||||||
title: "feat(icons): add [NAME]"
|
title: "feat(icons): add [NAME]"
|
||||||
labels: ["normal-icon"]
|
labels: ["normal-icon"]
|
||||||
body:
|
body:
|
||||||
@@ -10,16 +10,19 @@ body:
|
|||||||
Once you've submitted the issue, sombody from the team will review it, before adding a label which automatically creates a pull request with the other filetypes.
|
Once you've submitted the issue, sombody from the team will review it, before adding a label which automatically creates a pull request with the other filetypes.
|
||||||
If you submit a PNG icon, please note, that the SVG can not be generated from it.
|
If you submit a PNG icon, please note, that the SVG can not be generated from it.
|
||||||
- type: input
|
- type: input
|
||||||
|
id: name
|
||||||
attributes:
|
attributes:
|
||||||
label: Icon name
|
label: Icon name
|
||||||
description: The name has to be unique and should be kebab-case.
|
description: The name has to be unique and should be kebab-case.
|
||||||
placeholder: e.g. "icon-name"
|
placeholder: e.g. "icon-name"
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
id: icon
|
||||||
attributes:
|
attributes:
|
||||||
label: Paste icon
|
label: Paste icon
|
||||||
description: |
|
description: |
|
||||||
Please paste the icon here. It will automatically upload it to github.
|
Please paste the icon here. It will automatically upload it to github.
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
id: type
|
||||||
attributes:
|
attributes:
|
||||||
label: Icon type
|
label: Icon type
|
||||||
options:
|
options:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: "Update light/dark icon"
|
name: "Update light & dark icon"
|
||||||
description: Improve or update an existing light/dark icon.
|
description: Use this template to update an existing icon. Monochrome icons need both light and dark versions.
|
||||||
title: "feat(icons): update [NAME]"
|
title: "feat(icons): update [NAME]"
|
||||||
labels: ["monochrome-icon"]
|
labels: ["monochrome-icon"]
|
||||||
body:
|
body:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: "Update standard icon"
|
name: "Update normal icon"
|
||||||
description: Improve or update an existing standard icon.
|
description: Use this template to update an existing icon. Normal icons work for both light and dark themes.
|
||||||
title: "feat(icons): update [NAME]"
|
title: "feat(icons): update [NAME]"
|
||||||
labels: ["normal-icon"]
|
labels: ["normal-icon"]
|
||||||
body:
|
body:
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
"recommended": true,
|
"recommended": true,
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noArrayIndexKey": "off"
|
"noArrayIndexKey": "off"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"noDangerouslySetInnerHtml": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^12.7.3",
|
"framer-motion": "^12.7.3",
|
||||||
|
|||||||
249
web/pnpm-lock.yaml
generated
249
web/pnpm-lock.yaml
generated
@@ -101,6 +101,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
cmdk:
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@@ -157,7 +160,7 @@ importers:
|
|||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
tailwindcss-motion:
|
tailwindcss-motion:
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(tailwindcss@4.1.4)
|
version: 1.1.0(tailwindcss@4.1.3)
|
||||||
tw-animate-css:
|
tw-animate-css:
|
||||||
specifier: ^1.2.5
|
specifier: ^1.2.5
|
||||||
version: 1.2.5
|
version: 1.2.5
|
||||||
@@ -173,13 +176,13 @@ importers:
|
|||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.3
|
specifier: ^4.1.3
|
||||||
version: 4.1.4
|
version: 4.1.3
|
||||||
'@types/canvas-confetti':
|
'@types/canvas-confetti':
|
||||||
specifier: ^1.9.0
|
specifier: ^1.9.0
|
||||||
version: 1.9.0
|
version: 1.9.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.14.0
|
specifier: ^22.14.0
|
||||||
version: 22.14.1
|
version: 22.14.0
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
@@ -188,13 +191,13 @@ importers:
|
|||||||
version: 19.1.2(@types/react@19.1.0)
|
version: 19.1.2(@types/react@19.1.0)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.3
|
specifier: ^4.1.3
|
||||||
version: 4.1.4
|
version: 4.1.3
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.8.3
|
specifier: ^5.8.3
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
wrangler:
|
wrangler:
|
||||||
specifier: ^4.12.0
|
specifier: ^4.12.0
|
||||||
version: 4.12.1
|
version: 4.12.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -272,32 +275,32 @@ packages:
|
|||||||
workerd:
|
workerd:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-darwin-64@1.20250417.0':
|
'@cloudflare/workerd-darwin-64@1.20250416.0':
|
||||||
resolution: {integrity: sha512-4Adfl92aKepjxb8e6af2d+xpD2sBOADgHqvkyXsFmoLb80weMEDDRGJi1p1m5q1M78/oVnGcpdmuRCAathanRg==}
|
resolution: {integrity: sha512-aZgF8Swp9eVYxJPWOoZbAgAaYjWuYqGmEA+QJ2ecRGDBqm87rT4GEw7/mmLpxrpllny3VfEEhkk9iYCGv8nlFw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@cloudflare/workerd-darwin-arm64@1.20250417.0':
|
'@cloudflare/workerd-darwin-arm64@1.20250416.0':
|
||||||
resolution: {integrity: sha512-dSlk18F4i3T1OTzFBxx3pKpXRMP6w2xZ26+oIV32BFWrCi/HxGzUd6gVA0q37oLGqITRt8xU693J4Gl1CwC/Ag==}
|
resolution: {integrity: sha512-FhswG1QYRfaTZ4FAlUkfVWaoM2lrlqumiBTrhbo9czMJdGR/oBXS4SGynuI6zyhApHeBf3/fZpA/SBAe4cXdgg==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@cloudflare/workerd-linux-64@1.20250417.0':
|
'@cloudflare/workerd-linux-64@1.20250416.0':
|
||||||
resolution: {integrity: sha512-27MVzOa/lENcqewC2L9EcqstXW843UhjBMcwV1umDfsjwLyZOEv6Gtm/6j5r0L0gASvkRTam3fAmtPk/gt48TA==}
|
resolution: {integrity: sha512-G+nXEAJ/9y+A857XShwxKeRdfxok6UcjiQe6G+wQeCn/Ofkp/EWydacKdyeVU6QIm1oHS78DwJ7AzbCYywf9aw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@cloudflare/workerd-linux-arm64@1.20250417.0':
|
'@cloudflare/workerd-linux-arm64@1.20250416.0':
|
||||||
resolution: {integrity: sha512-34qBk0htAXmUneOTQxW6/g6pjNVR91r0vJzz2FID84cAIOYVl4hZLijkjmVl+MMDU6boXUs+yDwhItdg06YvAg==}
|
resolution: {integrity: sha512-U6oVW0d9w1fpnDYNrjPJ9SFkDlGJWJWbXHlTBObXl6vccP16WewvuxyHkKqyUhUc8hyBaph7sxeKzKmuCFQ4SA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@cloudflare/workerd-windows-64@1.20250417.0':
|
'@cloudflare/workerd-windows-64@1.20250416.0':
|
||||||
resolution: {integrity: sha512-PDwATFioff+geVHfgTzSWsxgwjgotrdXStb0EL0lMyMT5zNmHArAnOx83CbDtud63Uv9rVX1BAfPP4tyD1O+5A==}
|
resolution: {integrity: sha512-YAjjTzL1z9YYeN4sqYfj1dtQXd2Bblj+B+hl4Rz2aOhblpZEZAdhapZlOCRvLLkOJshKJUnRD3mDlytAdgwybQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1362,93 +1365,81 @@ packages:
|
|||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.4':
|
'@tailwindcss/node@4.1.3':
|
||||||
resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==}
|
resolution: {integrity: sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA==}
|
||||||
|
|
||||||
'@tailwindcss/oxide-android-arm64@4.1.4':
|
'@tailwindcss/oxide-android-arm64@4.1.3':
|
||||||
resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==}
|
resolution: {integrity: sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@tailwindcss/oxide-darwin-arm64@4.1.4':
|
'@tailwindcss/oxide-darwin-arm64@4.1.3':
|
||||||
resolution: {integrity: sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==}
|
resolution: {integrity: sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@tailwindcss/oxide-darwin-x64@4.1.4':
|
'@tailwindcss/oxide-darwin-x64@4.1.3':
|
||||||
resolution: {integrity: sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==}
|
resolution: {integrity: sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@tailwindcss/oxide-freebsd-x64@4.1.4':
|
'@tailwindcss/oxide-freebsd-x64@4.1.3':
|
||||||
resolution: {integrity: sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==}
|
resolution: {integrity: sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4':
|
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3':
|
||||||
resolution: {integrity: sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==}
|
resolution: {integrity: sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.4':
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.3':
|
||||||
resolution: {integrity: sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==}
|
resolution: {integrity: sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.4':
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.3':
|
||||||
resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==}
|
resolution: {integrity: sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.4':
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.3':
|
||||||
resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==}
|
resolution: {integrity: sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.1.4':
|
'@tailwindcss/oxide-linux-x64-musl@4.1.3':
|
||||||
resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==}
|
resolution: {integrity: sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.1.4':
|
'@tailwindcss/oxide-win32-arm64-msvc@4.1.3':
|
||||||
resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==}
|
resolution: {integrity: sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg==}
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
cpu: [wasm32]
|
|
||||||
bundledDependencies:
|
|
||||||
- '@napi-rs/wasm-runtime'
|
|
||||||
- '@emnapi/core'
|
|
||||||
- '@emnapi/runtime'
|
|
||||||
- '@tybys/wasm-util'
|
|
||||||
- '@emnapi/wasi-threads'
|
|
||||||
- tslib
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.4':
|
|
||||||
resolution: {integrity: sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==}
|
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.4':
|
'@tailwindcss/oxide-win32-x64-msvc@4.1.3':
|
||||||
resolution: {integrity: sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==}
|
resolution: {integrity: sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@tailwindcss/oxide@4.1.4':
|
'@tailwindcss/oxide@4.1.3':
|
||||||
resolution: {integrity: sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==}
|
resolution: {integrity: sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@tailwindcss/postcss@4.1.4':
|
'@tailwindcss/postcss@4.1.3':
|
||||||
resolution: {integrity: sha512-bjV6sqycCEa+AQSt2Kr7wpGF1bOZJ5wsqnLEkqSbM/JEHxx/yhMH8wHmdkPyApF9xhHeMSwnnkDUUMMM/hYnXw==}
|
resolution: {integrity: sha512-6s5nJODm98F++QT49qn8xJKHQRamhYHfMi3X7/ltxiSQ9dyRsaFSfFkfaMsanWzf+TMYQtbk8mt5f6cCVXJwfg==}
|
||||||
|
|
||||||
'@tanstack/react-virtual@3.13.6':
|
'@tanstack/react-virtual@3.13.6':
|
||||||
resolution: {integrity: sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==}
|
resolution: {integrity: sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==}
|
||||||
@@ -1489,8 +1480,8 @@ packages:
|
|||||||
'@types/d3-timer@3.0.2':
|
'@types/d3-timer@3.0.2':
|
||||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||||
|
|
||||||
'@types/node@22.14.1':
|
'@types/node@22.14.0':
|
||||||
resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==}
|
resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==}
|
||||||
|
|
||||||
'@types/react-dom@19.1.2':
|
'@types/react-dom@19.1.2':
|
||||||
resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==}
|
resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==}
|
||||||
@@ -1549,6 +1540,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
cmdk@1.1.1:
|
||||||
|
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -1891,8 +1888,8 @@ packages:
|
|||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
miniflare@4.20250417.0:
|
miniflare@4.20250416.0:
|
||||||
resolution: {integrity: sha512-bROKLQKr4CoS93tnGuw5e08VaNwM3VowTL3Z2Cps1HzY6a4Bq8uNtggQ7WogriMq77jcHn6kbz64bvWyF//Jkw==}
|
resolution: {integrity: sha512-261PhPgD9zs5/BTdbWqwiaXtWxb+Av5zKCwTU+HXrA5E4tf3qnULwh3u6SVUOAEArEroFuKJzawsQ9COtNBurQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -2156,8 +2153,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: '>=3.0.0 || insiders'
|
tailwindcss: '>=3.0.0 || insiders'
|
||||||
|
|
||||||
tailwindcss@4.1.4:
|
tailwindcss@4.1.3:
|
||||||
resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==}
|
resolution: {integrity: sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==}
|
||||||
|
|
||||||
tapable@2.2.1:
|
tapable@2.2.1:
|
||||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||||
@@ -2222,17 +2219,17 @@ packages:
|
|||||||
web-vitals@4.2.4:
|
web-vitals@4.2.4:
|
||||||
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
|
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
|
||||||
|
|
||||||
workerd@1.20250417.0:
|
workerd@1.20250416.0:
|
||||||
resolution: {integrity: sha512-naz6oJiVODd3/Lkp9l3vtc56HKOOvx+AWDvEsTa5eSfi5SI9V0HYpLYSPblAwrfazbQ4ff1Vl3jkTl/5JxqCAA==}
|
resolution: {integrity: sha512-Yrx/bZAKbmSvomdTAzzIpOHwpYhs0ldr2wqed22UEhQ0mIplAHY4xmY+SjAJhP/TydZrciOVzBxwM1+4T40KNA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
wrangler@4.12.1:
|
wrangler@4.12.0:
|
||||||
resolution: {integrity: sha512-jYrz8y2ffhsRqvQLO2dXFi9HLvPUJk3jn7U71GWfBBCHm0I6r2ik7Vs9ajpRcTGlbNw1RY0uIHVJBVR/7bEN5A==}
|
resolution: {integrity: sha512-4rfAXOi5KqM3ECvOrZJ97k3zEqxVwtdt4bijd8jcRBZ6iJYvEtjgjVi4TsfkVa/eXGhpfHTUnKu2uk8UHa8M2w==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@cloudflare/workers-types': ^4.20250417.0
|
'@cloudflare/workers-types': ^4.20250415.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@cloudflare/workers-types':
|
'@cloudflare/workers-types':
|
||||||
optional: true
|
optional: true
|
||||||
@@ -2305,25 +2302,25 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
|
|
||||||
'@cloudflare/unenv-preset@2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250417.0)':
|
'@cloudflare/unenv-preset@2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250416.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
unenv: 2.0.0-rc.15
|
unenv: 2.0.0-rc.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
workerd: 1.20250417.0
|
workerd: 1.20250416.0
|
||||||
|
|
||||||
'@cloudflare/workerd-darwin-64@1.20250417.0':
|
'@cloudflare/workerd-darwin-64@1.20250416.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-darwin-arm64@1.20250417.0':
|
'@cloudflare/workerd-darwin-arm64@1.20250416.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-linux-64@1.20250417.0':
|
'@cloudflare/workerd-linux-64@1.20250416.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-linux-arm64@1.20250417.0':
|
'@cloudflare/workerd-linux-arm64@1.20250416.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-windows-64@1.20250417.0':
|
'@cloudflare/workerd-windows-64@1.20250416.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cspotcode/source-map-support@0.8.1':
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
@@ -3267,71 +3264,67 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.4':
|
'@tailwindcss/node@4.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
enhanced-resolve: 5.18.1
|
enhanced-resolve: 5.18.1
|
||||||
jiti: 2.4.2
|
jiti: 2.4.2
|
||||||
lightningcss: 1.29.2
|
lightningcss: 1.29.2
|
||||||
tailwindcss: 4.1.4
|
tailwindcss: 4.1.3
|
||||||
|
|
||||||
'@tailwindcss/oxide-android-arm64@4.1.4':
|
'@tailwindcss/oxide-android-arm64@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-darwin-arm64@4.1.4':
|
'@tailwindcss/oxide-darwin-arm64@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-darwin-x64@4.1.4':
|
'@tailwindcss/oxide-darwin-x64@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-freebsd-x64@4.1.4':
|
'@tailwindcss/oxide-freebsd-x64@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4':
|
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.4':
|
'@tailwindcss/oxide-linux-arm64-gnu@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.4':
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.4':
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.1.4':
|
'@tailwindcss/oxide-linux-x64-musl@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.1.4':
|
'@tailwindcss/oxide-win32-arm64-msvc@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.4':
|
'@tailwindcss/oxide-win32-x64-msvc@4.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.4':
|
'@tailwindcss/oxide@4.1.3':
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@tailwindcss/oxide@4.1.4':
|
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@tailwindcss/oxide-android-arm64': 4.1.4
|
'@tailwindcss/oxide-android-arm64': 4.1.3
|
||||||
'@tailwindcss/oxide-darwin-arm64': 4.1.4
|
'@tailwindcss/oxide-darwin-arm64': 4.1.3
|
||||||
'@tailwindcss/oxide-darwin-x64': 4.1.4
|
'@tailwindcss/oxide-darwin-x64': 4.1.3
|
||||||
'@tailwindcss/oxide-freebsd-x64': 4.1.4
|
'@tailwindcss/oxide-freebsd-x64': 4.1.3
|
||||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.4
|
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.3
|
||||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.4
|
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.3
|
||||||
'@tailwindcss/oxide-linux-arm64-musl': 4.1.4
|
'@tailwindcss/oxide-linux-arm64-musl': 4.1.3
|
||||||
'@tailwindcss/oxide-linux-x64-gnu': 4.1.4
|
'@tailwindcss/oxide-linux-x64-gnu': 4.1.3
|
||||||
'@tailwindcss/oxide-linux-x64-musl': 4.1.4
|
'@tailwindcss/oxide-linux-x64-musl': 4.1.3
|
||||||
'@tailwindcss/oxide-wasm32-wasi': 4.1.4
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.3
|
||||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
|
'@tailwindcss/oxide-win32-x64-msvc': 4.1.3
|
||||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.4
|
|
||||||
|
|
||||||
'@tailwindcss/postcss@4.1.4':
|
'@tailwindcss/postcss@4.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
'@tailwindcss/node': 4.1.4
|
'@tailwindcss/node': 4.1.3
|
||||||
'@tailwindcss/oxide': 4.1.4
|
'@tailwindcss/oxide': 4.1.3
|
||||||
postcss: 8.5.3
|
postcss: 8.5.3
|
||||||
tailwindcss: 4.1.4
|
tailwindcss: 4.1.3
|
||||||
|
|
||||||
'@tanstack/react-virtual@3.13.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@tanstack/react-virtual@3.13.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3367,7 +3360,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/d3-timer@3.0.2': {}
|
'@types/d3-timer@3.0.2': {}
|
||||||
|
|
||||||
'@types/node@22.14.1':
|
'@types/node@22.14.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
@@ -3424,6 +3417,18 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
cmdk@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.0)(react@19.1.0)
|
||||||
|
'@radix-ui/react-dialog': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.0)(react@19.1.0)
|
||||||
|
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- '@types/react-dom'
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -3730,7 +3735,7 @@ snapshots:
|
|||||||
|
|
||||||
mime@3.0.0: {}
|
mime@3.0.0: {}
|
||||||
|
|
||||||
miniflare@4.20250417.0:
|
miniflare@4.20250416.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
acorn: 8.14.0
|
acorn: 8.14.0
|
||||||
@@ -3739,7 +3744,7 @@ snapshots:
|
|||||||
glob-to-regexp: 0.4.1
|
glob-to-regexp: 0.4.1
|
||||||
stoppable: 1.1.0
|
stoppable: 1.1.0
|
||||||
undici: 5.29.0
|
undici: 5.29.0
|
||||||
workerd: 1.20250417.0
|
workerd: 1.20250416.0
|
||||||
ws: 8.18.0
|
ws: 8.18.0
|
||||||
youch: 3.3.4
|
youch: 3.3.4
|
||||||
zod: 3.22.3
|
zod: 3.22.3
|
||||||
@@ -4020,11 +4025,11 @@ snapshots:
|
|||||||
|
|
||||||
tailwind-merge@3.2.0: {}
|
tailwind-merge@3.2.0: {}
|
||||||
|
|
||||||
tailwindcss-motion@1.1.0(tailwindcss@4.1.4):
|
tailwindcss-motion@1.1.0(tailwindcss@4.1.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 4.1.4
|
tailwindcss: 4.1.3
|
||||||
|
|
||||||
tailwindcss@4.1.4: {}
|
tailwindcss@4.1.3: {}
|
||||||
|
|
||||||
tapable@2.2.1: {}
|
tapable@2.2.1: {}
|
||||||
|
|
||||||
@@ -4095,24 +4100,24 @@ snapshots:
|
|||||||
|
|
||||||
web-vitals@4.2.4: {}
|
web-vitals@4.2.4: {}
|
||||||
|
|
||||||
workerd@1.20250417.0:
|
workerd@1.20250416.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@cloudflare/workerd-darwin-64': 1.20250417.0
|
'@cloudflare/workerd-darwin-64': 1.20250416.0
|
||||||
'@cloudflare/workerd-darwin-arm64': 1.20250417.0
|
'@cloudflare/workerd-darwin-arm64': 1.20250416.0
|
||||||
'@cloudflare/workerd-linux-64': 1.20250417.0
|
'@cloudflare/workerd-linux-64': 1.20250416.0
|
||||||
'@cloudflare/workerd-linux-arm64': 1.20250417.0
|
'@cloudflare/workerd-linux-arm64': 1.20250416.0
|
||||||
'@cloudflare/workerd-windows-64': 1.20250417.0
|
'@cloudflare/workerd-windows-64': 1.20250416.0
|
||||||
|
|
||||||
wrangler@4.12.1:
|
wrangler@4.12.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cloudflare/kv-asset-handler': 0.4.0
|
'@cloudflare/kv-asset-handler': 0.4.0
|
||||||
'@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250417.0)
|
'@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250416.0)
|
||||||
blake3-wasm: 2.1.5
|
blake3-wasm: 2.1.5
|
||||||
esbuild: 0.25.2
|
esbuild: 0.25.2
|
||||||
miniflare: 4.20250417.0
|
miniflare: 4.20250416.0
|
||||||
path-to-regexp: 6.3.0
|
path-to-regexp: 6.3.0
|
||||||
unenv: 2.0.0-rc.15
|
unenv: 2.0.0-rc.15
|
||||||
workerd: 1.20250417.0
|
workerd: 1.20250416.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
sharp: 0.33.5
|
sharp: 0.33.5
|
||||||
|
|||||||
6
web/public/robots.txt
Normal file
6
web/public/robots.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Allow all user agents
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Sitemap location (adjust if needed)
|
||||||
|
Sitemap: https://dashboardicons.com/sitemap.xml
|
||||||
@@ -32,16 +32,16 @@ export default function ErrorPage({
|
|||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold">Something went wrong</h1>
|
<h1 className="text-2xl font-bold">Something went wrong</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Unable to load this page. We're looking into the issue.
|
An unexpected error occurred while loading this page. We've been notified and are looking into it.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center pt-4">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center pt-4">
|
||||||
<Button variant="outline" onClick={() => reset()} className="cursor-pointer">
|
<Button variant="outline" onClick={() => reset()} className="cursor-pointer">
|
||||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||||
Retry
|
Try again
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleGoBack} className="cursor-pointer">
|
<Button onClick={handleGoBack} className="cursor-pointer">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{error.digest && <p className="text-xs text-muted-foreground mt-6">Error ID: {error.digest}</p>}
|
{error.digest && <p className="text-xs text-muted-foreground mt-6">Error ID: {error.digest}</p>}
|
||||||
|
|||||||
@@ -118,6 +118,19 @@
|
|||||||
transform: rotate(-5deg) scale(0.9);
|
transform: rotate(-5deg) scale(0.9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--animate-shiny-text: shiny-text 8s infinite;
|
||||||
|
@keyframes shiny-text {
|
||||||
|
0%,
|
||||||
|
90%,
|
||||||
|
100% {
|
||||||
|
background-position: calc(-100% - var(--shiny-width)) 0;
|
||||||
|
}
|
||||||
|
30%,
|
||||||
|
60% {
|
||||||
|
background-position: calc(100% + var(--shiny-width)) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -186,7 +199,7 @@
|
|||||||
--secondary: oklch(0.31 0.03 266.71);
|
--secondary: oklch(0.31 0.03 266.71);
|
||||||
--secondary-foreground: oklch(0.92 0 0);
|
--secondary-foreground: oklch(0.92 0 0);
|
||||||
--muted: oklch(0.31 0.03 266.71);
|
--muted: oklch(0.31 0.03 266.71);
|
||||||
--muted-foreground: oklch(0.78 0 0);
|
--muted-foreground: oklch(0.72 0 0);
|
||||||
--accent: oklch(0.34 0.06 267.59);
|
--accent: oklch(0.34 0.06 267.59);
|
||||||
--accent-foreground: oklch(0.88 0.06 254.13);
|
--accent-foreground: oklch(0.88 0.06 254.13);
|
||||||
--destructive: oklch(0.64 0.21 25.33);
|
--destructive: oklch(0.64 0.21 25.33);
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { readFile } from "node:fs/promises"
|
import { readFile } from "node:fs/promises"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
import { SITE_NAME, SITE_TAGLINE, WEB_URL, getIconDescription } from "@/constants"
|
||||||
import { getAllIcons } from "@/lib/api"
|
import { getAllIcons } from "@/lib/api"
|
||||||
import { ImageResponse } from "next/og"
|
import { ImageResponse } from "next/og"
|
||||||
import {
|
|
||||||
SITE_NAME,
|
|
||||||
SITE_TAGLINE,
|
|
||||||
getIconDescription,
|
|
||||||
WEB_URL
|
|
||||||
} from "@/constants"
|
|
||||||
|
|
||||||
export const dynamic = "force-static"
|
export const dynamic = "force-static"
|
||||||
|
|
||||||
@@ -38,10 +33,9 @@ export default async function Image({ params }: { params: { icon: string } }) {
|
|||||||
let iconData: Buffer | null = null
|
let iconData: Buffer | null = null
|
||||||
try {
|
try {
|
||||||
const iconPath = join(process.cwd(), `../png/${icon}.png`)
|
const iconPath = join(process.cwd(), `../png/${icon}.png`)
|
||||||
console.log(`Generating opengraph image for ${icon} (${index + 1} / ${totalIcons}) from path ${iconPath}`)
|
|
||||||
iconData = await readFile(iconPath)
|
iconData = await readFile(iconPath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Icon ${icon} was not found locally`)
|
// Icon file might not be found, fallback handled below
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the image data to a data URL or use placeholder
|
// Convert the image data to a data URL or use placeholder
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { IconDetails } from "@/components/icon-details"
|
import { IconDetails } from "@/components/icon-details"
|
||||||
import { StructuredData } from "@/components/structured-data"
|
import { StructuredData } from "@/components/structured-data"
|
||||||
import { BASE_URL, GITHUB_URL, ICON_DETAIL_KEYWORDS, SITE_NAME, SITE_TAGLINE, TITLE_SEPARATOR, WEB_URL, getIconDescription, getIconSchema } from "@/constants"
|
import {
|
||||||
|
BASE_URL,
|
||||||
|
GITHUB_URL,
|
||||||
|
ICON_DETAIL_KEYWORDS,
|
||||||
|
SITE_NAME,
|
||||||
|
SITE_TAGLINE,
|
||||||
|
TITLE_SEPARATOR,
|
||||||
|
WEB_URL,
|
||||||
|
getIconDescription,
|
||||||
|
getIconSchema,
|
||||||
|
} from "@/constants"
|
||||||
import { getAllIcons, getAuthorData } from "@/lib/api"
|
import { getAllIcons, getAuthorData } from "@/lib/api"
|
||||||
import type { Metadata, ResolvingMetadata } from "next"
|
import type { Metadata, ResolvingMetadata } from "next"
|
||||||
import Script from "next/script"
|
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
|
import Script from "next/script"
|
||||||
|
|
||||||
export const dynamicParams = false
|
export const dynamicParams = false
|
||||||
|
|
||||||
@@ -18,12 +28,12 @@ export async function generateStaticParams() {
|
|||||||
export const dynamic = "force-static"
|
export const dynamic = "force-static"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{ icon: string }>
|
params: { icon: string }
|
||||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
|
export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
|
||||||
const { icon } = await params
|
const { icon } = params
|
||||||
const iconsData = await getAllIcons()
|
const iconsData = await getAllIcons()
|
||||||
if (!iconsData[icon]) {
|
if (!iconsData[icon]) {
|
||||||
notFound()
|
notFound()
|
||||||
@@ -33,8 +43,6 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
|
|||||||
const updateDate = new Date(iconsData[icon].update.timestamp)
|
const updateDate = new Date(iconsData[icon].update.timestamp)
|
||||||
const totalIcons = Object.keys(iconsData).length
|
const totalIcons = Object.keys(iconsData).length
|
||||||
|
|
||||||
console.debug(`Generated metadata for ${icon} by ${authorName} (${authorData.html_url}) updated at ${updateDate.toLocaleString()}`)
|
|
||||||
|
|
||||||
const iconImageUrl = `${BASE_URL}/png/${icon}.png`
|
const iconImageUrl = `${BASE_URL}/png/${icon}.png`
|
||||||
const pageUrl = `${WEB_URL}/icons/${icon}`
|
const pageUrl = `${WEB_URL}/icons/${icon}`
|
||||||
const formattedIconName = icon
|
const formattedIconName = icon
|
||||||
@@ -61,7 +69,7 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
|
|||||||
follow: true,
|
follow: true,
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: fullTitle,
|
title: title,
|
||||||
description,
|
description,
|
||||||
type: "article",
|
type: "article",
|
||||||
url: pageUrl,
|
url: pageUrl,
|
||||||
@@ -73,7 +81,7 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
|
|||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: fullTitle,
|
title: title,
|
||||||
description,
|
description,
|
||||||
images: [iconImageUrl],
|
images: [iconImageUrl],
|
||||||
},
|
},
|
||||||
@@ -85,14 +93,11 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
|
|||||||
webp: `${BASE_URL}/webp/${icon}.webp`,
|
webp: `${BASE_URL}/webp/${icon}.webp`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
other: {
|
|
||||||
"revisit-after": "7 days",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function IconPage({ params }: { params: Promise<{ icon: string }> }) {
|
export default async function IconPage({ params }: { params: { icon: string } }) {
|
||||||
const { icon } = await params
|
const { icon } = params
|
||||||
const iconsData = await getAllIcons()
|
const iconsData = await getAllIcons()
|
||||||
const originalIconData = iconsData[icon]
|
const originalIconData = iconsData[icon]
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@ export default async function IconPage({ params }: { params: Promise<{ icon: str
|
|||||||
authorName,
|
authorName,
|
||||||
authorData.html_url,
|
authorData.html_url,
|
||||||
updateDate.toISOString(),
|
updateDate.toISOString(),
|
||||||
Object.keys(iconsData).length
|
Object.keys(iconsData).length,
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
87
web/src/app/icons/components.tsx
Normal file
87
web/src/app/icons/components.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { BASE_URL } from "@/constants"
|
||||||
|
import type { IconSearchProps, IconWithName } from "@/types/icons"
|
||||||
|
import { Search } from "lucide-react"
|
||||||
|
import Image from "next/image"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
export function IconSearch({ icons, initialQuery = "" }: IconSearchProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState(initialQuery)
|
||||||
|
const [filteredIcons, setFilteredIcons] = useState<IconWithName[]>(() => {
|
||||||
|
if (!initialQuery.trim()) return icons
|
||||||
|
|
||||||
|
const q = initialQuery.toLowerCase()
|
||||||
|
return icons.filter(({ name, data }) => {
|
||||||
|
if (name.toLowerCase().includes(q)) return true
|
||||||
|
if (data.aliases.some((alias) => alias.toLowerCase().includes(q))) return true
|
||||||
|
if (data.categories.some((category) => category.toLowerCase().includes(q))) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearch = (query: string) => {
|
||||||
|
setSearchQuery(query)
|
||||||
|
|
||||||
|
if (!query.trim()) {
|
||||||
|
setFilteredIcons(icons)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = query.toLowerCase()
|
||||||
|
const filtered = icons.filter(({ name, data }) => {
|
||||||
|
if (name.toLowerCase().includes(q)) return true
|
||||||
|
if (data.aliases.some((alias) => alias.toLowerCase().includes(q))) return true
|
||||||
|
if (data.categories.some((category) => category.toLowerCase().includes(q))) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
setFilteredIcons(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative w-full max-w-md">
|
||||||
|
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground transition-all duration-300" />
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search icons by name, aliases, or categories..."
|
||||||
|
className="w-full pl-8 transition-all duration-300 text-sm md:text-base"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredIcons.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h2 className="text-xl font-semibold">No icons found</h2>
|
||||||
|
<p className="text-muted-foreground mt-2">Try a different search term.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 mt-8">
|
||||||
|
{filteredIcons.map(({ name, data }) => (
|
||||||
|
<Link
|
||||||
|
key={name}
|
||||||
|
href={`/icons/${name}`}
|
||||||
|
className="group flex flex-col items-center p-4 rounded-lg border border-border hover:border-primary hover:bg-accent transition-colors"
|
||||||
|
>
|
||||||
|
<div className="relative h-16 w-16 mb-2">
|
||||||
|
<Image
|
||||||
|
src={`${BASE_URL}/${data.base}/${name}.${data.base}`}
|
||||||
|
alt={`${name} icon`}
|
||||||
|
fill
|
||||||
|
className="object-contain p-1 group-hover:scale-110 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-center truncate w-full">{name.replace(/-/g, " ")}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { BASE_URL } from "@/constants"
|
import { BASE_URL } from "@/constants"
|
||||||
import type { Icon, IconSearchProps } from "@/types/icons"
|
import type { Icon, IconSearchProps } from "@/types/icons"
|
||||||
import { ArrowDownAZ, ArrowUpZA, Calendar, ChevronLeft, ChevronRight, Filter, Search, SortAsc, X } from "lucide-react"
|
import { ArrowDownAZ, ArrowUpZA, Calendar, Filter, Search, SortAsc, X } from "lucide-react"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
@@ -27,82 +27,24 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|||||||
import posthog from "posthog-js"
|
import posthog from "posthog-js"
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
|
||||||
|
|
||||||
type SortOption = "relevance" | "alphabetical-asc" | "alphabetical-desc" | "newest"
|
type SortOption = "relevance" | "alphabetical-asc" | "alphabetical-desc" | "newest"
|
||||||
|
|
||||||
// Get the display rows count based on viewport size
|
|
||||||
function getDefaultRowsPerPage() {
|
|
||||||
if (typeof window === "undefined") return 3; // Default for SSR
|
|
||||||
|
|
||||||
// Calculate based on viewport height and width
|
|
||||||
const vh = window.innerHeight;
|
|
||||||
const vw = window.innerWidth;
|
|
||||||
|
|
||||||
// Determine number of columns based on viewport width
|
|
||||||
let columns = 2; // Default for small screens (sm)
|
|
||||||
if (vw >= 1280) columns = 8; // xl breakpoint
|
|
||||||
else if (vw >= 1024) columns = 6; // lg breakpoint
|
|
||||||
else if (vw >= 768) columns = 4; // md breakpoint
|
|
||||||
else if (vw >= 640) columns = 3; // sm breakpoint
|
|
||||||
|
|
||||||
// Calculate rows (accounting for pagination UI space)
|
|
||||||
const rowHeight = 130; // Approximate height of each row in pixels
|
|
||||||
const availableHeight = vh * 0.6; // 60% of viewport height
|
|
||||||
|
|
||||||
// Ensure at least 1 row, maximum 5 rows
|
|
||||||
return Math.max(1, Math.min(5, Math.floor(availableHeight / rowHeight)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IconSearch({ icons }: IconSearchProps) {
|
export function IconSearch({ icons }: IconSearchProps) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const initialQuery = searchParams.get("q")
|
const initialQuery = searchParams.get("q")
|
||||||
const initialCategories = searchParams.getAll("category")
|
const initialCategories = searchParams.getAll("category")
|
||||||
const initialSort = (searchParams.get("sort") as SortOption) || "relevance"
|
const initialSort = (searchParams.get("sort") as SortOption) || "relevance"
|
||||||
const initialPage = Number(searchParams.get("page") || "1")
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const [searchQuery, setSearchQuery] = useState(initialQuery ?? "")
|
const [searchQuery, setSearchQuery] = useState(initialQuery ?? "")
|
||||||
const [debouncedQuery, setDebouncedQuery] = useState(initialQuery ?? "")
|
const [debouncedQuery, setDebouncedQuery] = useState(initialQuery ?? "")
|
||||||
const [selectedCategories, setSelectedCategories] = useState<string[]>(initialCategories ?? [])
|
const [selectedCategories, setSelectedCategories] = useState<string[]>(initialCategories ?? [])
|
||||||
const [sortOption, setSortOption] = useState<SortOption>(initialSort)
|
const [sortOption, setSortOption] = useState<SortOption>(initialSort)
|
||||||
const [currentPage, setCurrentPage] = useState(initialPage)
|
|
||||||
const [iconsPerPage, setIconsPerPage] = useState(getDefaultRowsPerPage() * 8) // Default cols is 8 for xl screens
|
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const { resolvedTheme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
const [isLazyRequestSubmitted, setIsLazyRequestSubmitted] = useState(false)
|
const [isLazyRequestSubmitted, setIsLazyRequestSubmitted] = useState(false)
|
||||||
|
|
||||||
// Add resize observer to update iconsPerPage when window size changes
|
|
||||||
useEffect(() => {
|
|
||||||
const updateIconsPerPage = () => {
|
|
||||||
const rows = getDefaultRowsPerPage();
|
|
||||||
|
|
||||||
// Determine columns based on current viewport
|
|
||||||
const vw = window.innerWidth;
|
|
||||||
let columns = 2; // Default for small screens
|
|
||||||
if (vw >= 1280) columns = 8; // xl breakpoint
|
|
||||||
else if (vw >= 1024) columns = 6; // lg breakpoint
|
|
||||||
else if (vw >= 768) columns = 4; // md breakpoint
|
|
||||||
else if (vw >= 640) columns = 3; // sm breakpoint
|
|
||||||
|
|
||||||
setIconsPerPage(rows * columns);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial setup
|
|
||||||
updateIconsPerPage();
|
|
||||||
|
|
||||||
// Add resize listener
|
|
||||||
window.addEventListener('resize', updateIconsPerPage);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => window.removeEventListener('resize', updateIconsPerPage);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Reset page when search parameters change
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentPage(1);
|
|
||||||
}, [debouncedQuery, selectedCategories, sortOption]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setDebouncedQuery(searchQuery)
|
setDebouncedQuery(searchQuery)
|
||||||
@@ -196,7 +138,7 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
}, [filterIcons, debouncedQuery, selectedCategories, sortOption])
|
}, [filterIcons, debouncedQuery, selectedCategories, sortOption])
|
||||||
|
|
||||||
const updateResults = useCallback(
|
const updateResults = useCallback(
|
||||||
(query: string, categories: string[], sort: SortOption, page = 1) => {
|
(query: string, categories: string[], sort: SortOption) => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (query) params.set("q", query)
|
if (query) params.set("q", query)
|
||||||
|
|
||||||
@@ -210,11 +152,6 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
params.set("sort", sort)
|
params.set("sort", sort)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add page parameter if not the first page
|
|
||||||
if (page > 1) {
|
|
||||||
params.set("page", page.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname
|
const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname
|
||||||
router.push(newUrl, { scroll: false })
|
router.push(newUrl, { scroll: false })
|
||||||
},
|
},
|
||||||
@@ -260,20 +197,11 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
[updateResults, searchQuery, selectedCategories],
|
[updateResults, searchQuery, selectedCategories],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePageChange = useCallback(
|
|
||||||
(page: number) => {
|
|
||||||
setCurrentPage(page);
|
|
||||||
updateResults(searchQuery, selectedCategories, sortOption, page);
|
|
||||||
},
|
|
||||||
[updateResults, searchQuery, selectedCategories, sortOption],
|
|
||||||
)
|
|
||||||
|
|
||||||
const clearFilters = useCallback(() => {
|
const clearFilters = useCallback(() => {
|
||||||
setSearchQuery("")
|
setSearchQuery("")
|
||||||
setSelectedCategories([])
|
setSelectedCategories([])
|
||||||
setSortOption("relevance")
|
setSortOption("relevance")
|
||||||
setCurrentPage(1)
|
updateResults("", [], "relevance")
|
||||||
updateResults("", [], "relevance", 1)
|
|
||||||
}, [updateResults])
|
}, [updateResults])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -300,11 +228,11 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
const getSortLabel = (sort: SortOption) => {
|
const getSortLabel = (sort: SortOption) => {
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case "relevance":
|
case "relevance":
|
||||||
return "Relevance"
|
return "Best match"
|
||||||
case "alphabetical-asc":
|
case "alphabetical-asc":
|
||||||
return "Name (A-Z)"
|
return "A to Z"
|
||||||
case "alphabetical-desc":
|
case "alphabetical-desc":
|
||||||
return "Name (Z-A)"
|
return "Z to A"
|
||||||
case "newest":
|
case "newest":
|
||||||
return "Newest first"
|
return "Newest first"
|
||||||
default:
|
default:
|
||||||
@@ -337,7 +265,7 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search for icons..."
|
placeholder="Search icons by name, alias, or category..."
|
||||||
className="w-full h-10 pl-9 cursor-text transition-all duration-300 text-sm md:text-base border-border shadow-sm"
|
className="w-full h-10 pl-9 cursor-text transition-all duration-300 text-sm md:text-base border-border shadow-sm"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
@@ -349,18 +277,18 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
{/* Filter dropdown */}
|
{/* Filter dropdown */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button variant="outline" size="sm" className="flex-1 sm:flex-none cursor-pointer bg-background border-border shadow-sm ">
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="flex-1 sm:flex-none cursor-pointer bg-background border-border shadow-sm"
|
|
||||||
aria-label="Filter icons"
|
|
||||||
>
|
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
<Filter className="h-4 w-4 mr-2" />
|
||||||
<span>{selectedCategories.length > 0 ? `Filters (${selectedCategories.length})` : "Filter"}</span>
|
<span>Filter</span>
|
||||||
|
{selectedCategories.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-2 px-1.5">
|
||||||
|
{selectedCategories.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-64 sm:w-56">
|
<DropdownMenuContent align="start" className="w-64 sm:w-56">
|
||||||
<DropdownMenuLabel className="font-semibold">Select Categories</DropdownMenuLabel>
|
<DropdownMenuLabel className="font-semibold">Categories</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<div className="max-h-[40vh] overflow-y-auto p-1">
|
<div className="max-h-[40vh] overflow-y-auto p-1">
|
||||||
@@ -386,7 +314,7 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
}}
|
}}
|
||||||
className="cursor-pointer focus: focus:bg-rose-50 dark:focus:bg-rose-950/20"
|
className="cursor-pointer focus: focus:bg-rose-50 dark:focus:bg-rose-950/20"
|
||||||
>
|
>
|
||||||
Clear categories
|
Clear all filters
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -402,18 +330,18 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
<DropdownMenuContent align="start" className="w-56">
|
||||||
<DropdownMenuLabel className="font-semibold">Sort Icons</DropdownMenuLabel>
|
<DropdownMenuLabel className="font-semibold">Sort By</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuRadioGroup value={sortOption} onValueChange={(value) => handleSortChange(value as SortOption)}>
|
<DropdownMenuRadioGroup value={sortOption} onValueChange={(value) => handleSortChange(value as SortOption)}>
|
||||||
<DropdownMenuRadioItem value="relevance" className="cursor-pointer">
|
<DropdownMenuRadioItem value="relevance" className="cursor-pointer">
|
||||||
<Search className="h-4 w-4 mr-2" />
|
<Search className="h-4 w-4 mr-2" />
|
||||||
Relevance
|
Best match
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
<DropdownMenuRadioItem value="alphabetical-asc" className="cursor-pointer">
|
<DropdownMenuRadioItem value="alphabetical-asc" className="cursor-pointer">
|
||||||
<ArrowDownAZ className="h-4 w-4 mr-2" />Name (A-Z)
|
<ArrowDownAZ className="h-4 w-4 mr-2" />A to Z
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
<DropdownMenuRadioItem value="alphabetical-desc" className="cursor-pointer">
|
<DropdownMenuRadioItem value="alphabetical-desc" className="cursor-pointer">
|
||||||
<ArrowUpZA className="h-4 w-4 mr-2" />Name (Z-A)
|
<ArrowUpZA className="h-4 w-4 mr-2" />Z to A
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
<DropdownMenuRadioItem value="newest" className="cursor-pointer">
|
<DropdownMenuRadioItem value="newest" className="cursor-pointer">
|
||||||
<Calendar className="h-4 w-4 mr-2" />
|
<Calendar className="h-4 w-4 mr-2" />
|
||||||
@@ -425,15 +353,9 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
|
|
||||||
{/* Clear all button */}
|
{/* Clear all button */}
|
||||||
{(searchQuery || selectedCategories.length > 0 || sortOption !== "relevance") && (
|
{(searchQuery || selectedCategories.length > 0 || sortOption !== "relevance") && (
|
||||||
<Button
|
<Button variant="outline" size="sm" onClick={clearFilters} className="flex-1 sm:flex-none cursor-pointer bg-background">
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearFilters}
|
|
||||||
className="flex-1 sm:flex-none cursor-pointer bg-background"
|
|
||||||
aria-label="Reset all filters"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4 mr-2" />
|
<X className="h-4 w-4 mr-2" />
|
||||||
<span>Reset</span>
|
<span>Clear all</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -441,7 +363,7 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
{/* Active filter badges */}
|
{/* Active filter badges */}
|
||||||
{selectedCategories.length > 0 && (
|
{selectedCategories.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center gap-2 mt-2">
|
<div className="flex flex-wrap items-center gap-2 mt-2">
|
||||||
<span className="text-sm text-muted-foreground">Selected:</span>
|
<span className="text-sm text-muted-foreground">Filters:</span>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{selectedCategories.map((category) => (
|
{selectedCategories.map((category) => (
|
||||||
<Badge key={category} variant="secondary" className="flex items-center gap-1 pl-2 pr-1">
|
<Badge key={category} variant="secondary" className="flex items-center gap-1 pl-2 pr-1">
|
||||||
@@ -467,7 +389,7 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
}}
|
}}
|
||||||
className="text-xs h-7 px-2 cursor-pointer"
|
className="text-xs h-7 px-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
Clear
|
Clear all
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -478,33 +400,27 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
{filteredIcons.length === 0 ? (
|
{filteredIcons.length === 0 ? (
|
||||||
<div className="flex flex-col gap-8 py-12 max-w-2xl mx-auto items-center">
|
<div className="flex flex-col gap-8 py-12 max-w-2xl mx-auto items-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-3xl sm:text-5xl font-semibold">Icon not found</h2>
|
<h2 className="text-3xl sm:text-5xl font-semibold">We don't have this one...yet!</h2>
|
||||||
<p className="text-lg text-muted-foreground mt-2">Help us expand our collection</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 items-center w-full">
|
|
||||||
<IconSubmissionContent />
|
|
||||||
<div className="mt-4 flex items-center gap-2 justify-center">
|
|
||||||
<span className="text-sm text-muted-foreground">Can't submit it yourself?</span>
|
|
||||||
<Button
|
|
||||||
className="cursor-pointer"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setIsLazyRequestSubmitted(true)
|
|
||||||
toast("Request received!", {
|
|
||||||
description: `We've noted your request for "${searchQuery || "this icon"}". Thanks for your suggestion.`,
|
|
||||||
})
|
|
||||||
posthog.capture("lazy icon request", {
|
|
||||||
query: searchQuery,
|
|
||||||
categories: selectedCategories,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
disabled={isLazyRequestSubmitted}
|
|
||||||
>
|
|
||||||
Request this icon
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
className="cursor-pointer motion-preset-pop"
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
onClick={() => {
|
||||||
|
setIsLazyRequestSubmitted(true)
|
||||||
|
toast("We hear you!", {
|
||||||
|
description: `Okay, okay... we'll consider adding "${searchQuery || "that icon"}" just for you. 😉`,
|
||||||
|
})
|
||||||
|
posthog.capture("lazy icon request", {
|
||||||
|
query: searchQuery,
|
||||||
|
categories: selectedCategories,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
disabled={isLazyRequestSubmitted}
|
||||||
|
>
|
||||||
|
I want this icon added but I'm too lazy to add it myself
|
||||||
|
</Button>
|
||||||
|
<IconSubmissionContent />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -519,14 +435,7 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IconsGrid
|
<IconsGrid filteredIcons={filteredIcons} matchedAliases={matchedAliases} />
|
||||||
filteredIcons={filteredIcons}
|
|
||||||
matchedAliases={matchedAliases}
|
|
||||||
currentPage={currentPage}
|
|
||||||
iconsPerPage={iconsPerPage}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
totalIcons={filteredIcons.length}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -536,13 +445,15 @@ export function IconSearch({ icons }: IconSearchProps) {
|
|||||||
function IconCard({
|
function IconCard({
|
||||||
name,
|
name,
|
||||||
data: iconData,
|
data: iconData,
|
||||||
|
matchedAlias,
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
data: Icon
|
data: Icon
|
||||||
|
matchedAlias?: string | null
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<MagicCard className="rounded-md shadow-md cursor-pointer">
|
<MagicCard className="rounded-md shadow-md">
|
||||||
<Link prefetch={false} href={`/icons/${name}`} className="group flex flex-col items-center p-3 sm:p-4">
|
<Link prefetch={false} href={`/icons/${name}`} className="group flex flex-col items-center p-3 sm:p-4 cursor-pointer">
|
||||||
<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2">
|
<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2">
|
||||||
<Image
|
<Image
|
||||||
src={`${BASE_URL}/${iconData.base}/${name}.${iconData.base}`}
|
src={`${BASE_URL}/${iconData.base}/${name}.${iconData.base}`}
|
||||||
@@ -551,9 +462,11 @@ function IconCard({
|
|||||||
className="object-contain p-1 group-hover:scale-110 transition-transform duration-300"
|
className="object-contain p-1 group-hover:scale-110 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs sm:text-sm text-center truncate w-full capitalize group-hover:text-rose-500 dark:group-hover:text-rose-400 transition-colors duration-200 font-medium">
|
<span className="text-xs sm:text-sm text-center truncate w-full capitalize group- dark:group-hover:text-rose-400 transition-colors duration-200 font-medium">
|
||||||
{name.replace(/-/g, " ")}
|
{name.replace(/-/g, " ")}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{matchedAlias && <span className="text-[10px] text-center truncate w-full mt-1">Alias: {matchedAlias}</span>}
|
||||||
</Link>
|
</Link>
|
||||||
</MagicCard>
|
</MagicCard>
|
||||||
)
|
)
|
||||||
@@ -562,253 +475,17 @@ function IconCard({
|
|||||||
interface IconsGridProps {
|
interface IconsGridProps {
|
||||||
filteredIcons: { name: string; data: Icon }[]
|
filteredIcons: { name: string; data: Icon }[]
|
||||||
matchedAliases: Record<string, string>
|
matchedAliases: Record<string, string>
|
||||||
currentPage: number
|
|
||||||
iconsPerPage: number
|
|
||||||
onPageChange: (page: number) => void
|
|
||||||
totalIcons: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function IconsGrid({ filteredIcons, matchedAliases, currentPage, iconsPerPage, onPageChange, totalIcons }: IconsGridProps) {
|
function IconsGrid({ filteredIcons, matchedAliases }: IconsGridProps) {
|
||||||
// Calculate pagination values
|
|
||||||
const totalPages = Math.ceil(totalIcons / iconsPerPage)
|
|
||||||
const indexOfLastIcon = currentPage * iconsPerPage
|
|
||||||
const indexOfFirstIcon = indexOfLastIcon - iconsPerPage
|
|
||||||
const currentIcons = filteredIcons.slice(indexOfFirstIcon, indexOfLastIcon)
|
|
||||||
|
|
||||||
// Calculate letter ranges for each page
|
|
||||||
const getLetterRange = (pageNum: number) => {
|
|
||||||
if (filteredIcons.length === 0) return '';
|
|
||||||
const start = (pageNum - 1) * iconsPerPage;
|
|
||||||
const end = Math.min(start + iconsPerPage - 1, filteredIcons.length - 1);
|
|
||||||
|
|
||||||
if (start >= filteredIcons.length) return '';
|
|
||||||
|
|
||||||
const firstLetter = filteredIcons[start].name.charAt(0).toUpperCase();
|
|
||||||
const lastLetter = filteredIcons[end].name.charAt(0).toUpperCase();
|
|
||||||
|
|
||||||
return firstLetter === lastLetter ? firstLetter : `${firstLetter} - ${lastLetter}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get current page letter range
|
|
||||||
const currentLetterRange = getLetterRange(currentPage);
|
|
||||||
|
|
||||||
// Handle direct page input
|
|
||||||
const [pageInput, setPageInput] = useState(currentPage.toString());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPageInput(currentPage.toString());
|
|
||||||
}, [currentPage]);
|
|
||||||
|
|
||||||
const handlePageInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setPageInput(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageInputSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const pageNumber = parseInt(pageInput);
|
|
||||||
if (!isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= totalPages) {
|
|
||||||
onPageChange(pageNumber);
|
|
||||||
} else {
|
|
||||||
// Reset to current page if invalid
|
|
||||||
setPageInput(currentPage.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AnimatePresence mode="wait">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 mt-2">
|
||||||
<motion.div
|
{filteredIcons.slice(0, 120).map(({ name, data }) => (
|
||||||
key={currentPage}
|
<IconCard key={name} name={name} data={data} matchedAlias={matchedAliases[name] || null} />
|
||||||
initial={{ opacity: 0, y: 20 }}
|
))}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
</div>
|
||||||
exit={{ opacity: 0, y: -20 }}
|
{filteredIcons.length > 120 && <p className="text-sm text-muted-foreground">And {filteredIcons.length - 120} more...</p>}
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4 mt-2"
|
|
||||||
>
|
|
||||||
{currentIcons.map(({ name, data }) => (
|
|
||||||
<IconCard key={name} name={name} data={data} />
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<div className="flex flex-col gap-4 mt-8">
|
|
||||||
{/* Mobile view: centered content */}
|
|
||||||
<div className="text-sm text-muted-foreground text-center md:text-left md:hidden">
|
|
||||||
Showing {indexOfFirstIcon + 1}-{Math.min(indexOfLastIcon, totalIcons)} of {totalIcons} icons
|
|
||||||
{currentLetterRange && (
|
|
||||||
<span className="ml-2 font-medium">({currentLetterRange})</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop view layout */}
|
|
||||||
<div className="hidden md:flex justify-between items-center">
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Showing {indexOfFirstIcon + 1}-{Math.min(indexOfLastIcon, totalIcons)} of {totalIcons} icons
|
|
||||||
{currentLetterRange && (
|
|
||||||
<span className="ml-2 font-medium">({currentLetterRange})</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* Page input and total count */}
|
|
||||||
<form onSubmit={handlePageInputSubmit} className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={totalPages}
|
|
||||||
value={pageInput}
|
|
||||||
onChange={handlePageInputChange}
|
|
||||||
className="w-16 h-8 text-center cursor-text"
|
|
||||||
aria-label="Go to page"
|
|
||||||
/>
|
|
||||||
<span className="text-sm whitespace-nowrap">of {totalPages}</span>
|
|
||||||
<Button type="submit" size="sm" variant="outline" className="h-8 cursor-pointer">Go</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Pagination controls */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 rounded-r-none cursor-pointer"
|
|
||||||
aria-label="Previous page"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex items-center overflow-hidden">
|
|
||||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
|
||||||
// Show pages around current page
|
|
||||||
let pageNum;
|
|
||||||
if (totalPages <= 5) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (currentPage <= 3) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (currentPage >= totalPages - 2) {
|
|
||||||
pageNum = totalPages - 4 + i;
|
|
||||||
} else {
|
|
||||||
pageNum = currentPage - 2 + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate letter range for this page
|
|
||||||
const letterRange = getLetterRange(pageNum);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={pageNum}
|
|
||||||
onClick={() => onPageChange(pageNum)}
|
|
||||||
variant={pageNum === currentPage ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
className={`h-8 w-8 p-0 rounded-none relative group cursor-pointer transition-colors duration-200 ${
|
|
||||||
pageNum === currentPage ? "font-medium" : ""
|
|
||||||
}`}
|
|
||||||
aria-label={`Page ${pageNum}`}
|
|
||||||
aria-current={pageNum === currentPage ? "page" : undefined}
|
|
||||||
>
|
|
||||||
{pageNum}
|
|
||||||
{letterRange && (
|
|
||||||
<span className="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-popover text-popover-foreground px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity shadow-md whitespace-nowrap">
|
|
||||||
{letterRange}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 rounded-l-none cursor-pointer"
|
|
||||||
aria-label="Next page"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile-only pagination layout - centered */}
|
|
||||||
<div className="flex flex-col items-center gap-4 md:hidden">
|
|
||||||
{/* Mobile pagination controls */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 rounded-r-none cursor-pointer"
|
|
||||||
aria-label="Previous page"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex items-center overflow-hidden">
|
|
||||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
|
||||||
// Show pages around current page - same logic as desktop
|
|
||||||
let pageNum;
|
|
||||||
if (totalPages <= 5) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (currentPage <= 3) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (currentPage >= totalPages - 2) {
|
|
||||||
pageNum = totalPages - 4 + i;
|
|
||||||
} else {
|
|
||||||
pageNum = currentPage - 2 + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={pageNum}
|
|
||||||
onClick={() => onPageChange(pageNum)}
|
|
||||||
variant={pageNum === currentPage ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
className={`h-8 w-8 p-0 rounded-none cursor-pointer ${
|
|
||||||
pageNum === currentPage ? "font-medium" : ""
|
|
||||||
}`}
|
|
||||||
aria-label={`Page ${pageNum}`}
|
|
||||||
aria-current={pageNum === currentPage ? "page" : undefined}
|
|
||||||
>
|
|
||||||
{pageNum}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-8 rounded-l-none cursor-pointer"
|
|
||||||
aria-label="Next page"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile page input */}
|
|
||||||
<form onSubmit={handlePageInputSubmit} className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={totalPages}
|
|
||||||
value={pageInput}
|
|
||||||
onChange={handlePageInputChange}
|
|
||||||
className="w-16 h-8 text-center cursor-text"
|
|
||||||
aria-label="Go to page"
|
|
||||||
/>
|
|
||||||
<span className="text-sm whitespace-nowrap">of {totalPages}</span>
|
|
||||||
<Button type="submit" size="sm" variant="outline" className="h-8 cursor-pointer">Go</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { BASE_URL, BROWSE_KEYWORDS, DEFAULT_OG_IMAGE, GITHUB_URL, ORGANIZATION_NAME, ORGANIZATION_SCHEMA, SITE_NAME, SITE_TAGLINE, TITLE_SEPARATOR, WEB_URL, getBrowseDescription } from "@/constants"
|
import { StructuredData } from "@/components/structured-data"
|
||||||
|
import {
|
||||||
|
BASE_URL,
|
||||||
|
BROWSE_KEYWORDS,
|
||||||
|
DEFAULT_OG_IMAGE,
|
||||||
|
GITHUB_URL,
|
||||||
|
ORGANIZATION_NAME,
|
||||||
|
ORGANIZATION_SCHEMA,
|
||||||
|
SITE_NAME,
|
||||||
|
SITE_TAGLINE,
|
||||||
|
TITLE_SEPARATOR,
|
||||||
|
WEB_URL,
|
||||||
|
getBrowseDescription,
|
||||||
|
} from "@/constants"
|
||||||
import { getIconsArray } from "@/lib/api"
|
import { getIconsArray } from "@/lib/api"
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { IconSearch } from "./components/icon-search"
|
import { IconSearch } from "./components/icon-search"
|
||||||
import { StructuredData } from "@/components/structured-data"
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const icons = await getIconsArray()
|
const icons = await getIconsArray()
|
||||||
@@ -16,7 +28,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
description,
|
description,
|
||||||
keywords: BROWSE_KEYWORDS,
|
keywords: BROWSE_KEYWORDS,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `Browse Icons ${TITLE_SEPARATOR} ${SITE_NAME} ${TITLE_SEPARATOR} ${SITE_TAGLINE}`,
|
title: title,
|
||||||
description,
|
description,
|
||||||
type: "website",
|
type: "website",
|
||||||
url: `${WEB_URL}/icons`,
|
url: `${WEB_URL}/icons`,
|
||||||
@@ -24,16 +36,13 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: `Browse Icons ${TITLE_SEPARATOR} ${SITE_NAME} ${TITLE_SEPARATOR} ${SITE_TAGLINE}`,
|
title: title,
|
||||||
description,
|
description,
|
||||||
images: [DEFAULT_OG_IMAGE.url],
|
images: [DEFAULT_OG_IMAGE.url],
|
||||||
},
|
},
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${WEB_URL}/icons`,
|
canonical: `${WEB_URL}/icons`,
|
||||||
},
|
},
|
||||||
other: {
|
|
||||||
"revisit-after": "3 days",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,15 +54,15 @@ export default async function IconsPage() {
|
|||||||
const gallerySchema = {
|
const gallerySchema = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "ImageGallery",
|
"@type": "ImageGallery",
|
||||||
"name": `${SITE_NAME} - Browse ${icons.length} Icons - ${SITE_TAGLINE}`,
|
name: `${SITE_NAME} - Browse ${icons.length} Icons - ${SITE_TAGLINE}`,
|
||||||
"description": getBrowseDescription(icons.length),
|
description: getBrowseDescription(icons.length),
|
||||||
"url": `${WEB_URL}/icons`,
|
url: `${WEB_URL}/icons`,
|
||||||
"numberOfItems": icons.length,
|
numberOfItems: icons.length,
|
||||||
"creator": {
|
creator: {
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": ORGANIZATION_NAME,
|
name: ORGANIZATION_NAME,
|
||||||
"url": GITHUB_URL
|
url: GITHUB_URL,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +74,9 @@ export default async function IconsPage() {
|
|||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Icons</h1>
|
<h1 className="text-3xl font-bold">Icons</h1>
|
||||||
<p className="text-muted-foreground">Search our collection of {icons.length} icons - {SITE_TAGLINE}.</p>
|
<p className="text-muted-foreground">
|
||||||
|
Search our collection of {icons.length} icons - {SITE_TAGLINE}.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,20 @@ import type { Metadata, Viewport } from "next"
|
|||||||
import { Inter } from "next/font/google"
|
import { Inter } from "next/font/google"
|
||||||
import { Toaster } from "sonner"
|
import { Toaster } from "sonner"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
import { DEFAULT_KEYWORDS, DEFAULT_OG_IMAGE, GITHUB_URL, ORGANIZATION_NAME, ORGANIZATION_SCHEMA, SITE_NAME, SITE_TAGLINE, WEB_URL, getDescription, getWebsiteSchema, websiteFullTitle, websiteTitle } from "@/constants"
|
import {
|
||||||
|
DEFAULT_KEYWORDS,
|
||||||
|
DEFAULT_OG_IMAGE,
|
||||||
|
GITHUB_URL,
|
||||||
|
ORGANIZATION_NAME,
|
||||||
|
ORGANIZATION_SCHEMA,
|
||||||
|
SITE_NAME,
|
||||||
|
SITE_TAGLINE,
|
||||||
|
WEB_URL,
|
||||||
|
getDescription,
|
||||||
|
getWebsiteSchema,
|
||||||
|
websiteFullTitle,
|
||||||
|
websiteTitle,
|
||||||
|
} from "@/constants"
|
||||||
import { ThemeProvider } from "./theme-provider"
|
import { ThemeProvider } from "./theme-provider"
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
@@ -32,7 +45,10 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
metadataBase: new URL(WEB_URL),
|
metadataBase: new URL(WEB_URL),
|
||||||
title: websiteTitle,
|
title: {
|
||||||
|
default: websiteTitle,
|
||||||
|
template: `%s | ${websiteTitle}`,
|
||||||
|
},
|
||||||
description,
|
description,
|
||||||
keywords: DEFAULT_KEYWORDS,
|
keywords: DEFAULT_KEYWORDS,
|
||||||
robots: {
|
robots: {
|
||||||
@@ -99,10 +115,7 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
|
|||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className={`${inter.variable} antialiased bg-background flex flex-col min-h-screen`}>
|
<body className={`${inter.variable} antialiased bg-background flex flex-col min-h-screen`}>
|
||||||
<PostHogProvider>
|
<PostHogProvider>
|
||||||
<WebsiteStructuredData
|
<WebsiteStructuredData websiteSchema={websiteSchema} organizationSchema={ORGANIZATION_SCHEMA} />
|
||||||
websiteSchema={websiteSchema}
|
|
||||||
organizationSchema={ORGANIZATION_SCHEMA}
|
|
||||||
/>
|
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||||
<HeaderWrapper />
|
<HeaderWrapper />
|
||||||
<main className="flex-grow">{children}</main>
|
<main className="flex-grow">{children}</main>
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ export default function NotFound({
|
|||||||
<div className="mx-auto w-16 h-16 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center text-red-600 dark:text-red-400">
|
<div className="mx-auto w-16 h-16 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center text-red-600 dark:text-red-400">
|
||||||
<AlertTriangle className="w-8 h-8" />
|
<AlertTriangle className="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold mt-6">Not found</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold mt-6">Icon not found</h1>
|
||||||
<p className="text-muted-foreground mt-3 max-w-md">
|
<p className="text-muted-foreground mt-3 max-w-md">
|
||||||
This icon does not exist or could not be loaded.
|
The icon you are looking for could not be found or there was an error loading it.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -25,16 +25,16 @@ export default function NotFound({
|
|||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href="/icons">
|
<Link href="/icons">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to icons
|
Back to all icons
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-border pt-8 mt-8">
|
<div className="border-t border-border pt-8 mt-8">
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<h2 className="text-xl font-semibold">Missing an icon?</h2>
|
<h2 className="text-xl font-semibold">Can't find what you're looking for?</h2>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-muted-foreground mt-2">
|
||||||
Submit a new icon or suggest improvements to our collection.
|
Contribute to our icon collection by suggesting a new icon or improving an existing one.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import { HeroSection } from "@/components/hero"
|
import { HeroSection } from "@/components/hero"
|
||||||
import { RecentlyAddedIcons } from "@/components/recently-added-icons"
|
import { RecentlyAddedIcons } from "@/components/recently-added-icons"
|
||||||
import { StructuredData } from "@/components/structured-data"
|
import {
|
||||||
import { BASE_URL, DEFAULT_KEYWORDS, DEFAULT_OG_IMAGE, GITHUB_URL, ORGANIZATION_NAME, ORGANIZATION_SCHEMA, SITE_NAME, SITE_TAGLINE, WEB_URL, REPO_NAME, getHomeDescription, websiteFullTitle, websiteTitle } from "@/constants"
|
BASE_URL,
|
||||||
|
DEFAULT_KEYWORDS,
|
||||||
|
DEFAULT_OG_IMAGE,
|
||||||
|
GITHUB_URL,
|
||||||
|
ORGANIZATION_NAME,
|
||||||
|
ORGANIZATION_SCHEMA,
|
||||||
|
REPO_NAME,
|
||||||
|
SITE_NAME,
|
||||||
|
SITE_TAGLINE,
|
||||||
|
WEB_URL,
|
||||||
|
getHomeDescription,
|
||||||
|
websiteFullTitle,
|
||||||
|
websiteTitle,
|
||||||
|
} from "@/constants"
|
||||||
import { getRecentlyAddedIcons, getTotalIcons } from "@/lib/api"
|
import { getRecentlyAddedIcons, getTotalIcons } from "@/lib/api"
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
|
|
||||||
@@ -39,7 +52,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
async function getGitHubStars() {
|
async function getGitHubStars() {
|
||||||
const response = await fetch(`https://api.github.com/repos/${REPO_NAME}`)
|
const response = await fetch(`https://api.github.com/repos/${REPO_NAME}`)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
console.log(`GitHub stars: ${data.stargazers_count}`)
|
// TODO: Consider caching this result or fetching at build time to avoid rate limits.
|
||||||
return data.stargazers_count
|
return data.stargazers_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
User-Agent: *
|
|
||||||
Allow: /
|
|
||||||
Sitemap: https://dashboardicons.com/sitemap.xml
|
|
||||||
@@ -7,7 +7,7 @@ export function Carbon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const serve = "CW7IP27L"
|
const serve = "CW7IKKQM"
|
||||||
const placement = "dashboardiconscom"
|
const placement = "dashboardiconscom"
|
||||||
ref.current.innerHTML = ""
|
ref.current.innerHTML = ""
|
||||||
const s = document.createElement("script")
|
const s = document.createElement("script")
|
||||||
|
|||||||
138
web/src/components/command-menu.tsx
Normal file
138
web/src/components/command-menu.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
|
||||||
|
import { useMediaQuery } from "@/hooks/use-media-query"
|
||||||
|
import { fuzzySearch } from "@/lib/utils"
|
||||||
|
import { Icon } from "@/types/icons"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
interface CommandMenuProps {
|
||||||
|
icons: {
|
||||||
|
name: string
|
||||||
|
data: {
|
||||||
|
categories: string[]
|
||||||
|
aliases: string[]
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
triggerButtonId?: string
|
||||||
|
open?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalOnOpenChange }: CommandMenuProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
const [internalOpen, setInternalOpen] = useState(false)
|
||||||
|
const [query, setQuery] = useState("")
|
||||||
|
const isDesktop = useMediaQuery("(min-width: 768px)")
|
||||||
|
|
||||||
|
// Use either external or internal state for controlling open state
|
||||||
|
const isOpen = externalOpen !== undefined ? externalOpen : internalOpen
|
||||||
|
|
||||||
|
// Wrap setIsOpen in useCallback to fix dependency issue
|
||||||
|
const setIsOpen = useCallback(
|
||||||
|
(value: boolean) => {
|
||||||
|
if (externalOnOpenChange) {
|
||||||
|
externalOnOpenChange(value)
|
||||||
|
} else {
|
||||||
|
setInternalOpen(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[externalOnOpenChange],
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredIcons = getFilteredIcons(icons, query)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
(e.key === "k" && (e.metaKey || e.ctrlKey)) ||
|
||||||
|
(e.key === "/" && document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA")
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsOpen(!isOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown)
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown)
|
||||||
|
}, [isOpen, setIsOpen])
|
||||||
|
|
||||||
|
function getFilteredIcons(iconList: CommandMenuProps["icons"], query: string) {
|
||||||
|
if (!query) {
|
||||||
|
// Return a limited number of icons when no query is provided
|
||||||
|
return iconList.slice(0, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate scores for each icon
|
||||||
|
const scoredIcons = iconList.map((icon) => {
|
||||||
|
// Calculate scores for different fields
|
||||||
|
const nameScore = fuzzySearch(icon.name, query) * 2.0 // Give more weight to name matches
|
||||||
|
|
||||||
|
// Get max score from aliases
|
||||||
|
const aliasScore =
|
||||||
|
icon.data.aliases && icon.data.aliases.length > 0
|
||||||
|
? Math.max(...icon.data.aliases.map((alias) => fuzzySearch(alias, query))) * 1.8 // Increased weight for aliases
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// Get max score from categories
|
||||||
|
const categoryScore =
|
||||||
|
icon.data.categories && icon.data.categories.length > 0
|
||||||
|
? Math.max(...icon.data.categories.map((category) => fuzzySearch(category, query)))
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// Use the highest score
|
||||||
|
const score = Math.max(nameScore, aliasScore, categoryScore)
|
||||||
|
|
||||||
|
return { icon, score, matchedField: score === nameScore ? "name" : score === aliasScore ? "alias" : "category" }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Filter icons with a minimum score and sort by highest score
|
||||||
|
return scoredIcons
|
||||||
|
.filter((item) => item.score > 0.3) // Higher threshold for more accurate results
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, 20) // Limit the number of results
|
||||||
|
.map((item) => item.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelect = (name: string) => {
|
||||||
|
setIsOpen(false)
|
||||||
|
router.push(`/icons/${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<CommandInput placeholder="Search for icons by name, category, or purpose..." value={query} onValueChange={setQuery} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No matching icons found. Try a different search term or browse all icons.</CommandEmpty>
|
||||||
|
<CommandGroup heading="Icons">
|
||||||
|
{filteredIcons.map(({ name, data }) => {
|
||||||
|
// Find matched alias for display if available
|
||||||
|
const matchedAlias =
|
||||||
|
query && data.aliases && data.aliases.length > 0
|
||||||
|
? data.aliases.find((alias) => alias.toLowerCase().includes(query.toLowerCase()))
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandItem key={name} value={name} onSelect={() => handleSelect(name)} className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<div className="flex-shrink-0 h-5 w-5 relative">
|
||||||
|
<div className="h-5 w-5 bg-rose-100 dark:bg-rose-900/30 rounded-md flex items-center justify-center">
|
||||||
|
<span className="text-[10px] font-medium text-rose-800 dark:text-rose-300">{name.substring(0, 2).toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="flex-grow capitalize">{name.replace(/-/g, " ")}</span>
|
||||||
|
{matchedAlias && <span className="text-xs text-primary-500 truncate max-w-[100px]">alias: {matchedAlias}</span>}
|
||||||
|
{!matchedAlias && data.categories && data.categories.length > 0 && (
|
||||||
|
<span className="text-xs text-muted-foreground truncate max-w-[100px]">
|
||||||
|
{data.categories[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</CommandDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export function Footer() {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="font-bold text-lg text-foreground/90">Dashboard Icons</h3>
|
<h3 className="font-bold text-lg text-foreground/90">Dashboard Icons</h3>
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
Collection of icons for applications, services, and tools - designed for dashboards and app directories.
|
A collection of curated icons for services, applications and tools, designed specifically for dashboards and app directories.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,42 @@
|
|||||||
import { IconSubmissionForm } from "@/components/icon-submission-form"
|
import { IconSubmissionForm } from "@/components/icon-submission-form"
|
||||||
import { ThemeSwitcher } from "@/components/theme-switcher"
|
import { ThemeSwitcher } from "@/components/theme-switcher"
|
||||||
import { REPO_PATH } from "@/constants"
|
import { REPO_PATH } from "@/constants"
|
||||||
|
import { getIconsArray } from "@/lib/api"
|
||||||
|
import type { IconWithName } from "@/types/icons"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import { Github } from "lucide-react"
|
import { Github, Search } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { CommandMenu } from "./command-menu"
|
||||||
import { HeaderNav } from "./header-nav"
|
import { HeaderNav } from "./header-nav"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
|
const [iconsData, setIconsData] = useState<IconWithName[]>([])
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
const [commandMenuOpen, setCommandMenuOpen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadIcons() {
|
||||||
|
try {
|
||||||
|
const icons = await getIconsArray()
|
||||||
|
setIconsData(icons)
|
||||||
|
setIsLoaded(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load icons:", error)
|
||||||
|
setIsLoaded(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIcons()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Function to open the command menu
|
||||||
|
const openCommandMenu = () => {
|
||||||
|
setCommandMenuOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.header
|
<motion.header
|
||||||
className="border-b sticky top-0 z-50 backdrop-blur-2xl bg-background/50 border-border/50"
|
className="border-b sticky top-0 z-50 backdrop-blur-2xl bg-background/50 border-border/50"
|
||||||
@@ -28,6 +56,30 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 md:gap-4">
|
<div className="flex items-center gap-2 md:gap-4">
|
||||||
|
{/* Desktop search button */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<Button variant="outline" className="gap-2 cursor-pointer transition-all duration-300" onClick={openCommandMenu}>
|
||||||
|
<Search className="h-4 w-4 transition-all duration-300" />
|
||||||
|
<span>Find icons</span>
|
||||||
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border border-border/80 bg-muted/80 px-1.5 font-mono text-[10px] font-medium opacity-100">
|
||||||
|
<span className="text-xs">⌘</span>K
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile search button */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-lg cursor-pointer transition-all duration-300 hover:ring-2 "
|
||||||
|
onClick={openCommandMenu}
|
||||||
|
>
|
||||||
|
<Search className="h-5 w-5 transition-all duration-300" />
|
||||||
|
<span className="sr-only">Find icons</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="hidden md:flex items-center gap-2 md:gap-4">
|
<div className="hidden md:flex items-center gap-2 md:gap-4">
|
||||||
<IconSubmissionForm />
|
<IconSubmissionForm />
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -54,6 +106,9 @@ export function Header() {
|
|||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Single instance of CommandMenu */}
|
||||||
|
{isLoaded && <CommandMenu icons={iconsData} open={commandMenuOpen} onOpenChange={setCommandMenuOpen} />}
|
||||||
</motion.header>
|
</motion.header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,61 +205,13 @@ export function HeroSection({ totalIcons, stars }: { totalIcons: number; stars:
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8 mt-4 py-20">
|
<div className="relative z-10 container mx-auto px-4 md:px-6 mt-4 py-20">
|
||||||
<div className="max-w-4xl mx-auto text-center flex flex-col gap-4 ">
|
<div className="max-w-4xl mx-auto text-center flex flex-col gap-4 ">
|
||||||
<h1 className="relative text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-preset-slide-up motion-duration-500 ">
|
<h1 className="relative text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-preset-slide-up motion-duration-500 ">
|
||||||
Your definitive source for
|
Your definitive source for
|
||||||
<motion.span
|
<Sparkles className="absolute -right-1 -bottom-3 text-rose-500 h-8 w-8 sm:h-12 sm:w-12 md:h-16 md:w-12 motion-delay-300 motion-preset-seesaw-lg motion-scale-in-[0.5] motion-translate-x-in-[-120%] motion-translate-y-in-[-60%] motion-opacity-in-[33%] motion-rotate-in-[-1080deg] motion-blur-in-[10px] motion-duration-500 motion-delay-[0.13s]/scale motion-duration-[0.13s]/opacity motion-duration-[0.40s]/rotate motion-duration-[0.05s]/blur motion-delay-[0.20s]/blur motion-ease-spring-bouncier" />
|
||||||
className="absolute -right-1 -bottom-3"
|
|
||||||
initial={{ opacity: 0, scale: 0.5, x: -20, y: -10 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.5,
|
|
||||||
delay: 0.3,
|
|
||||||
ease: "easeOut"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
animate={{
|
|
||||||
y: [0, -3, 0],
|
|
||||||
rotate: [0, 5, 0]
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 3,
|
|
||||||
repeat: Infinity,
|
|
||||||
repeatType: "reverse",
|
|
||||||
ease: "easeInOut"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Sparkles className="text-rose-500 h-8 w-8 sm:h-12 sm:w-12 md:h-16 md:w-12" />
|
|
||||||
</motion.div>
|
|
||||||
</motion.span>
|
|
||||||
<br />
|
<br />
|
||||||
<motion.span
|
<Sparkles className="absolute -left-1 -top-3 text-rose-500 h-5 w-5 sm:h-8 sm:w-8 md:h-12 md:w-12 motion-delay-300 motion-preset-seesaw-lg motion-scale-in-[0.5] motion-translate-x-in-[159%] motion-translate-y-in-[-60%] motion-opacity-in-[33%] motion-rotate-in-[-1080deg] motion-blur-in-[10px] motion-duration-500 motion-delay-[0.13s]/scale motion-duration-[0.13s]/opacity motion-duration-[0.40s]/rotate motion-duration-[0.05s]/blur motion-delay-[0.20s]/blur motion-ease-spring-bouncier" />
|
||||||
className="absolute -left-1 -top-3"
|
|
||||||
initial={{ opacity: 0, scale: 0.5, x: 20, y: -10 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.5,
|
|
||||||
delay: 0.3,
|
|
||||||
ease: "easeOut"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
animate={{
|
|
||||||
y: [0, -3, 0],
|
|
||||||
rotate: [0, -5, 0]
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 4,
|
|
||||||
repeat: Infinity,
|
|
||||||
repeatType: "reverse",
|
|
||||||
ease: "easeInOut"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Sparkles className="text-rose-500 h-5 w-5 sm:h-8 sm:w-8 md:h-12 md:w-12" />
|
|
||||||
</motion.div>
|
|
||||||
</motion.span>
|
|
||||||
<AuroraText colors={["#FA5352", "#FA5352", "orange"]}>dashboard icons</AuroraText>
|
<AuroraText colors={["#FA5352", "#FA5352", "orange"]}>dashboard icons</AuroraText>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -272,7 +224,7 @@ export function HeroSection({ totalIcons, stars }: { totalIcons: number; stars:
|
|||||||
<SearchInput searchQuery={searchQuery} setSearchQuery={setSearchQuery} totalIcons={totalIcons} />
|
<SearchInput searchQuery={searchQuery} setSearchQuery={setSearchQuery} totalIcons={totalIcons} />
|
||||||
<div className="w-full flex gap-3 md:gap-4 flex-wrap justify-center motion-preset-slide-down motion-duration-500">
|
<div className="w-full flex gap-3 md:gap-4 flex-wrap justify-center motion-preset-slide-down motion-duration-500">
|
||||||
<Link href="/icons">
|
<Link href="/icons">
|
||||||
<InteractiveHoverButton className="rounded-md bg-input/30">Browse icons</InteractiveHoverButton>
|
<InteractiveHoverButton className="rounded-md bg-input/30">Explore icons</InteractiveHoverButton>
|
||||||
</Link>
|
</Link>
|
||||||
<GiveUsAStarButton stars={stars} />
|
<GiveUsAStarButton stars={stars} />
|
||||||
<GiveUsMoneyButton />
|
<GiveUsMoneyButton />
|
||||||
@@ -497,12 +449,12 @@ export function GiveUsMoneyButton() {
|
|||||||
<div className="flex justify-between items-center pt-2">
|
<div className="flex justify-between items-center pt-2">
|
||||||
<Link href={openCollectiveUrl} target="_blank" rel="noopener noreferrer">
|
<Link href={openCollectiveUrl} target="_blank" rel="noopener noreferrer">
|
||||||
<Button variant="default" size="sm" className="bg-primary hover:bg-primary/90">
|
<Button variant="default" size="sm" className="bg-primary hover:bg-primary/90">
|
||||||
Support
|
Donate
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`${openCollectiveUrl}/transactions`} target="_blank" rel="noopener noreferrer">
|
<Link href={`${openCollectiveUrl}/transactions`} target="_blank" rel="noopener noreferrer">
|
||||||
<Button variant="link" size="sm" className="flex items-center gap-1 text-xs text-secondary-foreground">
|
<Button variant="link" size="sm" className="flex items-center gap-1 text-xs text-secondary-foreground">
|
||||||
View transactions
|
View expenses
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -526,7 +478,7 @@ function SearchInput({ searchQuery, setSearchQuery, totalIcons }: SearchInputPro
|
|||||||
name="q"
|
name="q"
|
||||||
autoFocus
|
autoFocus
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search for icons..."
|
placeholder={`Find any of ${totalIcons} icons by name or category...`}
|
||||||
className="pl-10 h-10 md:h-12 rounded-lg w-full border-border focus:border-primary/20 text-sm md:text-base"
|
className="pl-10 h-10 md:h-12 rounded-lg w-full border-border focus:border-primary/20 text-sm md:text-base"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 rounded-lg cursor-pointer"
|
className="h-8 w-8 rounded-lg cursor-pointer"
|
||||||
onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)}
|
onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)}
|
||||||
aria-label={`Download ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
|
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -224,7 +223,6 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 rounded-lg cursor-pointer"
|
className="h-8 w-8 rounded-lg cursor-pointer"
|
||||||
onClick={(e) => handleCopy(imageUrl, `btn-${variantKey}`, e)}
|
onClick={(e) => handleCopy(imageUrl, `btn-${variantKey}`, e)}
|
||||||
aria-label={`Copy URL for ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
|
|
||||||
>
|
>
|
||||||
{copiedVariants[`btn-${variantKey}`] ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
{copiedVariants[`btn-${variantKey}`] ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -236,18 +234,8 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild>
|
||||||
variant="outline"
|
<Link href={githubUrl} target="_blank" rel="noopener noreferrer">
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 rounded-lg"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={githubUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label={`View ${iconName} ${format} file on GitHub`}
|
|
||||||
>
|
|
||||||
<Github className="w-4 h-4" />
|
<Github className="w-4 h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -264,7 +252,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto pt-12 pb-14 px-4 sm:px-6 lg:px-8">
|
<div className="container mx-auto pt-12 pb-14">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||||
{/* Left Column: Icon Info and Author */}
|
{/* Left Column: Icon Info and Author */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
@@ -318,7 +306,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
|
|
||||||
{iconData.categories && iconData.categories.length > 0 && (
|
{iconData.categories && iconData.categories.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Categories</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground">Categories</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{iconData.categories.map((category) => (
|
{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">
|
||||||
@@ -339,7 +327,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
|
|
||||||
{iconData.aliases && iconData.aliases.length > 0 && (
|
{iconData.aliases && iconData.aliases.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Aliases</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground">Aliases</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{iconData.aliases.map((alias) => (
|
{iconData.aliases.map((alias) => (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -356,17 +344,19 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<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">About this icon</h3>
|
||||||
<div className="text-xs text-muted-foreground space-y-2">
|
<div className="text-xs text-muted-foreground space-y-2">
|
||||||
<p>
|
<p>
|
||||||
Available in {availableFormats.length > 1
|
Available in{" "}
|
||||||
? `${availableFormats.length} formats (${availableFormats.map((f) => f.toUpperCase()).join(", ")}) `
|
{availableFormats.length > 1
|
||||||
: `${availableFormats[0].toUpperCase()} format `}
|
? `${availableFormats.length} formats (${availableFormats.map((f) => f.toUpperCase()).join(", ")})`
|
||||||
|
: `${availableFormats[0].toUpperCase()} format`}{" "}
|
||||||
with a base format of {iconData.base.toUpperCase()}.
|
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.colors && " Includes both light and dark theme variants for better integration with different UI designs."}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Perfect for adding to dashboards, app directories, documentation, or anywhere you need the {icon.replace(/-/g, " ")} logo.
|
Use the {icon} icon in your web applications, dashboards, or documentation to enhance visual communication and user
|
||||||
|
experience.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,7 +412,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="">
|
<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">Base format</h3>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FileType className="w-4 h-4 text-blue-500" />
|
<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>
|
||||||
@@ -430,7 +420,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="">
|
<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">Available formats</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{availableFormats.map((format) => (
|
{availableFormats.map((format) => (
|
||||||
<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">
|
||||||
@@ -442,7 +432,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
|
|
||||||
{iconData.colors && (
|
{iconData.colors && (
|
||||||
<div className="">
|
<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">Color variants</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.entries(iconData.colors).map(([theme, variant]) => (
|
{Object.entries(iconData.colors).map(([theme, variant]) => (
|
||||||
<div key={theme} className="flex items-center gap-2">
|
<div key={theme} className="flex items-center gap-2">
|
||||||
@@ -456,7 +446,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<h3 className="text-sm font-semibold text-muted-foreground mb-2">Source</h3>
|
<h3 className="text-sm font-semibold text-muted-foreground">Source</h3>
|
||||||
<Button variant="outline" className="w-full" asChild>
|
<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" />
|
<Github className="w-4 h-4 mr-2" />
|
||||||
|
|||||||
@@ -11,32 +11,32 @@ import { useState } from "react"
|
|||||||
export const ISSUE_TEMPLATES = [
|
export const ISSUE_TEMPLATES = [
|
||||||
{
|
{
|
||||||
id: "add_monochrome_icon",
|
id: "add_monochrome_icon",
|
||||||
name: "Add light/dark icon",
|
name: "Add light & dark icon",
|
||||||
description: "Submit a new icon with light and dark versions.",
|
description: "Submit a new icon with both light and dark versions for optimal theme compatibility.",
|
||||||
url: `${REPO_PATH}/issues/new?template=add_monochrome_icon.yml`,
|
url: `${REPO_PATH}/issues/new?template=add_monochrome_icon.yml`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "add_normal_icon",
|
id: "add_normal_icon",
|
||||||
name: "Add standard icon",
|
name: "Add normal icon",
|
||||||
description: "Submit a new icon for both themes.",
|
description: "Submit a new icon that works well across both light and dark themes.",
|
||||||
url: `${REPO_PATH}/issues/new?template=add_normal_icon.yml`,
|
url: `${REPO_PATH}/issues/new?template=add_normal_icon.yml`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "update_monochrome_icon",
|
id: "update_monochrome_icon",
|
||||||
name: "Update light/dark icon",
|
name: "Update light & dark icon",
|
||||||
description: "Improve or update an existing light/dark icon.",
|
description: "Improve an existing icon by updating both light and dark versions.",
|
||||||
url: `${REPO_PATH}/issues/new?template=update_monochrome_icon.yml`,
|
url: `${REPO_PATH}/issues/new?template=update_monochrome_icon.yml`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "update_normal_icon",
|
id: "update_normal_icon",
|
||||||
name: "Update standard icon",
|
name: "Update normal icon",
|
||||||
description: "Improve or update an existing standard icon.",
|
description: "Improve an existing icon that works across both light and dark themes.",
|
||||||
url: `${REPO_PATH}/issues/new?template=update_normal_icon.yml`,
|
url: `${REPO_PATH}/issues/new?template=update_normal_icon.yml`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "blank_issue",
|
id: "blank_issue",
|
||||||
name: "Other request",
|
name: "Something else",
|
||||||
description: "Submit another type of request.",
|
description: "Create a custom issue for other suggestions, bug reports, or improvements.",
|
||||||
url: `${REPO_PATH}/issues/new?template=BLANK_ISSUE`,
|
url: `${REPO_PATH}/issues/new?template=BLANK_ISSUE`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -73,13 +73,13 @@ export function IconSubmissionForm() {
|
|||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" className="hidden md:inline-flex cursor-pointer transition-all duration-300">
|
<Button variant="outline" className="hidden md:inline-flex cursor-pointer transition-all duration-300">
|
||||||
<PlusCircle className="h-4 w-4 transition-all duration-300" /> Submit icon(s)
|
<PlusCircle className="h-4 w-4 transition-all duration-300" /> Contribute new icon
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="md:max-w-4xl backdrop-blur-2xl bg-background">
|
<DialogContent className="md:max-w-4xl backdrop-blur-2xl bg-background">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Submit an icon</DialogTitle>
|
<DialogTitle>Contribute a new icon</DialogTitle>
|
||||||
<DialogDescription>Select an option below to submit or update an icon.</DialogDescription>
|
<DialogDescription>Choose a template below to suggest a new icon or improve an existing one.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<IconSubmissionContent onClose={() => setOpen(false)} />
|
<IconSubmissionContent onClose={() => setOpen(false)} />
|
||||||
|
|||||||
33
web/src/components/magicui/animated-shiny-text.tsx
Normal file
33
web/src/components/magicui/animated-shiny-text.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { CSSProperties, ComponentPropsWithoutRef, FC } from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<"span"> {
|
||||||
|
shimmerWidth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({ children, className, shimmerWidth = 100, ...props }) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--shiny-width": `${shimmerWidth}px`,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70",
|
||||||
|
|
||||||
|
// Shine effect
|
||||||
|
"animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
|
||||||
|
|
||||||
|
// Shine gradient
|
||||||
|
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
|
||||||
|
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { motion, useMotionTemplate, useMotionValue } from "motion/react"
|
import { motion, useMotionTemplate, useMotionValue } from "motion/react"
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef } from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@ export function MagicCard({
|
|||||||
const cardRef = useRef<HTMLDivElement>(null)
|
const cardRef = useRef<HTMLDivElement>(null)
|
||||||
const mouseX = useMotionValue(-gradientSize)
|
const mouseX = useMotionValue(-gradientSize)
|
||||||
const mouseY = useMotionValue(-gradientSize)
|
const mouseY = useMotionValue(-gradientSize)
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
|
||||||
|
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseMove = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
@@ -61,14 +60,6 @@ export function MagicCard({
|
|||||||
}, [handleMouseMove, mouseX, gradientSize, mouseY])
|
}, [handleMouseMove, mouseX, gradientSize, mouseY])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMounted(true)
|
|
||||||
mouseX.set(-gradientSize)
|
|
||||||
mouseY.set(-gradientSize)
|
|
||||||
}, [gradientSize, mouseX, mouseY])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isMounted) return
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", handleMouseMove)
|
document.addEventListener("mousemove", handleMouseMove)
|
||||||
document.addEventListener("mouseout", handleMouseOut)
|
document.addEventListener("mouseout", handleMouseOut)
|
||||||
document.addEventListener("mouseenter", handleMouseEnter)
|
document.addEventListener("mouseenter", handleMouseEnter)
|
||||||
@@ -78,10 +69,15 @@ export function MagicCard({
|
|||||||
document.removeEventListener("mouseout", handleMouseOut)
|
document.removeEventListener("mouseout", handleMouseOut)
|
||||||
document.removeEventListener("mouseenter", handleMouseEnter)
|
document.removeEventListener("mouseenter", handleMouseEnter)
|
||||||
}
|
}
|
||||||
}, [isMounted, handleMouseEnter, handleMouseMove, handleMouseOut])
|
}, [handleMouseEnter, handleMouseMove, handleMouseOut])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mouseX.set(-gradientSize)
|
||||||
|
mouseY.set(-gradientSize)
|
||||||
|
}, [gradientSize, mouseX, mouseY])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("group relative rounded-[inherit]", className)}>
|
<div ref={cardRef} className={cn("group relative rounded-[inherit]", className)}>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="pointer-events-none absolute inset-0 rounded-[inherit] bg-border duration-300 group-hover:opacity-100"
|
className="pointer-events-none absolute inset-0 rounded-[inherit] bg-border duration-300 group-hover:opacity-100"
|
||||||
style={{
|
style={{
|
||||||
@@ -104,7 +100,7 @@ export function MagicCard({
|
|||||||
opacity: gradientOpacity,
|
opacity: gradientOpacity,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div ref={cardRef} className="relative">{children}</div>
|
<div className="relative">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) {
|
|||||||
{/* Background glow */}
|
{/* Background glow */}
|
||||||
<div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true" />
|
<div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true" />
|
||||||
|
|
||||||
<div className="mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto px-6 lg:px-8">
|
||||||
<div className="mx-auto max-w-2xl text-center my-4">
|
<div className="mx-auto max-w-2xl text-center my-4">
|
||||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl bg-clip-text text-transparent bg-gradient-to-r from-rose-600 to-rose-500 motion-safe:motion-preset-fade-lg motion-duration-500">
|
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl bg-clip-text text-transparent bg-gradient-to-r from-rose-600 to-rose-500 motion-safe:motion-preset-fade-lg motion-duration-500">
|
||||||
Recently Added Icons
|
Recently Added Icons
|
||||||
@@ -61,7 +61,7 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) {
|
|||||||
href="/icons"
|
href="/icons"
|
||||||
className="font-medium inline-flex items-center py-2 px-4 rounded-full border transition-all duration-200 group hover-lift soft-shadow"
|
className="font-medium inline-flex items-center py-2 px-4 rounded-full border transition-all duration-200 group hover-lift soft-shadow"
|
||||||
>
|
>
|
||||||
View all icons
|
View complete collection
|
||||||
<ArrowRight className="w-4 h-4 ml-1.5 transition-transform duration-200 group-hover:translate-x-1" />
|
<ArrowRight className="w-4 h-4 ml-1.5 transition-transform duration-200 group-hover:translate-x-1" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +1,22 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
type StructuredDataProps = {
|
type StructuredDataProps = {
|
||||||
data: any
|
data: Record<string, unknown>
|
||||||
id?: string
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StructuredData = ({ data, id }: StructuredDataProps) => {
|
export const StructuredData = ({ data, id }: StructuredDataProps) => {
|
||||||
return (
|
return <script id={id} type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} />
|
||||||
<script
|
|
||||||
id={id}
|
|
||||||
type="application/ld+json"
|
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteStructuredDataProps = {
|
type WebsiteStructuredDataProps = {
|
||||||
websiteSchema: any
|
websiteSchema: Record<string, unknown>
|
||||||
organizationSchema: any
|
organizationSchema: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WebsiteStructuredData = ({
|
export const WebsiteStructuredData = ({ websiteSchema, organizationSchema }: WebsiteStructuredDataProps) => {
|
||||||
websiteSchema,
|
return (
|
||||||
organizationSchema
|
<>
|
||||||
}: WebsiteStructuredDataProps) => {
|
<StructuredData data={websiteSchema} id="website-schema" />
|
||||||
return (
|
<StructuredData data={organizationSchema} id="organization-schema" />
|
||||||
<>
|
</>
|
||||||
<StructuredData data={websiteSchema} id="website-schema" />
|
)
|
||||||
<StructuredData data={organizationSchema} id="organization-schema" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
177
web/src/components/ui/command.tsx
Normal file
177
web/src/components/ui/command.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent className="overflow-hidden p-0">
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ export const DEFAULT_KEYWORDS = [
|
|||||||
"free icons",
|
"free icons",
|
||||||
"SVG icons",
|
"SVG icons",
|
||||||
"web dashboard",
|
"web dashboard",
|
||||||
"app directory"
|
"app directory",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const BROWSE_KEYWORDS = [
|
export const BROWSE_KEYWORDS = [
|
||||||
@@ -44,72 +44,79 @@ export const BROWSE_KEYWORDS = [
|
|||||||
"minimal icons",
|
"minimal icons",
|
||||||
"dashboard design",
|
"dashboard design",
|
||||||
"UI icons",
|
"UI icons",
|
||||||
...DEFAULT_KEYWORDS
|
...DEFAULT_KEYWORDS,
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ICON_DETAIL_KEYWORDS = (iconName: string) => [
|
// Add format-specific keywords
|
||||||
`${iconName} icon`,
|
export const ICON_DETAIL_KEYWORDS = (iconName: string): string[] => [
|
||||||
`${iconName} logo`,
|
`${iconName} icon`, // e.g., "Homarr icon"
|
||||||
`${iconName} svg`,
|
`${iconName} logo`, // e.g., "Homarr logo"
|
||||||
`${iconName} download`,
|
`${iconName} svg icon`, // e.g., "Homarr svg icon"
|
||||||
`${iconName} dashboard icon`,
|
`${iconName} png icon`, // e.g., "Homarr png icon"
|
||||||
...DEFAULT_KEYWORDS
|
`${iconName} webp icon`, // e.g., "Homarr webp icon"
|
||||||
|
`${iconName} download`, // e.g., "Homarr download"
|
||||||
|
`${iconName} dashboard icon`, // e.g., "Homarr dashboard icon"
|
||||||
|
...DEFAULT_KEYWORDS,
|
||||||
]
|
]
|
||||||
|
|
||||||
// Core structured data for the website (JSON-LD)
|
// Core structured data for the website (JSON-LD)
|
||||||
export const getWebsiteSchema = (totalIcons: number) => ({
|
export const getWebsiteSchema = (totalIcons: number) => ({
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": SITE_NAME,
|
name: SITE_NAME,
|
||||||
"url": WEB_URL,
|
url: WEB_URL,
|
||||||
"description": getDescription(totalIcons),
|
description: getDescription(totalIcons),
|
||||||
"potentialAction": {
|
potentialAction: {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
"target": {
|
target: {
|
||||||
"@type": "EntryPoint",
|
"@type": "EntryPoint",
|
||||||
"urlTemplate": `${WEB_URL}/icons?q={search_term_string}`
|
urlTemplate: `${WEB_URL}/icons?q={search_term_string}`,
|
||||||
},
|
},
|
||||||
"query-input": "required name=search_term_string"
|
"query-input": "required name=search_term_string",
|
||||||
},
|
},
|
||||||
"slogan": SITE_TAGLINE
|
slogan: SITE_TAGLINE,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Organization schema
|
// Organization schema
|
||||||
export const ORGANIZATION_SCHEMA = {
|
export const ORGANIZATION_SCHEMA = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": ORGANIZATION_NAME,
|
name: ORGANIZATION_NAME,
|
||||||
"url": `https://github.com/${REPO_NAME}`,
|
url: `https://github.com/${REPO_NAME}`,
|
||||||
"logo": `${WEB_URL}/og-image.png`,
|
logo: `${WEB_URL}/og-image.png`,
|
||||||
"sameAs": [
|
sameAs: [`https://github.com/${REPO_NAME}`, "https://homarr.dev"],
|
||||||
`https://github.com/${REPO_NAME}`,
|
slogan: SITE_TAGLINE,
|
||||||
"https://homarr.dev"
|
|
||||||
],
|
|
||||||
"slogan": SITE_TAGLINE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Social media
|
// Social media
|
||||||
export const GITHUB_URL = `https://github.com/${REPO_NAME}`
|
export const GITHUB_URL = `https://github.com/${REPO_NAME}`
|
||||||
|
|
||||||
// Image schemas
|
// Image schemas
|
||||||
export const getIconSchema = (iconName: string, iconId: string, authorName: string, authorUrl: string, updateDate: string, totalIcons: number) => ({
|
export const getIconSchema = (
|
||||||
|
iconName: string,
|
||||||
|
iconId: string,
|
||||||
|
authorName: string,
|
||||||
|
authorUrl: string,
|
||||||
|
updateDate: string,
|
||||||
|
totalIcons: number,
|
||||||
|
) => ({
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "ImageObject",
|
"@type": "ImageObject",
|
||||||
"name": `${iconName} Icon`,
|
name: `${iconName} Icon`,
|
||||||
"description": getIconDescription(iconName, totalIcons),
|
description: getIconDescription(iconName, totalIcons),
|
||||||
"contentUrl": `${BASE_URL}/png/${iconId}.png`,
|
contentUrl: `${BASE_URL}/png/${iconId}.png`,
|
||||||
"thumbnailUrl": `${BASE_URL}/png/${iconId}.png`,
|
thumbnailUrl: `${BASE_URL}/png/${iconId}.png`,
|
||||||
"uploadDate": updateDate,
|
uploadDate: updateDate,
|
||||||
"author": {
|
author: {
|
||||||
"@type": "Person",
|
"@type": "Person",
|
||||||
"name": authorName,
|
name: authorName,
|
||||||
"url": authorUrl
|
url: authorUrl,
|
||||||
},
|
},
|
||||||
"encodingFormat": ["image/png", "image/svg+xml", "image/webp"],
|
encodingFormat: ["image/png", "image/svg+xml", "image/webp"],
|
||||||
"contentSize": "Variable",
|
contentSize: "Variable",
|
||||||
"representativeOfPage": true,
|
representativeOfPage: true,
|
||||||
"creditText": `Icon contributed by ${authorName} to the ${SITE_NAME} collection by ${ORGANIZATION_NAME}`,
|
creditText: `Icon contributed by ${authorName} to the ${SITE_NAME} collection by ${ORGANIZATION_NAME}`,
|
||||||
"embedUrl": `${WEB_URL}/icons/${iconId}`
|
embedUrl: `${WEB_URL}/icons/${iconId}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
// OpenGraph defaults
|
// OpenGraph defaults
|
||||||
@@ -118,5 +125,5 @@ export const DEFAULT_OG_IMAGE = {
|
|||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: `${SITE_NAME} - ${SITE_TAGLINE}`,
|
alt: `${SITE_NAME} - ${SITE_TAGLINE}`,
|
||||||
type: "image/png"
|
type: "image/png",
|
||||||
}
|
}
|
||||||
|
|||||||
25
web/src/hooks/use-media-query.ts
Normal file
25
web/src/hooks/use-media-query.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
export function useMediaQuery(query: string): boolean {
|
||||||
|
const [matches, setMatches] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const media = window.matchMedia(query)
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
if (media.matches !== matches) {
|
||||||
|
setMatches(media.matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup listener for changes
|
||||||
|
const listener = () => setMatches(media.matches)
|
||||||
|
media.addEventListener("change", listener)
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => media.removeEventListener("change", listener)
|
||||||
|
}, [query, matches])
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user