mirror of
https://github.com/web3privacy/explorer-app.git
synced 2024-10-15 16:46:26 +02:00
Merge pull request #23 from web3privacy/dk/mobile-adjustments
dk/mobile-adjustments
This commit is contained in:
commit
0bafae3aee
16 changed files with 264 additions and 100 deletions
|
@ -4,7 +4,9 @@ import type { ProjectRating, ProjectShallow } from '~/types'
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
project: ProjectShallow
|
project: ProjectShallow
|
||||||
}>()
|
}>()
|
||||||
const { switcher, ecosystems } = storeToRefs(useData())
|
const { switcher, ecosystems, filter } = storeToRefs(useData())
|
||||||
|
|
||||||
|
const isLargeScreen = useMediaQuery('(min-width: 1024px)')
|
||||||
|
|
||||||
const ratings: { label: string, type: string, rating: ProjectRating }[] = (props.project.ratings || []).map(rating => ({ label: rating.name, type: 'rating', rating: rating }))
|
const ratings: { label: string, type: string, rating: ProjectRating }[] = (props.project.ratings || []).map(rating => ({ label: rating.name, type: 'rating', rating: rating }))
|
||||||
const ecosystem: { label: string[], type: string } = { label: ecosystems.value.filter(e => (props.project.ecosystem || []).includes(e.id)).map(e => e.icon!), type: 'ecosystem' }
|
const ecosystem: { label: string[], type: string } = { label: ecosystems.value.filter(e => (props.project.ecosystem || []).includes(e.id)).map(e => e.icon!), type: 'ecosystem' }
|
||||||
|
@ -60,15 +62,17 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
gap-8px
|
gap-8px
|
||||||
@click.prevent="navigateTo(project.website, { external: true, open: { target: '_blank' } })"
|
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
text="14px app-white"
|
text="14px app-white"
|
||||||
font-700
|
font-700
|
||||||
line-clamp-1
|
line-clamp-1
|
||||||
hover:underline
|
hover:underline
|
||||||
underline-offset-3
|
underline-offset-3
|
||||||
leading="20px lg:32px"
|
leading="20px lg:32px"
|
||||||
|
class="relative inline-block"
|
||||||
>
|
>
|
||||||
{{ project.title1 }}
|
{{ project.title1 }}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -107,10 +111,11 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
||||||
gap-16px
|
gap-16px
|
||||||
>
|
>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="projectItem.label[1]"
|
v-if="projectItem.label[0]"
|
||||||
:to="projectItem.label[1]"
|
:to="projectItem.label[0]"
|
||||||
external
|
external
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<UnoIcon
|
<UnoIcon
|
||||||
i-ic-baseline-language
|
i-ic-baseline-language
|
||||||
|
@ -118,10 +123,11 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
||||||
/>
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="projectItem.label[2]"
|
v-if="projectItem.label[1]"
|
||||||
:to="projectItem.label[2]"
|
:to="projectItem.label[1]"
|
||||||
external
|
external
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<UnoIcon
|
<UnoIcon
|
||||||
i-mdi-github
|
i-mdi-github
|
||||||
|
@ -130,10 +136,11 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
||||||
/>
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="projectItem.label[0]"
|
v-if="projectItem.label[2]"
|
||||||
:to="projectItem.label[0]"
|
:to="projectItem.label[2]"
|
||||||
external
|
external
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<UnoIcon
|
<UnoIcon
|
||||||
i-bi-twitter-x
|
i-bi-twitter-x
|
||||||
|
@ -171,13 +178,23 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
||||||
w-full
|
w-full
|
||||||
gap-16px
|
gap-16px
|
||||||
>
|
>
|
||||||
<UnoIcon
|
<NuxtLink
|
||||||
|
v-if="project.website"
|
||||||
block
|
block
|
||||||
lg:hidden
|
lg:hidden
|
||||||
|
:to="project.website"
|
||||||
|
external
|
||||||
|
target="_blank"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<UnoIcon
|
||||||
|
|
||||||
i-iconoir-internet
|
i-iconoir-internet
|
||||||
text="24px"
|
text="24px"
|
||||||
/>
|
/>
|
||||||
|
</NuxtLink>
|
||||||
<div
|
<div
|
||||||
|
v-if="filter.sortby === 'score' || filter.sortby === 'title' || isLargeScreen"
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
justify-center
|
justify-center
|
||||||
|
@ -192,6 +209,12 @@ const projectItems: { label: string | string[], type: string, rating?: ProjectRa
|
||||||
>
|
>
|
||||||
{{ project.percentage }} %
|
{{ project.percentage }} %
|
||||||
</div>
|
</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>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,7 +26,7 @@ function onOptionSelected(value: string | number) {
|
||||||
>
|
>
|
||||||
<div class="relative font-700 font-24px">
|
<div class="relative font-700 font-24px">
|
||||||
<HeadlessListboxButton
|
<HeadlessListboxButton
|
||||||
class="relative cursor-pointer py-6px px-14px text-left border-2px bg-app-white text-app-black 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"
|
||||||
>
|
>
|
||||||
<span class="block truncate mr-8px font">{{ isOptionSelected?.label }} <span
|
<span class="block truncate mr-8px font">{{ isOptionSelected?.label }} <span
|
||||||
v-if="titleShowCount"
|
v-if="titleShowCount"
|
||||||
|
|
|
@ -50,7 +50,6 @@ const cardTitles = ref< { label: string, sortKey: string, togglable?: boolean }[
|
||||||
gap-x-12px
|
gap-x-12px
|
||||||
w-full
|
w-full
|
||||||
mb="8px lg:16px"
|
mb="8px lg:16px"
|
||||||
mt-22px
|
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
text="app-white 16px lg:24px"
|
text="app-white 16px lg:24px"
|
||||||
|
@ -93,6 +92,8 @@ const cardTitles = ref< { label: string, sortKey: string, togglable?: boolean }[
|
||||||
Project name
|
Project name
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
|
hidden
|
||||||
|
lg:block
|
||||||
type="button"
|
type="button"
|
||||||
:class="['title' === filter.sortby ? filter.sortDirection === 'desc' ? 'i-ic-baseline-arrow-drop-up'
|
: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'
|
||||||
|
@ -107,23 +108,9 @@ const cardTitles = ref< { label: string, sortKey: string, togglable?: boolean }[
|
||||||
gap-4px
|
gap-4px
|
||||||
lg:hidden
|
lg:hidden
|
||||||
>
|
>
|
||||||
<p
|
<SortSelectBox
|
||||||
text="12px lg:14px app-text-grey"
|
v-model="filter.sortby"
|
||||||
leading="16px lg:24px"
|
:options="['title', 'openess', 'technology', 'privacy', 'score'].map((o) => ({ label: capitalize(o), value: o }))"
|
||||||
>
|
|
||||||
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>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,26 +1,17 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Project } from '~/types'
|
import type { Project, ProjectRating } from '~/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
project: Project
|
project: Project
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// const availableSupport = computed(() => {
|
const isLargeScreen = useMediaQuery('(min-width: 1024px)')
|
||||||
// const filteredKeys = ['forum', 'discord', 'twitter', 'lens', 'farcaster', 'telegram']
|
|
||||||
// if (typeof props.project.links === 'object' && (props.project.links !== null || props.project.links !== undefined))
|
|
||||||
// return Object.keys(props.project.links).filter(key => filteredKeys.includes(key)).length
|
|
||||||
|
|
||||||
// return 0
|
const selectedMobileRating = ref<ProjectRating>()
|
||||||
// })
|
|
||||||
|
|
||||||
/**
|
function onSelectMobileRating(rating: ProjectRating) {
|
||||||
* From data points
|
selectedMobileRating.value = selectedMobileRating.value?.type === rating.type ? undefined : rating
|
||||||
- product readiness
|
}
|
||||||
- docs (yes/no)
|
|
||||||
- github (yes/no)
|
|
||||||
- team: anon / public
|
|
||||||
- audit: yes / no
|
|
||||||
*/
|
|
||||||
|
|
||||||
const logo = props.project?.logos?.at(0)?.url
|
const logo = props.project?.logos?.at(0)?.url
|
||||||
</script>
|
</script>
|
||||||
|
@ -172,6 +163,7 @@ const logo = props.project?.logos?.at(0)?.url
|
||||||
flex
|
flex
|
||||||
flex-col
|
flex-col
|
||||||
lg:flex-row
|
lg:flex-row
|
||||||
|
gap-y-4px
|
||||||
items-center
|
items-center
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
|
@ -183,7 +175,10 @@ const logo = props.project?.logos?.at(0)?.url
|
||||||
<ProjectRating
|
<ProjectRating
|
||||||
:rating="rating"
|
:rating="rating"
|
||||||
:percentage="rating.points"
|
:percentage="rating.points"
|
||||||
|
:disable-popover="!isLargeScreen"
|
||||||
compact
|
compact
|
||||||
|
:selected="rating.type === selectedMobileRating?.type && !isLargeScreen"
|
||||||
|
@selected="onSelectMobileRating(rating)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -218,9 +213,18 @@ const logo = props.project?.logos?.at(0)?.url
|
||||||
py="2px lg:8px"
|
py="2px lg:8px"
|
||||||
lg:py-4px
|
lg:py-4px
|
||||||
>
|
>
|
||||||
{{ calculateScore(project) }} %
|
{{ project.percentage }} %
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div col-span-4 flex items-center justify-center w-full v-if="selectedMobileRating && !isLargeScreen">
|
||||||
|
<ProjectRating
|
||||||
|
:rating="selectedMobileRating"
|
||||||
|
:percentage="selectedMobileRating.points"
|
||||||
|
:disable-popover="!isLargeScreen"
|
||||||
|
compact
|
||||||
|
show-only-popover
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -104,6 +104,9 @@ function tooltipMouseOut() {
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
gap-4px
|
||||||
:color="color ?? '#FFF'"
|
:color="color ?? '#FFF'"
|
||||||
text-14px
|
text-14px
|
||||||
:class="`sm:text-${textSize ?? '16px'} font-${bold ? '700' : '400'} opacity-${opacity}`"
|
:class="`sm:text-${textSize ?? '16px'} font-${bold ? '700' : '400'} opacity-${opacity}`"
|
||||||
|
|
|
@ -54,7 +54,7 @@ defineProps<{
|
||||||
>
|
>
|
||||||
<UnoIcon
|
<UnoIcon
|
||||||
i-ic-twotone-open-in-new
|
i-ic-twotone-open-in-new
|
||||||
text="22px app-text-grey"
|
text="20px app-text-grey"
|
||||||
/>
|
/>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</ProjectInfoItem>
|
</ProjectInfoItem>
|
||||||
|
|
|
@ -6,8 +6,13 @@ const props = defineProps<{
|
||||||
rating: ProjectRating
|
rating: ProjectRating
|
||||||
percentage: number
|
percentage: number
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
disablePopover?: boolean
|
||||||
|
showOnlyPopover?: boolean
|
||||||
|
selected?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits(['selected'])
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'#ff0000', // 0-10%
|
'#ff0000', // 0-10%
|
||||||
'#ff4500', // 11-20%
|
'#ff4500', // 11-20%
|
||||||
|
@ -26,11 +31,24 @@ const backgroundColorByScore = computed(() => {
|
||||||
const colorIndex = Math.floor(normalizedPercentage / 10)
|
const colorIndex = Math.floor(normalizedPercentage / 10)
|
||||||
return colors[colorIndex]
|
return colors[colorIndex]
|
||||||
})
|
})
|
||||||
|
const isLargeScreen = useMediaQuery('(min-width: 1024px)')
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
if (isLargeScreen.value)
|
||||||
|
return
|
||||||
|
if (props.disablePopover)
|
||||||
|
emits('selected')
|
||||||
|
else
|
||||||
|
isPopoverVisible.value = !isPopoverVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
const isPopoverVisible = ref(false)
|
const isPopoverVisible = ref(false)
|
||||||
|
|
||||||
let hideTimeout: ReturnType<typeof setTimeout> | null = null
|
let hideTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
const showPopover = () => {
|
const showPopover = () => {
|
||||||
|
if (!isLargeScreen.value)
|
||||||
|
return
|
||||||
|
console.log('show')
|
||||||
if (hideTimeout) {
|
if (hideTimeout) {
|
||||||
clearTimeout(hideTimeout)
|
clearTimeout(hideTimeout)
|
||||||
hideTimeout = null
|
hideTimeout = null
|
||||||
|
@ -39,6 +57,8 @@ const showPopover = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const hidePopover = () => {
|
const hidePopover = () => {
|
||||||
|
if (!isLargeScreen.value)
|
||||||
|
return
|
||||||
hideTimeout = setTimeout(() => {
|
hideTimeout = setTimeout(() => {
|
||||||
isPopoverVisible.value = false
|
isPopoverVisible.value = false
|
||||||
}, 100) // Delay of 200ms before hiding
|
}, 100) // Delay of 200ms before hiding
|
||||||
|
@ -46,17 +66,24 @@ const hidePopover = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div
|
||||||
|
class="relative"
|
||||||
|
:class="{ 'w-full': showOnlyPopover }"
|
||||||
|
>
|
||||||
<!-- Main div that shows rating and triggers the popover on hover -->
|
<!-- Main div that shows rating and triggers the popover on hover -->
|
||||||
<div
|
<div
|
||||||
|
v-if="!showOnlyPopover"
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
p-12px
|
p-12px
|
||||||
gap-4px
|
gap-4px
|
||||||
hover:bg-app-bg-rating-hover
|
hover:bg-app-bg-rating-hover
|
||||||
|
:class="{ 'bg-app-bg-rating-hover rounded-8px': selected }"
|
||||||
hover:rounded-8px
|
hover:rounded-8px
|
||||||
|
cursor-pointer
|
||||||
@mouseenter="showPopover"
|
@mouseenter="showPopover"
|
||||||
@mouseleave="hidePopover"
|
@mouseleave="hidePopover"
|
||||||
|
@click.prevent="onClick()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="point of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
v-for="point of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
||||||
|
@ -68,6 +95,7 @@ const hidePopover = () => {
|
||||||
|
|
||||||
<!-- Popover panel that appears on hover -->
|
<!-- Popover panel that appears on hover -->
|
||||||
<transition
|
<transition
|
||||||
|
v-if="!showOnlyPopover"
|
||||||
enter-active-class="transition duration-300 ease-out"
|
enter-active-class="transition duration-300 ease-out"
|
||||||
enter-from-class="transform scale-95 opacity-0"
|
enter-from-class="transform scale-95 opacity-0"
|
||||||
enter-to-class="transform scale-100 opacity-100"
|
enter-to-class="transform scale-100 opacity-100"
|
||||||
|
@ -76,10 +104,10 @@ const hidePopover = () => {
|
||||||
leave-to-class="transform scale-95 opacity-0"
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isPopoverVisible"
|
v-if="(isPopoverVisible && !disablePopover)"
|
||||||
class="absolute mt-2 p-2 bg-app-bg-rating-hover w-240px shadow-lg rounded"
|
class="absolute mt-2 p-2 bg-app-bg-rating-hover w-240px shadow-lg rounded"
|
||||||
z-100
|
z-100
|
||||||
style="left: 50%; transform: translateX(-50%);"
|
left="-250% lg:50%"
|
||||||
flex
|
flex
|
||||||
flex-col
|
flex-col
|
||||||
gap-14px
|
gap-14px
|
||||||
|
@ -88,44 +116,13 @@ const hidePopover = () => {
|
||||||
@mouseenter="showPopover"
|
@mouseenter="showPopover"
|
||||||
@mouseleave="hidePopover"
|
@mouseleave="hidePopover"
|
||||||
>
|
>
|
||||||
<div
|
<ProjectRatingInfo :items="rating.items" />
|
||||||
v-for="item in rating.items"
|
|
||||||
:key="item.label"
|
|
||||||
flex
|
|
||||||
justify-between
|
|
||||||
items-center
|
|
||||||
text-12px
|
|
||||||
font-700
|
|
||||||
leading-20px
|
|
||||||
:class="[item.isValid ? 'text-app-white': 'text-app-text-rating-negative']"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
gap-6px
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
:class="[item.isValid ? 'i-ic-sharp-thumb-up' : 'i-ic-sharp-thumb-down']"
|
|
||||||
text-20px
|
|
||||||
mt--4px
|
|
||||||
/>
|
|
||||||
{{ item.label }}
|
|
||||||
</div>
|
|
||||||
<NuxtLink
|
|
||||||
v-if="item.isValid && item.positive === 'Link'"
|
|
||||||
:to="item.value"
|
|
||||||
target="_blank"
|
|
||||||
external
|
|
||||||
underline
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
{{ item.positive }}
|
|
||||||
</NuxtLink>
|
|
||||||
<div v-else>
|
|
||||||
{{ item.isValid ? item.positive : item.negative }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
<ProjectRatingInfo
|
||||||
|
v-if="showOnlyPopover"
|
||||||
|
:items="rating.items"
|
||||||
|
:compact="false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
55
components/Project/ProjectRatingInfo.vue
Normal file
55
components/Project/ProjectRatingInfo.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ProjectRatingItem } from '~/types'
|
||||||
|
|
||||||
|
withDefaults(defineProps<{ items: ProjectRatingItem[], compact?: boolean }>(), {
|
||||||
|
compact: true,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
gap-8px
|
||||||
|
:class="{ 'mt-16px': !compact }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.label"
|
||||||
|
flex
|
||||||
|
justify-between
|
||||||
|
items-center
|
||||||
|
text-12px
|
||||||
|
font-700
|
||||||
|
leading-20px
|
||||||
|
w-full
|
||||||
|
:class="[item.isValid ? 'text-app-white': 'text-app-text-rating-negative']"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
gap-6px
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[item.isValid ? 'i-ic-sharp-thumb-up' : 'i-ic-sharp-thumb-down']"
|
||||||
|
text-20px
|
||||||
|
mt--4px
|
||||||
|
/>
|
||||||
|
{{ item.label }}
|
||||||
|
</div>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="item.isValid && item.positive === 'Link'"
|
||||||
|
:to="item.value"
|
||||||
|
target="_blank"
|
||||||
|
external
|
||||||
|
underline
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
{{ item.positive }}
|
||||||
|
</NuxtLink>
|
||||||
|
<div v-else>
|
||||||
|
{{ item.isValid ? item.positive : item.negative }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
78
components/SortSelectBox.vue
Normal file
78
components/SortSelectBox.vue
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { InputOption } from '~/types'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SelectProps>(), {
|
||||||
|
isMarginTop: true,
|
||||||
|
blackAndWhite: true,
|
||||||
|
})
|
||||||
|
const emits = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
interface SelectProps {
|
||||||
|
options: InputOption[]
|
||||||
|
modelValue: string
|
||||||
|
isMarginTop?: boolean
|
||||||
|
blackAndWhite?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedValue = useVModel(props, 'modelValue', emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<HeadlessListbox
|
||||||
|
v-model="selectedValue"
|
||||||
|
as="div"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative font-700"
|
||||||
|
: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"
|
||||||
|
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
block
|
||||||
|
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">
|
||||||
|
<UnoIcon
|
||||||
|
i-ic-baseline-arrow-drop-down
|
||||||
|
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</HeadlessListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition ease-in duration-100"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<HeadlessListboxOptions
|
||||||
|
class="absolute z-100 max-h-60 w-full divide-y-2px border-2px overflow-auto bg-app-black text-app-white focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<HeadlessListboxOption
|
||||||
|
v-for="option in props.options"
|
||||||
|
:key="option.value"
|
||||||
|
v-slot="{ active, selected }"
|
||||||
|
as="template"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="w-full relative cursor-pointer select-none py-8px p-16px text-xs sm:text-sm"
|
||||||
|
:class="[active ? 'bg-#ffffff1a' : 'text-white']"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="block truncate"
|
||||||
|
:class="[selected ? 'font-semibold' : 'font-normal']"
|
||||||
|
>{{ option.label }}</span>
|
||||||
|
</li>
|
||||||
|
</HeadlessListboxOption>
|
||||||
|
</HeadlessListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</HeadlessListbox>
|
||||||
|
</template>
|
|
@ -60,9 +60,10 @@ export const useData = defineStore('data', () => {
|
||||||
features: Feature[]
|
features: Feature[]
|
||||||
ranks: Rank[]
|
ranks: Rank[]
|
||||||
}>('/api/data')
|
}>('/api/data')
|
||||||
|
data.projects.forEach(project => project.ratings = generateProjectRating(project))
|
||||||
projects.value = data.projects.map(project => ({
|
projects.value = data.projects.map(project => ({
|
||||||
...project,
|
...project,
|
||||||
ratings: generateProjectRating(project),
|
percentage: Math.round((project.ratings?.reduce((a, b) => a + b.points, 0) || 0) / 1.5),
|
||||||
})).filter(p => p.name)
|
})).filter(p => p.name)
|
||||||
|
|
||||||
categories.value = data.categories
|
categories.value = data.categories
|
||||||
|
@ -98,7 +99,7 @@ export const useData = defineStore('data', () => {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
title1: project.name,
|
title1: project.name,
|
||||||
description: project.description ?? 'N/A',
|
description: project.description ?? 'N/A',
|
||||||
percentage: Math.round((project.ratings?.reduce((a, b) => a + b.points, 0) || 0) / 1.5),
|
percentage: project.percentage,
|
||||||
forum: project.links?.forum,
|
forum: project.links?.forum,
|
||||||
explorer: project.links?.block_explorer,
|
explorer: project.links?.block_explorer,
|
||||||
twitter: project.links?.twitter,
|
twitter: project.links?.twitter,
|
||||||
|
|
|
@ -81,6 +81,8 @@ watch([scrollY, top, y], (newValues, oldValues) => {
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
gap-16px
|
gap-16px
|
||||||
|
overflow-x-auto
|
||||||
|
pb="24px md:0"
|
||||||
>
|
>
|
||||||
<CategorySelectBox
|
<CategorySelectBox
|
||||||
v-model="selectedCategoryId"
|
v-model="selectedCategoryId"
|
||||||
|
|
|
@ -18,9 +18,10 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formkit/auto-animate": "^0.8.2",
|
"@formkit/auto-animate": "^0.8.2",
|
||||||
"@iconify-json/bi": "^1.2.0",
|
"@iconify-json/bi": "^1.2.0",
|
||||||
|
"@iconify-json/bx": "^1.2.0",
|
||||||
|
"@iconify-json/eos-icons": "^1.1.10",
|
||||||
"@iconify-json/heroicons-outline": "^1.2.0",
|
"@iconify-json/heroicons-outline": "^1.2.0",
|
||||||
"@iconify-json/heroicons-solid": "^1.2.0",
|
"@iconify-json/heroicons-solid": "^1.2.0",
|
||||||
"@iconify-json/eos-icons": "^1.1.10",
|
|
||||||
"@iconify-json/ic": "^1.2.0",
|
"@iconify-json/ic": "^1.2.0",
|
||||||
"@iconify-json/iconoir": "^1.2.0",
|
"@iconify-json/iconoir": "^1.2.0",
|
||||||
"@iconify-json/material-symbols": "^1.2.1",
|
"@iconify-json/material-symbols": "^1.2.1",
|
||||||
|
@ -46,7 +47,8 @@
|
||||||
"vitest": "^2.0.5",
|
"vitest": "^2.0.5",
|
||||||
"vue-tsc": "^2.1.4",
|
"vue-tsc": "^2.1.4",
|
||||||
"yaml": "^2.5.1",
|
"yaml": "^2.5.1",
|
||||||
"yup": "^1.4.0"
|
"yup": "^1.4.0",
|
||||||
|
"moment": "^2.30.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vue": "latest"
|
"vue": "latest"
|
||||||
|
@ -57,8 +59,5 @@
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*": "eslint --fix"
|
"*": "eslint --fix"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"moment": "^2.30.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,6 @@ settings:
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
|
||||||
moment:
|
|
||||||
specifier: ^2.30.1
|
|
||||||
version: 2.30.1
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@formkit/auto-animate':
|
'@formkit/auto-animate':
|
||||||
specifier: ^0.8.2
|
specifier: ^0.8.2
|
||||||
|
@ -18,6 +14,9 @@ importers:
|
||||||
'@iconify-json/bi':
|
'@iconify-json/bi':
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
|
'@iconify-json/bx':
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0
|
||||||
'@iconify-json/eos-icons':
|
'@iconify-json/eos-icons':
|
||||||
specifier: ^1.1.10
|
specifier: ^1.1.10
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
|
@ -72,6 +71,9 @@ importers:
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.9.1
|
specifier: ^9.9.1
|
||||||
version: 9.9.1(jiti@1.21.6)
|
version: 9.9.1(jiti@1.21.6)
|
||||||
|
moment:
|
||||||
|
specifier: ^2.30.1
|
||||||
|
version: 2.30.1
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^3.13.0
|
specifier: ^3.13.0
|
||||||
version: 3.13.0(@parcel/watcher@2.4.1)(@types/node@20.8.7)(encoding@0.1.13)(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@4.21.2)(terser@5.22.0)(typescript@5.5.4)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0))(vue-tsc@2.1.4(typescript@5.5.4))
|
version: 3.13.0(@parcel/watcher@2.4.1)(@types/node@20.8.7)(encoding@0.1.13)(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@4.21.2)(terser@5.22.0)(typescript@5.5.4)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0))(vue-tsc@2.1.4(typescript@5.5.4))
|
||||||
|
@ -1128,6 +1130,9 @@ packages:
|
||||||
'@iconify-json/bi@1.2.0':
|
'@iconify-json/bi@1.2.0':
|
||||||
resolution: {integrity: sha512-kaBV87cQlyeMkBBiMqsf3b43Nsxdk/rYKvR29dnktht57WUyHCnBAuH+ca/bscX856CzRpVX+sYs7arjrJD0qA==}
|
resolution: {integrity: sha512-kaBV87cQlyeMkBBiMqsf3b43Nsxdk/rYKvR29dnktht57WUyHCnBAuH+ca/bscX856CzRpVX+sYs7arjrJD0qA==}
|
||||||
|
|
||||||
|
'@iconify-json/bx@1.2.0':
|
||||||
|
resolution: {integrity: sha512-UWTC5BwAYXAPdzpubCH9DDW0Q1SAQNA4CpN4GfsvqKm+SN4I/ppafhxH2EFWSep3liKfkUo0TkzDNh/iKIxyZw==}
|
||||||
|
|
||||||
'@iconify-json/eos-icons@1.2.0':
|
'@iconify-json/eos-icons@1.2.0':
|
||||||
resolution: {integrity: sha512-grdfoS20Z4gWAzNPza7ytguNBWeTOkx4Y6aZHs149t2Z6AhW7zG3VWkkq6M+YuL2G8ugHnBw7ZxgazZ6oiMnIQ==}
|
resolution: {integrity: sha512-grdfoS20Z4gWAzNPza7ytguNBWeTOkx4Y6aZHs149t2Z6AhW7zG3VWkkq6M+YuL2G8ugHnBw7ZxgazZ6oiMnIQ==}
|
||||||
|
|
||||||
|
@ -6629,6 +6634,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify-json/bx@1.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
'@iconify-json/eos-icons@1.2.0':
|
'@iconify-json/eos-icons@1.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
|
@ -16289,7 +16289,7 @@
|
||||||
{
|
{
|
||||||
"field": "links.telegram",
|
"field": "links.telegram",
|
||||||
"label": {
|
"label": {
|
||||||
"name": "Twitter",
|
"name": "Telegram",
|
||||||
"positive": "Link",
|
"positive": "Link",
|
||||||
"negative": "Not available"
|
"negative": "Not available"
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,7 @@ export interface Project {
|
||||||
token_link?: string
|
token_link?: string
|
||||||
[k: string]: unknown
|
[k: string]: unknown
|
||||||
}[]
|
}[]
|
||||||
|
percentage: number
|
||||||
description?: string
|
description?: string
|
||||||
project_type?: string
|
project_type?: string
|
||||||
product_launch_day?: string
|
product_launch_day?: string
|
||||||
|
|
|
@ -3,3 +3,8 @@ import moment from 'moment'
|
||||||
export function formatDate(date: Date | string) {
|
export function formatDate(date: Date | string) {
|
||||||
return moment(date).format('MM / YYYY')
|
return moment(date).format('MM / YYYY')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function capitalize(word: string): string {
|
||||||
|
if (!word) return word
|
||||||
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue