From 173905ecd38552994ceab8bf107c0c48504de721 Mon Sep 17 00:00:00 2001 From: pkupuk Date: Sat, 28 Feb 2026 04:55:25 +0800 Subject: [PATCH] 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. --- apps/frontend/src/components/CtaSection.astro | 34 ++ apps/frontend/src/components/Header.astro | 520 ++++++++++-------- apps/frontend/src/components/HeaderBg.astro | 10 + .../src/components/PortfolioCard.astro | 75 +-- .../src/components/SectionHeader.astro | 37 +- apps/frontend/src/pages/about-enchun.astro | 43 +- apps/frontend/src/pages/contact-us.astro | 390 ++----------- apps/frontend/src/pages/news.astro | 207 +------ apps/frontend/src/pages/teams.astro | 178 ++---- .../src/pages/website-portfolio.astro | 249 ++------- apps/frontend/src/sections/AboutHero.astro | 76 ++- .../src/sections/FeatureSection.astro | 176 +----- apps/frontend/src/sections/TeamsHero.astro | 76 ++- apps/frontend/src/styles/theme.css | 244 +++++--- 14 files changed, 902 insertions(+), 1413 deletions(-) create mode 100644 apps/frontend/src/components/CtaSection.astro create mode 100644 apps/frontend/src/components/HeaderBg.astro diff --git a/apps/frontend/src/components/CtaSection.astro b/apps/frontend/src/components/CtaSection.astro new file mode 100644 index 0000000..518a47a --- /dev/null +++ b/apps/frontend/src/components/CtaSection.astro @@ -0,0 +1,34 @@ +--- +/** + * CTA Section Component + */ +--- + +
+
+
+
+

+ 準備好開始新的旅程了嗎? +

+

+ 歡迎與我們聯絡 +

