fc3e: PostCard — header + title + truncated-desc + thumbnail strip + attachment chips
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<v-card class="fc-post-card" variant="outlined">
|
||||
<div class="fc-post-card__head">
|
||||
<v-chip size="x-small" variant="tonal">{{ post.source.platform }}</v-chip>
|
||||
<RouterLink
|
||||
:to="{ name: 'artist', params: { slug: post.artist.slug } }"
|
||||
class="fc-post-card__artist"
|
||||
>{{ post.artist.name }}</RouterLink>
|
||||
<span class="fc-post-card__date" :title="absoluteDate">{{ relativeDate }}</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="post.post_url"
|
||||
:href="post.post_url" target="_blank" rel="noopener"
|
||||
icon="mdi-open-in-new" size="x-small" variant="text"
|
||||
:aria-label="`open original post on ${post.source.platform}`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 v-if="post.post_title" class="fc-post-card__title">{{ post.post_title }}</h3>
|
||||
|
||||
<div v-if="descriptionToShow" class="fc-post-card__desc">
|
||||
<span class="fc-post-card__desc-text">{{ descriptionToShow }}</span>
|
||||
<button
|
||||
v-if="post.description_truncated && !expanded"
|
||||
class="fc-post-card__more" type="button"
|
||||
@click="expand"
|
||||
>Show more</button>
|
||||
<button
|
||||
v-if="expanded"
|
||||
class="fc-post-card__more" type="button"
|
||||
@click="expanded = false"
|
||||
>Show less</button>
|
||||
</div>
|
||||
|
||||
<div v-if="post.thumbnails.length" class="fc-post-card__thumbs">
|
||||
<RouterLink
|
||||
v-for="t in post.thumbnails"
|
||||
:key="t.image_id"
|
||||
:to="{ path: '/gallery', query: { post_id: post.id } }"
|
||||
class="fc-post-card__thumb"
|
||||
>
|
||||
<v-img
|
||||
:src="t.thumbnail_url" cover :alt="`thumbnail`"
|
||||
width="96" height="96" class="fc-post-card__thumb-img"
|
||||
/>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-if="post.thumbnails_more > 0"
|
||||
:to="{ path: '/gallery', query: { post_id: post.id } }"
|
||||
class="fc-post-card__more-thumbs"
|
||||
>+{{ post.thumbnails_more }} more</RouterLink>
|
||||
</div>
|
||||
|
||||
<div v-if="post.attachments.length" class="fc-post-card__attachments">
|
||||
<a
|
||||
v-for="att in post.attachments"
|
||||
:key="att.id"
|
||||
:href="att.download_url"
|
||||
download
|
||||
class="fc-post-card__att"
|
||||
>
|
||||
<v-icon size="small" class="fc-post-card__att-icon">mdi-paperclip</v-icon>
|
||||
<span class="fc-post-card__att-name">{{ att.original_filename }}</span>
|
||||
<span class="fc-post-card__att-size">({{ formatBytes(att.size_bytes) }})</span>
|
||||
</a>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { usePostsStore } from '../../stores/posts.js'
|
||||
|
||||
const props = defineProps({
|
||||
post: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const postsStore = usePostsStore()
|
||||
|
||||
const expanded = ref(false)
|
||||
const fullDescription = ref(null)
|
||||
|
||||
const sortDateIso = computed(() => props.post.post_date || props.post.downloaded_at)
|
||||
const absoluteDate = computed(() => new Date(sortDateIso.value).toLocaleString())
|
||||
const relativeDate = computed(() => {
|
||||
const then = new Date(sortDateIso.value).getTime()
|
||||
const diff = (Date.now() - then) / 1000
|
||||
if (diff < 60) return `${Math.floor(diff)}s ago`
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
|
||||
if (diff < 86400 * 30) return `${Math.floor(diff / 86400)}d ago`
|
||||
return new Date(sortDateIso.value).toLocaleDateString()
|
||||
})
|
||||
|
||||
const descriptionToShow = computed(() => {
|
||||
if (expanded.value && fullDescription.value) return fullDescription.value
|
||||
return props.post.description_plain
|
||||
})
|
||||
|
||||
async function expand() {
|
||||
if (!fullDescription.value) {
|
||||
const detail = await postsStore.getPostFull(props.post.id)
|
||||
fullDescription.value = detail?.description_full || props.post.description_plain
|
||||
}
|
||||
expanded.value = true
|
||||
}
|
||||
|
||||
function formatBytes(n) {
|
||||
if (!n) return '0 B'
|
||||
if (n < 1024) return `${n} B`
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
||||
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`
|
||||
return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-post-card {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.fc-post-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
.fc-post-card__artist {
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.fc-post-card__artist:hover { color: rgb(var(--v-theme-accent)); }
|
||||
.fc-post-card__date { white-space: nowrap; }
|
||||
.fc-post-card__title {
|
||||
font-size: 1.05rem;
|
||||
margin: 0.6rem 0 0.4rem;
|
||||
}
|
||||
.fc-post-card__desc {
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.92rem;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.fc-post-card__more {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgb(var(--v-theme-accent));
|
||||
cursor: pointer;
|
||||
padding: 0 0.25rem;
|
||||
font-size: inherit;
|
||||
}
|
||||
.fc-post-card__thumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.fc-post-card__thumb { line-height: 0; }
|
||||
.fc-post-card__thumb-img {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.fc-post-card__more-thumbs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border: 1px dashed rgb(var(--v-theme-on-surface-variant));
|
||||
border-radius: 4px;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.fc-post-card__more-thumbs:hover {
|
||||
color: rgb(var(--v-theme-accent));
|
||||
border-color: rgb(var(--v-theme-accent));
|
||||
}
|
||||
.fc-post-card__attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.fc-post-card__att {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border: 1px solid rgb(var(--v-theme-on-surface-variant));
|
||||
border-radius: 999px;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
text-decoration: none;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.fc-post-card__att:hover {
|
||||
color: rgb(var(--v-theme-accent));
|
||||
border-color: rgb(var(--v-theme-accent));
|
||||
}
|
||||
.fc-post-card__att-size { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user