CloudBridge/pages/blog/index.vue
2025-09-04 19:15:48 +08:00

170 lines
5.6 KiB
Vue

<template>
<div>
<!-- Hero Section -->
<section class="aws-gradient text-white section-padding">
<div class="container-custom text-center">
<h1 class="text-5xl font-bold mb-6">
{{ $t('blog.title') }}
</h1>
<p class="text-xl text-white/90 max-w-3xl mx-auto">
{{ $t('blog.subtitle') }}
</p>
</div>
</section>
<!-- Blog Posts -->
<section class="section-padding bg-white">
<div class="container-custom">
<div v-if="pending" class="text-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-aws-orange mx-auto"></div>
<p class="mt-4 text-gray-600">Loading articles...</p>
</div>
<div v-else-if="error" class="text-center py-12">
<p class="text-red-600">Error loading articles. Please try again later.</p>
</div>
<div v-else class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<article
v-for="article in articles"
:key="article._path"
class="card hover:shadow-aws transition-all duration-300 group"
>
<div v-if="article.image" class="aspect-video bg-gray-200 rounded-lg mb-4 overflow-hidden">
<img
:src="article.image"
:alt="article.title"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div class="p-6">
<div class="flex items-center text-sm text-gray-500 mb-3">
<Calendar class="w-4 h-4 mr-2" />
<time>{{ formatDate(article.date) }}</time>
<span class="mx-2"></span>
<span>{{ article.readingTime }} min read</span>
</div>
<h2 class="text-xl font-bold mb-3 group-hover:text-aws-orange transition-colors duration-200">
<NuxtLink :to="articleLink(article)">
{{ article.title }}
</NuxtLink>
</h2>
<p class="text-gray-600 mb-4 line-clamp-3">
{{ article.description }}
</p>
<div class="flex items-center justify-between">
<NuxtLink
:to="articleLink(article)"
class="text-aws-orange hover:underline font-medium"
>
{{ $t('blog.readMore') }}
</NuxtLink>
<div class="flex space-x-2">
<button
v-for="locale in availableLocales"
:key="locale.code"
@click="switchToTranslation(article, locale.code)"
:class="[
'px-2 py-1 text-xs rounded',
currentLocale === locale.code
? 'bg-aws-orange text-white'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
]"
>
{{ locale.name }}
</button>
</div>
</div>
</div>
</article>
</div>
<div v-if="articles.length === 0" class="text-center py-12">
<p class="text-gray-600">No articles found.</p>
</div>
</div>
</section>
</div>
</template>
<script setup>
import { Calendar } from 'lucide-vue-next'
// i18n
const { t, locale } = useI18n()
// SEO
useSeoMeta({
title: () => t('seo.blog.title'),
description: () => t('seo.blog.description')
})
const { locales } = useI18n()
const currentLocale = ref(locale.value)
// Fetch blog articles (filter by current _locale provided by @nuxt/content)
const { data: articles, pending, error } = await useAsyncData(() => `blog-${locale.value}`, async () => {
try {
return await queryContent()
.where({ _locale: locale.value })
.sort({ date: -1 })
.find()
} catch (err) {
console.error('Error fetching articles:', err)
return []
}
})
// Build localized link for article (app-level locale prefix + blog + slug without locale)
const articleLink = (a) => {
if (!a || !a._path) return '/blog'
const appPrefix = locale.value === 'en' ? '' : `/${locale.value}`
const slugPath = a._path.replace(/^\/(en|zh|zh-hant)(?=\/)/, '')
return `${appPrefix}/blog${slugPath}`
}
const availableLocales = computed(() =>
locales.value.filter(locale => locale.code !== 'en' || locale.code === currentLocale.value)
)
const formatDate = (date) => {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
const switchToTranslation = async (article, targetLocale) => {
if (targetLocale === currentLocale.value) return
try {
const basePath = article._path.replace(/^\/(en|zh|zh-hant)/, '')
const translated = await queryContent(`/${targetLocale}${basePath}`).findOne()
if (translated?._path) {
const appPrefix = targetLocale === 'en' ? '' : `/${targetLocale}`
const slugPath = translated._path.replace(/^\/(en|zh|zh-hant)(?=\/)/, '')
await navigateTo(`${appPrefix}/blog${slugPath}`)
return
}
throw new Error('Not found')
} catch (err) {
// Show translation not available message
await navigateTo(`/blog/no-translation?article=${encodeURIComponent(article._path)}&locale=${targetLocale}`)
}
}
</script>
<style scoped>
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>