mirror of
https://github.com/web3privacy/explorer-app.git
synced 2024-10-15 16:46:26 +02:00
Merge pull request #41 from web3privacy/dk/optimized-explorer
dk/eoptimized-explorer
This commit is contained in:
commit
926d3b9290
7 changed files with 392 additions and 329 deletions
|
@ -40,7 +40,6 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
|||
h="48px lg:64px"
|
||||
:class="switcher ? '' : 'lg:max-w-full! lg:w-full '"
|
||||
>
|
||||
<ClientOnly>
|
||||
<NuxtImg
|
||||
:src="project?.image || '/no-image-1-1.svg'"
|
||||
class="w-full h-auto"
|
||||
|
@ -51,7 +50,6 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
|||
object-fit
|
||||
bg="#121212"
|
||||
/>
|
||||
</ClientOnly>
|
||||
<div
|
||||
flex
|
||||
flex-col
|
||||
|
@ -88,7 +86,6 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<div
|
||||
v-for="(projectItem, index) of projectItems"
|
||||
:key="projectItem.label.toString()"
|
||||
|
@ -218,8 +215,6 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
|||
compact
|
||||
/>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import type { InputOption } from '~/types'
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
options: InputOption[]
|
||||
modelValue: string | number
|
||||
count?: number
|
||||
|
@ -26,6 +27,7 @@ function onOptionSelected(value: string | number) {
|
|||
>
|
||||
<div class="relative font-700 font-24px">
|
||||
<HeadlessListboxButton
|
||||
:id="`headless-listbox-button-${name}`"
|
||||
class="relative cursor-pointer py-6px px-14px text-left border-2px bg-app-white text-app-black text-xs sm:text-sm sm:leading-6"
|
||||
:class="{ 'text-app-danger!': isOptionSelected?.value === 'sunset' }"
|
||||
>
|
||||
|
|
|
@ -4,33 +4,8 @@ import type { ProjectShallow } from '~/types'
|
|||
const props = defineProps<{
|
||||
projects: { title: string, projects: ProjectShallow[] }[]
|
||||
}>()
|
||||
const { switcher, filter } = storeToRefs(useData())
|
||||
|
||||
const totalProjectsCount = props.projects.map(g => g.projects.length).reduce((a, b) => a + b, 0)
|
||||
|
||||
function onChangeSort(sortKey: string) {
|
||||
if (filter.value.sortby === sortKey) {
|
||||
if (filter.value.sortDirection === 'desc' && filter.value.sortby !== 'score') {
|
||||
filter.value.sortby = 'score'
|
||||
filter.value.sortDirection = 'desc'
|
||||
return
|
||||
}
|
||||
filter.value.sortDirection = filter.value.sortDirection === 'asc' ? 'desc' : 'asc'
|
||||
return
|
||||
}
|
||||
filter.value.sortby = sortKey
|
||||
filter.value.sortDirection = sortKey === 'score' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
const cardTitles = ref< { label: string, sortKey: string, togglable?: boolean }[]>([
|
||||
{ label: 'Usecase', sortKey: 'usecase' },
|
||||
{ label: 'Openess', sortKey: 'openess', togglable: true },
|
||||
{ label: 'Technology', sortKey: 'technology', togglable: true },
|
||||
{ label: 'Privacy', sortKey: 'privacy', togglable: true },
|
||||
{ label: 'Ecosystem', sortKey: 'ecosystem' },
|
||||
{ label: 'Links', sortKey: 'links' },
|
||||
{ label: 'W3PN Score', sortKey: 'score', togglable: true },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -39,123 +14,11 @@ const cardTitles = ref< { label: string, sortKey: string, togglable?: boolean }[
|
|||
flex-col
|
||||
items-start
|
||||
>
|
||||
<template
|
||||
<ProjectGridGroup
|
||||
v-for="group in projects"
|
||||
:key="group.title"
|
||||
>
|
||||
<div
|
||||
|
||||
flex
|
||||
items-center
|
||||
gap-x-12px
|
||||
w-full
|
||||
>
|
||||
<h2
|
||||
text="app-white 16px lg:24px"
|
||||
font-700
|
||||
leading="24px lg:32px"
|
||||
whitespace-nowrap
|
||||
>
|
||||
{{ group.projects.length }} {{ group.title }}
|
||||
</h2>
|
||||
<div
|
||||
h-2px
|
||||
w-full
|
||||
bg-app-text-grey
|
||||
:group="group"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
i-ic-baseline-arrow-drop-down
|
||||
text="app-text-grey 24px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
grid
|
||||
grid-cols="2 lg:10"
|
||||
w-full
|
||||
mb-16px
|
||||
>
|
||||
<div
|
||||
flex
|
||||
items-center
|
||||
gap-4px
|
||||
col-span="1 lg:2"
|
||||
:class="['title' === filter.sortby ? 'text-app-white' : 'text-app-text-grey', 'cursor-pointer']"
|
||||
@click="onChangeSort('title')"
|
||||
>
|
||||
<p
|
||||
text="12px lg:14px"
|
||||
leading="16px lg:24px"
|
||||
whitespace-nowrap
|
||||
>
|
||||
Project name
|
||||
</p>
|
||||
<button
|
||||
hidden
|
||||
lg:block
|
||||
type="button"
|
||||
:class="['title' === filter.sortby ? filter.sortDirection === 'desc' ? 'i-ic-baseline-arrow-drop-up'
|
||||
: 'i-ic-baseline-arrow-drop-down'
|
||||
: 'i-ic-baseline-arrow-drop-down']"
|
||||
text="20px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-4px
|
||||
lg:hidden
|
||||
>
|
||||
<SortSelectBox
|
||||
v-model="filter.sortby"
|
||||
:options="['title', 'openess', 'technology', 'privacy', 'score'].map((o) => ({ label: capitalize(o), value: o }))"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="(title, index) in cardTitles"
|
||||
:key="title.label"
|
||||
lg:flex
|
||||
items-center
|
||||
justify-start
|
||||
last:justify-end
|
||||
gap-4px
|
||||
hidden
|
||||
:class="[title.sortKey === filter.sortby ? 'text-app-white' : 'text-app-text-grey', { 'cursor-pointer': title.togglable, 'col-span-1 lg:col-span-2': index === 0 }]"
|
||||
@click="onChangeSort(title.sortKey)"
|
||||
>
|
||||
<p
|
||||
text="12px lg:14px "
|
||||
leading="16px lg:24px"
|
||||
whitespace-nowrap
|
||||
>
|
||||
{{ title.label }}
|
||||
</p>
|
||||
<button
|
||||
v-if="title.togglable"
|
||||
type="button"
|
||||
:class="[title.sortKey === filter.sortby ? filter.sortDirection === 'desc' ? 'i-ic-baseline-arrow-drop-up'
|
||||
: 'i-ic-baseline-arrow-drop-down'
|
||||
: 'i-ic-baseline-arrow-drop-down']"
|
||||
text=" 20px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
grid
|
||||
:class="switcher ? 'grid-cols-1 lg:grid-cols-1' : 'xl:grid-cols-3 lg:grid-cols-3 sm:grid-cols-2 grid-cols-1'"
|
||||
gap-16px
|
||||
text-white
|
||||
w-full
|
||||
mb="8px lg:24px"
|
||||
>
|
||||
<Card
|
||||
v-for="project in group.projects"
|
||||
:key="project.id"
|
||||
:project="project"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="totalProjectsCount === 0">
|
||||
<h3>No Projects found...</h3>
|
||||
</div>
|
||||
|
|
85
components/Project/ProjectGridGroup.vue
Normal file
85
components/Project/ProjectGridGroup.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ProjectShallow } from '~/types'
|
||||
|
||||
const props = defineProps<{
|
||||
group: {
|
||||
title: string
|
||||
projects: ProjectShallow[]
|
||||
}
|
||||
}>()
|
||||
|
||||
const { switcher } = storeToRefs(useData())
|
||||
|
||||
const groupCollapsed = ref(false)
|
||||
|
||||
const shownProjectsCount = ref(15)
|
||||
|
||||
const shownProjects = computed(() => props.group.projects.slice(0, shownProjectsCount.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div w-full>
|
||||
<div
|
||||
flex
|
||||
items-center
|
||||
gap-x-12px
|
||||
w-full
|
||||
mb="-8px lg:8px"
|
||||
>
|
||||
<h2
|
||||
text="app-white 16px lg:24px"
|
||||
font-700
|
||||
leading="24px lg:32px"
|
||||
whitespace-nowrap
|
||||
>
|
||||
{{ group.projects.length }} {{ group.title }}
|
||||
</h2>
|
||||
<div
|
||||
h-2px
|
||||
w-full
|
||||
bg-app-text-grey
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
:class="[groupCollapsed ? 'i-ic-baseline-arrow-drop-up' : 'i-ic-baseline-arrow-drop-down']"
|
||||
text="app-text-grey 24px"
|
||||
@click="groupCollapsed = !groupCollapsed"
|
||||
/>
|
||||
</div>
|
||||
<ProjectGridGroupSort v-if="!groupCollapsed" />
|
||||
<div
|
||||
v-if="!groupCollapsed"
|
||||
grid
|
||||
:class="switcher ? 'grid-cols-1 lg:grid-cols-1' : 'xl:grid-cols-3 lg:grid-cols-3 sm:grid-cols-2 grid-cols-1'"
|
||||
gap-16px
|
||||
text-white
|
||||
w-full
|
||||
mt="-16px lg:0"
|
||||
mb-8px
|
||||
>
|
||||
<ClientOnly>
|
||||
<Card
|
||||
v-for="project in group.projects.slice(0, shownProjectsCount)"
|
||||
:key="project.id"
|
||||
:project="project"
|
||||
/>
|
||||
<template #fallback>
|
||||
Loading...
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<button
|
||||
v-if="group.projects.length > shownProjects.length && !groupCollapsed"
|
||||
type="button"
|
||||
text="12px lg:14px"
|
||||
leading="24px lg:32px"
|
||||
font-bold
|
||||
text-app-text-grey
|
||||
@click="shownProjectsCount += 15"
|
||||
>
|
||||
LOAD MORE
|
||||
</button>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
103
components/Project/ProjectGridGroupSort.vue
Normal file
103
components/Project/ProjectGridGroupSort.vue
Normal file
|
@ -0,0 +1,103 @@
|
|||
<script lang="ts" setup>
|
||||
const { filter } = storeToRefs(useData())
|
||||
|
||||
function onChangeSort(sortKey: string) {
|
||||
if (filter.value.sortby === sortKey) {
|
||||
if (filter.value.sortDirection === 'desc' && filter.value.sortby !== 'score') {
|
||||
filter.value.sortby = 'score'
|
||||
filter.value.sortDirection = 'desc'
|
||||
return
|
||||
}
|
||||
filter.value.sortDirection = filter.value.sortDirection === 'asc' ? 'desc' : 'asc'
|
||||
return
|
||||
}
|
||||
filter.value.sortby = sortKey
|
||||
filter.value.sortDirection = sortKey === 'score' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
const cardTitles = ref< { label: string, sortKey: string, togglable?: boolean }[]>([
|
||||
{ label: 'Usecase', sortKey: 'usecase' },
|
||||
{ label: 'Openess', sortKey: 'openess', togglable: true },
|
||||
{ label: 'Technology', sortKey: 'technology', togglable: true },
|
||||
{ label: 'Privacy', sortKey: 'privacy', togglable: true },
|
||||
{ label: 'Ecosystem', sortKey: 'ecosystem' },
|
||||
{ label: 'Links', sortKey: 'links' },
|
||||
{ label: 'W3PN Score', sortKey: 'score', togglable: true },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
grid
|
||||
grid-cols="2 lg:10"
|
||||
w-full
|
||||
mb-16px
|
||||
>
|
||||
<div
|
||||
flex
|
||||
items-center
|
||||
gap-4px
|
||||
col-span="1 lg:2"
|
||||
:class="['title' === filter.sortby ? 'text-app-white' : 'text-app-text-grey', 'cursor-pointer']"
|
||||
@click="onChangeSort('title')"
|
||||
>
|
||||
<p
|
||||
text="12px lg:14px"
|
||||
leading="16px lg:24px"
|
||||
whitespace-nowrap
|
||||
>
|
||||
Project name
|
||||
</p>
|
||||
<button
|
||||
hidden
|
||||
lg:block
|
||||
type="button"
|
||||
:class="['title' === filter.sortby ? filter.sortDirection === 'desc' ? 'i-ic-baseline-arrow-drop-up'
|
||||
: 'i-ic-baseline-arrow-drop-down'
|
||||
: 'i-ic-baseline-arrow-drop-down']"
|
||||
text-24px
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-4px
|
||||
lg:hidden
|
||||
>
|
||||
<SortSelectBox
|
||||
v-model="filter.sortby"
|
||||
name="sort"
|
||||
:options="['title', 'openess', 'technology', 'privacy', 'score'].map((o) => ({ label: capitalize(o), value: o }))"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="(title, index) in cardTitles"
|
||||
:key="title.label"
|
||||
lg:flex
|
||||
items-center
|
||||
justify-start
|
||||
last:justify-end
|
||||
gap-4px
|
||||
hidden
|
||||
:class="[title.sortKey === filter.sortby ? 'text-app-white' : 'text-app-text-grey', { 'cursor-pointer': title.togglable, 'col-span-1 lg:col-span-2': index === 0 }]"
|
||||
@click="onChangeSort(title.sortKey)"
|
||||
>
|
||||
<p
|
||||
text="12px lg:14px "
|
||||
leading="16px lg:24px"
|
||||
whitespace-nowrap
|
||||
>
|
||||
{{ title.label }}
|
||||
</p>
|
||||
<button
|
||||
v-if="title.togglable"
|
||||
type="button"
|
||||
:class="[title.sortKey === filter.sortby ? filter.sortDirection === 'desc' ? 'i-ic-baseline-arrow-drop-up'
|
||||
: 'i-ic-baseline-arrow-drop-down'
|
||||
: 'i-ic-baseline-arrow-drop-down']"
|
||||
text-24px
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -8,6 +8,7 @@ const props = withDefaults(defineProps<SelectProps>(), {
|
|||
const emits = defineEmits(['update:modelValue'])
|
||||
|
||||
interface SelectProps {
|
||||
name: string
|
||||
options: InputOption[]
|
||||
modelValue: string
|
||||
isMarginTop?: boolean
|
||||
|
@ -27,23 +28,24 @@ const selectedValue = useVModel(props, 'modelValue', emits)
|
|||
:class="[isMarginTop ? 'mt-2' : 'mt-0', blackAndWhite ? 'bg-app-black' : 'bg-app-white']"
|
||||
>
|
||||
<HeadlessListboxButton
|
||||
class="relative w-full cursor-pointer py-8px p-16px text-left text-xs sm:text-sm sm:leading-6 flex items-center gap-4px"
|
||||
:id="`headless-listbox-button-${name}`"
|
||||
class="cursor-pointer py-8px text-left text-xs sm:text-sm sm:leading-6 flex items-center justify-end gap-4px"
|
||||
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']"
|
||||
>
|
||||
<span
|
||||
block
|
||||
<div
|
||||
text="12px lg:14px app-text-grey"
|
||||
leading="16px lg:24px"
|
||||
>
|
||||
Sort by:
|
||||
</span>
|
||||
<span class="block truncate mr-8px">{{ props.options.find(option => option.value === selectedValue)?.label }}</span>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
</div>
|
||||
<div class="truncate">
|
||||
{{ props.options.find(option => option.value === selectedValue)?.label }}
|
||||
</div>
|
||||
<UnoIcon
|
||||
i-ic-baseline-arrow-drop-down
|
||||
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']"
|
||||
text="18px lg:20px"
|
||||
/>
|
||||
</span>
|
||||
</HeadlessListboxButton>
|
||||
|
||||
<transition
|
||||
|
|
|
@ -124,23 +124,48 @@ export const useData = defineStore('data', () => {
|
|||
const shallowProjects = computed(() => projects.value.map(project => projectToShallow(project)))
|
||||
|
||||
const getProjectsByFilters = <T extends ProjectShallow>(options?: { shallow: boolean }): T[] => {
|
||||
const filteredProjects = projects.value
|
||||
.filter(project =>
|
||||
selectedCategoryId.value !== 'all' ? project.categories.includes(selectedCategoryId.value) : true,
|
||||
)
|
||||
.filter(project =>
|
||||
selectedUsecaseId.value !== 'all' ? selectedUsecaseId.value === 'sunset' ? project.sunset : project.usecases?.map(u => u.toLowerCase()).includes(selectedUsecaseId.value.toLowerCase()) : true,
|
||||
)
|
||||
.filter(project =>
|
||||
selectedEcosystemId.value !== 'all' ? project.ecosystem?.map(e => e.toLowerCase()).includes(selectedEcosystemId.value.toLowerCase()) : true,
|
||||
)
|
||||
.filter(project =>
|
||||
selectedAssetsUsedId.value !== 'all' ? project.assets_used?.map(a => a.toLowerCase()).includes(selectedAssetsUsedId.value.toLowerCase()) : true,
|
||||
)
|
||||
.filter(project =>
|
||||
selectedFeaturesId.value !== 'all' ? project.technology?.features?.map(f => f.toLowerCase()).includes(selectedFeaturesId.value.toLowerCase()) : true,
|
||||
)
|
||||
return (filteredProjects.map(project => options?.shallow ? projectToShallow(project) : project) as T[])
|
||||
const selectedCategory = selectedCategoryId.value !== 'all' ? selectedCategoryId.value : undefined
|
||||
const selectedUsecase = selectedUsecaseId.value !== 'all' ? selectedUsecaseId.value.toLowerCase() : undefined
|
||||
const selectedEcosystem = selectedEcosystemId.value !== 'all' ? selectedEcosystemId.value.toLowerCase() : undefined
|
||||
const selectedAssetsUsed = selectedAssetsUsedId.value !== 'all' ? selectedAssetsUsedId.value.toLowerCase() : undefined
|
||||
const selectedFeature = selectedFeaturesId.value !== 'all' ? selectedFeaturesId.value.toLowerCase() : undefined
|
||||
|
||||
return (projects.value.filter((project) => {
|
||||
if (selectedCategory && !project.categories.includes(selectedCategory))
|
||||
return false
|
||||
|
||||
if (selectedUsecase) {
|
||||
if (selectedUsecase === 'sunset') {
|
||||
if (!project.sunset)
|
||||
return false
|
||||
}
|
||||
else {
|
||||
const usecases = project.usecases?.map(u => u.toLowerCase()) || []
|
||||
if (!usecases.includes(selectedUsecase))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedEcosystem) {
|
||||
const ecosystems = project.ecosystem?.map(e => e.toLowerCase()) || []
|
||||
if (!ecosystems.includes(selectedEcosystem))
|
||||
return false
|
||||
}
|
||||
|
||||
if (selectedAssetsUsed) {
|
||||
const assetsUsed = project.assets_used?.map(a => a.toLowerCase()) || []
|
||||
if (!assetsUsed.includes(selectedAssetsUsed))
|
||||
return false
|
||||
}
|
||||
|
||||
if (selectedFeature) {
|
||||
const features = project.technology?.features?.map(f => f.toLowerCase()) || []
|
||||
if (!features.includes(selectedFeature))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}).map(project => (options?.shallow ? projectToShallow(project) : project)) as T[])
|
||||
}
|
||||
|
||||
const getProjectById = <T extends Project | ProjectShallow>(id: string, options?: { shallow: boolean }): T => {
|
||||
|
@ -149,51 +174,39 @@ export const useData = defineStore('data', () => {
|
|||
}
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
if (!projects.value)
|
||||
return []
|
||||
if (!projects.value) return []
|
||||
|
||||
const query = filter.query.toLowerCase()
|
||||
const sortDirection = filter.sortDirection === 'asc' ? 1 : -1
|
||||
const sortBy = filter.sortby
|
||||
|
||||
const filteredShallowProjects = getProjectsByFilters({ shallow: true })
|
||||
.filter((project) => {
|
||||
return (
|
||||
project
|
||||
&& project.title1
|
||||
&& project.title1.toLowerCase().includes(query)
|
||||
)
|
||||
}).filter((project) => {
|
||||
if (filter.sortby === 'anonymity')
|
||||
return project.anonymity === true
|
||||
else
|
||||
return true
|
||||
}).sort((a, b) => {
|
||||
if (filter.sortby === 'score')
|
||||
if (filter.sortDirection === 'asc')
|
||||
return a.percentage - b.percentage
|
||||
else
|
||||
return b.percentage - a.percentage
|
||||
if (filter.sortby === 'title')
|
||||
if (filter.sortDirection === 'asc')
|
||||
return a.title1.toLowerCase().localeCompare(b.title1.toLowerCase())
|
||||
else
|
||||
return b.title1.toLowerCase().localeCompare(a.title1.toLowerCase())
|
||||
if (filter.sortby === 'openess' || filter.sortby === 'technology' || filter.sortby === 'privacy') {
|
||||
const scoreA = a.ratings?.find(r => r.type === filter.sortby)?.points || 0
|
||||
const scoreB = b.ratings?.find(r => r.type === filter.sortby)?.points || 0
|
||||
if (filter.sortDirection === 'asc')
|
||||
return scoreB - scoreA
|
||||
else
|
||||
return scoreA - scoreB
|
||||
return project?.title1?.toLowerCase().includes(query)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'score') {
|
||||
return sortDirection * (a.percentage - b.percentage)
|
||||
}
|
||||
else
|
||||
|
||||
if (sortBy === 'title') {
|
||||
return sortDirection * a.title1.toLowerCase().localeCompare(b.title1.toLowerCase())
|
||||
}
|
||||
|
||||
if (sortBy === 'openess' || sortBy === 'technology' || sortBy === 'privacy') {
|
||||
const scoreA = a.ratings?.find(r => r.type === sortBy)?.points || 0
|
||||
const scoreB = b.ratings?.find(r => r.type === sortBy)?.points || 0
|
||||
return sortDirection * (scoreB - scoreA)
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return filteredShallowProjects
|
||||
})
|
||||
|
||||
const groupedProjectsPerCategory = computed(() => {
|
||||
const groupedProjects = categories.value.map((category) => {
|
||||
// Find all projects that include this category
|
||||
const projectsInCategory = filteredProjects.value.filter(project =>
|
||||
project.categories.includes(category.id),
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue