Files
sori.studio/pages/index.vue
zenn 5e485eb3ec 홈 중앙 메인 영역을 Thred 간격 기준으로 재구성.
Hero/Featured/Latest 섹션을 내부 컨테이너 기준 보더 정렬로 바꾸고, Latest 목록 카드를 원본 패턴의 리스트 메타 구조로 정리해 중앙 메인 영역의 시각 리듬을 맞췄다.

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

194 lines
7.5 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>
</div>
</article>
</div>
</div>
</div>
</section>
</MainColumn>
</template>