feat: more to explore (#360)
This commit is contained in:
parent
a36a26d745
commit
183b1659d1
23 changed files with 530 additions and 17 deletions
61
components/account/AccountBigCard.vue
Normal file
61
components/account/AccountBigCard.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Account } from 'masto'
|
||||
const { account, as = 'div' } = $defineProps<{
|
||||
account: Account
|
||||
as?: string
|
||||
}>()
|
||||
|
||||
cacheAccount(account)
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="as" block focus:outline-none focus-visible:ring="2 primary" v-bind="$attrs">
|
||||
<!-- Banner -->
|
||||
<div px2 pt2>
|
||||
<div rounded of-hidden bg="gray-500/20" aspect="3.19">
|
||||
<img h-full w-full object-cover :src="account.header" :alt="$t('account.profile_description', [account.username])">
|
||||
</div>
|
||||
</div>
|
||||
<div px-4 pb-4 space-y-2>
|
||||
<!-- User info -->
|
||||
<div flex sm:flex-row flex-col flex-gap-2>
|
||||
<div flex items-center justify-between>
|
||||
<div w-17 h-17 rounded-full border-4 border-bg-base z-2 mt--2 ml--1>
|
||||
<AccountAvatar :account="account" />
|
||||
</div>
|
||||
<a block sm:hidden href="javascript:;" @click.stop>
|
||||
<AccountFollowButton :account="account" />
|
||||
</a>
|
||||
</div>
|
||||
<div sm:mt-2>
|
||||
<div>
|
||||
<ContentRich
|
||||
font-bold text-lg line-clamp-1 ws-pre-wrap break-all
|
||||
:content="getDisplayName(account, { rich: true })"
|
||||
:emojis="account.emojis"
|
||||
/>
|
||||
</div>
|
||||
<AccountHandle text-sm :account="account" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Note -->
|
||||
<div v-if="account.note">
|
||||
<ContentRich
|
||||
:content="account.note" :emojis="account.emojis"
|
||||
line-clamp-2
|
||||
/>
|
||||
</div>
|
||||
<!-- Follow info -->
|
||||
<div flex justify-between items-center>
|
||||
<AccountPostsFollowers text-sm :account="account" />
|
||||
<a sm:block hidden href="javascript:;" @click.stop>
|
||||
<AccountFollowButton :account="account" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
30
components/account/AccountBigCardSkeleton.vue
Normal file
30
components/account/AccountBigCardSkeleton.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Banner -->
|
||||
<div px2 pt2>
|
||||
<div rounded of-hidden aspect="3.19" class="flex skeleton-loading-bg" />
|
||||
<div px-4 pb-4 flex="~ col gap-2">
|
||||
<!-- User info -->
|
||||
<div flex sm:flex-row flex-col flex-gap-2>
|
||||
<div flex items-center justify-between>
|
||||
<div w-17 h-17 rounded-full border-4 border-bg-base z-2 mt--2 ml--1 of-hidden bg-base>
|
||||
<div class="flex skeleton-loading-bg" w-full h-full />
|
||||
</div>
|
||||
<div block sm:hidden class="skeleton-loading-bg" h-8 w-30 rounded-full />
|
||||
</div>
|
||||
<div sm:mt-2 flex="~ col 1 gap-2">
|
||||
<div flex class="skeleton-loading-bg" h-5 w-20 rounded />
|
||||
<div flex class="skeleton-loading-bg" h-4 w-40 rounded />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Note -->
|
||||
<div flex class="skeleton-loading-bg" h-4 my3 w="3/5" rounded />
|
||||
<!-- Follow info -->
|
||||
<div flex justify-between items-center>
|
||||
<div flex class="skeleton-loading-bg" h-4 w="sm:1/2 full" rounded />
|
||||
<div sm:flex hidden class="skeleton-loading-bg" h-8 w-30 rounded-full />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
31
components/common/CommonAlert.vue
Normal file
31
components/common/CommonAlert.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: boolean
|
||||
}>(), {
|
||||
modelValue: true,
|
||||
})
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void
|
||||
(event: 'close'): void
|
||||
}>()
|
||||
const visible = useVModel(props, 'modelValue', emits, { passive: true })
|
||||
|
||||
function close() {
|
||||
emits('close')
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
flex="~ gap-2" justify-between items-center
|
||||
class="border-b border-base text-sm text-secondary px4 py2 sm:py4"
|
||||
>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<button text-xl hover:text-primary bg-hover-overflow w-1.4em h-1.4em @click="close()">
|
||||
<div i-ri:close-line />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
44
components/common/CommonRouteTabs.vue
Normal file
44
components/common/CommonRouteTabs.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
const { options, command, replace } = $defineProps<{
|
||||
options: {
|
||||
to: RouteLocationRaw
|
||||
display: string
|
||||
name?: string
|
||||
icon?: string
|
||||
}[]
|
||||
command?: boolean
|
||||
replace?: boolean
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useCommands(() => command
|
||||
? options.map(tab => ({
|
||||
scope: 'Tabs',
|
||||
|
||||
name: tab.display,
|
||||
icon: tab.icon ?? 'i-ri:file-list-2-line',
|
||||
onActivate: () => router.replace(tab.to),
|
||||
}))
|
||||
: [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex w-full items-center lg:text-lg of-x-auto scrollbar-hide>
|
||||
<NuxtLink
|
||||
v-for="(option, index) in options"
|
||||
:key="option?.name || index"
|
||||
:to="option.to"
|
||||
:replace="replace"
|
||||
relative flex flex-auto cursor-pointer sm:px6 px2 rounded transition-all
|
||||
tabindex="1"
|
||||
hover:bg-active transition-100
|
||||
exact-active-class="children:(font-bold !border-primary !op100)"
|
||||
@click="$scrollToTop"
|
||||
>
|
||||
<span ws-nowrap mxa sm:px2 sm:py3 py2 text-center border-b-3 op50 hover:op70 border-transparent>{{ option.display }}</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
23
components/common/CommonTrending.vue
Normal file
23
components/common/CommonTrending.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts" setup>
|
||||
import type { History } from 'masto'
|
||||
|
||||
const {
|
||||
history,
|
||||
maxDay = 2,
|
||||
} = $defineProps<{
|
||||
history: History[]
|
||||
maxDay?: number
|
||||
}>()
|
||||
|
||||
const ongoingHot = $computed(() => history.slice(0, maxDay))
|
||||
|
||||
const people = $computed(() =>
|
||||
ongoingHot.reduce((total: number, item) => total + (Number(item.accounts) || 0), 0),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>
|
||||
{{ $t('command.n-people-in-the-past-n-days', [people, maxDay]) }}
|
||||
</p>
|
||||
</template>
|
28
components/common/CommonTrendingCharts.vue
Normal file
28
components/common/CommonTrendingCharts.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts" setup>
|
||||
import type { History } from 'masto'
|
||||
import sparkline from '@fnando/sparkline'
|
||||
|
||||
const {
|
||||
history,
|
||||
} = $defineProps<{
|
||||
history?: History[]
|
||||
}>()
|
||||
|
||||
const historyNum = $computed(() => {
|
||||
if (!history)
|
||||
return [1, 1, 1, 1, 1, 1, 1]
|
||||
return [...history].reverse().map(item => Number(item.accounts) || 0)
|
||||
})
|
||||
|
||||
const sparklineEl = $ref<SVGSVGElement>()
|
||||
|
||||
watch([$$(historyNum), $$(sparklineEl)], ([historyNum, sparklineEl]) => {
|
||||
if (!sparklineEl)
|
||||
return
|
||||
sparkline(sparklineEl, historyNum)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg ref="sparklineEl" class="sparkline" width="60" height="40" stroke-width="3" />
|
||||
</template>
|
|
@ -10,7 +10,7 @@ const props = defineProps<{
|
|||
}>()
|
||||
const alt = $computed(() => `${props.card.title} - ${props.card.title}`)
|
||||
const isSquare = $computed(() => props.smallPictureOnly || props.card.width === props.card.height)
|
||||
const description = $computed(() => props.card.description ? props.card.description : new URL(props.card.url).hostname)
|
||||
const providerName = $computed(() => props.card.providerName ? props.card.providerName : new URL(props.card.url).hostname)
|
||||
|
||||
// TODO: handle card.type: 'photo' | 'video' | 'rich';
|
||||
</script>
|
||||
|
@ -32,10 +32,9 @@ const description = $computed(() => props.card.description ? props.card.descript
|
|||
v-if="card.image"
|
||||
flex flex-col
|
||||
display-block of-hidden
|
||||
|
||||
border="base"
|
||||
:class="{
|
||||
'min-w-32 w-32 h-32 border-r': isSquare,
|
||||
'sm:(min-w-32 w-32 h-32) min-w-22 w-22 h-22 border-r': isSquare,
|
||||
'w-full aspect-[1.91] border-b': !isSquare,
|
||||
'rounded-lg': root,
|
||||
}"
|
||||
|
@ -49,19 +48,41 @@ const description = $computed(() => props.card.description ? props.card.descript
|
|||
w-full h-full object-cover
|
||||
/>
|
||||
</div>
|
||||
<div v-else min-w-32 w-32 h-32 bg="slate-500/10" flex justify-center items-center>
|
||||
<div
|
||||
v-else
|
||||
min-w-22 w-22 h-22 sm="min-w-32 w-32 h-32" bg="slate-500/10" flex justify-center items-center
|
||||
:class="[
|
||||
root ? 'rounded-lg' : '',
|
||||
]"
|
||||
>
|
||||
<div i-ri:profile-line w="30%" h="30%" text-secondary />
|
||||
</div>
|
||||
<div
|
||||
p4 max-h-2xl
|
||||
px3 max-h-2xl
|
||||
flex flex-col
|
||||
:class="[
|
||||
root ? 'flex-gap-1 py1 sm:py3' : 'py3 justify-center sm:justify-start',
|
||||
]"
|
||||
>
|
||||
<p v-if="card.providerName" text-secondary line-clamp-1 text-ellipsis>
|
||||
{{ card.providerName }}
|
||||
<p
|
||||
text-secondary ws-pre-wrap break-all
|
||||
:class="[
|
||||
!card.description || root
|
||||
? 'line-clamp-1'
|
||||
: 'hidden sm:line-clamp-1',
|
||||
]"
|
||||
>
|
||||
{{ providerName }}
|
||||
</p>
|
||||
<strong v-if="card.title" line-clamp-1 text-ellipsis>{{ card.title }}</strong>
|
||||
<p v-if="description" text-secondary line-clamp-2 text-ellipsis>
|
||||
{{ description }}
|
||||
<strong
|
||||
v-if="card.title" font-normal sm:font-medium line-clamp-1
|
||||
break-all ws-pre-wrap
|
||||
>{{ card.title }}</strong>
|
||||
<p
|
||||
v-if="card.description"
|
||||
line-clamp-1 break-all sm:line-clamp-2 sm:break-words text-secondary ws-pre-wrap
|
||||
>
|
||||
{{ card.description }}
|
||||
</p>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
|
46
components/status/StatusPreviewCardSkeleton.vue
Normal file
46
components/status/StatusPreviewCardSkeleton.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
/** For the preview image, only the small image mode is displayed */
|
||||
square?: boolean
|
||||
/** When it is root card in the list, not appear as a child card */
|
||||
root?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
of-hidden
|
||||
:class="{
|
||||
'flex': square,
|
||||
'p-4': root,
|
||||
'rounded-lg border border-base': !root,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
flex flex-col
|
||||
display-block of-hidden
|
||||
border="base"
|
||||
:class="{
|
||||
'sm:(min-w-32 w-32 h-32) min-w-22 w-22 h-22 border-r': square,
|
||||
'w-full aspect-[1.91] border-b': !square,
|
||||
'rounded-lg': root,
|
||||
}"
|
||||
>
|
||||
<div w-full h-full class="skeleton-loading-bg" />
|
||||
</div>
|
||||
<div
|
||||
px3 max-h-2xl
|
||||
flex-1 flex flex-col flex-gap-2 sm:flex-gap-3
|
||||
:class="[
|
||||
root ? 'py2.5 sm:py3' : 'py3 justify-center sm:justify-start',
|
||||
]"
|
||||
>
|
||||
<div flex class="skeleton-loading-bg" h-4 w-30 rounded :class="root ? '' : 'hidden sm:block'" />
|
||||
<div flex class="skeleton-loading-bg" h-5 w="4/5" rounded />
|
||||
<div flex="~ col gap-2">
|
||||
<div flex class="skeleton-loading-bg" h-4 w-full rounded />
|
||||
<div sm:flex hidden class="skeleton-loading-bg" h-4 w="2/5" rounded />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
26
components/tag/TagCard.vue
Normal file
26
components/tag/TagCard.vue
Normal file
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Tag } from 'masto'
|
||||
|
||||
const {
|
||||
tag,
|
||||
} = $defineProps<{
|
||||
tag: Tag
|
||||
}>()
|
||||
|
||||
const to = $computed(() => new URL(tag.url).pathname)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="to" block p4 hover:bg-active flex justify-between>
|
||||
<div>
|
||||
<h4 text-size-base leading-normal font-medium line-clamp-1 break-all ws-pre-wrap>
|
||||
<span>#</span>
|
||||
<span hover:underline>{{ tag.name }}</span>
|
||||
</h4>
|
||||
<CommonTrending :history="tag.history" text-sm text-secondary line-clamp-1 ws-pre-wrap break-all />
|
||||
</div>
|
||||
<div flex items-center>
|
||||
<CommonTrendingCharts :history="tag.history" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
11
components/tag/TagCardSkeleton.vue
Normal file
11
components/tag/TagCardSkeleton.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div p4 flex justify-between>
|
||||
<div flex="~ col 1 gap-2">
|
||||
<div flex class="skeleton-loading-bg" h-5 w-30 rounded />
|
||||
<div flex class="skeleton-loading-bg" h-4 w-45 rounded />
|
||||
</div>
|
||||
<div flex items-center>
|
||||
<div flex class="skeleton-loading-bg" h-9 w-15 rounded />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
// @ts-expect-error missing types
|
||||
import { DynamicScrollerItem } from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import type { FilterContext, Paginator, Status, WsEvents } from 'masto'
|
||||
|
||||
const { paginator, stream } = defineProps<{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue