fix(front): simplify tags component (tags are just links)

This commit is contained in:
upsiflu 2025-03-27 00:10:30 +01:00
parent 70c19a70b2
commit 0b2e457052
9 changed files with 265 additions and 190 deletions

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import { type RouterLinkProps } from 'vue-router'
import Link from '~/components/ui/Link.vue'
import Layout from '~/components/ui/Layout.vue'
type Tab = {
title: string,
to: RouterLinkProps['to'],
icon?: string,
badge?: string | number
}
const tabs = defineModel<Tab[]>({ required: true })
</script>
<template>
<Layout
nav
flex
>
<Link
v-for="tab in tabs"
:key="tab.title"
v-bind="tab"
ghost
min-content
:class="$style.tab"
>
<Layout
stack
no-gap
>
<span :class="$style.fakeTitle">{{ tab.title }}</span>
<span :class="$style.realTitle">{{ tab.title }}</span>
<span
v-if="tab.badge"
:class="$style.badge"
>
{{ tab.badge }}
</span>
</Layout>
</Link>
</Layout>
</template>
<style module>
.fakeTitle {
font-size: 16px;
font-weight: 900;
opacity: 0;
pointer-events: none;
max-height: 0;
overflow: hidden;
}
.realTitle {
font-size: 16px;
font-weight: 400;
}
.tab {
--hover-background-color: transparent;
--exact-active-background-color: transparent;
}
.tab:global(.router-link-exact-active) .realTitle {
font-weight: 900;
}
.badge {
display: block;
height: 16px;
background-color: var(--fw-secondary);
width: 16px;
position: absolute;
inset: -10px -14px auto auto;
border-radius: 100vh;
font-size: 10px;
font-weight: 900;
padding: 5px;
line-height: 5px;
color: black;
}
:is(.tab:global(.router-link-exact-active), .tab:hover) .realTitle:after {
content: '';
display: block;
height: 4px;
background-color: var(--fw-secondary);
margin: 0 auto;
width: calc(10% + 2rem);
position: absolute;
inset: auto 0 -14px 0;
border-radius: 100vh;
}
</style>

View File

