mirror of
https://github.com/web3privacy/explorer-app.git
synced 2024-10-15 16:46:26 +02:00
fix(performance): dashboard performance - sorting, filtering, pagination
This commit is contained in:
parent
073d9d3bed
commit
c4e174a48b
7 changed files with 392 additions and 329 deletions
|
@ -40,18 +40,16 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
||||||
h="48px lg:64px"
|
h="48px lg:64px"
|
||||||
:class="switcher ? '' : 'lg:max-w-full! lg:w-full '"
|
:class="switcher ? '' : 'lg:max-w-full! lg:w-full '"
|
||||||
>
|
>
|
||||||
<ClientOnly>
|
<NuxtImg
|
||||||
<NuxtImg
|
:src="project?.image || '/no-image-1-1.svg'"
|
||||||
:src="project?.image || '/no-image-1-1.svg'"
|
class="w-full h-auto"
|
||||||
class="w-full h-auto"
|
max-h="md:64px 48px"
|
||||||
max-h="md:64px 48px"
|
max-w="md:64px 48px"
|
||||||
max-w="md:64px 48px"
|
self-center
|
||||||
self-center
|
z-10
|
||||||
z-10
|
object-fit
|
||||||
object-fit
|
bg="#121212"
|
||||||
bg="#121212"
|
/>
|
||||||
/>
|
|
||||||
</ClientOnly>
|
|
||||||
<div
|
<div
|
||||||
flex
|
flex
|
||||||
flex-col
|
flex-col
|
||||||
|
@ -88,138 +86,135 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ClientOnly>
|
<div
|
||||||
<div
|
v-for="(projectItem, index) of projectItems"
|
||||||
v-for="(projectItem, index) of projectItems"
|
:key="projectItem.label.toString()"
|
||||||
:key="projectItem.label.toString()"
|
hidden
|
||||||
hidden
|
lg:flex
|
||||||
lg:flex
|
items-center
|
||||||
items-center
|
text-14px
|
||||||
text-14px
|
leading-24px
|
||||||
leading-24px
|
:class="{ 'col-span-1 lg:col-span-2': index === 0 }"
|
||||||
:class="{ 'col-span-1 lg:col-span-2': index === 0 }"
|
>
|
||||||
|
<p
|
||||||
|
v-if="projectItem.type === 'array'"
|
||||||
|
text-app-text-grey
|
||||||
>
|
>
|
||||||
<p
|
{{ (projectItem.label as string[] || []).join(', ') }}
|
||||||
v-if="projectItem.type === 'array'"
|
</p>
|
||||||
text-app-text-grey
|
|
||||||
>
|
|
||||||
{{ (projectItem.label as string[] || []).join(', ') }}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
v-if="projectItem.type === 'links'"
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
justify-start
|
|
||||||
gap-16px
|
|
||||||
>
|
|
||||||
<NuxtLink
|
|
||||||
v-if="projectItem.label[0]"
|
|
||||||
:to="projectItem.label[0]"
|
|
||||||
external
|
|
||||||
target="_blank"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<UnoIcon
|
|
||||||
i-ic-baseline-language
|
|
||||||
text="24px app-text-grey"
|
|
||||||
/>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
v-if="projectItem.label[1]"
|
|
||||||
:to="projectItem.label[1]"
|
|
||||||
external
|
|
||||||
target="_blank"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<UnoIcon
|
|
||||||
i-mdi-github
|
|
||||||
text="24px app-text-grey"
|
|
||||||
text-27px
|
|
||||||
/>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
v-if="projectItem.label[2]"
|
|
||||||
:to="projectItem.label[2]"
|
|
||||||
external
|
|
||||||
target="_blank"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<UnoIcon
|
|
||||||
i-bi-twitter-x
|
|
||||||
text="24px app-text-grey"
|
|
||||||
/>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="projectItem.type === 'ecosystem'"
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
justify-start
|
|
||||||
gap-2px
|
|
||||||
>
|
|
||||||
<NuxtImg
|
|
||||||
v-for="ecosystem of projectItem.label"
|
|
||||||
:key="ecosystem"
|
|
||||||
:src="ecosystem"
|
|
||||||
w-24px
|
|
||||||
h-24px
|
|
||||||
rounded-full
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ProjectRating
|
|
||||||
v-if="projectItem.type! === 'rating' && projectItem.rating"
|
|
||||||
:percentage="projectItem.rating.points"
|
|
||||||
:rating="projectItem.rating"
|
|
||||||
:type="projectItem.rating.type"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
|
v-if="projectItem.type === 'links'"
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
justify-end
|
justify-start
|
||||||
w-full
|
|
||||||
gap-16px
|
gap-16px
|
||||||
>
|
>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="project.website"
|
v-if="projectItem.label[0]"
|
||||||
block
|
:to="projectItem.label[0]"
|
||||||
lg:hidden
|
|
||||||
:to="project.website"
|
|
||||||
external
|
external
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<UnoIcon
|
<UnoIcon
|
||||||
|
i-ic-baseline-language
|
||||||
i-iconoir-internet
|
text="24px app-text-grey"
|
||||||
text="24px"
|
|
||||||
/>
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<div
|
<NuxtLink
|
||||||
v-if="filter.sortby === 'score' || filter.sortby === 'title' || isLargeScreen"
|
v-if="projectItem.label[1]"
|
||||||
flex
|
:to="projectItem.label[1]"
|
||||||
items-center
|
external
|
||||||
justify-center
|
target="_blank"
|
||||||
border="2px app-white"
|
@click.stop
|
||||||
text="14px md:18px"
|
|
||||||
leading="24px md:32px"
|
|
||||||
max-h-="28px md:32px"
|
|
||||||
max-w="48px md:56px"
|
|
||||||
w-full
|
|
||||||
font-700
|
|
||||||
whitespace-nowrap
|
|
||||||
>
|
>
|
||||||
{{ project.percentage }} %
|
<UnoIcon
|
||||||
</div>
|
i-mdi-github
|
||||||
<ProjectRating
|
text="24px app-text-grey"
|
||||||
v-if="(filter.sortby === 'openess' || filter.sortby === 'technology' || filter.sortby === 'privacy') && project.ratings?.find((r) => r.type === filter.sortby) && !isLargeScreen"
|
text-27px
|
||||||
:percentage="project.ratings.find((r) => r.type === filter.sortby)!.points"
|
/>
|
||||||
:rating="project.ratings.find((r) => r.type === filter.sortby)!"
|
</NuxtLink>
|
||||||
compact
|
<NuxtLink
|
||||||
|
v-if="projectItem.label[2]"
|
||||||
|
:to="projectItem.label[2]"
|
||||||
|
external
|
||||||
|
target="_blank"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<UnoIcon
|
||||||
|
i-bi-twitter-x
|
||||||
|
text="24px app-text-grey"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="projectItem.type === 'ecosystem'"
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-start
|
||||||
|
gap-2px
|
||||||
|
>
|
||||||
|
<NuxtImg
|
||||||
|
v-for="ecosystem of projectItem.label"
|
||||||
|
:key="ecosystem"
|
||||||
|
:src="ecosystem"
|
||||||
|
w-24px
|
||||||
|
h-24px
|
||||||
|
rounded-full
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
<ProjectRating
|
||||||
</div>
|
v-if="projectItem.type! === 'rating' && projectItem.rating"
|
||||||
|
:percentage="projectItem.rating.points"
|
||||||
|
:rating="projectItem.rating"
|
||||||
|
:type="projectItem.rating.type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-end
|
||||||
|
w-full
|
||||||
|
gap-16px
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="project.website"
|
||||||
|
block
|
||||||
|
lg:hidden
|
||||||
|
:to="project.website"
|
||||||
|
external
|
||||||
|
target="_blank"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<UnoIcon
|
||||||
|
|
||||||
|
i-iconoir-internet
|
||||||
|
text="24px"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
<div
|
||||||
|
v-if="filter.sortby === 'score' || filter.sortby === 'title' || isLargeScreen"
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
border="2px app-white"
|
||||||
|
text="14px md:18px"
|
||||||
|
leading="24px md:32px"
|
||||||
|
max-h-="28px md:32px"
|
||||||
|
max-w="48px md:56px"
|
||||||
|
w-full
|
||||||
|
font-700
|
||||||
|
whitespace-nowrap
|
||||||
|
>
|
||||||
|
{{ project.percentage }} %
|
||||||
|
</div>
|
||||||
|
<ProjectRating
|
||||||
|
v-if="(filter.sortby === 'openess' || filter.sortby === 'technology' || filter.sortby === 'privacy') && project.ratings?.find((r) => r.type === filter.sortby) && !isLargeScreen"
|
||||||
|
:percentage="project.ratings.find((r) => r.type === filter.sortby)!.points"
|
||||||
|
:rating="project.ratings.find((r) => r.type === filter.sortby)!"
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import type { InputOption } from '~/types'
|
import type { InputOption } from '~/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
name: string
|
||||||
options: InputOption[]
|
options: InputOption[]
|
||||||
modelValue: string | number
|
modelValue: string | number
|
||||||
count?: number
|
count?: number
|
||||||
|
@ -26,6 +27,7 @@ function onOptionSelected(value: string | number) {
|
||||||
>
|
>
|
||||||
<div class="relative font-700 font-24px">
|
<div class="relative font-700 font-24px">
|
||||||
<HeadlessListboxButton
|
<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="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' }"
|
:class="{ 'text-app-danger!': isOptionSelected?.value === 'sunset' }"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,33 +4,8 @@ import type { ProjectShallow } from '~/types'
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
projects: { title: string, projects: ProjectShallow[] }[]
|
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)
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -39,123 +14,11 @@ const cardTitles = ref< { label: string, sortKey: string, togglable?: boolean }[
|
||||||
flex-col
|
flex-col
|
||||||
items-start
|
items-start
|
||||||
>
|
>
|
||||||
<template
|
<ProjectGridGroup
|
||||||
v-for="group in projects"
|
v-for="group in projects"
|
||||||
:key="group.title"
|
:key="group.title"
|
||||||
>
|
:group="group"
|
||||||
<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
|
|
||||||
/>
|
|
||||||
<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">
|
<div v-if="totalProjectsCount === 0">
|
||||||
<h3>No Projects found...</h3>
|
<h3>No Projects found...</h3>
|
||||||
</div>
|
</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'])
|
const emits = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
interface SelectProps {
|
interface SelectProps {
|
||||||
|
name: string
|
||||||
options: InputOption[]
|
options: InputOption[]
|
||||||
modelValue: string
|
modelValue: string
|
||||||
isMarginTop?: boolean
|
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']"
|
:class="[isMarginTop ? 'mt-2' : 'mt-0', blackAndWhite ? 'bg-app-black' : 'bg-app-white']"
|
||||||
>
|
>
|
||||||
<HeadlessListboxButton
|
<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']"
|
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
block
|
|
||||||
text="12px lg:14px app-text-grey"
|
text="12px lg:14px app-text-grey"
|
||||||
leading="16px lg:24px"
|
leading="16px lg:24px"
|
||||||
>
|
>
|
||||||
Sort by:
|
Sort by:
|
||||||
</span>
|
</div>
|
||||||
<span class="block truncate mr-8px">{{ props.options.find(option => option.value === selectedValue)?.label }}</span>
|
<div class="truncate">
|
||||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
{{ props.options.find(option => option.value === selectedValue)?.label }}
|
||||||
<UnoIcon
|
</div>
|
||||||
i-ic-baseline-arrow-drop-down
|
<UnoIcon
|
||||||
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']"
|
i-ic-baseline-arrow-drop-down
|
||||||
/>
|
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']"
|
||||||
</span>
|
text="18px lg:20px"
|
||||||
|
/>
|
||||||
</HeadlessListboxButton>
|
</HeadlessListboxButton>
|
||||||
|
|
||||||
<transition
|
<transition
|
||||||
|
|
|
@ -124,23 +124,48 @@ export const useData = defineStore('data', () => {
|
||||||
const shallowProjects = computed(() => projects.value.map(project => projectToShallow(project)))
|
const shallowProjects = computed(() => projects.value.map(project => projectToShallow(project)))
|
||||||
|
|
||||||
const getProjectsByFilters = <T extends ProjectShallow>(options?: { shallow: boolean }): T[] => {
|
const getProjectsByFilters = <T extends ProjectShallow>(options?: { shallow: boolean }): T[] => {
|
||||||
const filteredProjects = projects.value
|
const selectedCategory = selectedCategoryId.value !== 'all' ? selectedCategoryId.value : undefined
|
||||||
.filter(project =>
|
const selectedUsecase = selectedUsecaseId.value !== 'all' ? selectedUsecaseId.value.toLowerCase() : undefined
|
||||||
selectedCategoryId.value !== 'all' ? project.categories.includes(selectedCategoryId.value) : true,
|
const selectedEcosystem = selectedEcosystemId.value !== 'all' ? selectedEcosystemId.value.toLowerCase() : undefined
|
||||||
)
|
const selectedAssetsUsed = selectedAssetsUsedId.value !== 'all' ? selectedAssetsUsedId.value.toLowerCase() : undefined
|
||||||
.filter(project =>
|
const selectedFeature = selectedFeaturesId.value !== 'all' ? selectedFeaturesId.value.toLowerCase() : undefined
|
||||||
selectedUsecaseId.value !== 'all' ? selectedUsecaseId.value === 'sunset' ? project.sunset : project.usecases?.map(u => u.toLowerCase()).includes(selectedUsecaseId.value.toLowerCase()) : true,
|
|
||||||
)
|
return (projects.value.filter((project) => {
|
||||||
.filter(project =>
|
if (selectedCategory && !project.categories.includes(selectedCategory))
|
||||||
selectedEcosystemId.value !== 'all' ? project.ecosystem?.map(e => e.toLowerCase()).includes(selectedEcosystemId.value.toLowerCase()) : true,
|
return false
|
||||||
)
|
|
||||||
.filter(project =>
|
if (selectedUsecase) {
|
||||||
selectedAssetsUsedId.value !== 'all' ? project.assets_used?.map(a => a.toLowerCase()).includes(selectedAssetsUsedId.value.toLowerCase()) : true,
|
if (selectedUsecase === 'sunset') {
|
||||||
)
|
if (!project.sunset)
|
||||||
.filter(project =>
|
return false
|
||||||
selectedFeaturesId.value !== 'all' ? project.technology?.features?.map(f => f.toLowerCase()).includes(selectedFeaturesId.value.toLowerCase()) : true,
|
}
|
||||||
)
|
else {
|
||||||
return (filteredProjects.map(project => options?.shallow ? projectToShallow(project) : project) as T[])
|
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 => {
|
const getProjectById = <T extends Project | ProjectShallow>(id: string, options?: { shallow: boolean }): T => {
|
||||||
|
@ -149,51 +174,39 @@ export const useData = defineStore('data', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredProjects = computed(() => {
|
const filteredProjects = computed(() => {
|
||||||
if (!projects.value)
|
if (!projects.value) return []
|
||||||
return []
|
|
||||||
|
|
||||||
const query = filter.query.toLowerCase()
|
const query = filter.query.toLowerCase()
|
||||||
|
const sortDirection = filter.sortDirection === 'asc' ? 1 : -1
|
||||||
|
const sortBy = filter.sortby
|
||||||
|
|
||||||
const filteredShallowProjects = getProjectsByFilters({ shallow: true })
|
const filteredShallowProjects = getProjectsByFilters({ shallow: true })
|
||||||
.filter((project) => {
|
.filter((project) => {
|
||||||
return (
|
return project?.title1?.toLowerCase().includes(query)
|
||||||
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
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return 0
|
|
||||||
})
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (sortBy === 'score') {
|
||||||
|
return sortDirection * (a.percentage - b.percentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
return filteredShallowProjects
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupedProjectsPerCategory = computed(() => {
|
const groupedProjectsPerCategory = computed(() => {
|
||||||
const groupedProjects = categories.value.map((category) => {
|
const groupedProjects = categories.value.map((category) => {
|
||||||
// Find all projects that include this category
|
|
||||||
const projectsInCategory = filteredProjects.value.filter(project =>
|
const projectsInCategory = filteredProjects.value.filter(project =>
|
||||||
project.categories.includes(category.id),
|
project.categories.includes(category.id),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue