Site improvements (#1366)

* write cions details

* add details

* Add icons details

* update gitignore

* Add node details

* Move tags api to own composable

* remove overridden var

* remopve whitespace

* log directory existence

* Fix path name

* Fix build

* Fix tags fetching

* Move max related icons to prebuild

* Improve tags scroller

* Add categories call

* cleanup

* Add active state
This commit is contained in:
Eric Fennis
2023-06-15 14:44:43 +02:00
committed by GitHub
parent 82db590192
commit 34155d48e7
25 changed files with 351 additions and 81 deletions

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ docs/.vitepress/data/iconNodes
docs/.vitepress/data/iconMetaData.ts
docs/.vitepress/data/releaseMetaData.json
docs/.vitepress/data/releaseMetaData
docs/.vitepress/data/iconDetails
docs/.vitepress/data/relatedIcons.json
docs/.vercel
docs/.nitro

View File

@@ -0,0 +1,11 @@
import { eventHandler, setResponseHeader } from 'h3'
import iconMetaData from '../../data/iconMetaData'
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { categories }]) => [ name, categories ])
)
})

View File

@@ -8,6 +8,7 @@ export default eventHandler((event) => {
const withUniqueKeys = query.withUniqueKeys === 'true'
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
if (withUniqueKeys) {
return iconNodes

View File

@@ -38,6 +38,7 @@ export default eventHandler((event) => {
defaultContentType(event, 'image/svg+xml')
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return svg

View File

@@ -3,6 +3,7 @@ import iconMetaData from '../../data/iconMetaData'
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { tags }]) => [ name, tags ])

View File

@@ -5,6 +5,7 @@ import { createWriteStream } from 'node:fs'
import { resolve } from 'node:path'
import { SitemapStream } from 'sitemap'
import sidebar from './sidebar';
import fs from 'fs';
const links = []
@@ -35,7 +36,7 @@ export default defineConfig({
)
}
]
}
},
},
head: [
[ 'script', {
@@ -130,10 +131,6 @@ export default defineConfig({
return
}
if (pageData.relativePath === 'index.md') {
console.log('Home!');
}
if (pageData.relativePath.startsWith('icons/')) {
links.push({
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2'),

View File

@@ -1,30 +1,38 @@
<script setup lang="ts">
import type { IconEntity } from '../../types'
import { computed, ref, watch } from 'vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import IconButton from '../base/IconButton.vue';
import IconContributors from './IconContributors.vue';
import IconPreview from './IconPreview.vue';
import { x, expand } from '../../../data/iconNodes'
import { useRouter } from 'vitepress';
import IconInfo from './IconInfo.vue';
import Badge from '../base/Badge.vue';
import type { IconEntity } from '../../types'
import { computed, ref, watch } from 'vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import IconButton from '../base/IconButton.vue';
import IconContributors from './IconContributors.vue';
import IconPreview from './IconPreview.vue';
import { x, expand } from '../../../data/iconNodes'
import { useRouter } from 'vitepress';
import IconInfo from './IconInfo.vue';
import Badge from '../base/Badge.vue';
import { computedAsync } from '@vueuse/core';
const props = defineProps<{
icon: IconEntity
}>()
const props = defineProps<{
iconName: string
}>()
const emit = defineEmits(['close'])
const isOpen = computed(() => !!props.icon)
function onClose() {
emit('close')
const icon = computedAsync<IconEntity | null>(async () => {
if (props.iconName) {
return (await import(`../../../data/iconDetails/${props.iconName}.ts`)).default as IconEntity
}
return null
}, null)
const { go } = useRouter()
const emit = defineEmits(['close'])
const isOpen = computed(() => !!icon.value)
const CloseIcon = createLucideIcon('Close', x)
const Expand = createLucideIcon('Expand', expand)
function onClose() {
emit('close')
}
const { go } = useRouter()
const CloseIcon = createLucideIcon('Close', x)
const Expand = createLucideIcon('Expand', expand)
</script>
<template>
@@ -109,9 +117,8 @@
}
.icon-info {
padding: 0 24px;
padding-left: 24px;
flex-basis: 100%;
}
.icon-tags {

View File

@@ -17,7 +17,7 @@ const { go } = useRouter()
const { page } = useData()
const tags = computed(() => {
if (!props.icon) return []
if (!props.icon || !props?.icon?.tags) return []
return props.icon.tags.join(' • ')
})
</script>
@@ -27,9 +27,11 @@ const tags = computed(() => {
<IconDetailName class="icon-name">
{{ icon.name }}
</IconDetailName>
<p class="icon-tags">
{{ tags }}
</p>
<div class="tags-scroller" v-if="tags.length">
<p class="icon-tags horizontal-scroller">
{{ tags }}
</p>
</div>
<div class="group">
<Badge
v-for="category in icon.categories"
@@ -72,9 +74,61 @@ const tags = computed(() => {
font-size: 16px;
color: var(--vp-c-text-2);
font-weight: 500;
margin-top: 0;;
margin-bottom: 16px;
line-height: 28px;
white-space: nowrap;
position: absolute;
top: 0;
left: 0;
right: 0;
margin-top: 0;
margin-bottom: 0;
}
.tags-scroller {
position: relative;
max-width: 100%;
width: 100%;
height: 28px;
padding: 8px 0 16px;
margin-bottom: 16px;
margin-top: 8px;
align-items: center;
--gradient-background: var(--tags-gradient-background, var(--vp-c-bg-elv))
}
.horizontal-scroller {
overflow-x: scroll;
/* Hide Scrollbar */
-ms-overflow-style: none;
scrollbar-width: none;
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
scrollbar-color: currentColor transparent; /* foreground background */
}
.horizontal-scroller::-webkit-scrollbar {
width: 0;
display: none
}
.horizontal-scroller::-webkit-scrollbar-track {
background: transparent
}
.horizontal-scroller::-webkit-scrollbar-thumb {
background: transparent;
border: none
}
.tags-scroller::after {
content: '';
position: absolute;
bottom: 0;
width: 32px;
height: 100%;
/* Background Gradient left to right */
background: linear-gradient(to right, rgba(255,255,255,0) 0%,var(--gradient-background) 100%);
right: 0;
pointer-events: none;
}
.buttons {

View File

@@ -6,6 +6,9 @@ import InputSearch from '../base/InputSearch.vue'
import useSearchInput from '../../composables/useSearchInput'
import StickyBar from './StickyBar.vue'
import IconsCategory from './IconsCategory.vue'
import { useFetch } from '@vueuse/core'
import useFetchTags from '../../composables/useFetchTags'
import useFetchCategories from '../../composables/useFetchCategories'
const props = defineProps<{
icons: IconEntity[]
@@ -22,7 +25,26 @@ function setActiveIconName(name: string) {
activeIconName.value = name
}
const searchResults = useSearch(searchQuery, props.icons, [
const { execute: fetchTags, data: tags } = useFetchTags()
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories()
const mappedIcons = computed(() => {
if(tags.value == null) {
return props.icons
}
return props.icons.map((icon) => {
const iconTags = tags.value[icon.name]
const iconCategories = categoriesMap.value?.[icon.name] ?? []
return {
...icon,
tags: iconTags,
categories: iconCategories,
}
})
})
const searchResults = useSearch(searchQuery, mappedIcons, [
{ name: 'name', weight: 2 },
{ name: 'tags', weight: 1 },
])
@@ -37,7 +59,6 @@ const categories = computed(() => {
return iconCategories?.includes(name)
})
const searchedCategoryIcons = isSearching
? categoryIcons.filter(icon => searchResults.value.some((item) => item?.name === icon?.name))
: categoryIcons;
@@ -51,9 +72,14 @@ const categories = computed(() => {
.filter(({ icons }) => icons.length)
})
const activeIcon = computed(() =>
props.icons?.find((icon) => icon.name === activeIconName.value)
)
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags()
}
if (categoriesMap.value == null) {
fetchCategories()
}
}
const NoResults = defineAsyncComponent(() =>
import('./NoResults.vue')
@@ -71,6 +97,7 @@ const IconDetailOverlay = defineAsyncComponent(() =>
v-model="searchQuery"
class="input-wrapper"
ref="searchInput"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
@@ -87,7 +114,7 @@ const IconDetailOverlay = defineAsyncComponent(() =>
/>
<IconDetailOverlay
v-if="activeIconName != null"
:icon="activeIcon"
:iconName="activeIconName"
@close="setActiveIconName('')"
/>
</template>

View File

@@ -8,6 +8,8 @@ import useSearch from '../../composables/useSearch'
import EndOfPage from '../base/EndOfPage.vue'
import useSearchInput from '../../composables/useSearchInput'
import StickyBar from './StickyBar.vue'
import useFetchTags from '../../composables/useFetchTags'
import useFetchCategories from '../../composables/useFetchCategories'
const props = defineProps<{
icons: IconEntity[]
@@ -22,7 +24,7 @@ const isSmallScreen = useMediaQuery('(min-width: 640px)');
const pageSize = computed(() => {
if(isExtraLargeScreen.value) {
return 16 * 16;
return 16 * 20;
}
if(isLargeScreen.value) {
return 16 * 12;
@@ -38,8 +40,28 @@ const pageSize = computed(() => {
return 10 * 5;
})
const { execute: fetchTags, data: tags } = useFetchTags()
const { execute: fetchCategories, data: categories } = useFetchCategories()
const mappedIcons = computed(() => {
if(tags.value == null) {
return props.icons
}
return props.icons.map((icon) => {
const iconTags = tags.value[icon.name]
const iconCategories = categories.value?.[icon.name] ?? []
return {
...icon,
tags: iconTags,
categories: iconCategories,
}
})
})
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
const searchResults = useSearch(searchQueryThrottled, props.icons, [
const searchResults = useSearch(searchQueryThrottled, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
@@ -58,12 +80,19 @@ function setActiveIconName(name: string) {
activeIconName.value = name
}
const activeIcon = computed(() => props.icons.find((icon) => icon.name === activeIconName.value))
watch(searchQueryThrottled, (searchString) => {
currentPage.value = 1
})
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags()
}
if (categories.value == null) {
fetchCategories()
}
}
const NoResults = defineAsyncComponent(() =>
import('./NoResults.vue')
)
@@ -81,6 +110,7 @@ const IconDetailOverlay = defineAsyncComponent(() =>
v-model="searchQuery"
ref="searchInput"
class="input-wrapper"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
@@ -97,7 +127,7 @@ const IconDetailOverlay = defineAsyncComponent(() =>
<EndOfPage @end-of-page="next" class="bottom-page"/>
<IconDetailOverlay
v-if="activeIconName != null"
:icon="activeIcon"
:iconName="activeIconName"
@close="setActiveIconName('')"
/>
</template>

View File

@@ -32,8 +32,8 @@ useEventListener(document, 'mousemove', (mouseEvent) => {
No icons found for '{{ searchQuery }}'
</h2>
<VPButton text="Clear your search and try again" theme="alt" @click="$emit('clear')"/>
or
<VPButton text="Check if someone has already requested this icon"
<span class="text-divider">or</span>
<VPButton text="Search on Github issues"
theme="alt"
:href="`https://github.com/lucide-icons/lucide/issues?q=is%3Aopen+${searchQuery}`"
target="_blank"
@@ -74,4 +74,10 @@ useEventListener(document, 'mousemove', (mouseEvent) => {
margin-bottom: 32px;
text-align: center;
}
.text-divider {
margin: 12px 0;
font-size: 16px;
color: var(--vp-c-neutral);
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { shallowRef, type Ref, watch } from 'vue'
import { shallowRef, type Ref, watch, computed } from 'vue'
import { useCssVar, syncRef } from '@vueuse/core'
import { useIconStyleContext } from '../../composables/useIconStyle'
import { STYLE_DEFAULTS, useIconStyleContext } from '../../composables/useIconStyle'
import RangeSlider from '../base/RangeSlider.vue'
import InputField from '../base/InputField.vue'
import ColorPicker from '../base/ColorPicker.vue'
@@ -19,7 +19,7 @@ const colorCssVar = useCssVar(
'--customize-color',
props.rootEl?.value ?? documentRef.value,
{
initialValue: 'default'
initialValue: `${STYLE_DEFAULTS.color}`
}
)
@@ -27,7 +27,7 @@ const strokeWidthCssVar = useCssVar(
'--customize-strokeWidth',
props.rootEl?.value ?? documentRef.value,
{
initialValue: '2'
initialValue: `${STYLE_DEFAULTS.strokeWidth}`
}
)
@@ -35,7 +35,7 @@ const sizeCssVar = useCssVar(
'--customize-size',
props.rootEl?.value ?? documentRef.value,
{
initialValue: '24'
initialValue: `${STYLE_DEFAULTS.size}`
}
)
@@ -44,9 +44,9 @@ syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' })
syncRef(size, sizeCssVar, { direction: 'ltr' })
function resetStyle () {
color.value = 'currentColor'
strokeWidth.value = 2
size.value = 24
color.value = STYLE_DEFAULTS.color
strokeWidth.value = STYLE_DEFAULTS.strokeWidth
size.value = STYLE_DEFAULTS.size
}
watch(absoluteStrokeWidth, (enabled) => {
@@ -54,10 +54,18 @@ watch(absoluteStrokeWidth, (enabled) => {
htmlEl.classList.toggle('absolute-stroke-width', enabled)
})
const customizingActive = computed(() => {
return color.value !== STYLE_DEFAULTS.color
|| strokeWidth.value !== STYLE_DEFAULTS.strokeWidth
|| size.value !== STYLE_DEFAULTS.size
})
</script>
<template>
<div class="customizer-card">
<div class="customizer-card" :class="{ customized: customizingActive }">
<div class="card-header">
<h2 class="card-title">
Customizer
@@ -142,6 +150,12 @@ watch(absoluteStrokeWidth, (enabled) => {
margin-bottom: 24px;
position: relative;
z-index: 0;
border: 1px solid transparent;
transition: border-color .4s ease-in-out;
}
.customizer-card.customized {
border-color: var(--vp-c-brand);
}
.color-picker {

View File

@@ -0,0 +1,12 @@
import { useFetch } from "@vueuse/core"
const useFetchCategories = () => useFetch<Record<string, string[]>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/categories`,
{
immediate:
typeof window !== 'undefined'
&& new URLSearchParams(window.location.search).has('search'),
}
).json()
export default useFetchCategories

View File

@@ -0,0 +1,12 @@
import { useFetch } from "@vueuse/core"
const useFetchTags = () => useFetch<Record<string, string[]>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/tags`,
{
immediate:
typeof window !== 'undefined'
&& new URLSearchParams(window.location.search).has('search'),
}
).json()
export default useFetchTags

View File

@@ -10,8 +10,16 @@ interface IconSizeContext {
size: Ref<number>
strokeWidth: Ref<number>
color: Ref<string>
absoluteStrokeWidth: Ref<boolean>
}
export const STYLE_DEFAULTS = {
size: 24,
strokeWidth: 2,
color: 'currentColor',
absoluteStrokeWidth: false,
};
export const iconStyleContext = {
size: ref(24),
strokeWidth: ref(2),

View File

@@ -1,20 +1,22 @@
import Fuse from 'fuse.js';
import { shallowRef, computed, Ref } from 'vue';
const useSearch = <T>(query: Ref<string>, collection: T[], keys: Fuse.FuseOptionKey<T>[] = []) => {
const useSearch = <T>(query: Ref<string>, collection: Ref<T[]>, keys: Fuse.FuseOptionKey<T>[] = []) => {
const index = shallowRef(
new Fuse(collection, {
new Fuse(collection.value, {
threshold: 0.2,
keys,
})
)
const results = computed(() => {
index.value.setCollection(collection.value);
if (query.value) {
return index.value.search(query.value).map((result) => result.item);
}
return collection;
return collection.value;
});
return results;

View File

@@ -11,12 +11,16 @@ const useSearchInput = () => {
|| ''
)
)
const searchQueryThrottled = refThrottled(searchQuery, 200)
const searchQueryThrottled = refThrottled(searchQuery, 400)
watch(searchQueryThrottled, (searchString) => {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('search', searchString);
if(searchString === '') {
newUrl.searchParams.delete('search');
} else {
newUrl.searchParams.set('search', searchString);
}
nextTick(() => {
window.history.replaceState({}, '', newUrl)

View File

@@ -41,47 +41,47 @@ const codeExample = computed(() => data.codeExamples?.map(
<div :class="$style.iconPreviews">
<IconPreview
id="previewer"
:name="$params.name"
:iconNode="$params.iconNode"
:name="params.name"
:iconNode="params.iconNode"
:class="$style.preview"
/>
<IconPreviewSmall
:name="$params.name"
:iconNode="$params.iconNode"
:name="params.name"
:iconNode="params.iconNode"
:class="$style.smallPreview"
/>
</div>
<div >
<div :class="$style.info">
<IconInfo :icon="$params" />
<IconInfo :icon="params" />
<div :class="$style.meta">
<div
v-if="$params.createdRelease?.version"
v-if="params.createdRelease?.version"
:class="$style.version"
>
<Label>Created:</Label>
<Badge
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${$params.createdRelease.version}`"
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${params.createdRelease.version}`"
target="_blank"
rel="noreferrer noopener"
>
v{{$params.createdRelease.version}}
v{{params.createdRelease.version}}
</Badge>
</div>
<div
v-if="$params.changedRelease?.version"
v-if="params.changedRelease?.version"
:class="$style.version"
>
<Label>Last changed:</Label>
<Badge
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${$params.changedRelease.version}`"
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${params.changedRelease.version}`"
target="_blank"
rel="noreferrer noopener"
>
v{{$params.changedRelease.version}}
v{{params.changedRelease.version}}
</Badge>
</div>
<IconContributors :icon="$params" :class="$style.contributors"/>
<IconContributors :icon="params" :class="$style.contributors"/>
</div>
</div>
<CodeGroup
@@ -97,7 +97,7 @@ const codeExample = computed(() => data.codeExamples?.map(
</div>
</div>
<RelatedIcons :icons="$params.relatedIcons" />
<RelatedIcons :icons="params.relatedIcons" />
<style module>
.preview {
@@ -117,6 +117,10 @@ const codeExample = computed(() => data.codeExamples?.map(
margin-top: 24px;
}
.info {
--tags-gradient-background: var(--vp-c-bg);
}
.version, .contributors {
display: flex;
flex-wrap: wrap;

View File

@@ -1,18 +1,18 @@
import { getAllData } from "../.vitepress/lib/icons";
import relatedIcons from '../.vitepress/data/relatedIcons.json'
import iconNodes from '../.vitepress/data/iconNodes'
import * as iconDetails from '../.vitepress/data/iconDetails'
import { IconEntity } from "../.vitepress/theme/types";
export default {
paths: async () => {
const icons = await getAllData()
return icons.map((iconEntity) => {
return (Object.values(iconDetails) as unknown as IconEntity[]).map((iconEntity) => {
const params = {
...iconEntity,
relatedIcons: relatedIcons[iconEntity.name].map((name: string) => ({
name,
iconNode: iconNodes[name],
})),
}))
}
return {

View File

@@ -1,9 +1,9 @@
import { getAllData } from '../.vitepress/lib/icons'
import iconNodes from '../.vitepress/data/iconNodes'
export default {
async load() {
return {
icons: await getAllData(),
icons: Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode })),
}
}
}

View File

@@ -13,6 +13,7 @@
"prebuild:metaJson": "node ../scripts/writeIconMetaIndex.mjs",
"prebuild:releaseJson": "node ../scripts/writeReleaseMetadata.mjs",
"prebuild:relatedIcons": "node ../scripts/writeIconRelatedIcons.mjs",
"prebuild:iconDetails": "node ../scripts/writeIconDetails.mjs",
"postbuild:vercelJson": "node ../scripts/writeVercelOutput.mjs",
"dev": "npx nitropack dev",
"build:api": "npx nitropack build",

View File

@@ -0,0 +1,59 @@
import fs from 'fs';
import path from 'path';
import renderIconsObject from './render/renderIconsObject.mjs';
import { readSvgDirectory, toCamelCase } from './helpers.mjs';
const currentDir = process.cwd();
const ICONS_DIR = path.resolve(currentDir, '../icons');
const icons = readSvgDirectory(ICONS_DIR, '.json');
const iconDetailsDirectory = path.resolve(currentDir, '.vitepress/data', 'iconDetails');
if (fs.existsSync(iconDetailsDirectory)) {
fs.rmSync(iconDetailsDirectory, { recursive: true, force: true });
}
if (!fs.existsSync(iconDetailsDirectory)) {
fs.mkdirSync(iconDetailsDirectory);
}
const indexFile = path.resolve(iconDetailsDirectory, `index.ts`);
const writeIconFiles = icons.map(async (iconFileName) => {
const iconName = path.basename(iconFileName, '.json');
const location = path.resolve(iconDetailsDirectory, `${iconName}.ts`);
const contents = `\
import iconNode from '../iconNodes/${iconName}.node.json'
import metaData from '../../../../icons/${iconName}.json'
import releaseData from '../releaseMetadata/${iconName}.json'
const { tags, categories } = metaData
const iconDetails = {
name: '${iconName}',
iconNode,
contributors: ['ericfennis', 'karsa-mistmere'],
tags,
categories,
...releaseData,
}
export default iconDetails
`;
await fs.promises.writeFile(location, contents, 'utf-8');
await fs.promises.appendFile(
indexFile,
`export { default as ${toCamelCase(iconName)} } from './${iconName}';\n`,
'utf-8',
);
});
Promise.all(writeIconFiles)
.then(() => {
console.log('Successfully write', writeIconFiles.length, 'iconDetails files.');
})
.catch((error) => {
throw new Error(`Something went wrong generating iconNode files,\n ${error}`);
});

View File

@@ -31,7 +31,9 @@ const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }]
const output = JSON.stringify(iconNode, null, 2);
await fs.promises.writeFile(location, output, 'utf-8');
iconIndexFileImports.push(`import ${toCamelCase(iconName)}Node from './${iconName}.node.json';`);
iconIndexFileImports.push(
`import ${toCamelCase(iconName)}Node from './${iconName}.node.json' assert { type: "json" };`,
);
iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`);
iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`);
});

View File

@@ -16,6 +16,8 @@ const nameWeight = 5;
const tagWeight = 4;
const categoryWeight = 3;
const MAX_RELATED_ICONS = 4 * 17 // grid of 4x17 icons, = 68 icons
const arrayMatches = (a, b) => {
// let matches = 0;
// for (let i = 0; i < a.length; ++i) {
@@ -44,6 +46,7 @@ const getRelatedIcons = (currentIcon, icons) => {
.filter(a => a.similarity > 0) // @todo: maybe require a minimal non-zero similarity
.sort((a, b) => b.similarity - a.similarity)
.map(i => i.icon)
.slice(0, MAX_RELATED_ICONS)
;
}

View File

@@ -13,11 +13,20 @@ const currentDir = process.cwd();
const ICONS_DIR = path.resolve(currentDir, '../icons');
const iconJsonFiles = readSvgDirectory(ICONS_DIR, '.json');
const location = path.resolve(currentDir, '.vitepress/data', 'releaseMetaData.json');
const releaseMetaDataDirectory = path.resolve(currentDir, '.vitepress/data', 'releaseMetadata');
if (fs.existsSync(location)) {
fs.unlinkSync(location);
}
if (fs.existsSync(releaseMetaDataDirectory)) {
fs.rmSync(releaseMetaDataDirectory, { recursive: true, force: true });
}
if (!fs.existsSync(releaseMetaDataDirectory)) {
fs.mkdirSync(releaseMetaDataDirectory);
}
const fetchAllReleases = async () => {
await git.fetch('https://github.com/lucide-icons/lucide.git', '--tags');
@@ -129,6 +138,7 @@ try {
const releaseMetaData = await Promise.all(
iconJsonFiles.map(async (iconJsonFile) => {
const iconName = path.basename(iconJsonFile, '.json');
const metaDir = path.resolve(releaseMetaDataDirectory, `${iconName}.json`);
if (iconName in newReleaseMetaData === false) {
console.error(`Could not find release metadata for icon '${iconName}'.`);
@@ -153,6 +163,9 @@ try {
});
}
const output = JSON.stringify(contents, null, 2);
await fs.promises.writeFile(metaDir, output, 'utf-8');
return [iconName, contents];
}),
);