+
+ + 預約諮詢 + +
+
+
diff --git a/apps/frontend/src/components/Header.astro b/apps/frontend/src/components/Header.astro index 36e3bbb..556133a 100644 --- a/apps/frontend/src/components/Header.astro +++ b/apps/frontend/src/components/Header.astro @@ -1,29 +1,65 @@ --- -import { Image } from "astro:assets"; // Header component with scroll-based background and enhanced mobile animations +// --- TypeScript Interfaces (from payload-types.ts Header global) --- +interface NavLink { + type?: ("reference" | "custom") | null; + newTab?: boolean | null; + reference?: + | ({ relationTo: "pages"; value: string | { slug: string } } | null) + | ({ relationTo: "posts"; value: string | { slug: string } } | null); + url?: string | null; + label: string; +} + +interface NavItem { + link: NavLink; + id?: string | null; +} + +// --- Hardcoded fallback nav (used when CMS is unavailable) --- +const FALLBACK_NAV: NavItem[] = [ + { link: { type: "custom", url: "/about-enchun", label: "關於恩群" } }, + { + link: { + type: "custom", + url: "/marketing-solutions", + label: "行銷方案", + }, + }, + { link: { type: "custom", url: "/marketing-lens", label: "行銷放大鏡" } }, + { link: { type: "custom", url: "/enchun-basecamp", label: "恩群大本營" } }, + { link: { type: "custom", url: "/website-portfolio", label: "網站設計" } }, + { link: { type: "custom", url: "/contact", label: "聯絡我們" } }, +]; + // Use local backend in development, production URL from dev.vars/wrangler const isDev = import.meta.env.DEV; const PAYLOAD_CMS_URL = isDev - ? "http://localhost:3000" // Local backend in dev (port may vary) + ? "http://localhost:3000" : import.meta.env.PAYLOAD_CMS_URL || "https://enchun-admin.anlstudio.cc"; // Fetch navigation data from Payload CMS server-side -let navItems: any[] = []; +let navItems: NavItem[] = []; try { const response = await fetch( - `${PAYLOAD_CMS_URL}/api/globals/header?depth=2&draft=false&locale=undefined&trash=false`, + `${PAYLOAD_CMS_URL}/api/globals/header?depth=2&draft=false&trash=false`, ); if (response.ok) { const data = await response.json(); - navItems = data?.navItems || data || []; + navItems = data?.navItems || []; } } catch (error) { console.error("[Header SSR] Failed to fetch navigation:", error); } +// Use fallback when CMS returns empty +if (!navItems.length) { + navItems = FALLBACK_NAV; +} + // Helper to get link URL -function getLinkUrl(link: any): string { +function getLinkUrl(link: NavLink): string { if (!link) return "#"; if (link.type === "custom" && link.url) { @@ -32,10 +68,8 @@ function getLinkUrl(link: any): string { if (link.type === "reference" && link.reference?.value) { if (typeof link.reference.value === "string") { - // It's an ID, construct URL based on relationTo return `/${link.reference.relationTo || "pages"}/${link.reference.value}`; } else if (link.reference.value.slug) { - // It's a populated object with slug return `/${link.reference.value.slug}`; } } @@ -43,21 +77,16 @@ function getLinkUrl(link: any): string { return "#"; } -// Check if label should have a badge -function getBadgeForLabel(label: string): string { - if (label.includes("行銷方案")) { - return `Hot`; - } - if (label.includes("行銷放大鏡")) { - return `New`; - } - return ""; +// Return badge type for label (data-driven, no HTML strings) +function getBadgeType(label: string): "hot" | "new" | null { + if (label.includes("行銷方案")) return "hot"; + if (label.includes("行銷放大鏡")) return "new"; + return null; } // Check if link is active function isLinkActive(url: string): boolean { - const currentPath = Astro.url.pathname; - return currentPath === url || (url === "/" && currentPath === "/"); + return Astro.url.pathname === url; } --- @@ -65,53 +94,58 @@ function isLinkActive(url: string): boolean { class="fixed top-0 left-0 right-0 z-50 transition-all duration-300 ease-in-out will-change-transform" id="main-header" > - diff --git a/apps/frontend/src/components/HeaderBg.astro b/apps/frontend/src/components/HeaderBg.astro new file mode 100644 index 0000000..94ac3ce --- /dev/null +++ b/apps/frontend/src/components/HeaderBg.astro @@ -0,0 +1,10 @@ +--- + +--- + + +
+
diff --git a/apps/frontend/src/components/PortfolioCard.astro b/apps/frontend/src/components/PortfolioCard.astro index 58d30b6..d300b78 100644 --- a/apps/frontend/src/components/PortfolioCard.astro +++ b/apps/frontend/src/components/PortfolioCard.astro @@ -5,28 +5,30 @@ */ interface PortfolioItem { - slug: string - title: string - description: string - image?: string - tags?: string[] - externalUrl?: string + slug: string; + title: string; + description: string; + image?: string; + tags?: string[]; + externalUrl?: string; } interface Props { - item: PortfolioItem + item: PortfolioItem; } -const { item } = Astro.props +const { item } = Astro.props; -const imageUrl = item.image || '/placeholder-portfolio.jpg' -const title = item.title || 'Untitled' -const description = item.description || '' -const tags = item.tags || [] -const hasExternalLink = !!item.externalUrl -const linkHref = hasExternalLink ? item.externalUrl : `/website-portfolio/${item.slug}` -const linkTarget = hasExternalLink ? '_blank' : '_self' -const linkRel = hasExternalLink ? 'noopener noreferrer' : '' +const imageUrl = item.image || "/placeholder-portfolio.jpg"; +const title = item.title || "Untitled"; +const description = item.description || ""; +const tags = item.tags || []; +const hasExternalLink = !!item.externalUrl; +const linkHref = hasExternalLink + ? item.externalUrl + : `/website-portfolio/${item.slug}`; +const linkTarget = hasExternalLink ? "_blank" : "_self"; +const linkRel = hasExternalLink ? "noopener noreferrer" : ""; ---
  • @@ -36,37 +38,44 @@ const linkRel = hasExternalLink ? 'noopener noreferrer' : '' rel={linkRel} class="block bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 ease-in-out no-underline text-inherit hover:-translate-y-1 group" > - -
    - {title} -
    - -
    +
    -

    {title}

    +

    + {title} +

    { description && ( -

    {description}

    +

    + {description} +

    ) } + +
    + {title} +
    { tags.length > 0 && (
    {tags.map((tag) => ( - {tag} + + {tag} + ))}
    ) diff --git a/apps/frontend/src/components/SectionHeader.astro b/apps/frontend/src/components/SectionHeader.astro index c87c604..2564dc0 100644 --- a/apps/frontend/src/components/SectionHeader.astro +++ b/apps/frontend/src/components/SectionHeader.astro @@ -5,23 +5,40 @@ */ interface Props { - title: string - subtitle: string - class?: string - sectionBg?:string + title: string; + subtitle: string; + class?: string; + sectionBg?: string; } -const { title, subtitle, class: className, sectionBg:classNameBg } = Astro.props +const { + title, + subtitle, + class: className, + sectionBg: classNameBg, +} = Astro.props; ---
    - -
    +
    -

    {title}

    -

    {subtitle}

    +

    + {title} +

    +

    + {subtitle} +

    -
    +
    diff --git a/apps/frontend/src/pages/about-enchun.astro b/apps/frontend/src/pages/about-enchun.astro index 4bd733c..1dcec69 100644 --- a/apps/frontend/src/pages/about-enchun.astro +++ b/apps/frontend/src/pages/about-enchun.astro @@ -3,21 +3,37 @@ * About Page - 關於恩群數位 * 展示公司特色、服務優勢和與其他公司的差異 */ -import Layout from '../layouts/Layout.astro' -import AboutHero from '../sections/AboutHero.astro' -import FeatureSection from '../sections/FeatureSection.astro' -import ComparisonSection from '../sections/ComparisonSection.astro' -import CTASection from '../sections/CTASection.astro' +import Layout from "../layouts/Layout.astro"; +import AboutHero from "../sections/AboutHero.astro"; +import FeatureSection from "../sections/FeatureSection.astro"; +import ComparisonSection from "../sections/ComparisonSection.astro"; +import CTASection from "../sections/CTASection.astro"; +import SectionHeader from "../components/SectionHeader.astro"; +import CtaSection from "@/components/CtaSection.astro"; // Metadata for SEO -const title = '關於恩群數位 | 專業數位行銷服務團隊' -const description = '恩群數位行銷成立於2018年,提供全方位數位行銷服務。我們在地化優先、數據驅動,是您最可信赖的數位行銷夥伴。' +const title = "關於恩群數位 | 專業數位行銷服務團隊"; +const description = + "恩群數位行銷成立於2018年,提供全方位數位行銷服務。我們在地化優先、數據驅動,是您最可信赖的數位行銷夥伴。"; --- - + + + @@ -25,14 +41,5 @@ const description = '恩群數位行銷成立於2018年,提供全方位數位 - + diff --git a/apps/frontend/src/pages/contact-us.astro b/apps/frontend/src/pages/contact-us.astro index a62a60b..dfaad08 100644 --- a/apps/frontend/src/pages/contact-us.astro +++ b/apps/frontend/src/pages/contact-us.astro @@ -12,102 +12,102 @@ const description = '有任何問題嗎?歡迎聯絡恩群數位行銷,我 --- -
    -
    +
    +
    -
    -

    聯絡我們

    -

    +

    +

    聯絡我們

    +

    有任何問題嗎?歡迎聯絡我們,我們將竭誠為您服務。

    -

    +

    * 標註欄位為必填

    -
    + -
    -