557 lines
22 KiB
Vue
557 lines
22 KiB
Vue
<script setup>
|
||
const { data: posts } = await useFetch('/api/posts', {
|
||
default: () => []
|
||
})
|
||
|
||
const { data: tags } = await useFetch('/api/tags', {
|
||
default: () => []
|
||
})
|
||
|
||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||
default: () => ({})
|
||
})
|
||
|
||
const postFeedStyleStorageKey = 'POST_FEED_STYLE'
|
||
|
||
const postFeedStyleOpen = ref(false)
|
||
const postFeedStyle = ref('compact')
|
||
|
||
/** @typedef {'list' | 'compact' | 'cards'} PostFeedStyle */
|
||
|
||
/**
|
||
* 저장·표시용 피드 보기 방식을 정규화한다.
|
||
* @param {string|null|undefined} value - 원본 값
|
||
* @returns {PostFeedStyle}
|
||
*/
|
||
const normalizePostFeedStyle = (value) => {
|
||
if (value === 'list' || value === 'cards') {
|
||
return value
|
||
}
|
||
|
||
return 'compact'
|
||
}
|
||
|
||
/**
|
||
* Latest 피드 보기 방식을 저장한다.
|
||
* @param {'list' | 'compact' | 'cards' | 'articles'} value - 보기 방식
|
||
* @returns {void}
|
||
*/
|
||
const setPostFeedStyle = (value) => {
|
||
const nextStyle = normalizePostFeedStyle(value === 'articles' ? 'compact' : value)
|
||
postFeedStyle.value = nextStyle
|
||
|
||
if (import.meta.client) {
|
||
localStorage.setItem(postFeedStyleStorageKey, nextStyle)
|
||
}
|
||
}
|
||
|
||
/** @type {import('vue').ComputedRef<boolean>} */
|
||
const isPostFeedCards = computed(() => postFeedStyle.value === 'cards')
|
||
|
||
/** @type {import('vue').ComputedRef<boolean>} */
|
||
const showPostFeedMedia = computed(() => postFeedStyle.value === 'compact' || postFeedStyle.value === 'cards')
|
||
|
||
/**
|
||
* Latest 피드 컨테이너 클래스
|
||
* @param {PostFeedStyle} style - 보기 방식
|
||
* @returns {string}
|
||
*/
|
||
const getPostFeedContainerClass = (style) => {
|
||
if (style === 'cards') {
|
||
return 'post-feed post-feed--cards mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2'
|
||
}
|
||
|
||
return 'post-feed post-feed--stack flex flex-col divide-y divide-[var(--site-line)]'
|
||
}
|
||
|
||
/**
|
||
* Latest 게시물 카드 클래스
|
||
* @param {PostFeedStyle} style - 보기 방식
|
||
* @returns {string}
|
||
*/
|
||
const getPostFeedArticleClass = (style) => {
|
||
if (style === 'cards') {
|
||
return 'post-feed__card group relative flex flex-col rounded-[10px] border border-[var(--site-line)] bg-[var(--site-bg)] p-3'
|
||
}
|
||
|
||
if (style === 'compact') {
|
||
return 'post-feed__item post-feed__item--compact group relative flex flex-row gap-3 py-3'
|
||
}
|
||
|
||
return 'post-feed__item post-feed__item--list group relative flex flex-row gap-3 py-4'
|
||
}
|
||
|
||
/**
|
||
* 요약 줄 수 클래스
|
||
* @param {PostFeedStyle} style - 보기 방식
|
||
* @returns {string}
|
||
*/
|
||
const getPostFeedExcerptClass = (style) => {
|
||
if (style === 'list') {
|
||
return 'post-summary-clamp post-summary-clamp--three'
|
||
}
|
||
|
||
return 'post-summary-clamp post-summary-clamp--two'
|
||
}
|
||
|
||
const closePostFeedStyleMenu = () => {
|
||
postFeedStyleOpen.value = false
|
||
}
|
||
|
||
const onDocumentPointerDown = (event) => {
|
||
if (!postFeedStyleOpen.value) {
|
||
return
|
||
}
|
||
|
||
const target = /** @type {HTMLElement | null} */ (event.target instanceof HTMLElement ? event.target : null)
|
||
if (!target) {
|
||
closePostFeedStyleMenu()
|
||
return
|
||
}
|
||
|
||
if (target.closest('[data-feed-style-root]')) {
|
||
return
|
||
}
|
||
|
||
closePostFeedStyleMenu()
|
||
}
|
||
|
||
/**
|
||
* 태그 슬러그로 태그 정보 조회
|
||
* @param {string | undefined} slug - 태그 슬러그
|
||
* @returns {{name: string, color: string}} 태그 정보
|
||
*/
|
||
const getTagMeta = (slug) => {
|
||
if (!slug) {
|
||
return {
|
||
name: '',
|
||
color: '#4d4d4d'
|
||
}
|
||
}
|
||
|
||
const matchedTag = tags.value.find((item) => item.slug === slug)
|
||
|
||
return {
|
||
name: matchedTag?.name || String(slug).toUpperCase(),
|
||
color: matchedTag?.color || '#4d4d4d'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Latest 목록 데이터 변환
|
||
* @param {Object} post - API 게시물
|
||
* @returns {Object} 화면 표시 데이터
|
||
*/
|
||
const mapLatestPost = (post) => {
|
||
const primaryTagSlug = post.tags?.[0]
|
||
const tagMeta = getTagMeta(primaryTagSlug)
|
||
|
||
return {
|
||
title: post.title,
|
||
excerpt: createPostSummary(post.excerpt, post.content, {
|
||
maxLength: 320,
|
||
appendEllipsis: false
|
||
}),
|
||
featuredImage: post.featuredImage,
|
||
tagName: tagMeta.name,
|
||
tagColor: tagMeta.color,
|
||
publishedAt: formatPostDate(post.publishedAt),
|
||
publishedAtIso: post.publishedAt || '',
|
||
to: `/post/${post.slug}`,
|
||
isFeatured: Boolean(post.isFeatured),
|
||
commentCount: Number(post.commentCount || 0)
|
||
}
|
||
}
|
||
|
||
const featuredPosts = computed(() => posts.value.filter((post) => post.isFeatured).slice(0, 6))
|
||
const latestPosts = computed(() => posts.value.map(mapLatestPost))
|
||
|
||
const featuredTrackRef = ref(null)
|
||
/** Featured 트랙이 스크롤 시작에 붙었는지 — 이전 화살표 비활성 */
|
||
const featuredAtStart = ref(true)
|
||
/** Featured 트랙이 스크롤 끝에 붙었는지 — 다음 화살표 비활성 */
|
||
const featuredAtEnd = ref(true)
|
||
|
||
let unbindFeaturedScroll = () => {}
|
||
|
||
/**
|
||
* Featured 가로 스크롤 위치에 따라 이전/다음 버튼 상태를 갱신한다.
|
||
* @param {HTMLElement | null} el - 스크롤 컨테이너
|
||
* @returns {void}
|
||
*/
|
||
const updateFeaturedScrollEdges = (el) => {
|
||
const target = el || featuredTrackRef.value
|
||
|
||
if (!target) {
|
||
featuredAtStart.value = true
|
||
featuredAtEnd.value = true
|
||
return
|
||
}
|
||
|
||
const { scrollLeft, scrollWidth, clientWidth } = target
|
||
const maxScroll = Math.max(0, scrollWidth - clientWidth)
|
||
const epsilon = 2
|
||
|
||
featuredAtStart.value = scrollLeft <= epsilon
|
||
featuredAtEnd.value = scrollLeft >= maxScroll - epsilon
|
||
}
|
||
|
||
watch(featuredTrackRef, (el) => {
|
||
unbindFeaturedScroll()
|
||
unbindFeaturedScroll = () => {}
|
||
|
||
if (!import.meta.client || !el) {
|
||
updateFeaturedScrollEdges(null)
|
||
return
|
||
}
|
||
|
||
const onScroll = () => {
|
||
updateFeaturedScrollEdges(el)
|
||
}
|
||
|
||
onScroll()
|
||
el.addEventListener('scroll', onScroll, { passive: true })
|
||
const resizeObserver = new ResizeObserver(onScroll)
|
||
resizeObserver.observe(el)
|
||
unbindFeaturedScroll = () => {
|
||
el.removeEventListener('scroll', onScroll)
|
||
resizeObserver.disconnect()
|
||
}
|
||
}, { immediate: true })
|
||
|
||
watch(featuredPosts, () => {
|
||
if (!import.meta.client) {
|
||
return
|
||
}
|
||
|
||
nextTick(() => {
|
||
updateFeaturedScrollEdges(featuredTrackRef.value)
|
||
})
|
||
})
|
||
|
||
onMounted(() => {
|
||
if (!import.meta.client) {
|
||
return
|
||
}
|
||
|
||
const storedStyle = localStorage.getItem(postFeedStyleStorageKey)
|
||
postFeedStyle.value = normalizePostFeedStyle(storedStyle)
|
||
|
||
document.addEventListener('pointerdown', onDocumentPointerDown)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
if (!import.meta.client) {
|
||
return
|
||
}
|
||
|
||
unbindFeaturedScroll()
|
||
document.removeEventListener('pointerdown', onDocumentPointerDown)
|
||
})
|
||
|
||
/**
|
||
* Featured 가로 트랙을 좌우로 이동한다.
|
||
* @param {'left' | 'right'} direction - 이동 방향
|
||
* @returns {void}
|
||
*/
|
||
const scrollFeatured = (direction) => {
|
||
if (!featuredTrackRef.value) {
|
||
return
|
||
}
|
||
|
||
if (direction === 'left' && featuredAtStart.value) {
|
||
return
|
||
}
|
||
|
||
if (direction === 'right' && featuredAtEnd.value) {
|
||
return
|
||
}
|
||
|
||
const firstCard = featuredTrackRef.value.querySelector('[data-featured-slide]')
|
||
const cardWidth = firstCard ? firstCard.getBoundingClientRect().width : 244
|
||
const gap = 24
|
||
const offset = direction === 'left' ? -(cardWidth + gap) : cardWidth + gap
|
||
|
||
featuredTrackRef.value.scrollBy({
|
||
left: offset,
|
||
behavior: 'smooth'
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<MainColumn>
|
||
<section v-if="siteSettings?.homeCoverImageUrl || siteSettings?.homeCoverDarkImageUrl" class="home-page__hero">
|
||
<HomeHero
|
||
:image-url="siteSettings.homeCoverImageUrl"
|
||
:dark-image-url="siteSettings.homeCoverDarkImageUrl"
|
||
:title="siteSettings.homeCoverTitle"
|
||
:text="siteSettings.homeCoverText"
|
||
/>
|
||
</section>
|
||
|
||
<section v-if="featuredPosts.length" class="py-4 px-6">
|
||
<div class="mx-auto max-w-[720px]">
|
||
<div class="flex items-end justify-between gap-2 border-b border-[var(--site-line)] pb-2">
|
||
<h2 class="text-sm font-medium uppercase site-muted">Featured</h2>
|
||
<div class="flex justify-between gap-2">
|
||
<button
|
||
class="featured-nav-prev cursor-pointer p-1 text-[var(--site-text)] hover:opacity-75 disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:opacity-35"
|
||
type="button"
|
||
aria-label="Featured 이전"
|
||
:disabled="featuredAtStart"
|
||
@click="scrollFeatured('left')"
|
||
>
|
||
‹
|
||
</button>
|
||
<button
|
||
class="featured-nav-next cursor-pointer p-1 text-[var(--site-text)] hover:opacity-75 disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:opacity-35"
|
||
type="button"
|
||
aria-label="Featured 다음"
|
||
:disabled="featuredAtEnd"
|
||
@click="scrollFeatured('right')"
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
ref="featuredTrackRef"
|
||
class="featured-posts-track mt-4 flex snap-x snap-mandatory gap-6 overflow-x-auto overscroll-x-contain scroll-smooth pb-1 touch-pan-x [-webkit-overflow-scrolling:touch] [--slides:1.4] sm:[--slides:1.6] lg:[--slides:2.6] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||
>
|
||
<NuxtLink
|
||
v-for="post in featuredPosts"
|
||
:key="`featured-${post.slug}`"
|
||
:to="`/post/${post.slug}`"
|
||
class="group relative block aspect-video w-[calc((100%-(24px*(var(--slides)-1)))/var(--slides))] shrink-0 snap-start overflow-hidden rounded-[10px]"
|
||
data-featured-slide
|
||
>
|
||
<img
|
||
v-if="post.featuredImage"
|
||
:src="post.featuredImage"
|
||
:alt="post.title"
|
||
class="h-full w-full object-cover brightness-75 contrast-125 transition-all duration-200 group-hover:brightness-90 group-hover:contrast-110"
|
||
loading="lazy"
|
||
>
|
||
<div
|
||
v-else
|
||
class="post-card-media__placeholder flex h-full w-full items-center justify-center bg-[#F7F4EF] p-5 text-center text-sm font-medium leading-snug text-[var(--site-muted)] transition-all duration-200 group-hover:opacity-90"
|
||
:aria-label="post.title"
|
||
>
|
||
<span class="post-card-media__placeholder-text line-clamp-4">{{ post.title }}</span>
|
||
</div>
|
||
<h3 v-if="post.featuredImage" class="absolute right-0 bottom-2.5 left-0 px-3 text-sm font-medium leading-tight text-white line-clamp-2">
|
||
{{ post.title }}
|
||
</h3>
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<SiteAdSlot
|
||
class="home-page__ad-slot px-6 py-4"
|
||
:code="siteSettings?.adHomeFeedCode"
|
||
location="home-feed"
|
||
/>
|
||
|
||
<section class="latest-posts-section min-h-[360px] py-4 px-6">
|
||
<div class="mx-auto max-w-[720px]">
|
||
<div class="flex items-end justify-between gap-2 border-b border-[var(--site-line)] pb-2">
|
||
<h2 class="text-sm font-medium uppercase site-muted">Latest</h2>
|
||
<div class="relative flex flex-col gap-1 text-sm font-medium" data-feed-style-root>
|
||
<button
|
||
class="site-input relative flex cursor-pointer items-center justify-center gap-1 rounded-[10px] border px-2 py-1.5 pr-1.5 leading-none hover:bg-[var(--site-panel)]"
|
||
type="button"
|
||
aria-label="피드 보기 방식 선택"
|
||
data-feed-style-toggle
|
||
@click="postFeedStyleOpen = !postFeedStyleOpen"
|
||
>
|
||
<span class="pointer-events-none" v-show="postFeedStyle === 'list'">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M9 6h11" />
|
||
<path d="M9 12h11" />
|
||
<path d="M9 18h11" />
|
||
<path d="M5 6v.01" />
|
||
<path d="M5 12v.01" />
|
||
<path d="M5 18v.01" />
|
||
</svg>
|
||
</span>
|
||
<span class="pointer-events-none" v-show="postFeedStyle === 'compact'">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M13 5h8" />
|
||
<path d="M13 9h5" />
|
||
<path d="M13 15h8" />
|
||
<path d="M13 19h5" />
|
||
<path d="M3 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5" />
|
||
<path d="M3 15a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-4" />
|
||
</svg>
|
||
</span>
|
||
<span class="pointer-events-none" v-show="postFeedStyle === 'cards'">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6" />
|
||
<path d="M4 16a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2" />
|
||
</svg>
|
||
</span>
|
||
<span class="pointer-events-none opacity-75">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="m6 9 6 6 6-6" />
|
||
</svg>
|
||
</span>
|
||
</button>
|
||
|
||
<menu
|
||
class="absolute top-9 right-0 z-10 flex w-44 flex-col gap-0.5 rounded-[10px] border border-[var(--site-line)] bg-[var(--site-bg)] p-1.5 shadow transition-[transform,opacity,visibility,scale] duration-200"
|
||
:class="postFeedStyleOpen ? 'visible translate-y-0 scale-100 opacity-100' : 'invisible -translate-y-3 scale-95 opacity-0'"
|
||
>
|
||
<li class="w-full">
|
||
<button class="flex w-full cursor-pointer items-center gap-1.5 rounded-[10px] px-2 py-1 hover:bg-[var(--site-panel)]" type="button" @click="setPostFeedStyle('list'); closePostFeedStyleMenu()">
|
||
<span class="pointer-events-none">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M9 6h11" />
|
||
<path d="M9 12h11" />
|
||
<path d="M9 18h11" />
|
||
<path d="M5 6v.01" />
|
||
<path d="M5 12v.01" />
|
||
<path d="M5 18v.01" />
|
||
</svg>
|
||
</span>
|
||
<span>List</span>
|
||
</button>
|
||
</li>
|
||
<li class="w-full">
|
||
<button class="flex w-full cursor-pointer items-center gap-1.5 rounded-[10px] px-2 py-1 hover:bg-[var(--site-panel)]" type="button" @click="setPostFeedStyle('compact'); closePostFeedStyleMenu()">
|
||
<span class="pointer-events-none">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M13 5h8" />
|
||
<path d="M13 9h5" />
|
||
<path d="M13 15h8" />
|
||
<path d="M13 19h5" />
|
||
<path d="M3 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5" />
|
||
<path d="M3 15a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-4" />
|
||
</svg>
|
||
</span>
|
||
<span>Compact</span>
|
||
</button>
|
||
</li>
|
||
<li class="w-full">
|
||
<button class="flex w-full cursor-pointer items-center gap-1.5 rounded-[10px] px-2 py-1 hover:bg-[var(--site-panel)]" type="button" @click="setPostFeedStyle('cards'); closePostFeedStyleMenu()">
|
||
<span class="pointer-events-none">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6" />
|
||
<path d="M4 16a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2" />
|
||
</svg>
|
||
</span>
|
||
<span>Cards</span>
|
||
</button>
|
||
</li>
|
||
<li class="w-full">
|
||
<button class="flex w-full cursor-pointer items-center gap-1.5 rounded-[10px] px-2 py-1 hover:bg-[var(--site-panel)]" type="button" @click="setPostFeedStyle('compact'); closePostFeedStyleMenu()">
|
||
<span class="pointer-events-none">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3.06 13a9 9 0 1 0 .49-4.087" />
|
||
<path d="M3 4.001v5h5" />
|
||
<path d="M11 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0" />
|
||
</svg>
|
||
</span>
|
||
<span>Default</span>
|
||
</button>
|
||
</li>
|
||
</menu>
|
||
</div>
|
||
</div>
|
||
<div class="post-feed-section mb-8 flex flex-col">
|
||
<div
|
||
class="post-feed-section__list"
|
||
data-post-feed="latest"
|
||
:class="getPostFeedContainerClass(postFeedStyle)"
|
||
>
|
||
<article
|
||
v-for="post in latestPosts"
|
||
:key="post.to"
|
||
data-post-card
|
||
:data-featured="post.isFeatured ? '' : undefined"
|
||
:class="getPostFeedArticleClass(postFeedStyle)"
|
||
>
|
||
<PostCardMedia
|
||
v-if="showPostFeedMedia"
|
||
:to="post.to"
|
||
:title="post.title"
|
||
:featured-image="post.featuredImage"
|
||
:link-class="isPostFeedCards ? 'post-feed__media post-feed__media--cards mb-3 block aspect-video w-full' : 'post-feed__media post-feed__media--list relative flex-1 aspect-square min-w-16 sm:aspect-video'"
|
||
:aspect-class="isPostFeedCards ? 'aspect-video' : 'aspect-square sm:aspect-video'"
|
||
/>
|
||
|
||
<div
|
||
class="post-feed__content relative min-w-0"
|
||
:class="isPostFeedCards ? 'flex flex-col' : 'flex flex-[3] flex-col gap-1.5 md:flex-[4]'"
|
||
>
|
||
<div class="post-feed__content-inner flex min-h-0 flex-1 flex-col gap-1">
|
||
<h2 class="max-w-[90%] text-sm font-medium leading-tight">
|
||
<NuxtLink :to="post.to" class="flex items-center transition-opacity duration-200 hover:opacity-75">
|
||
<span v-if="post.isFeatured" class="post-feed__featured-icon mr-1 inline-flex h-4 w-4 items-center justify-center text-[var(--site-accent)]">
|
||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M13 3v7h6l-8 11v-7H5l8-11" />
|
||
</svg>
|
||
</span>
|
||
{{ post.title }}
|
||
</NuxtLink>
|
||
</h2>
|
||
|
||
<p
|
||
v-if="post.excerpt"
|
||
class="flex-1 text-[0.8rem] leading-tight site-muted text-[#6E6661]"
|
||
:class="getPostFeedExcerptClass(postFeedStyle)"
|
||
>
|
||
{{ post.excerpt }}
|
||
</p>
|
||
|
||
<div class="flex flex-wrap items-center gap-2 text-xs site-muted sm:gap-1.5">
|
||
<time v-if="post.publishedAt" :datetime="post.publishedAtIso">{{ post.publishedAt }}</time>
|
||
<template v-if="post.tagName">
|
||
<span v-if="post.publishedAt" class="text-[var(--site-line)]">/</span>
|
||
<span
|
||
class="rounded-md px-1.5 py-px font-medium text-[var(--site-text)]"
|
||
:style="{ backgroundColor: `${post.tagColor}1a` }"
|
||
>
|
||
{{ post.tagName }}
|
||
</span>
|
||
</template>
|
||
<span v-if="post.publishedAt || post.tagName" class="text-[var(--site-line)]">/</span>
|
||
<span class="flex items-center gap-1">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="-mt-px">
|
||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||
</svg>
|
||
<span>{{ post.commentCount }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
class="absolute top-0 right-0 flex cursor-pointer items-center gap-1 hover:opacity-75"
|
||
:class="isPostFeedCards ? '' : 'md:top-auto md:right-0 md:bottom-0 md:invisible md:opacity-0 md:transition-[opacity,visibility] md:duration-200 md:group-hover:visible md:group-hover:opacity-100'"
|
||
type="button"
|
||
aria-label="Share this post"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
class="h-4 w-4"
|
||
>
|
||
<path d="M17 7 7 17" />
|
||
<path d="M8 7h9v9" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</MainColumn>
|
||
</template>
|