feat(frontend): update pages, components and branding

Refresh Astro frontend implementation including new pages (Portfolio, Teams, Services), components, and styling updates.
This commit is contained in:
2026-02-11 11:50:42 +08:00
parent be7fc902fb
commit 9c2181f743
49 changed files with 9699 additions and 899 deletions

View File

@@ -1,61 +1,289 @@
---
import Layout from '../layouts/Layout.astro';
/**
* News/Blog Listing Page - 行銷放大鏡
* URL: /news
* Displays all published blog posts from Payload CMS
*/
import Layout from '../layouts/Layout.astro'
import ArticleCard from '../components/blog/ArticleCard.astro'
import CategoryFilter from '../components/blog/CategoryFilter.astro'
import { fetchPosts, fetchCategories } from '../lib/api/blog'
// Placeholder for blog posts - would fetch from CMS
const posts = [
{ slug: 'en-qun-shu-wei-zui-xin-gong-gao', title: '恩群數位最新公告', date: '2023-01-01' },
{ slug: 'google-xiao-xue-tang', title: 'Google小學堂', date: '2023-01-02' },
// Add more
];
// Metadata for SEO
const title = '行銷放大鏡 | 恩群數位行銷'
const description = '閱讀恩群數位的專業行銷文章掌握最新的數位行銷趨勢、社群經營技巧、Google 廣告策略等實用內容。'
// Pagination settings
const PAGE_SIZE = 12
// Get query parameters
const url = Astro.url
const pageParam = url.searchParams.get('page')
const page = Math.max(1, parseInt(pageParam || '1'))
// Fetch posts from Payload CMS
const postsData = await fetchPosts(page, PAGE_SIZE)
const posts = postsData.docs
const totalPages = postsData.totalPages
const hasPreviousPage = postsData.hasPreviousPage
const hasNextPage = postsData.hasNextPage
// Fetch categories
const categories = await fetchCategories()
// Filter out categories without slugs and the legacy "文章分類" container
const validCategories = categories.filter(c =>
c.slug && c.slug !== 'wen-zhang-fen-lei'
)
---
<Layout>
<section class="news-section">
<Layout title={title} description={description}>
<!-- Blog Hero Section -->
<section class="blog-hero" aria-labelledby="blog-heading">
<div class="container">
<h1>行銷放大鏡</h1>
<div class="posts-grid">
{posts.map(post => (
<article class="post-card">
<h2><a href={`/wen-zhang-fen-lei/${post.slug}`}>{post.title}</a></h2>
<p>{post.date}</p>
</article>
))}
<div class="section_header_w_line">
<div class="divider_line"></div>
<div class="header_subtitle">
<h2 id="blog-heading" class="header_subtitle_head">行銷放大鏡</h2>
<p class="header_subtitle_paragraph">Marketing Blog</p>
</div>
<div class="divider_line"></div>
</div>
</div>
</section>
<!-- Category Filter -->
<section class="filter-section">
<div class="container">
<CategoryFilter categories={validCategories} />
</div>
</section>
<!-- Blog Posts Grid -->
<section class="blog-section" aria-label="文章列表">
<div class="container">
{
posts.length > 0 ? (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-10 w-full max-w-[1200px] mx-auto">
{posts.map((post) => (
<ArticleCard post={post} />
))}
</div>
) : (
<div class="empty-state">
<p class="empty-text">暫無文章</p>
</div>
)
}
<!-- Pagination -->
{
totalPages > 1 && (
<nav class="pagination" aria-label="分頁導航">
<div class="pagination-container">
{
hasPreviousPage && (
<a
href={`?page=${page - 1}`}
class="pagination-link pagination-link-prev"
aria-label="上一頁"
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
</svg>
上一頁
</a>
)
}
<div class="pagination-info">
<span class="current-page">{page}</span>
<span class="page-separator">/</span>
<span class="total-pages">{totalPages}</span>
</div>
{
hasNextPage && (
<a
href={`?page=${page + 1}`}
class="pagination-link pagination-link-next"
aria-label="下一頁"
>
下一頁
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</svg>
</a>
)
}
</div>
</nav>
)
}
</div>
</section>
</Layout>
<style>
.news-section {
padding: 40px 0;
/* Blog Hero Section */
.blog-hero {
padding: 60px 20px;
background-color: #ffffff;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
.section_header_w_line {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.header_subtitle {
text-align: center;
margin-bottom: 30px;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
.header_subtitle_head {
color: var(--color-enchunblue, #23608c);
font-family: "Noto Sans TC", sans-serif;
font-weight: 700;
font-size: 2rem;
margin-bottom: 8px;
}
.post-card {
border: 1px solid #ddd;
padding: 20px;
.header_subtitle_paragraph {
color: var(--color-gray-600, #666);
font-family: "Quicksand", "Noto Sans TC", sans-serif;
font-weight: 400;
font-size: 1rem;
}
.divider_line {
width: 40px;
height: 2px;
background-color: var(--color-enchunblue, #23608c);
}
/* Filter Section */
.filter-section {
background-color: #ffffff;
padding-bottom: 20px;
}
/* Blog Section */
.blog-section {
padding: 40px 20px 60px;
background-color: #f8f9fa;
min-height: 60vh;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 80px 20px;
}
.empty-text {
font-size: 1.125rem;
color: var(--color-gray-500, #999);
}
/* Pagination */
.pagination {
padding-top: 20px;
}
.pagination-container {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
}
.pagination-link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #ffffff;
border: 2px solid var(--color-gray-300, #e0e0e0);
border-radius: 8px;
}
.post-card h2 {
margin-top: 0;
}
.post-card a {
color: var(--color-gray-700, #555);
font-family: "Noto Sans TC", sans-serif;
font-size: 0.9375rem;
font-weight: 500;
text-decoration: none;
color: #333;
transition: all 0.25s ease;
}
.post-card a:hover {
color: #007bff;
.pagination-link:hover {
border-color: var(--color-enchunblue, #23608c);
color: var(--color-enchunblue, #23608c);
background: rgba(35, 96, 140, 0.05);
}
</style>
.pagination-info {
display: flex;
align-items: center;
gap: 8px;
font-family: "Quicksand", sans-serif;
font-size: 1rem;
color: var(--color-gray-600, #666);
}
.current-page {
font-weight: 700;
color: var(--color-enchunblue, #23608c);
}
.page-separator {
color: var(--color-gray-400, #ccc);
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.header_subtitle_head {
font-size: 1.75rem;
}
}
@media (max-width: 767px) {
.blog-hero {
padding: 40px 16px;
}
.section_header_w_line {
flex-wrap: wrap;
gap: 12px;
}
.divider_line {
width: 30px;
}
.blog-section {
padding: 30px 16px 50px;
}
.pagination-container {
gap: 16px;
}
.pagination-link {
padding: 8px 16px;
font-size: 0.875rem;
}
.header_subtitle_head {
font-size: 1.5rem;
}
.header_subtitle_paragraph {
font-size: 0.9375rem;
}
}
</style>