Extract generic UI components

Reduces duplication across marketing pages by converting sections into
reusable components like CtaSection and HeaderBg. Consolidates styling
patterns to improve maintainability and consistency of the user interface.
This commit is contained in:
2026-02-28 04:55:25 +08:00
parent b199f89998
commit 173905ecd3
14 changed files with 902 additions and 1413 deletions

View File

@@ -39,29 +39,29 @@ const validCategories = categories.filter(c =>
<Layout title={title} description={description}>
<!-- Blog Hero Section -->
<section class="blog-hero" aria-labelledby="blog-heading">
<div class="container">
<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>
<section class="py-[60px] px-5 bg-white lg:py-10 md:py-10 md:px-4" aria-labelledby="blog-heading">
<div class="max-w-[1200px] mx-auto">
<div class="flex items-center justify-center gap-4 md:flex-wrap md:gap-3">
<div class="w-10 h-0.5 bg-link-hover md:w-[30px]"></div>
<div class="text-center">
<h2 id="blog-heading" class="text-2xl font-bold text-link-hover mb-2 lg:text-[1.75rem] md:text-[1.5rem]">行銷放大鏡</h2>
<p class="text-base text-slate-600 font-accent md:text-[0.9375rem]">Marketing Blog</p>
</div>
<div class="divider_line"></div>
<div class="w-10 h-0.5 bg-link-hover md:w-[30px]"></div>
</div>
</div>
</section>
<!-- Category Filter -->
<section class="filter-section">
<div class="container">
<section class="bg-white pb-5">
<div class="max-w-[1200px] mx-auto">
<CategoryFilter categories={validCategories} />
</div>
</section>
<!-- Blog Posts Grid -->
<section class="blog-section" aria-label="文章列表">
<div class="container">
<section class="py-10 px-5 pb-[60px] bg-slate-100 min-h-[60vh] md:py-[30px] md:px-4 md:pb-[50px]" aria-label="文章列表">
<div class="max-w-[1200px] mx-auto">
{
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">
@@ -70,8 +70,8 @@ const validCategories = categories.filter(c =>
))}
</div>
) : (
<div class="empty-state">
<p class="empty-text">暫無文章</p>
<div class="text-center py-20 px-5">
<p class="text-lg text-slate-500">暫無文章</p>
</div>
)
}
@@ -79,13 +79,13 @@ const validCategories = categories.filter(c =>
<!-- Pagination -->
{
totalPages > 1 && (
<nav class="pagination" aria-label="分頁導航">
<div class="pagination-container">
<nav class="pt-5" aria-label="分頁導航">
<div class="flex items-center justify-center gap-6 md:gap-4">
{
hasPreviousPage && (
<a
href={`?page=${page - 1}`}
class="pagination-link pagination-link-prev"
class="inline-flex items-center gap-2 px-5 py-[10px] bg-white border-2 border-slate-300 rounded-md text-slate-700 text-[0.9375rem] font-medium no-underline transition-all duration-[250ms] hover:border-link-hover hover:text-link-hover hover:bg-[rgba(35,96,140,0.05)] md:px-4 md:py-2 md:text-[0.875rem]"
aria-label="上一頁"
>
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
@@ -96,17 +96,17 @@ const validCategories = categories.filter(c =>
)
}
<div class="pagination-info">
<span class="current-page">{page}</span>
<span class="page-separator">/</span>
<span class="total-pages">{totalPages}</span>
<div class="flex items-center gap-2 font-accent text-base text-slate-600">
<span class="font-bold text-link-hover">{page}</span>
<span class="text-slate-400">/</span>
<span>{totalPages}</span>
</div>
{
hasNextPage && (
<a
href={`?page=${page + 1}`}
class="pagination-link pagination-link-next"
class="inline-flex items-center gap-2 px-5 py-[10px] bg-white border-2 border-slate-300 rounded-md text-slate-700 text-[0.9375rem] font-medium no-underline transition-all duration-[250ms] hover:border-link-hover hover:text-link-hover hover:bg-[rgba(35,96,140,0.05)] md:px-4 md:py-2 md:text-[0.875rem]"
aria-label="下一頁"
>
下一頁
@@ -124,166 +124,3 @@ const validCategories = categories.filter(c =>
</section>
</Layout>
<style>
/* Blog Hero Section */
.blog-hero {
padding: 60px 20px;
background-color: #ffffff;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.section_header_w_line {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.header_subtitle {
text-align: center;
}
.header_subtitle_head {
color: var(--color-enchunblue, #23608c);
font-family: "Noto Sans TC", sans-serif;
font-weight: 700;
font-size: 2rem;
margin-bottom: 8px;
}
.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;
color: var(--color-gray-700, #555);
font-family: "Noto Sans TC", sans-serif;
font-size: 0.9375rem;
font-weight: 500;
text-decoration: none;
transition: all 0.25s ease;
}
.pagination-link:hover {
border-color: var(--color-enchunblue, #23608c);
color: var(--color-enchunblue, #23608c);
background: rgba(35, 96, 140, 0.05);
}
.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>