feat(projects-grid): redesign projects grid

This commit is contained in:
Daniel Klein 2024-09-12 08:42:37 +02:00
parent b6e1984046
commit e699a2acfe
8 changed files with 244 additions and 337 deletions

View file

@ -1,10 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ProjectShallow } from '~/types' import type { ProjectShallow } from '~/types'
defineProps<{ const props = defineProps<{
project: ProjectShallow project: ProjectShallow
}>() }>()
const { switcher } = storeToRefs(useData()) const { switcher } = storeToRefs(useData())
const projectItems = ['Swap,Mixer', { label: 'Openess', rating: props.project.ratings.openess, type: 'openess' }, { label: 'Technology', rating: props.project.ratings.technology, type: 'technology' }, { label: 'Privacy', rating: props.project.ratings.privacy, type: 'privacy' }, 'Ecosystem', 'Links']
</script> </script>
<template> <template>
@ -18,230 +20,119 @@ const { switcher } = storeToRefs(useData())
transition-all transition-all
> >
<div <div
relative grid
max-w="96px lg:200px" grid-cols="2 lg:10"
w-full w-full
h="96px lg:200px"
:class="switcher ? '' : 'lg:max-w-full! lg:w-full '"
> >
<div <div
col-span="1 lg:3"
flex flex
items-center items-center
justify-center gap="12px lg:16px"
relative
w-full w-full
my-auto
h-full h-full
h="48px lg:64px"
:class="switcher ? '' : 'lg:max-w-full! lg:w-full '"
> >
<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:196px 96px" max-h="md:64px 48px"
max-w="md:64px 48px"
self-center self-center
z-10 z-10
object-fit object-fit
bg="#121212" bg="#121212"
/> />
</div>
<ClientOnly>
<Badge
v-if="project.percentage"
absolute
bottom-0.5
lg:bottom-0
right-0.5
lg:right-0
mr-2px
mb-2px
:text="`${project.percentage}%`"
class="leading-12px! text-12px! lg:text-18px! border-0!"
px="4px! lg:16px!"
py="4px! lg:8px!"
/>
</ClientOnly>
</div>
<div
h="96px lg:200px"
lg:py-24px
lg:pr-24px
flex
flex-col
justify-center
lg:justify-between
lg:gap-24px
w-full
text-white
:class="switcher ? '' : 'lg:p-16px! lg:py-16px!'"
>
<div
w-full
h-fit
flex
flex-col
gap-8px
>
<div <div
w-fit flex
flex-col
gap-y-4px
lg:flex-row
justify-center
>
<div
w-fit
flex
items-center
gap-8px
@click.prevent="navigateTo(project.website, { external: true, open: { target: '_blank' } })"
>
<h1
text="14px app-white"
font-700
line-clamp-1
hover:underline
underline-offset-3
leading="20px lg:32px"
>
{{ project.title1 }}
</h1>
</div>
<p
text-12px
leading-16px
lg:hidden
>
Usecases
</p>
</div>
</div>
<div
v-for="(projectItem, index) of projectItems"
:key="projectItem.toString()"
hidden
lg:flex
items-center
justify-start
text-14px
leading-24px
>
<p
v-if="typeof projectItem === 'string'"
text-app-text-grey
>
{{ projectItem }}
</p>
<ProjectRating
v-else
:score="index"
:rating="projectItem.rating"
:type="projectItem.type"
/>
</div>
<div
flex
items-center
justify-end
w-full
gap-16px
>
<UnoIcon
block
lg:hidden
i-iconoir-internet
text="24px"
/>
<div
flex flex
items-center items-center
gap-8px justify-center
@click.prevent="navigateTo(project.website, { external: true, open: { target: '_blank' } })" border="2px app-white"
> text="14px md:18px"
<h1 leading="24px md:32px"
text="18px lg:24px app-white" max-h-="28px md:32px"
font-700 max-w="48px md:56px"
line-clamp-1
hover:underline
underline-offset-3
>
{{ project.title1 }}
</h1>
<UnoIcon
i-web-open
text-16px
/>
</div>
<h2
text="14px app-text-grey"
overflow-hidden
text-ellipsis
line-clamp-2
lg:line-clamp-2
>
{{ project.description }}
</h2>
</div>
<div
w-full
flex
justify-between
>
<div
w-full w-full
max-w-692px font-700
grid
whitespace-nowrap whitespace-nowrap
:class="switcher ? 'grid-cols-5' : 'grid-cols-3'"
gap-24px
lg:grid
hidden
> >
<ProjectInfoItem {{ project.percentage }} %
:check-undefined="project?.github"
:link="project?.github"
title="Github"
bold
text-size="18px"
>
<div
flex
items-center
gap-8px
>
<UnoIcon
i-web-github
text-16px
/>
<span>{{ project?.github ? 'YES' : 'NO' }}</span>
</div>
</ProjectInfoItem>
<ProjectInfoItem
:check-undefined="project.readyness"
title="Readyness"
text-size="18px"
>
<div
flex
items-center
gap-12px
>
<UnoIcon
i-web-live
text-10px
:class="(project.readyness === 'Mainnet') ? 'color-#18FF2F' : (project.readyness === 'Testnet') ? 'color-#FFA800' : (project.readyness === 'Alpha') ? 'color-#FF0000' : ''"
/>
<span :class="(project.readyness === 'Alpha') ? 'color-#FFA800' : 'color-white'">{{ project.readyness }}</span>
</div>
</ProjectInfoItem>
<ProjectInfoItem
:check-undefined="true"
title="Team"
text-size="18px"
>
<span v-if="project.team?.length">{{ `${project.team?.length} members` }}</span>
<span
v-else
color="#FF0000"
>{{ 'Anonymous' }}</span>
</ProjectInfoItem>
<ProjectInfoItem
:check-undefined="project?.docs"
:link="project?.docs"
:color="project?.docs ? '#18FF2F' : '#FF0000'"
title="Docs"
bold
text-size="18px"
>
{{ project?.docs ? 'YES' : 'NO' }}
</ProjectInfoItem>
<ProjectInfoItem
:check-undefined="project.audits"
:link="project?.audits?.[0]?.link ?? undefined"
:color="project?.audits ? '#18FF2F' : '#FF0000'"
title="Audit"
bold
text-size="18px"
>
{{ project.audits ? 'YES' : 'NO' }}
</ProjectInfoItem>
</div>
<div
hidden
lg:flex
items-center
gap-16px
>
<UnoIcon
v-if="project.forum"
i-web-forum
text-28px
opacity-50
hover:opacity-100
@click.prevent="navigateTo(project.forum, { external: true, open: { target: '_blank' } })"
/>
<UnoIcon
v-if="project.explorer"
i-web-explorer
text-32px
opacity-50
hover:opacity-100
@click.prevent="navigateTo(project.explorer, { external: true, open: { target: '_blank' } })"
/>
<UnoIcon
v-if="project.twitter"
i-web-twitter_x
text-22px
opacity-50
hover:opacity-100
@click.prevent="navigateTo(project.twitter, { external: true, open: { target: '_blank' } })"
/>
<UnoIcon
v-if="project.coingecko"
i-web-coingecko
text-24px
opacity-50
hover:opacity-100
@click.prevent="navigateTo(project.coingecko, { external: true, open: { target: '_blank' } })"
/>
<UnoIcon
v-if="project.newsletter"
i-web-news
text-28px
opacity-50
hover:opacity-100
@click.prevent="navigateTo(project.newsletter, { external: true, open: { target: '_blank' } })"
/>
</div> </div>
</div> </div>
</div> </div>
</NuxtLink> </NuxtLink>
</template> </template>

