Add version filter

This commit is contained in:
Eric Fennis
2025-04-11 17:01:58 +02:00
parent c68ee7f9a0
commit 4acd574de2
12 changed files with 675 additions and 332 deletions
@@ -0,0 +1,13 @@
import { eventHandler, setResponseHeader } from 'h3';
import releaseMeta from '../../data/releaseMetaData.json';
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
return Object.fromEntries(
Object.entries(releaseMeta).map(
([name, { createdRelease }]) => [name, createdRelease.version]
)
);
});
-3
View File
@@ -1,3 +0,0 @@
export default eventHandler(() => {
return { nitro: 'Is Awesome! asda' };
});
@@ -29,16 +29,17 @@ defineExpose({
<template>
<div class="input-wrapper">
<slot name="icon" class="icon" />
<slot name="startIcon" class="icon" />
<input
:type="type"
class="input"
:class="{'has-icon': $slots.icon}"
:class="{'has-icon': $slots.startIcon }"
ref="input"
:value="modelValue"
v-bind="$attrs"
@input="$emit('update:modelValue', $event.target.value)"
/>
<slot name="endIcon" />
</div>
</template>
@@ -55,6 +56,7 @@ defineExpose({
height: 40px;
background-color: var(--vp-c-bg-alt);
font-size: 14px;
transition: border-color .2s ease-in-out;
}
.input:hover, .input:focus {
@@ -70,7 +72,7 @@ defineExpose({
</style>
<style>
.input-wrapper svg {
.input-wrapper > svg {
position: absolute;
left: 16px;
top: 12px;
@@ -8,9 +8,10 @@ export default {
import { computed, ref } from 'vue'
import Input from './Input.vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { search } from '../../../data/iconNodes'
import { search, x } from '../../../data/iconNodes'
const SearchIcon = createLucideIcon('search', search)
const XIcon = createLucideIcon('close', x)
interface Props {
modelValue: string
@@ -32,6 +33,10 @@ const value = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const clear = () => {
value.value = ''
}
</script>
<template>
@@ -43,9 +48,16 @@ const value = computed({
v-model="value"
class="input-wrapper"
>
<template #icon>
<template #startIcon>
<component :is="SearchIcon" class="search-icon" />
</template>
<template #endIcon>
<button class="clear-button" type="button" v-if="value">
<component :is="XIcon" class="x-icon" @click="clear" />
</button>
</template>
</Input>
</template>
@@ -57,7 +69,9 @@ const value = computed({
padding: 0 10px 0 12px;
width: 100%;
height: 40px;
color: var(--vp-c-text-1);
background-color: var(--vp-c-bg-alt);
transition: border-color .15s ease-in-out;
}
.input:hover, .input:focus {
@@ -66,10 +80,30 @@ const value = computed({
}
.input-wrapper:deep(.input) {
/* padding: 12px 24px; */
padding-block: 12px;
font-size: 14px;
height: 48px;
}
.clear-button {
position: absolute;
padding: 4px;
right: 8px;
top: 8px;
width: 32px;
height: 32px;
background: transparent;
border-radius: 6px;
transition: background 0.2s ease;
}
.clear-button:hover {
background: var(--vp-button-alt-bg);
}
.input-wrapper:deep(.input)::-webkit-search-decoration,
.input-wrapper:deep(.input)::-webkit-search-cancel-button,
.input-wrapper:deep(.input)::-webkit-search-results-button,
.input-wrapper:deep(.input)::-webkit-search-results-decoration { display: none; }
</style>
@@ -45,7 +45,6 @@
width: 20px;
height: 20px;
border-radius: 50%;
/* background-color: var(--vp-c-neutral); */
box-shadow: var(--vp-shadow-1);
}
@@ -2,7 +2,7 @@
import { useData } from 'vitepress';
import { useSessionStorage } from '@vueuse/core';
import IconButton from '../base/IconButton.vue';
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue'
// import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue'
import { x } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { onMounted, ref } from 'vue';
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue';
import { ref, computed, defineAsyncComponent, onMounted, watch, watchEffect } from 'vue';
import type { IconEntity } from '../../types';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import IconGrid from './IconGrid.vue';
@@ -9,8 +9,11 @@ import useSearchInput from '../../composables/useSearchInput';
import StickyBar from './StickyBar.vue';
import useFetchTags from '../../composables/useFetchTags';
import useFetchCategories from '../../composables/useFetchCategories';
import useFetchReleaseInfo from '../../composables/useFetchReleaseInfo';
import chunkArray from '../../utils/chunkArray';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
import VersionSelect from './VersionSelect.vue';
import { sort, satisfies } from 'semver';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
@@ -21,9 +24,11 @@ const props = defineProps<{
}>();
const activeIconName = ref(null);
const selectedVersion = ref('Latest version');
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categories } = useFetchCategories();
const { execute: fetchReleaseInfo, data: releaseInfo } = useFetchReleaseInfo();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl)
@@ -33,22 +38,48 @@ const columnSize = computed(() => {
});
const mappedIcons = computed(() => {
if (tags.value == null) {
return props.icons;
let icons = props.icons;
if (tags.value != null && categories.value != null) {
icons = props.icons.map((icon) => {
const iconTags = tags.value[icon.name];
const iconCategories = categories.value?.[icon.name] ?? [];
return {
...icon,
tags: iconTags,
categories: iconCategories,
};
});
}
return props.icons.map((icon) => {
const iconTags = tags.value[icon.name];
const iconCategories = categories.value?.[icon.name] ?? [];
if (selectedVersion.value === 'Latest version' || releaseInfo.value == null) {
console.log('no release info');
return {
...icon,
tags: iconTags,
categories: iconCategories,
};
return icons;
}
return icons.filter((icon) => {
return satisfies(
releaseInfo.value[icon.name], `<=${selectedVersion.value}`
);
});
});
const versions = computed(() => {
if (releaseInfo.value == null) {
return [];
}
const allVersions = Array.from<string>(
new Set(Object.values<string>(releaseInfo.value)).values()
)
return sort(allVersions, {
loose: true,
}).reverse();
});
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
@@ -88,6 +119,12 @@ function onFocusSearchInput() {
}
}
function onFocusVersionSelect() {
if (releaseInfo.value == null) {
fetchReleaseInfo();
}
}
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
@@ -107,12 +144,17 @@ function handleCloseDrawer() {
<div ref="overviewEl" class="overview-container">
<StickyBar>
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
:placeholder="`Search ${mappedIcons.length} icons ...`"
v-model="searchQuery"
ref="searchInput"
class="input-wrapper"
@focus="onFocusSearchInput"
/>
<VersionSelect
v-model="selectedVersion"
:versions="versions"
@focus="onFocusVersionSelect"
/>
</StickyBar>
<NoResults
v-if="list.length === 0 && searchQuery !== ''"
@@ -121,7 +163,6 @@ function handleCloseDrawer() {
/>
<IconGrid
v-else-if="list.length === 0"
:key="index"
overlayMode
:icons="[...searchResults].splice(0, DEFAULT_GRID_ITEMS)"
:activeIcon="activeIconName"
@@ -14,6 +14,7 @@
margin-top: -32px;
width: 100%;
display: flex;
gap: 16px;
margin-bottom: 32px;
background: var(--vp-c-bg);
box-shadow: 0 16px 24px var(--vp-c-bg);
@@ -0,0 +1,213 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
Combobox,
ComboboxInput,
ComboboxButton,
ComboboxOptions,
ComboboxOption,
} from '@headlessui/vue'
import { chevronsUpDown, check } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
const model = defineModel<string>()
const props = defineProps<{
versions: string[]
}>()
let query = ref('')
let filteredVersions = computed(() =>
query.value === ''
? ['Latest version', ...props.versions]
: props.versions.filter((version) =>
version
.toLowerCase()
.replace(/\s+/g, '')
.includes(query.value.toLowerCase().replace(/\s+/g, ''))
)
)
const emit = defineEmits(['focus'])
const ChevronUpDown = createLucideIcon('ChevronUpDown', chevronsUpDown)
const Check = createLucideIcon('Check', check)
</script>
<template>
<Combobox v-model="model">
<div class="combobox-container">
<div class="combobox-input-container">
<ComboboxInput
class="combobox-input"
placeholder="Latest version"
@change="query = $event.target.value"
@focus="emit('focus')"
/>
<ComboboxButton class="combobox-button">
<ChevronUpDown class="icon-chevron" aria-hidden="true" />
</ComboboxButton>
</div>
<ComboboxOptions class="combobox-options">
<div
v-if="versions.length === 0 && query !== ''"
class="combobox-no-results"
>
No match!
</div>
<ComboboxOption
v-for="version in filteredVersions"
as="template"
:key="version"
:value="version"
v-slot="{ selected, active }"
>
<li
class="combobox-option"
:class="{ 'active': active }"
>
<span
class="combobox-option-text"
:class="{ 'selected': selected }"
>
{{ version }}
</span>
<span
v-if="selected"
class="combobox-option-icon"
:class="{ 'active': active }"
>
<Check class="icon-check" aria-hidden="true" />
</span>
</li>
</ComboboxOption>
</ComboboxOptions>
</div>
</Combobox>
</template>
<style scoped>
.combobox-input-container {
position: relative;
width: 100%;
height: 48px;
min-width: 160px;
cursor: default;
overflow: hidden;
background-color: var(--vp-c-bg-alt);
text-align: left;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
outline: none;
}
.combobox-input {
width: 100%;
border: none;
padding: 12px;
border-radius: 8px;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--vp-c-text-1);
border: 1px solid transparent;
transition: border-color .15s ease-in-out;
}
.combobox-input:hover, .combobox-input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.combobox-button {
position: absolute;
top: 4px;
padding: 9px 8px;
right: 0;
display: flex;
align-items: center;
padding-right: 0.5rem;
z-index: 10;;
}
.icon-chevron {
height: 20px;
width: 20px;
color: #9ca3af;
}
.combobox-options {
position: absolute;
margin-top: 0.25rem;
width: 100%;
max-width: 160px;
padding: 12px;
min-width: 128px;
overflow-y: auto;
border-radius: 12px;
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg-elv);
box-shadow: var(--vp-shadow-3);
transition: background-color 0.5s;
max-height: 240px;
outline: none;
right: 0;
}
.combobox-no-results {
position: relative;
cursor: default;
padding: 0.5rem 1rem;
color: #4b5563;
}
.combobox-option {
position: relative;
cursor: default;
padding: 2px 8px;
border-radius: 6px;
color: var(--vp-c-text-1);
line-height: 32px;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
transition: background-color .25s,color .25s;
}
.combobox-option.active {
color: var(--vp-c-brand);
background-color: var(--vp-c-default-soft);
}
.combobox-option-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.combobox-option-text.selected {
font-weight: 500;
}
.combobox-option-icon {
position: absolute;
right: 8px;
top: 8px;
display: flex;
align-items: center;
padding-left: 0.75rem;
color: var(--vp-c-brand) !important;
}
.combobox-option-icon.active {
color: var(--vp-c-brand) !important;
}
.icon-check {
height:20px;
width:20px;
}
</style>
@@ -0,0 +1,12 @@
import { useFetch } from '@vueuse/core';
const useFetchTags = () =>
useFetch<Record<string, string>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/release-info`,
{
immediate:
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('version'),
},
).json();
export default useFetchTags;
+1 -1
View File
@@ -33,7 +33,7 @@
"h3": "^1.8.0",
"nitropack": "2.8.1",
"rollup-plugin-copy": "^3.4.0",
"vitepress": "^1.3.1"
"vitepress": "^1.6.3"
},
"dependencies": {
"@floating-ui/vue": "^1.0.3",
+338 -307
View File
File diff suppressed because it is too large Load Diff