feat(mobile): update rating to support mobile and some more mobile adjustments

This commit is contained in:
Daniel Klein 2024-09-18 20:46:47 +02:00
parent 6a8a305ce7
commit 6830e499cd
15 changed files with 265 additions and 97 deletions

View file

@ -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,17 +62,24 @@ 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' } })" @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="group relative inline-block"
> >
{{ project.title1 }} {{ project.title1 }}
<div
class=" i-bx-link-external ml-4px text-14px opacity-0 group-hover:opacity-100 transition-opacity"
/>
</h1> </h1>
</div> </div>
<p <p
@ -107,10 +116,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 +128,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 +141,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 +183,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
i-iconoir-internet :to="project.website"
text="24px" external
/> target="_blank"
@click.stop
>
<UnoIcon
i-iconoir-internet
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 +214,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>

View file

@ -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"

View file

@ -93,6 +93,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 +109,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

View file

@ -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>

View file

@ -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}`"

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -63,6 +63,7 @@ export const useData = defineStore('data', () => {
projects.value = data.projects.map(project => ({ projects.value = data.projects.map(project => ({
...project, ...project,
ratings: generateProjectRating(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,

View file

@ -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"
} }
} }

View file

@ -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

View file

@ -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"
}, },

View file

@ -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

View file

@ -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()
}