View file

@ -34,7 +34,7 @@ function onOptionSelected(value: string) {
>({{ isOptionSelected?.count }})</span></span> >({{ isOptionSelected?.count }})</span></span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<UnoIcon <UnoIcon
i-heroicons-solid-chevron-down i-ic-baseline-arrow-drop-down
text-app-black text-app-black
/> />
</span> </span>

View file

@ -13,6 +13,7 @@ defineProps<{
> >
<UnoIcon <UnoIcon
i-heroicons-solid-pencil i-heroicons-solid-pencil
text-app-text-grey
text-24px text-24px
/> />
<slot /> <slot />

View file

@ -2,7 +2,7 @@
import type { ProjectShallow } from '~/types' import type { ProjectShallow } from '~/types'
const props = defineProps<{ const props = defineProps<{
projects: ProjectShallow[] projects: { title: string, projects: ProjectShallow[] }[]
}>() }>()
const { switcher } = storeToRefs(useData()) const { switcher } = storeToRefs(useData())
@ -29,77 +29,102 @@ const cardTitles = ref< { label: string, togglable?: boolean, toggled?: boolean
flex-col flex-col
items-start items-start
> >
<div <template
v-if="displayedProjects.length" v-for="group in projects"
flex :key="group.title"
items-center
gap-x-12px
w-full
mb="8px md:16px"
>
<h2
text="app-white 16px md:24px"
font-700
leading="24px md:32px"
whitespace-nowrap
>
24 Defi
</h2>
<div
h-2px
w-full
bg-app-white
/>
<button
type="button"
i-heroicons-solid-chevron-down
text="app-white 24px"
/>
</div>
<div
v-if="displayedProjects.length"
flex
items-center
justify-between
w-full
mb-16px
> >
<div <div
flex flex
items-center items-center
gap-4px gap-x-12px
w-full
mb="8px lg:16px"
mt-22px
> >
<p <h2
text="12px md:14px" text="app-white 16px lg:24px"
leading="16px md:24px" font-700
leading="24px lg:32px"
whitespace-nowrap whitespace-nowrap
> >
Project name {{ group.projects.length }} {{ group.title }}
</p> </h2>
<div
h-2px
w-full
bg-app-text-grey
/>
<button <button
type="button" type="button"
i-heroicons-solid-chevron-down i-ic-baseline-arrow-drop-down
text="app-white 20px" text="app-text-grey 24px"
/> />
</div> </div>
<div <div
hidden grid
md:flex grid-cols="2 lg:10"
items-center
justify-end
gap-48px
w-full w-full
mb-16px
> >
<div <div
v-for="title in cardTitles"
:key="title.label"
flex flex
items-center items-center
gap-4px gap-4px
col-span="1 lg:3"
> >
<p <p
text="12px md:14px" text="12px lg:14px app-text-grey"
leading="16px md:24px" leading="16px lg:24px"
whitespace-nowrap
>
Project name
</p>
<button
type="button"
i-ic-baseline-arrow-drop-down
text="app-text-grey 20px"
/>
</div>
<div
flex
items-center
justify-end
gap-4px
lg:hidden
>
<p
text="12px lg:14px app-text-grey"
leading="16px lg:24px"
>
Sort by:
</p>
<p
text="12px lg:14px"
leading="16px lg:24px"
font-700
>
Score
</p>
<button
type="button"
i-ic-baseline-arrow-drop-down
text="app-text-grey 20px"
/>
</div>
<div
v-for="title in cardTitles"
:key="title.label"
lg:flex
items-center
justify-start
last:justify-end
gap-4px
hidden
>
<p
text="12px lg:14px app-text-grey"
leading="16px lg:24px"
whitespace-nowrap whitespace-nowrap
> >
{{ title.label }} {{ title.label }}
@ -108,69 +133,44 @@ const cardTitles = ref< { label: string, togglable?: boolean, toggled?: boolean
v-if="title.togglable" v-if="title.togglable"
type="button" type="button"
:class="[title.toggled :class="[title.toggled
? 'i-heroicons-solid-chevron-up' ? 'i-ic-baseline-arrow-drop-up'
: 'i-heroicons-solid-chevron-down']" : 'i-ic-baseline-arrow-drop-down']"
text="app-white 20px" text="app-text-grey 20px"
@click="title.toggled = !title.toggled" @click="title.toggled = !title.toggled"
/> />
</div> </div>
</div> </div>
<div <div
flex v-if="displayedProjects.length"
items-center grid
gap-4px :class="switcher ? 'grid-cols-1 lg:grid-cols-1' : 'xl:grid-cols-3 lg:grid-cols-3 sm:grid-cols-2 grid-cols-1'"
md:hidden gap-16px
text-white
w-full
> >
<p <NewCard
text="12px md:14px" v-for="project in group.projects"
leading="16px md:24px" :key="project.id"
> :project="project"
Sort by:
</p>
<p
text="12px md:14px"
leading="16px md:24px"
font-700
>
Score
</p>
<button
type="button"
i-heroicons-solid-chevron-down
text="app-white 20px"
/> />
</div> </div>
</div> <div v-else>
<div <h3>No Projects found...</h3>
v-if="displayedProjects.length" </div>
grid <button
:class="switcher ? 'grid-cols-1 lg:grid-cols-1' : 'xl:grid-cols-3 lg:grid-cols-3 sm:grid-cols-2 grid-cols-1'" v-if="displayedProjects.length < projects.length"
gap-16px mt-29px
text-white text="14px"
w-full leading-24px
> font-700
<Card px-12px
v-for="project in displayedProjects" py-4px
:key="project.id" border-2px
:project="project" border-app-white
/> @click="showMoreProjects"
</div> >
<div v-else> Load more projects
<h3>No Projects found...</h3> </button>
</div> </template>
<button
v-if="displayedProjects.length < projects.length"
mt-29px
text="14px"
leading-24px
font-700
px-12px
py-4px
border-2px
border-app-white
@click="showMoreProjects"
>
Load more projects
</button>
</div> </div>
</template> </template>

View file

@ -33,7 +33,7 @@ const selectedValue = useVModel(props, 'modelValue', emits)
<span class="block truncate mr-8px">{{ props.options.find(option => option.value === selectedValue)?.label }}</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"> <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<UnoIcon <UnoIcon
i-heroicons-solid-chevron-down i-ic-baseline-arrow-drop-down
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']" :class="[blackAndWhite ? ' text-app-white' : 'text-app-black']"
/> />
</span> </span>

View file

@ -1,27 +1,26 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { InputOption } from '~/types' import type { InputOption } from '~/types'
const { categories, filteredProjectsCount, selectedCategoryId } = storeToRefs(useData()) const { categories, usecases, ecosystems, assets, features, filteredProjectsCount, selectedCategoryId } = storeToRefs(useData())
const selectedUsecaseId = ref('usecase') const selectedUsecaseId = ref('all')
const selectedEcosystemId = ref('ecosystem') const selectedEcosystemId = ref('all')
const selectedAssetsUsedId = ref('assetsUsed') const selectedAssetsUsedId = ref('all')
const selectedFeaturesId = ref('features') const selectedFeaturesId = ref('all')
const categoriesOptions = ref(categories.value ? categories.value.map(c => ({ label: c.name, value: c.id, count: c.projectsCount })) : []) const categoryOptions = ref<InputOption>(categories.value ? [{ label: 'Category', value: 'all' }, ...categories.value.map(c => ({ label: c.name, value: c.id, count: c.projectsCount }))] : [])
const extendedOptions: InputOption[] = [ const usecaseOptions = ref<InputOption>(usecases.value ? [{ label: 'Usecase', value: 'all' }, ...usecases.value.map(u => ({ label: u.name, value: u.id }))] : [])
{ label: 'Category', value: 'all' }, const ecosystemOptions = ref<InputOption>(ecosystems.value ? [{ label: 'Ecosystem', value: 'all' }, ...ecosystems.value.map(e => ({ label: e.name, value: e.id }))] : [])
...categoriesOptions.value, const assetOptions = ref<InputOption>(assets.value ? [{ label: 'Asset used', value: 'all' }, ...assets.value.map(a => ({ label: a.name, value: a.id }))] : [])
] const featureOptions = ref<InputOption>(features.value ? [{ label: 'Feature', value: 'all' }, ...features.value.map(f => ({ label: f.name, value: f.id }))] : [])
// const selectedCategory = computed(() => {
// return categories.value.find(c => c.id === selectedCategoryId.value)
// })
const selectedCategory = computed(() => { // const sortedFilteredCategories = computed(() => ([
return categories.value.find(c => c.id === selectedCategoryId.value) // categories.value.find(c => c.id === 'defi')!,
}) // ...[...categories.value].sort((a, b) => a.name.localeCompare(b.name)).filter(c => c.id !== 'defi'),
// ]))
const sortedFilteredCategories = computed(() => ([
categories.value.find(c => c.id === 'defi')!,
...[...categories.value].sort((a, b) => a.name.localeCompare(b.name)).filter(c => c.id !== 'defi'),
]))
const { showBar } = storeToRefs(useNavigaiton()) const { showBar } = storeToRefs(useNavigaiton())
const swipeEl = ref() const swipeEl = ref()
@ -78,6 +77,7 @@ watch([scrollY, top, y], (newValues, oldValues) => {
> >
<SearchBox <SearchBox
flex-1 flex-1
placeholder:text-app-text-grey
:placeholder="`Search in ${filteredProjectsCount} Projects`" :placeholder="`Search in ${filteredProjectsCount} Projects`"
/> />
<div <div
@ -85,32 +85,40 @@ watch([scrollY, top, y], (newValues, oldValues) => {
flex flex
items-center items-center
gap-16px gap-16px
overflow-x-auto
> >
<CategorySelectBox <CategorySelectBox
v-model="selectedCategoryId" v-model="selectedCategoryId"
:options="extendedOptions" :options="categoryOptions"
name="categorySelect"
w-full w-full
@selected="selectedCategoryId === 'all' ? navigateTo(`/`) : navigateTo(`/category/${selectedCategoryId}`)" @selected="selectedCategoryId === 'all' ? navigateTo(`/`) : navigateTo(`/category/${selectedCategoryId}`)"
/> />
<CategorySelectBox <CategorySelectBox
v-if="usecases.length"
v-model="selectedUsecaseId" v-model="selectedUsecaseId"
:options="[{ label: 'Usecase', value: 'usecase' }]" name="usecaseSelect"
:options="usecaseOptions"
w-full w-full
/> />
<CategorySelectBox <CategorySelectBox
v-if="ecosystems.length"
v-model="selectedEcosystemId" v-model="selectedEcosystemId"
:options="[{ label: 'Ecosystem', value: 'ecosystem' }]" name="ecosystemSelect"
:options="ecosystemOptions"
w-full w-full
/> />
<CategorySelectBox <CategorySelectBox
v-if="assets.length"
v-model="selectedAssetsUsedId" v-model="selectedAssetsUsedId"
:options="[{ label: 'Assets used', value: 'assetsUsed' }]" name="assetsUsedSelect"
:options="assetOptions"
w-full w-full
/> />
<CategorySelectBox <CategorySelectBox
v-if="features.length"
v-model="selectedFeaturesId" v-model="selectedFeaturesId"
:options="[{ label: 'Features', value: 'features' }]" name="featuresSelect"
:options="featureOptions"
w-full w-full
/> />
</div> </div>

View file

@ -7,9 +7,11 @@ useSeoMeta({
ogDescription: 'There are challenges in finding crucial technical details and comparing various privacy-focused projects.', ogDescription: 'There are challenges in finding crucial technical details and comparing various privacy-focused projects.',
ogImage: '/web3privacy_eye.webp', ogImage: '/web3privacy_eye.webp',
}) })
const { filteredProjects } = storeToRefs(useData()) const { groupedProjectsPerCategory } = storeToRefs(useData())
</script> </script>
<template> <template>
<ProjectGrid :projects="filteredProjects" /> <div>
<ProjectGrid :projects="groupedProjectsPerCategory" />
</div>
</template> </template>

5
utils/text.ts Normal file
View file

@ -0,0 +1,5 @@
import moment from 'moment'
export function formatDate(date: Date | string) {
return moment(date).format('MM / YYYY')
}