원본 패턴에 맞춰 목록 카드의 우하단 hover 액션 버튼을 홈 Latest와 태그 상세 목록에 동일하게 반영해 상호작용 피드백을 보강했다. Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
8.0 KiB
Vue
201 lines
8.0 KiB
Vue
<script setup>
|
||
const { data: posts } = await useFetch('/api/posts', {
|
||
default: () => []
|
||
})
|
||
|
||
const { data: tags } = await useFetch('/api/tags', {
|
||
default: () => []
|
||
})
|
||
|
||
/**
|
||
* 날짜 표시 형식 변환
|
||
* @param {string | null} value - ISO 날짜 문자열
|
||
* @returns {string} 화면 표시 날짜
|
||
*/
|
||
const formatPostDate = (value) => {
|
||
if (!value) {
|
||
return ''
|
||
}
|
||
|
||
return new Intl.DateTimeFormat('en-US', {
|
||
month: 'short',
|
||
day: 'numeric'
|
||
}).format(new Date(value))
|
||
}
|
||
|
||
/**
|
||
* 태그 슬러그로 태그 정보 조회
|
||
* @param {string | undefined} slug - 태그 슬러그
|
||
* @returns {{name: string, color: string}} 태그 정보
|
||
*/
|
||
const getTagMeta = (slug) => {
|
||
const matchedTag = tags.value.find((item) => item.slug === slug)
|
||
|
||
return {
|
||
name: matchedTag?.name || (slug ? slug.toUpperCase() : 'POST'),
|
||
color: matchedTag?.color || '#4d4d4d'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Latest 목록 데이터 변환
|
||
* @param {Object} post - API 게시물
|
||
* @param {number} index - 목록 인덱스
|
||
* @returns {Object} 화면 표시 데이터
|
||
*/
|
||
const mapLatestPost = (post, index) => {
|
||
const primaryTagSlug = post.tags?.[0]
|
||
const tagMeta = getTagMeta(primaryTagSlug)
|
||
|
||
return {
|
||
title: post.title,
|
||
excerpt: post.excerpt,
|
||
featuredImage: post.featuredImage,
|
||
tagName: tagMeta.name,
|
||
tagColor: tagMeta.color,
|
||
publishedAt: formatPostDate(post.publishedAt),
|
||
to: `/post/${post.slug}`,
|
||
isFeatured: index === 0
|
||
}
|
||
}
|
||
|
||
const featuredPosts = computed(() => posts.value.slice(0, 6))
|
||
const latestPosts = computed(() => posts.value.map(mapLatestPost))
|
||
</script>
|
||
|
||
<template>
|
||
<MainColumn>
|
||
<section class="px-5 py-6 sm:px-6 md:py-8">
|
||
<div class="mx-auto flex max-w-[720px] flex-col-reverse gap-6">
|
||
<div class="z-[2] flex flex-col items-center justify-center gap-2 text-center">
|
||
<h1 class="text-xl font-semibold leading-[1.125] md:text-2xl">
|
||
Ideas <em>published</em> for meaningful conversation, <em>discussed</em> and shaped by the community
|
||
</h1>
|
||
<p class="max-w-md text-base leading-snug site-muted">
|
||
A modern Ghost theme for curated, community-driven publishing, where members join the conversation.
|
||
</p>
|
||
<form class="group relative mt-1 flex w-full max-w-xs flex-col items-start">
|
||
<fieldset class="flex w-full flex-wrap gap-2 text-sm">
|
||
<legend class="sr-only">Personal information</legend>
|
||
<input class="site-input flex-[2] rounded-[10px] px-3 py-1.5 text-sm" type="email" placeholder="Your email" aria-label="Your email">
|
||
<button class="site-button flex-1 cursor-pointer rounded-[10px] border border-[var(--site-invert)] bg-gradient-to-b from-[rgba(17,17,17,0.75)] to-[rgba(17,17,17,0.95)] px-3 py-1.5 font-medium text-[var(--site-invert-text)] hover:opacity-90" type="button">
|
||
Subscribe
|
||
</button>
|
||
</fieldset>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="px-5 py-4 sm: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="cursor-pointer p-1 hover:opacity-75" type="button" aria-label="Previous">‹</button>
|
||
<button class="cursor-pointer p-1 hover:opacity-75" type="button" aria-label="Next">›</button>
|
||
</div>
|
||
</div>
|
||
<div class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||
<NuxtLink
|
||
v-for="post in featuredPosts"
|
||
:key="`featured-${post.slug}`"
|
||
:to="`/post/${post.slug}`"
|
||
class="group relative block aspect-video overflow-hidden rounded-[10px]"
|
||
>
|
||
<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="h-full w-full bg-[linear-gradient(135deg,#071b22,#5f6f85)]"
|
||
/>
|
||
<h3 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>
|
||
|
||
<section class="px-5 py-4 sm: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>
|
||
<button class="site-input flex cursor-pointer items-center justify-center gap-1 rounded-[10px] border px-2 py-1.5 text-sm hover:bg-[var(--site-panel)]" type="button">
|
||
<span>☷</span>
|
||
<span>⌄</span>
|
||
</button>
|
||
</div>
|
||
<div class="mb-8 flex flex-col">
|
||
<div class="flex flex-col divide-y divide-[var(--site-line)]">
|
||
<article
|
||
v-for="post in latestPosts"
|
||
:key="post.to"
|
||
class="group relative flex flex-row gap-3 py-4"
|
||
>
|
||
<NuxtLink :to="post.to" class="relative aspect-square min-w-16 flex-1 sm:aspect-video">
|
||
<figure class="overflow-hidden rounded-[10px]">
|
||
<img
|
||
v-if="post.featuredImage"
|
||
:src="post.featuredImage"
|
||
:alt="post.title"
|
||
class="aspect-square w-full rounded-[inherit] object-cover transition-opacity duration-200 group-hover:opacity-90 sm:aspect-video"
|
||
loading="lazy"
|
||
>
|
||
<div
|
||
v-else
|
||
class="aspect-square w-full rounded-[inherit] bg-[linear-gradient(135deg,#253444,#8f9dad)] sm:aspect-video"
|
||
/>
|
||
</figure>
|
||
</NuxtLink>
|
||
|
||
<div class="relative flex-[3] md:flex-[4]">
|
||
<div class="flex h-full flex-col gap-1.5">
|
||
<h2 class="max-w-[90%] text-sm font-medium leading-tight">
|
||
<NuxtLink :to="post.to" class="transition-opacity duration-200 hover:opacity-75">
|
||
<span v-if="post.isFeatured" class="mr-1 inline-flex text-[var(--site-accent)]">⚡</span>
|
||
{{ post.title }}
|
||
</NuxtLink>
|
||
</h2>
|
||
|
||
<p class="line-clamp-2 flex-1 text-[0.8rem] leading-tight site-muted">
|
||
{{ post.excerpt }}
|
||
</p>
|
||
|
||
<div class="flex flex-wrap items-center gap-2 text-xs site-muted sm:gap-1.5">
|
||
<time>{{ post.publishedAt }}</time>
|
||
<span class="text-[var(--site-line)]">/</span>
|
||
<span
|
||
class="rounded-sm px-1.5 py-px font-medium text-[var(--site-text)]"
|
||
:style="{ backgroundColor: `${post.tagColor}1a` }"
|
||
>
|
||
{{ post.tagName }}
|
||
</span>
|
||
<span class="text-[var(--site-line)]">/</span>
|
||
<span class="flex items-center gap-0.5">
|
||
<span>◌</span>
|
||
<span>0</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<button
|
||
class="absolute top-0 right-0 flex cursor-pointer items-center gap-1 hover:opacity-75 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"
|
||
>
|
||
<span class="text-sm">↗</span>
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</MainColumn>
|
||
</template>
|