@ -1,11 +1,10 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import Layout from '~/components/ui/Layout.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Tab from '~/components/ui/Tab.vue'
import Nav from '~/components/ui/Nav.vue'
const { t } = useI18n()
const labels = computed(() => ({
@ -13,6 +12,34 @@ const labels = computed(() => ({
secondaryMenu: t('views.admin.library.Base.menu.secondary')
}))
const tabs = ref([
{
title: t('views.admin.library.Base.link.edits'),
to: {name: 'manage.library.edits'}
}, {
title: t('views.admin.library.Base.link.channels'),
to: {name: 'manage.channels'}
}, {
title: t('views.admin.library.Base.link.artists'),
to: {name: 'manage.library.artists'}
}, {
title: t('views.admin.library.Base.link.albums'),
to: {name: 'manage.library.albums'}
}, {
title: t('views.admin.library.Base.link.tracks'),
to: {name: 'manage.library.tracks'}
}, {
title: t('views.admin.library.Base.link.libraries'),
to: {name: 'manage.library.libraries'}
}, {
title: t('views.admin.library.Base.link.uploads'),
to: {name: 'manage.library.uploads'}
}, {
title: t('views.admin.library.Base.link.tags'),
to: {name: 'manage.library.tags'}
}
])
const route = useRoute()
</script>
@ -23,40 +50,8 @@ const route = useRoute()
stack
class="page-admin-library"
>
<Tabs>
<Tab
:title="t('views.admin.library.Base.link.edits')"
:to="{name: 'manage.library.edits'}"
/>
<Tab
:title="t('views.admin.library.Base.link.channels')"
:to="{name: 'manage.channels'}"
/>
<Tab
:title="t('views.admin.library.Base.link.artists')"
:to="{name: 'manage.library.artists'}"
/>
<Tab
:title="t('views.admin.library.Base.link.albums')"
:to="{name: 'manage.library.albums'}"
/>
<Tab
:title="t('views.admin.library.Base.link.tracks')"
:to="{name: 'manage.library.tracks'}"
/>
<Tab
:title="t('views.admin.library.Base.link.libraries')"
:to="{name: 'manage.library.libraries'}"
/>
<Tab
:title="t('views.admin.library.Base.link.uploads')"
:to="{name: 'manage.library.uploads'}"
/>
<Tab
:title="t('views.admin.library.Base.link.tags')"
:to="{name: 'manage.library.tags'}"
/>
</Tabs>
<Nav v-model="tabs" />
<router-view :key="route.fullPath" />
</Layout>
</template>

View File

@ -8,8 +8,7 @@ import { useRoute } from 'vue-router'
import axios from 'axios'
import Layout from '~/components/ui/Layout.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Tab from '~/components/ui/Tab.vue'
import Nav from '~/components/ui/Nav.vue'
const store = useStore()
@ -22,6 +21,24 @@ const labels = computed(() => ({
secondaryMenu: t('views.admin.moderation.Base.menu.secondary')
}))
const tabs = ref([{
title: t('views.admin.moderation.Base.link.reports'),
to: { name: 'manage.moderation.reports.list', query: { q: 'resolved:no' } },
badge: store.state.ui.notifications.pendingReviewReports > 0 ? store.state.ui.notifications.pendingReviewReports : undefined
}, {
title: t('views.admin.moderation.Base.link.userRequests'),
to: { name: 'manage.moderation.requests.list', query: { q: 'status:pending' } },
badge: store.state.ui.notifications.pendingReviewRequests > 0 ? store.state.ui.notifications.pendingReviewRequests : undefined
}, {
title: t('views.admin.moderation.Base.link.domains'),
to: { name: 'manage.moderation.domains.list' }
}, {
title: t('views.admin.moderation.Base.link.accounts'),
to: { name: 'manage.moderation.accounts.list' }
}
])
const fetchNodeInfo = async () => {
const response = await axios.get('instance/nodeinfo/2.1/')
allowListEnabled.value = get(response.data, 'metadata.allowList.enabled', false)
@ -37,41 +54,8 @@ fetchNodeInfo()
main
no-gap
>
<Tabs
role="navigation"
:aria-label="labels.secondaryMenu"
>
<Tab
:title="t('views.admin.moderation.Base.link.reports')"
:to="{name: 'manage.moderation.reports.list', query: {q: 'resolved:no'}}"
>
<div
v-if="store.state.ui.notifications.pendingReviewReports > 0"
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
>
{{ store.state.ui.notifications.pendingReviewReports }}
</div>
</Tab>
<Tab
:title="t('views.admin.moderation.Base.link.userRequests')"
:to="{name: 'manage.moderation.requests.list', query: {q: 'status:pending'}}"
>
<div
v-if="store.state.ui.notifications.pendingReviewRequests > 0"
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']"
>
{{ store.state.ui.notifications.pendingReviewRequests }}
</div>
</Tab>
<Tab
:title="t('views.admin.moderation.Base.link.domains')"
:to="{name: 'manage.moderation.domains.list'}"
/>
<Tab
:title="t('views.admin.moderation.Base.link.accounts')"
:to="{name: 'manage.moderation.accounts.list'}"
/>
</Tabs>
<Nav v-model="tabs" />
<router-view
:key="route.fullPath"
:allow-list-enabled="allowListEnabled"

View File

@ -1,10 +1,9 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import Layout from '~/components/ui/Layout.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Tab from '~/components/ui/Tab.vue'
import Nav from '~/components/ui/Nav.vue'
const { t } = useI18n()
@ -12,6 +11,18 @@ const labels = computed(() => ({
manageUsers: t('views.admin.users.Base.title'),
secondaryMenu: t('views.admin.users.Base.menu.secondary')
}))
const tabs = ref([
{
title: t('views.admin.users.Base.link.users'),
to: { name: 'manage.users.users.list' }
},
{
title: t('views.admin.users.Base.link.invitations'),
to: { name: 'manage.users.invitations.list' }
}
])
</script>
<template>
@ -20,17 +31,8 @@ const labels = computed(() => ({
main
stack
>
<Tabs>
<Tab
:title="t('views.admin.users.Base.link.users')"
:to="{name: 'manage.users.users.list'}"
/>
<Nav v-model="tabs" />
<Tab
:title="t('views.admin.users.Base.link.invitations')"
:to="{name: 'manage.users.invitations.list'}"
/>
</Tabs>
<router-view :key="$route.fullPath" />
</Layout>
</template>

View File

@ -16,8 +16,7 @@ import RenderedDescription from '~/components/common/RenderedDescription.vue'
import Layout from '~/components/ui/Layout.vue'
import Section from '~/components/ui/Section.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Tab from '~/components/ui/Tab.vue'
import Nav from '~/components/ui/Nav.vue'
interface Events {
(e: 'updated', value: Actor): void
@ -79,6 +78,21 @@ const fetchData = async () => {
watch(props, fetchData, { immediate: true })
const recentActivity = ref(0)
const tabs = ref([{
title: t('views.auth.ProfileBase.link.overview') ,
to: { name: 'profile.overview', params: routerParams }
}, {
title: t('views.auth.ProfileBase.link.activity') ,
to: { name: 'profile.activity', params: routerParams }
}, ...(
store.state.auth.authenticated && object.value && object.value.full_username === store.state.auth.fullUsername
? [{
title: t('views.auth.ProfileBase.link.manageUploads') ,
to: { name: 'profile.manageUploads', params: routerParams }
}]
: []
)])
</script>
<template>
@ -135,38 +149,12 @@ const recentActivity = ref(0)
/>
</Section>
</Layout>
<Tabs>
<Tab
:title="t('views.auth.ProfileBase.link.overview')"
:to="{name: 'profile.overview', params: routerParams}"
>
<router-view
:object="object"
@updated="fetchData"
/>
</Tab>
<Nav v-model="tabs" />
<Tab
:title="t('views.auth.ProfileBase.link.activity')"
:to="{name: 'profile.activity', params: routerParams}"
>
<router-view
:object="object"
@updated="fetchData"
/>
</Tab>
<Tab
v-if="store.state.auth.authenticated && object && object.full_username === store.state.auth.fullUsername"
:title="t('views.auth.ProfileBase.link.manageUploads')"
:to="{ name: 'profile.manageUploads', params: routerParams }"
>
<router-view
:object="object"
@updated="fetchData"
/>
</Tab>
</Tabs>
<router-view
:object="object"
@updated="fetchData"
/>
</Layout>
</template>

View File

@ -24,8 +24,7 @@ import RadioButton from '~/components/radios/Button.vue'
import Loader from '~/components/ui/Loader.vue'
import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Tab from '~/components/ui/Tab.vue'
import Nav from '~/components/ui/Nav.vue'
import OptionsButton from '~/components/ui/button/Options.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
@ -60,7 +59,6 @@ const showEditModal = ref(false)
const showSubscribeModal = ref(false)
const isOwner = computed(() => store.state.auth.authenticated && object.value?.attributed_to.full_username === store.state.auth.fullUsername)
const isPodcast = computed(() => object.value?.artist?.content_category === 'podcast')
const isPlayable = computed(() => totalTracks.value > 0)
const externalDomain = computed(() => {
const parser = document.createElement('a')
@ -147,6 +145,18 @@ const updateSubscriptionCount = (delta: number) => {
object.value.subscriptions_count += delta
}
}
const tabs = ref([
{
title: t('views.channels.DetailBase.link.channelOverview'),
to: {name: 'channels.detail', params: { id: props.id }}
},
{
title: t('views.channels.DetailBase.link.channelEpisodes'),
to: {name: 'channels.detail.episodes', params: { id: props.id }}
}
])
</script>
<template>
@ -353,7 +363,7 @@ const updateSubscriptionCount = (delta: number) => {
<template v-if="store.state.auth.availablePermissions['library']">
<hr>
<PopoverItem
:to="{name: 'manage.channels.detail', params: {id: object.uuid}}"
:to="{ name: 'manage.channels.detail', params: { id: object.uuid } }"
icon="bi-wrench"
>
{{ t('views.channels.DetailBase.link.moderation') }}
@ -482,28 +492,13 @@ const updateSubscriptionCount = (delta: number) => {
:limit="5"
:show-more="true"
/>
<Tabs>
<Tab
:title="t('views.channels.DetailBase.link.channelOverview')"
:to="{name: 'channels.detail', params: {id: id}}"
>
<router-view
v-if="object"
:object="object"
@tracks-loaded="totalTracks = $event"
/>
</Tab>
<Tab
:title="t('views.channels.DetailBase.link.channelEpisodes')"
:to="{name: 'channels.detail.episodes', params: {id: id}}"
>
<router-view
v-if="object"
:object="object"
@tracks-loaded="totalTracks = $event"
/>
</Tab>
</Tabs>
<Nav v-model="tabs" />
<router-view
v-if="object"
:object="object"
@tracks-loaded="totalTracks = $event"
/>
</section>
</template>
</Layout>

View File

@ -17,9 +17,7 @@ import OptionsButton from '~/components/ui/button/Options.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import Header from '~/components/ui/Header.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Tab from '~/components/ui/Tab.vue'
import Button from '~/components/ui/Button.vue'
import Nav from '~/components/ui/Nav.vue'
import useErrorHandler from '~/composables/useErrorHandler'
import useReport from '~/composables/moderation/useReport'
@ -86,10 +84,22 @@ watchEffect(() => {
})
const updateUploads = (count: number) => {
if (object.value) {
object.value.uploads_count += count
}
object.value = object.value
? { ...object.value, uploads_count: object.value.uploads_count + count }
: null
}
const tabs = ref([{
title: t('views.library.LibraryBase.link.artists'),
to: { name: 'library.detail' }
}, {
title: t('views.library.LibraryBase.link.albums'),
to: { name: 'library.detail.albums' }
}, {
title: t('views.library.LibraryBase.link.tracks'),
to: { name: 'library.detail.tracks' }
}
])
</script>
<template>
@ -177,13 +187,16 @@ const updateUploads = (count: number) => {
<i class="bi bi-globe" />
{{ labels.visibility.everyone }}
</span>
<span class="middledot icon">
<span
v-if="object"
class="middledot icon"
>
<i class="bi bi-music-note-list" />
{{ t('views.library.LibraryBase.meta.tracks', object.uploads_count) }}
</span>
<span v-if="object?.size">
<span v-if="object && 'size' in object && object.size">
<i class="bi bi-database-fill" />
{{ humanSize(object?.size) }}
{{ humanSize(object.size) }}
</span>
</Layout>
@ -222,40 +235,13 @@ const updateUploads = (count: number) => {
</p>
</div>
</Layout>
<Tabs>
<Tab
:title="t('views.library.LibraryBase.link.artists')"
:to="{name: 'library.detail'}"
>
<router-view
:is-owner="isOwner"
:object="object"
@updated="fetchData"
@uploads-finished="updateUploads"
/>
</Tab>
<Tab
:title="t('views.library.LibraryBase.link.albums')"
:to="{name: 'library.detail.albums'}"
>
<router-view
:is-owner="isOwner"
:object="object"
@updated="fetchData"
@uploads-finished="updateUploads"
/>
</Tab>
<Tab
:title="t('views.library.LibraryBase.link.tracks')"
:to="{name: 'library.detail.tracks'}"
>
<router-view
:is-owner="isOwner"
:object="object"
@updated="fetchData"
@uploads-finished="updateUploads"
/>
</Tab>
</Tabs>
<Nav v-model="tabs" />
<router-view
:is-owner="isOwner"
:object="object"
@updated="fetchData"
@uploads-finished="updateUploads"
/>
</Layout>
</template>

View File

@ -29,6 +29,7 @@ export default defineConfig({
{ text: 'Pagination', link: '/components/ui/pagination' },
{ text: 'Table of Contents', link: '/components/ui/toc' },
{ text: 'Tabs', link: '/components/ui/tabs' },
{ text: 'Nav', link: '/components/ui/nav' },
],
},
{

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import { ref } from 'vue'
import Nav from '~/components/ui/Nav.vue'
const nav = ref([{ title: 'Go up', to: '../' }, { title: 'Home', to: './', badge: "2" }])
</script>
```ts
import Nav from "~/components/ui/Nav.vue"
```
# Nav
This is just a list of links, styled like tabs.
You can add a `badge` or an `icon` to each tab link.
<Link to="/">Hello</Link>
```ts
import { ref } from 'vue'
const nav = ref([{ title: 'Go up', to: '../' }, { title: 'Home', to: './', badge: "2" }])
```
```vue-html
<Nav v-model="nav" />
```
<Nav v-model="nav" />