Files
sori.studio/pages/index.vue
zenn 1a4086336f 카드 hover 우하단 액션 화살표를 홈과 태그 상세에 추가.
원본 패턴에 맞춰 목록 카드의 우하단 hover 액션 버튼을 홈 Latest와 태그 상세 목록에 동일하게 반영해 상호작용 피드백을 보강했다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 18:11:36 +09:00

201 lines
8.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>