feat(artist): tab split (Overview/Settings) so DangerZone is reachable without exhausting the infinite-scroll image grid — Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,69 +45,92 @@
|
||||
>Credential health · FC-3b</v-chip>
|
||||
</div>
|
||||
|
||||
<section v-if="store.overview.cooccurring_tags.length" class="fc-artist__sec">
|
||||
<h2 class="fc-h2">Frequent tags</h2>
|
||||
<div class="fc-artist__tags">
|
||||
<v-chip
|
||||
v-for="t in store.overview.cooccurring_tags" :key="t.id"
|
||||
size="small" @click="openTag(t.id)"
|
||||
>{{ t.name }} <span class="fc-artist__tagc">{{ t.count }}</span></v-chip>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Tabs split (2026-05-25): Settings was previously slotted at the
|
||||
bottom of the page after the infinite-scroll image grid, which
|
||||
made it effectively unreachable for any artist with more than
|
||||
a couple of pages of content. The Settings tab now hosts
|
||||
destructive admin actions (artist+content cascade-delete) and
|
||||
any future per-artist management UI. v-tabs is `position:
|
||||
sticky; top: 64px` (under the 64px AppShell TopNav) so it
|
||||
stays parked while the gallery scrolls. -->
|
||||
<v-tabs
|
||||
v-model="tab" color="accent" class="mb-4"
|
||||
style="position: sticky; top: 64px; z-index: 4;
|
||||
background: rgb(var(--v-theme-surface));"
|
||||
>
|
||||
<v-tab value="overview">Overview</v-tab>
|
||||
<v-tab value="settings">Settings</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<section v-if="store.overview.activity.length" class="fc-artist__sec">
|
||||
<h2 class="fc-h2">Activity</h2>
|
||||
<svg class="fc-artist__spark" :viewBox="`0 0 ${sparkW} ${sparkH}`"
|
||||
preserveAspectRatio="none" role="img" aria-label="posts over time">
|
||||
<polyline :points="sparkPoints" fill="none"
|
||||
stroke="rgb(var(--v-theme-accent))" stroke-width="2" />
|
||||
</svg>
|
||||
</section>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="overview">
|
||||
<section v-if="store.overview.cooccurring_tags.length" class="fc-artist__sec">
|
||||
<h2 class="fc-h2">Frequent tags</h2>
|
||||
<div class="fc-artist__tags">
|
||||
<v-chip
|
||||
v-for="t in store.overview.cooccurring_tags" :key="t.id"
|
||||
size="small" @click="openTag(t.id)"
|
||||
>{{ t.name }} <span class="fc-artist__tagc">{{ t.count }}</span></v-chip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="store.overview.sources.length" class="fc-artist__sec">
|
||||
<div class="fc-artist__sec-head">
|
||||
<h2 class="fc-h2">Sources</h2>
|
||||
<RouterLink
|
||||
:to="`/subscriptions?artist_id=${store.overview.id}`"
|
||||
class="fc-artist__manage"
|
||||
>Manage subscriptions →</RouterLink>
|
||||
</div>
|
||||
<v-table density="compact">
|
||||
<thead>
|
||||
<tr><th>Platform</th><th>URL</th><th class="text-right">Images</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="s in store.overview.sources" :key="s.id">
|
||||
<td>{{ s.platform }}</td>
|
||||
<td class="fc-artist__url">{{ s.url }}</td>
|
||||
<td class="text-right">{{ s.image_count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</section>
|
||||
<section v-if="store.overview.activity.length" class="fc-artist__sec">
|
||||
<h2 class="fc-h2">Activity</h2>
|
||||
<svg class="fc-artist__spark" :viewBox="`0 0 ${sparkW} ${sparkH}`"
|
||||
preserveAspectRatio="none" role="img" aria-label="posts over time">
|
||||
<polyline :points="sparkPoints" fill="none"
|
||||
stroke="rgb(var(--v-theme-accent))" stroke-width="2" />
|
||||
</svg>
|
||||
</section>
|
||||
|
||||
<section class="fc-artist__sec">
|
||||
<h2 class="fc-h2">Images</h2>
|
||||
<MasonryGrid
|
||||
:items="store.images"
|
||||
:loading="store.imagesLoading"
|
||||
:has-more="store.hasMoreImages"
|
||||
@load-more="store.loadMoreImages(slug)"
|
||||
@open="openImage"
|
||||
/>
|
||||
</section>
|
||||
<section v-if="store.overview.sources.length" class="fc-artist__sec">
|
||||
<div class="fc-artist__sec-head">
|
||||
<h2 class="fc-h2">Sources</h2>
|
||||
<RouterLink
|
||||
:to="`/subscriptions?artist_id=${store.overview.id}`"
|
||||
class="fc-artist__manage"
|
||||
>Manage subscriptions →</RouterLink>
|
||||
</div>
|
||||
<v-table density="compact">
|
||||
<thead>
|
||||
<tr><th>Platform</th><th>URL</th><th class="text-right">Images</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="s in store.overview.sources" :key="s.id">
|
||||
<td>{{ s.platform }}</td>
|
||||
<td class="fc-artist__url">{{ s.url }}</td>
|
||||
<td class="text-right">{{ s.image_count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</section>
|
||||
|
||||
<ArtistDangerZone
|
||||
:slug="slug"
|
||||
:artist-id="store.overview.id"
|
||||
:artist-name="store.overview.name"
|
||||
/>
|
||||
<section class="fc-artist__sec">
|
||||
<h2 class="fc-h2">Images</h2>
|
||||
<MasonryGrid
|
||||
:items="store.images"
|
||||
:loading="store.imagesLoading"
|
||||
:has-more="store.hasMoreImages"
|
||||
@load-more="store.loadMoreImages(slug)"
|
||||
@open="openImage"
|
||||
/>
|
||||
</section>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="settings">
|
||||
<ArtistDangerZone
|
||||
:slug="slug"
|
||||
:artist-id="store.overview.id"
|
||||
:artist-name="store.overview.name"
|
||||
/>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</template>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
||||
import { useArtistStore } from '../stores/artist.js'
|
||||
import { useModalStore } from '../stores/modal.js'
|
||||
@@ -120,8 +143,18 @@ const store = useArtistStore()
|
||||
const modal = useModalStore()
|
||||
|
||||
const slug = computed(() => route.params.slug)
|
||||
// Per-artist tab — defaults to Overview. Settings tab hosts destructive
|
||||
// admin actions (DangerZone). Switching artists resets to Overview so the
|
||||
// destructive surface isn't re-shown by accident when navigating between
|
||||
// artists.
|
||||
const tab = ref('overview')
|
||||
|
||||
watch(slug, (s) => { if (s) store.load(s) }, { immediate: true })
|
||||
watch(slug, (s) => {
|
||||
if (s) {
|
||||
store.load(s)
|
||||
tab.value = 'overview'
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const dateRange = computed(() => {
|
||||
const r = store.overview?.date_range
|
||||
|
||||
Reference in New Issue
Block a user