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

@@ -0,0 +1,34 @@
---
/**
* CTA Section Component
*/
---
<section
class="py-20 px-5 bg-white text-center mx-auto lg:py-[60px] lg:px-5 md:py-10"
aria-labelledby="cta-heading"
>
<div class="max-w-3xl mx-auto">
<div class="flex flex-row items-center justify-center gap-10">
<div class="flex flex-col items-start">
<h2
id="cta-heading"
class="text-xl font-bold text-(--dark-blue) lg:text-3xl"
>
準備好開始新的旅程了嗎?
</h2>
<p
class="text-xl font-semibold text-(--dark-blue) mb-4 lg:text-3xl"
>
歡迎與我們聯絡
</p>
</div>
<a
href="http://heyform.anlstudio.cc/form/FdA22OCm"
class="flex text-xl bg-(--color-notification-red) text-white px-6 py-3 rounded-lg font-semibold transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:bg-(--color-accent-dark)"
>
預約諮詢
</a>
</div>
</div>
</section>

View File

@@ -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 `<span class="absolute -top-2 -right-3 bg-red-500 text-white text-[6px] font-bold px-1.5 py-0.5 rounded-full uppercase tracking-wider shadow-sm animate-pulse">Hot</span>`;
}
if (label.includes("行銷放大鏡")) {
return `<span class="absolute -top-2 -right-3 bg-red-500 text-white text-[6px] font-bold px-1.5 py-0.5 rounded-full uppercase tracking-wider shadow-sm">New</span>`;
}
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;
}
---
@@ -72,46 +101,51 @@ function isLinkActive(url: string): boolean {
<ul class="flex items-center justify-between list-none">
<li class="shrink-0">
<a href="/" class="block">
<!-- Uses Astro's optimized Image component for the site logo -->
<Image
<img
src="/enchun-logo.svg"
alt="Enchun Digital Marketing"
class="w-32 h-auto transition-opacity duration-300 drop-shadow-md"
width={919}
height={201}
class="w-32 h-auto transition-transform duration-300 drop-shadow-md"
width="919"
height="201"
loading="eager"
decoding="async"
/>
</a>
</li>
<li class="hidden md:flex items-center space-x-6">
{
navItems.map((item) => {
const link = item.link;
const href = getLinkUrl(link);
const label = link.label || "未命名";
const badge = getBadgeForLabel(label);
const activeClass = isLinkActive(href)
? "nav-active"
: "";
const hasBadge = badge ? "relative inline-block" : "";
const badge = getBadgeType(label);
const active = isLinkActive(href);
return (
<li class="hidden md:block">
<a
href={href}
class={`${hasBadge} text-base font-medium transition-all duration-200 px-3 py-2 rounded ${activeClass} hover:bg-white/10`}
class:list={[
"nav-link text-base font-medium transition-all duration-200 px-3 py-2 rounded hover:bg-white/10",
{ "relative inline-block": badge },
{ "nav-active": active },
]}
{...(link.newTab && {
target: "_blank",
rel: "noopener noreferrer",
})}
>
{label}
{badge && <Fragment set:html={badge} />}
{badge === "hot" && (
<span class="badge-nav badge-hot">Hot</span>
)}
{badge === "new" && (
<span class="badge-nav badge-new">New</span>
)}
</a>
</li>
);
})
}
</li>
<!-- Mobile menu button with animated hamburger/X icon -->
<li class="md:hidden">
<button
@@ -154,10 +188,12 @@ function isLinkActive(url: string): boolean {
</button>
</li>
</ul>
<!-- Mobile menu with full-screen overlay -->
<div
class="md:hidden fixed left-0 right-0 top-20 h-[calc(100vh-80px)] h-[calc(100dvh-80px)] bg-white/20 backdrop-blur-xl opacity-0 invisible transition-[opacity,visibility,top] duration-300 ease-in-out"
</nav>
<!-- Mobile menu with full-screen overlay (separate nav for semantics) -->
<nav
class="md:hidden fixed left-0 right-0 mobile-menu-top mobile-menu-height bg-white/20 backdrop-blur-xl opacity-0 invisible transition-[opacity,visibility,top] duration-300 ease-in-out"
id="mobile-menu"
aria-label="Mobile navigation"
>
<ul
class="flex flex-col items-center justify-center h-full space-y-6 px-4"
@@ -171,12 +207,12 @@ function isLinkActive(url: string): boolean {
return (
<li
class={`opacity-0`}
class="mobile-nav-item"
style={`animation-delay: ${index * 50}ms`}
>
<a
href={href}
class="text-2xl text-grey-700 font-medium transition-all duration-200 hover:scale-105 p-3 min-h-12 flex items-center [text-shadow:0_1px_2px_rgba(0,0,0,0.05)] hover:[text-shadow:0_2px_4px_rgba(0,0,0,0.1)]"
class="text-2xl text-gray-700 font-medium transition-all duration-200 hover:scale-105 p-3 min-h-12 flex items-center"
{...(link.newTab && {
target: "_blank",
rel: "noopener noreferrer",
@@ -189,20 +225,20 @@ function isLinkActive(url: string): boolean {
})
}
</ul>
</div>
</nav>
</header>
<script>
// Scroll-based header with smooth hide/show animation
// Uses AbortController for cleanup on re-mount (View Transitions)
let lastScrollY = 0;
let ticking = false;
let isHeaderHidden = false;
let abortController: AbortController | null = null;
function initScrollEffect() {
const header = document.getElementById("main-header");
const nav = document.getElementById("main-nav");
const mobileMenu = document.getElementById("mobile-menu");
if (!header || !nav) return;
const handleScroll = () => {
@@ -210,90 +246,36 @@ function isLinkActive(url: string): boolean {
const scrollDelta = scrollY - lastScrollY;
const scrollDirection = scrollDelta > 0 ? "down" : "up";
// Only update lastScrollY if there's meaningful movement
if (Math.abs(scrollDelta) > 1) {
lastScrollY = scrollY;
}
// Header shrinks and gets background on scroll
// Toggle single class for scrolled state (CSS handles all visuals)
if (scrollY > 10) {
if (!header.classList.contains("header-scrolled")) {
header.classList.add(
"bg-white/15",
"backdrop-blur-xl",
"shadow-md",
"header-scrolled",
);
header.classList.remove("bg-transparent");
nav.classList.add("py-2");
nav.classList.remove("py-4");
// Adjust mobile menu top position and height when header shrinks
if (mobileMenu) {
mobileMenu.classList.remove(
"top-20",
"h-[calc(100vh-80px)]",
"h-[calc(100dvh-80px)]",
);
mobileMenu.classList.add(
"top-16",
"h-[calc(100vh-64px)]",
"h-[calc(100dvh-64px)]",
);
}
}
header.classList.add("header-scrolled");
} else {
if (header.classList.contains("header-scrolled")) {
header.classList.remove(
"bg-white/15",
"bg-white/90",
"backdrop-blur-xl",
"shadow-md",
"border-b",
"border-gray-200/50",
"header-scrolled",
);
header.classList.add("bg-transparent");
nav.classList.remove("py-2");
nav.classList.add("py-4");
// Reset mobile menu top position and height when header expands
if (mobileMenu) {
mobileMenu.classList.remove(
"top-16",
"h-[calc(100vh-64px)]",
"h-[calc(100dvh-64px)]",
);
mobileMenu.classList.add(
"top-20",
"h-[calc(100vh-80px)]",
"h-[calc(100dvh-80px)]",
);
}
}
header.classList.remove("header-scrolled");
}
// Hide/show header based on scroll direction - with threshold
const hideThreshold = 150; // Higher threshold before hiding
const showThreshold = 50; // Lower threshold to show (makes it feel smoother)
// Hide/show header based on scroll direction
const hideThreshold = 150;
const showThreshold = 50;
if (
scrollY > hideThreshold &&
scrollDirection === "down" &&
!isHeaderHidden
) {
// Only hide if scrolling down consistently
if (scrollDelta > 5) {
// Must be scrolling down with some speed
header.classList.remove("translate-in");
header.classList.add("translate-out");
isHeaderHidden = true;
}
} else if (scrollDirection === "up" && isHeaderHidden) {
// Show immediately when scrolling up
header.classList.remove("translate-out");
header.classList.add("translate-in");
isHeaderHidden = false;
} else if (scrollY < showThreshold) {
// Always show when near top
header.classList.remove("translate-out");
header.classList.add("translate-in");
isHeaderHidden = false;
@@ -302,44 +284,23 @@ function isLinkActive(url: string): boolean {
ticking = false;
};
const signal = abortController!.signal;
window.addEventListener(
"scroll",
() => {
if (!ticking) {
window.requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
window.requestAnimationFrame(handleScroll);
ticking = true;
}
},
{ passive: true },
{ passive: true, signal },
);
handleScroll();
// Animate mobile menu items on page load
setTimeout(() => {
const mobileItems = document.querySelectorAll("#mobile-nav li");
mobileItems.forEach((item) => {
item.classList.remove("opacity-0");
item.classList.add(
"opacity-100",
"translate-y-0",
"transition-all",
"duration-300",
"ease-out",
);
});
}, 100);
}
// Enhanced mobile menu toggle with animations
let mobileMenuInitialized = false;
function initMobileMenu() {
if (mobileMenuInitialized) return;
const button = document.getElementById("mobile-menu-button");
const menu = document.getElementById("mobile-menu");
const hamburgerIcon = document.getElementById("hamburger-icon");
@@ -348,13 +309,13 @@ function isLinkActive(url: string): boolean {
.getElementById("mobile-nav")
?.querySelectorAll("li");
if (!button || !menu || !hamburgerIcon || !closeIcon) {
// Retry after a delay if elements aren't ready
setTimeout(initMobileMenu, 100);
return;
}
if (!button || !menu || !hamburgerIcon || !closeIcon) return;
button.addEventListener("click", () => {
const signal = abortController!.signal;
button.addEventListener(
"click",
() => {
const isMenuOpen = menu.classList.contains("opacity-100");
if (isMenuOpen) {
@@ -367,11 +328,13 @@ function isLinkActive(url: string): boolean {
button.setAttribute("aria-expanded", "false");
mobileNavItems?.forEach((item) => {
item.classList.remove("opacity-100", "translate-y-0");
item.classList.add("opacity-0", "translate-y-4");
item.classList.remove("mobile-nav-visible");
item.classList.add("mobile-nav-hidden");
});
document.body.style.overflow = "";
document.documentElement.classList.remove(
"mobile-menu-open",
);
} else {
menu.classList.remove("opacity-0", "invisible");
menu.classList.add("opacity-100", "visible");
@@ -383,44 +346,61 @@ function isLinkActive(url: string): boolean {
mobileNavItems?.forEach((item, index) => {
setTimeout(() => {
item.classList.remove("opacity-0", "translate-y-4");
item.classList.add(
"opacity-100",
"translate-y-0",
"transition-all",
"duration-300",
"ease-out",
);
item.classList.remove("mobile-nav-hidden");
item.classList.add("mobile-nav-visible");
}, index * 50);
});
document.body.style.overflow = "hidden";
document.documentElement.classList.add("mobile-menu-open");
}
});
},
{ signal },
);
menu.addEventListener("click", (e) => {
menu.addEventListener(
"click",
(e) => {
if (e.target === menu) {
button.click();
}
});
},
{ signal },
);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && menu.classList.contains("opacity-100")) {
document.addEventListener(
"keydown",
(e) => {
if (
e.key === "Escape" &&
menu.classList.contains("opacity-100")
) {
button.click();
}
});
mobileMenuInitialized = true;
},
{ signal },
);
}
document.addEventListener("DOMContentLoaded", () => {
function init() {
// Cleanup previous listeners (View Transitions re-mount safety)
abortController?.abort();
abortController = new AbortController();
// Reset state
lastScrollY = 0;
ticking = false;
isHeaderHidden = false;
initScrollEffect();
initMobileMenu();
});
}
document.addEventListener("DOMContentLoaded", init);
document.addEventListener("astro:after-swap", init);
</script>
<style>
/* JS-controlled animation states - cannot be converted to Tailwind */
/* JS-controlled animation states */
#main-header.translate-out {
transform: translateY(-100%);
}
@@ -429,13 +409,44 @@ function isLinkActive(url: string): boolean {
transform: translateY(0);
}
/* --- Scrolled state (all visual changes via single CSS class) --- */
#main-header.header-scrolled {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));
}
#main-header:not(.header-scrolled) {
background: transparent;
}
#main-header.header-scrolled #main-nav {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
/* Logo shrinks when header is scrolled */
#main-header.header-scrolled img {
transform: scale(0.85);
transition: transform 0.3s ease-in-out;
}
/* Navigation links - text-shadow not in Tailwind */
/* Mobile menu positioning: adjusts based on header scroll state */
.mobile-menu-top {
top: 5rem; /* = py-4 header height */
}
.mobile-menu-height {
height: calc(100dvh - 5rem);
}
#main-header.header-scrolled ~ #mobile-menu,
#main-header.header-scrolled #mobile-menu {
top: 4rem; /* = py-2 header height */
height: calc(100dvh - 4rem);
}
/* Navigation links */
#main-nav a {
color: var(--color-nav-link);
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
@@ -445,19 +456,53 @@ function isLinkActive(url: string): boolean {
color: var(--color-primary);
}
/* Nav link underline animation */
.nav-link {
position: relative;
}
/* Active state */
.nav-active {
position: relative;
color: var(--color-enchunblue) !important;
font-weight: 600;
}
/* Badge positioning */
#main-nav a[class*="relative"] .absolute {
/* Badge positioning for nav */
.badge-nav {
position: absolute;
top: -4px;
right: -8px;
font-size: 6px;
font-weight: 700;
padding: 2px 6px;
border-radius: 9999px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: white;
box-shadow: var(--shadow-sm);
}
/* Mobile menu - backdrop-blur-lg already in class */
.badge-nav.badge-hot {
background-color: var(--color-badge-hot, #ea384c);
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.badge-nav.badge-new {
background-color: var(--color-badge-hot, #ea384c);
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Mobile menu */
@media (max-width: 767px) {
#mobile-menu {
overflow-y: auto;
@@ -477,4 +522,31 @@ function isLinkActive(url: string): boolean {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
/* Mobile nav item animation states (CSS-driven) */
.mobile-nav-item {
opacity: 0;
transform: translateY(1rem);
transition:
opacity 0.3s ease-out,
transform 0.3s ease-out;
}
.mobile-nav-item.mobile-nav-visible {
opacity: 1;
transform: translateY(0);
}
.mobile-nav-item.mobile-nav-hidden {
opacity: 0;
transform: translateY(1rem);
}
/* iOS scroll lock */
:global(html.mobile-menu-open) {
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,10 @@
---
---
<!-- nav bg-placeholder -->
<div
id="header-bg"
class="h-[90px] -mt-[100px] bg-linear-to-b from-(--color-dark-blue) to-(--color-enchunblue)"
>
</div>

View File

@@ -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" : "";
---
<li class="list-none">
@@ -36,8 +38,25 @@ 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"
>
<!-- Content -->
<div class="p-2 md:p-3">
<!-- Title -->
<h3
class="font-['Noto_Sans_TC'] text-xl md:text-2xl font-semibold text-[var(--color-tarawera)] mb-2"
>
{title}
</h3>
<!-- Description -->
{
description && (
<p class="font-['Noto_Sans_TC'] font-thin text-sm md:text-sm text-(--color-gray-600) leading- tracking-wide mb-4">
{description}
</p>
)
}
<!-- Image Wrapper -->
<div class="relative w-full aspect-video overflow-hidden">
<div class="relative w-full rounded-lg aspect-video mb-4 overflow-hidden">
<img
src={imageUrl}
alt={title}
@@ -49,24 +68,14 @@ const linkRel = hasExternalLink ? 'noopener noreferrer' : ''
/>
</div>
<!-- Content -->
<div class="p-6 md:p-5">
<!-- Title -->
<h3 class="font-['Noto_Sans_TC'] text-xl md:text-lg font-semibold text-[var(--color-tarawera)] mb-2">{title}</h3>
<!-- Description -->
{
description && (
<p class="font-['Noto_Sans_TC'] text-sm md:text-[0.8125rem] text-[var(--color-gray-600)] leading-normal mb-4">{description}</p>
)
}
<!-- Tags -->
{
tags.length > 0 && (
<div class="flex flex-wrap gap-2">
{tags.map((tag) => (
<span class="px-2.5 py-1 bg-[var(--color-gray-100)] rounded-full text-xs font-medium text-[var(--color-gray-700)]">{tag}</span>
<span class="px-2.5 py-1 bg-(--color-accent-dark)/5 border-1 border-(--color-accent-dark)/20 rounded-full text-xs font-base text-(--color-accent-dark)">
{tag}
</span>
))}
</div>
)

View File

@@ -5,22 +5,39 @@
*/
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;
---
<section class:list={classNameBg}>
<div class:list={['flex max-w-3xl items-center justify-center gap-4 py-12 mx-auto max-lg:flex-wrap', className]}>
<div
class:list={[
"flex flex-rows sm:max-w-lg px-8 md:px-0 md:max-w-xl lg:max-w-3xl items-center justify-center gap-4 py-12 mx-auto",
className,
]}
>
<div class="w-full h-px bg-(--color-enchunblue)"></div>
<div class="text-center">
<h2 class="text-(--color-dark-blue) font-['Noto_Sans_TC'] font-bold text-3xl max-md:text-xl whitespace-nowrap">{title}</h2>
<p class="text-gray-600) font-['Quicksand'] font-thin text-2xl tracking-wider whitespace-nowrap">{subtitle}</p>
<h2
class="text-(--color-dark-blue) font-['Noto_Sans_TC'] font-bold text-xl md:text-3xl whitespace-nowrap"
>
{title}
</h2>
<p
class="text-gray-600) font-['Quicksand'] font-thin text-lg md:text-2xl tracking-wider whitespace-nowrap"
>
{subtitle}
</p>
</div>
<div class="w-full h-px bg-(--color-enchunblue)"></div>
</div>

View File

@@ -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年提供全方位數位行銷服務。我們在地化優先、數據驅動是您最可信赖的數位行銷夥伴。";
---
<Layout title={title} description={description}>
<!-- Hero Section -->
<AboutHero />
<AboutHero
title="關於恩群數位"
subtitle="About Enchun digital"
backgroundImage={{
url: "https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b2535942cd4_IMG_9090%201.jpg",
alt: "關於恩群數位的背景圖",
}}
/>
<!-- Section Header -->
<SectionHeader
title="關於恩群"
subtitle="About Enchun"
sectionBg="bg-white"
/>
<!-- Service Features Section -->
<FeatureSection />
@@ -25,14 +41,5 @@ const description = '恩群數位行銷成立於2018年提供全方位數位
<ComparisonSection />
<!-- CTA Section -->
<CTASection
homeData={{
ctaSection: {
headline: '準備好開始新的旅程了嗎',
description: '讓我們一起為您的品牌打造獨特的數位行銷策略',
buttonText: '聯絡我們',
buttonLink: '/contact-us',
},
}}
/>
<CtaSection />
</Layout>

View File

@@ -12,102 +12,102 @@ const description = '有任何問題嗎?歡迎聯絡恩群數位行銷,我
---
<Layout title={title} description={description}>
<section class="contact-section" id="contact">
<div class="contactus_wrapper">
<section class="py-16 bg-background scroll-mt-20 lg:py-8 md:py-8" id="contact">
<div class="grid grid-cols-2 gap-12 items-center max-w-[1200px] mx-auto px-5 lg:grid-cols-1 lg:gap-8">
<!-- Contact Form Side -->
<div class="contact_form_wrapper">
<h1 class="contact_head">聯絡我們</h1>
<p class="contact_parafraph">
<div class="w-full">
<h1 class="text-[2.5rem] font-bold leading-tight text-text-primary mb-4 lg:text-2xl md:text-[1.5rem]">聯絡我們</h1>
<p class="text-lg font-normal leading-relaxed text-slate-600 mb-2">
有任何問題嗎?歡迎聯絡我們,我們將竭誠為您服務。
</p>
<p class="contact_reminder">
<p class="text-sm italic text-text-muted mb-8">
* 標註欄位為必填
</p>
<!-- Contact Form -->
<form id="contact-form" class="contact_form" novalidate>
<form id="contact-form" class="p-8 bg-surface rounded-lg shadow-lg lg:p-6 md:p-4" novalidate>
<!-- Success Message -->
<div id="form-success" class="w-form-done" style="display: none;">
<div id="form-success" class="p-4 px-6 rounded-md mt-4 text-center font-medium bg-[#d4edda] text-[#155724] border border-[#c3e6cb] animate-[slideUp_0.3s_ease-out]" style="display: none;">
感謝您的留言!我們會盡快回覆您。
</div>
<!-- Error Message -->
<div id="form-error" class="w-form-fail" style="display: none;">
<div id="form-error" class="p-4 px-6 rounded-md mt-4 text-center font-medium bg-[#f8d7da] text-[#721c24] border border-[#f5c6cb] animate-[slideUp_0.3s_ease-out]" style="display: none;">
送出失敗,請稍後再試或直接來電。
</div>
<!-- Form Fields -->
<div class="contact-form-grid">
<div class="grid grid-cols-2 gap-6 mb-6 md:grid-cols-1 md:gap-4">
<!-- Name Field -->
<div class="contact_field_wrapper">
<label for="Name" class="contact_field_name">
姓名 <span>*</span>
<div class="flex flex-col mb-6">
<label for="Name" class="text-sm font-semibold text-text-primary mb-2 block">
姓名 <span class="text-primary">*</span>
</label>
<input
type="text"
id="Name"
name="Name"
class="input_field"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]"
required
minlength="2"
maxlength="256"
placeholder="請輸入您的姓名"
/>
<span class="error-message" id="Name-error"></span>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Name-error"></span>
</div>
<!-- Phone Field -->
<div class="contact_field_wrapper">
<label for="Phone" class="contact_field_name">
聯絡電話 <span>*</span>
<div class="flex flex-col mb-6">
<label for="Phone" class="text-sm font-semibold text-text-primary mb-2 block">
聯絡電話 <span class="text-primary">*</span>
</label>
<input
type="tel"
id="Phone"
name="Phone"
class="input_field"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]"
required
placeholder="請輸入您的電話號碼"
/>
<span class="error-message" id="Phone-error"></span>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Phone-error"></span>
</div>
</div>
<!-- Email Field -->
<div class="contact_field_wrapper">
<label for="Email" class="contact_field_name">
Email <span>*</span>
<div class="flex flex-col mb-6">
<label for="Email" class="text-sm font-semibold text-text-primary mb-2 block">
Email <span class="text-primary">*</span>
</label>
<input
type="email"
id="Email"
name="Email"
class="input_field"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]"
required
placeholder="請輸入您的 Email"
/>
<span class="error-message" id="Email-error"></span>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Email-error"></span>
</div>
<!-- Message Field -->
<div class="contact_field_wrapper">
<label for="Message" class="contact_field_name">
聯絡訊息 <span>*</span>
<div class="flex flex-col mb-8">
<label for="Message" class="text-sm font-semibold text-text-primary mb-2 block">
聯絡訊息 <span class="text-primary">*</span>
</label>
<textarea
id="Message"
name="Message"
class="input_field"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted min-h-[120px] resize-y [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]"
minlength="10"
maxlength="5000"
required
placeholder="請輸入您的訊息(至少 10 個字元)"
></textarea>
<span class="error-message" id="Message-error"></span>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Message-error"></span>
</div>
<!-- Submit Button -->
<button type="submit" class="submit-button" id="submit-btn">
<button type="submit" class="bg-primary text-white border-none rounded-md px-8 py-[0.875rem] text-base font-semibold cursor-pointer transition-all duration-150 text-center inline-flex items-center justify-center min-w-[200px] w-full hover:bg-primary-hover hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 disabled:bg-slate-500 disabled:cursor-not-allowed disabled:opacity-80" id="submit-btn">
<span class="button-text">送出訊息</span>
<span class="button-loading" style="display: none;">送出中...</span>
</button>
@@ -115,26 +115,27 @@ const description = '有任何問題嗎?歡迎聯絡恩群數位行銷,我
</div>
<!-- Contact Image Side -->
<div class="contact-image">
<div class="image-wrapper">
<div class="flex flex-col gap-8 justify-center items-center">
<div class="w-full rounded-lg overflow-hidden shadow-md bg-slate-100 aspect-[3/2]">
<img
src="/placeholder-contact.jpg"
alt="聯絡恩群數位"
width="600"
height="400"
class="w-full h-full object-cover"
/>
</div>
<!-- Contact Info Card -->
<div class="contact-info-card">
<h3>聯絡資訊</h3>
<div class="info-item">
<svg class="info-icon" viewBox="0 0 24 24"><path fill="currentColor" d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47-1.14.75-1.02zM2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02L2.05 21.05zM19.95 2.95c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36.11-.74-.47-1.14-.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53z"/></svg>
<div class="w-full p-6 bg-white rounded-lg shadow-lg">
<h3 class="text-xl font-semibold text-text-primary mb-4">聯絡資訊</h3>
<div class="flex items-center gap-3 mb-4 text-[0.95rem] text-text-secondary">
<svg class="w-5 h-5 flex-shrink-0 text-primary" viewBox="0 0 24 24"><path fill="currentColor" d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47-1.14.75-1.02L2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02L2.05 21.05zM19.95 2.95c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36.11.74-.47-1.14-.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.02.75 1.02L2.05 21.05c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36-.11-.74.47-1.14.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11-.74.47 1.14.75 1.02L2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02z"/></svg>
<span>諮詢電話: 02 5570 0527</span>
</div>
<div class="info-item">
<svg class="info-icon" viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
<span>Email: <a href="mailto:enchuntaiwan@gmail.com">enchuntaiwan@gmail.com</a></span>
<div class="flex items-center gap-3 mb-4 text-[0.95rem] text-text-secondary">
<svg class="w-5 h-5 flex-shrink-0 text-primary" viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
<span>Email: <a href="mailto:enchuntaiwan@gmail.com" class="text-primary no-underline hover:underline">enchuntaiwan@gmail.com</a></span>
</div>
</div>
</div>
@@ -142,315 +143,6 @@ const description = '有任何問題嗎?歡迎聯絡恩群數位行銷,我
</section>
</Layout>
<style>
/* Contact Section Styles - Pixel-perfect from Webflow */
.contact-section {
padding: 4rem 0;
background: var(--color-background);
scroll-margin-top: 80px;
}
.contactus_wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Form Side */
.contact_form_wrapper {
width: 100%;
}
/* Headings */
.contact_head {
font-size: 2.5rem;
font-weight: 700;
line-height: 1.2;
color: var(--color-text-primary);
margin-bottom: 1rem;
}
.contact_parafraph {
font-size: 1.125rem;
font-weight: 400;
line-height: 1.6;
color: var(--color-gray-600);
margin-bottom: 0.5rem;
}
.contact_reminder {
font-size: 0.875rem;
font-style: italic;
color: var(--color-text-muted);
margin-bottom: 2rem;
}
/* Form Container */
.contact_form {
padding: 2rem;
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
/* Form Grid */
.contact-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
/* Field Wrapper */
.contact_field_wrapper {
display: flex;
flex-direction: column;
margin-bottom: 1.5rem;
}
.contact_field_wrapper:last-child {
margin-bottom: 2rem;
}
/* Field Label */
.contact_field_name {
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 0.5rem;
display: block;
}
.contact_field_name span {
color: var(--color-primary);
}
/* Input Fields */
.input_field {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-size: 1rem;
line-height: 1.5;
background: var(--color-background);
transition: all var(--transition-fast);
font-family: inherit;
color: var(--color-text-primary);
}
.input_field:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(56, 152, 236, 0.1);
transform: translateY(-1px);
}
.input_field::placeholder {
color: var(--color-text-muted);
}
.input_field.error {
border-color: #dc3545 !important;
background-color: #fff5f5;
}
#Message {
min-height: 120px;
resize: vertical;
}
/* Error Message */
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
display: none;
}
.error-message:not(:empty) {
display: block;
}
/* Submit Button */
.submit-button {
background: var(--color-primary);
color: white;
border: none;
border-radius: var(--radius);
padding: 0.875rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 200px;
width: 100%;
}
.submit-button:hover:not(:disabled) {
background: var(--color-primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.submit-button:active:not(:disabled) {
transform: translateY(0);
}
.submit-button:disabled {
background: var(--color-gray-500);
cursor: not-allowed;
opacity: 0.8;
}
/* Form Success/Error Messages */
.w-form-done,
.w-form-fail {
padding: 1rem 1.5rem;
border-radius: var(--radius);
margin-top: 1rem;
text-align: center;
font-weight: 500;
animation: slideUp 0.3s ease-out;
}
.w-form-done {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.w-form-fail {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Image Side */
.contact-image {
display: flex;
flex-direction: column;
gap: 2rem;
justify-content: center;
align-items: center;
}
.image-wrapper {
width: 100%;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-md);
background: var(--color-gray-100);
aspect-ratio: 3/2;
}
.image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Contact Info Card */
.contact-info-card {
width: 100%;
padding: 1.5rem;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.contact-info-card h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 1rem;
}
.info-item {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
font-size: 0.95rem;
color: var(--color-text-secondary);
}
.info-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
color: var(--color-primary);
}
.info-item a {
color: var(--color-primary);
text-decoration: none;
}
.info-item a:hover {
text-decoration: underline;
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.contactus_wrapper {
grid-template-columns: 1fr;
gap: 2rem;
}
.contact_head {
font-size: 2rem;
}
}
@media (max-width: 767px) {
.contact-section {
padding: 2rem 0;
}
.contact_form {
padding: 1.5rem;
}
.contact-form-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.submit-button {
min-width: 160px;
}
}
@media (max-width: 479px) {
.contact_form {
padding: 1rem;
}
.contact_head {
font-size: 1.5rem;
}
}
</style>
<script>
// Form validation and submission handler

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>

View File

@@ -4,20 +4,28 @@
* 展示工作環境、公司故事和員工福利
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../layouts/Layout.astro'
import TeamsHero from '../sections/TeamsHero.astro'
import EnvironmentSlider from '../sections/EnvironmentSlider.astro'
import CompanyStory from '../sections/CompanyStory.astro'
import BenefitsSection from '../sections/BenefitsSection.astro'
import Layout from "../layouts/Layout.astro";
import TeamsHero from "../sections/TeamsHero.astro";
import EnvironmentSlider from "../sections/EnvironmentSlider.astro";
import CompanyStory from "../sections/CompanyStory.astro";
import BenefitsSection from "../sections/BenefitsSection.astro";
// Metadata for SEO
const title = '恩群大本營 | 恩群數位行銷'
const description = '加入恩群數位的團隊,享受優質的工作環境與完善的員工福利。我們重視個人的特質發揮,歡迎樂於學習、善於建立關係的你加入我們。'
const title = "恩群大本營 | 恩群數位行銷";
const description =
"加入恩群數位的團隊,享受優質的工作環境與完善的員工福利。我們重視個人的特質發揮,歡迎樂於學習、善於建立關係的你加入我們。";
---
<Layout title={title} description={description}>
<!-- Hero Section -->
<TeamsHero />
<TeamsHero
title="恩群大本營"
subtitle="Team members of Enchun"
backgroundImage={{
url: "https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b3141942d00_IMG_9088%20copy.JPG",
alt: "Teams Hero",
}}
/>
<!-- Environment Slider Section -->
<EnvironmentSlider />
@@ -29,15 +37,25 @@ const description = '加入恩群數位的團隊,享受優質的工作環境
<BenefitsSection />
<!-- CTA Section -->
<section class="section-call4action" aria-labelledby="cta-heading">
<div class="container w-container">
<div class="c4a-grid">
<div class="c4a-content">
<h3 id="cta-heading" class="career-c4a-heading">
<section
class="py-20 px-5 bg-slate-100 text-center lg:py-[60px] lg:px-4 md:py-10"
aria-labelledby="cta-heading"
>
<div class="max-w-[1200px] mx-auto">
<div
class="grid grid-cols-[1fr_auto] gap-8 items-center relative lg:grid-cols-1 lg:text-center"
>
<div class="text-left lg:text-center">
<h3
id="cta-heading"
class="text-[1.75rem] font-semibold text-[#23608c] mb-4 leading-snug lg:text-[1.5rem] md:text-[1.5rem]"
>
以人的成長為優先<br />
創造人的最大價值
</h3>
<p class="career-c4a-paragraph">
<p
class="text-base text-slate-600 leading-relaxed max-w-[500px] lg:max-w-full md:text-[0.95rem]"
>
在恩群數位裡我們重視個人的特質能夠完全發揮,只要你樂於學習、善於跟人建立關係,並且重要的是你有一個善良的心,恩群數位歡迎你的加入
</p>
</div>
@@ -45,137 +63,15 @@ const description = '加入恩群數位的團隊,享受優質的工作環境
href="https://www.104.com.tw/company/1a2x6bkoaj?jobsource=joblist_r_cust"
target="_blank"
rel="noopener noreferrer"
class="c4a-button"
class="inline-flex items-center justify-center bg-link-hover text-white px-8 py-4 rounded-md font-semibold text-base transition-all duration-200 whitespace-nowrap hover:-translate-y-0.5 hover:shadow-md hover:bg-[#1a4d6e] lg:w-full lg:max-w-[300px] md:py-[14px] md:px-6 md:text-[0.95rem]"
>
立刻申請面試
</a>
<div class="c4a-bg"></div>
<div
class="absolute inset-0 bg-gradient-to-br from-[rgba(35,96,140,0.05)] to-[rgba(35,96,140,0.02)] rounded-lg -z-10 lg:hidden"
>
</div>
</div>
</div>
</section>
</Layout>
<style>
/* CTA Section Styles - Pixel-perfect from Webflow */
.section-call4action {
padding: 80px 20px;
text-align: center;
background-color: var(--color-gray-100, #f8f9fa);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.w-container {
max-width: 1200px;
margin: 0 auto;
}
.c4a-grid {
display: grid;
grid-template-columns: 1fr auto;
gap: 32px;
align-items: center;
position: relative;
}
.c4a-content {
text-align: left;
}
.career-c4a-heading {
font-family: "Noto Sans TC", sans-serif;
font-weight: 600;
font-size: 1.75rem;
color: var(--color-tarawera, #23608c);
margin-bottom: 16px;
line-height: 1.4;
}
.career-c4a-paragraph {
font-family: "Noto Sans TC", sans-serif;
font-weight: 400;
font-size: 1rem;
color: var(--color-gray-600);
line-height: 1.6;
max-width: 500px;
}
.c4a-button {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--color-enchunblue);
color: white;
padding: 16px 32px;
border-radius: var(--radius, 8px);
font-weight: 600;
font-size: 1rem;
text-decoration: none;
transition: all var(--transition-base, 0.3s ease);
white-space: nowrap;
}
.c4a-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
background-color: var(--color-enchunblue-hover, #1a4d6e);
}
.c4a-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(35, 96, 140, 0.05) 0%, rgba(35, 96, 140, 0.02) 100%);
border-radius: var(--radius-lg, 12px);
z-index: -1;
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.section-call4action {
padding: 60px 16px;
}
.c4a-grid {
grid-template-columns: 1fr;
text-align: center;
}
.c4a-content {
text-align: center;
}
.career-c4a-paragraph {
max-width: 100%;
}
.c4a-button {
width: 100%;
max-width: 300px;
}
}
@media (max-width: 767px) {
.section-call4action {
padding: 40px 16px;
}
.career-c4a-heading {
font-size: 1.5rem;
}
.career-c4a-paragraph {
font-size: 0.95rem;
}
.c4a-button {
padding: 14px 24px;
font-size: 0.95rem;
}
}
</style>

View File

@@ -3,223 +3,78 @@
* Portfolio Listing Page - 案例分享列表頁
* Pixel-perfect implementation based on Webflow design
*/
import Layout from '../layouts/Layout.astro'
import PortfolioCard from '../components/PortfolioCard.astro'
import Layout from "../layouts/Layout.astro";
import PortfolioCard from "../components/PortfolioCard.astro";
import SectionHeader from "../components/SectionHeader.astro";
import HeaderBg from "../components/HeaderBg.astro";
import CtaSection from "../components/CtaSection.astro";
// Metadata for SEO
const title = '案例分享 | 恩群數位行銷'
const description = '瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。'
const title = "案例分享 | 恩群數位行銷";
const description =
"瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。";
// Portfolio items - can be fetched from Payload CMS
const portfolioItems = [
{
slug: 'corporate-website-1',
title: '企業官網設計案例',
description: '為知名製造業打造的現代化企業官網,整合產品展示與新聞發佈功能。',
image: '/placeholder-portfolio-1.jpg',
tags: ['企業官網', '響應式設計'],
slug: "corporate-website-1",
title: "Saas 一頁式頁面",
description:
"現代SaaS一頁式網站吸引目標客戶並提升轉化率而設計。採用React、Tailwind CSS和TypeScript等先進技術構建響應式流暢導航、視覺效果和性能。非常適合初創公司和科技新創",
image:
"https://enchun-cms.anlstudio.cc/api/media/file/67a99c71f1be7be0b6fb5b6b_saas_website_design-p-1080.webp",
tags: ["SaaS", "一頁式頁面"],
},
{
slug: 'ecommerce-site-1',
title: '電商平台建置',
description: 'B2C 電商網站,包含會員系統、購物車、金流整合等完整功能。',
image: '/placeholder-portfolio-2.jpg',
tags: ['電商網站', '金流整合'],
slug: "ecommerce-site-1",
title: "BioNova銷售頁",
description:
"本網站以簡潔、專業、易於操作的介面,向受眾傳達 BioNova 超能複方菁萃膠囊的產品特色、優勢及功效,並鼓勵消費者立即購買。",
image:
"https://enchun-cms.anlstudio.cc/api/media/file/67a9a0aa4ef074c15c2b4409_landingpage_website_design-p-1080.webp",
tags: ["電商網站", "金流整合"],
},
{
slug: 'brand-website-1',
title: '品牌形象網站',
description: '以視覺故事為核心的品牌網站,展現品牌獨特價值與理念。',
image: '/placeholder-portfolio-3.jpg',
tags: ['品牌網站', '視覺設計'],
slug: "brand-website-1",
title: "美髮業預約頁面",
description:
"美髮官網線上預約系統,透過便捷高效的線上預約系統已成為吸引顧客、提升服務品質的關鍵。一個專業、時尚、易於操作的線上平台,讓顧客隨時隨地輕鬆預約美髮服務,同時展示美髮店的專業形象、服務特色及最新資訊。",
image:
"https://enchun-cms.anlstudio.cc/api/media/file/67aacde54fbce6ce004c18d2_booking_website_design_alt-p-1080.webp",
tags: ["品牌網站", "視覺設計"],
},
{
slug: 'landing-page-1',
title: '活動行銷頁面',
description: '高轉換率的活動頁面設計,有效的 CTA 配置與使用者體驗規劃。',
image: '/placeholder-portfolio-4.jpg',
tags: ['活動頁面', '行銷'],
slug: "landing-page-1",
title: "物流業官方網站",
description:
"專業、高效的物流網站已成為必然趨勢,透過設計出操作便捷的物流網站,以滿足現代物流業務的需求。提供線上客服功能,解答客戶疑問提供幫助。",
image:
"https://enchun-cms.anlstudio.cc/api/media/file/67a9acb0737c25d4ba894081_freight_website_design-p-1080.webp",
tags: ["活動頁面", "行銷"],
},
]
];
---
<Layout title={title} description={description}>
<!-- Portfolio Header -->
<section class="portfolio-header" aria-labelledby="portfolio-heading">
<div class="container">
<h1 id="portfolio-heading" class="portfolio-title">案例分享</h1>
<p class="portfolio-subtitle">Selected Works</p>
<div class="divider-line"></div>
<div class="divider-line"></div>
</div>
</section>
<HeaderBg />
<SectionHeader
title="案例分享"
subtitle="Selected Works"
sectionBg="bg-white"
/>
<!-- Portfolio Grid -->
<section class="portfolio-grid-section" aria-label="作品列表">
<ul class="portfolio-grid">
{
portfolioItems.map((item) => (
<PortfolioCard item={item} />
))
}
<section
class="py-0 px-5 pb-20 bg-white lg:px-4 lg:pb-10"
aria-label="作品列表"
>
<ul
class="grid grid-cols-2 gap-5 max-w-3xl mx-auto p-0 list-none lg:gap-8 md:grid-cols-2 md:gap-6"
>
{portfolioItems.map((item) => <PortfolioCard item={item} />)}
</ul>
</section>
<!-- CTA Section -->
<section class="portfolio-cta" aria-labelledby="cta-heading">
<div class="container">
<h2 id="cta-heading" class="cta-title">
有興趣與我們合作嗎?
</h2>
<p class="cta-description">
讓我們一起為您的品牌打造獨特的數位體驗
</p>
<a href="/contact-us" class="cta-button">
聯絡我們
</a>
</div>
</section>
<CtaSection />
</Layout>
<style>
/* Portfolio Page Styles - Pixel-perfect from Webflow */
/* Header Section */
.portfolio-header {
text-align: center;
padding: 60px 20px;
background-color: #ffffff;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.portfolio-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 2.5rem;
font-weight: 700;
color: var(--color-enchunblue);
margin-bottom: 8px;
}
.portfolio-subtitle {
font-family: "Quicksand", "Noto Sans TC", sans-serif;
font-size: 1.125rem;
color: var(--color-gray-700);
margin-bottom: 24px;
}
.divider-line {
width: 100px;
height: 2px;
background-color: var(--color-enchunblue);
margin: 0 auto;
}
.divider-line:last-child {
width: 60px;
margin-top: 4px;
}
/* Grid Section */
.portfolio-grid-section {
padding: 0 20px 60px;
background-color: #f8f9fa;
}
.portfolio-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
max-width: 1200px;
margin: 0 auto;
padding: 0;
list-style: none;
}
/* CTA Section */
.portfolio-cta {
text-align: center;
padding: 80px 20px;
background-color: #ffffff;
}
.cta-title {
font-family: "Noto Sans TC", sans-serif;
font-size: 1.75rem;
font-weight: 600;
color: var(--color-enchunblue);
margin-bottom: 16px;
}
.cta-description {
font-size: 1rem;
color: var(--color-gray-600);
margin-bottom: 32px;
}
.cta-button {
display: inline-block;
background-color: var(--color-enchunblue);
color: white;
padding: 16px 32px;
border-radius: var(--radius, 8px);
font-weight: 600;
text-decoration: none;
transition: all var(--transition-base, 0.3s ease);
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
background-color: var(--color-enchunblue-hover, #1a4d6e);
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.portfolio-header {
padding: 50px 20px;
}
.portfolio-title {
font-size: 2rem;
}
.portfolio-grid {
gap: 16px;
}
}
@media (max-width: 767px) {
.portfolio-header {
padding: 40px 16px;
}
.portfolio-title {
font-size: 1.75rem;
}
.portfolio-subtitle {
font-size: 1rem;
}
.portfolio-grid-section {
padding: 0 16px 40px;
}
.portfolio-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.portfolio-cta {
padding: 60px 16px;
}
.cta-title {
font-size: 1.5rem;
}
}
</style>

View File

@@ -4,21 +4,73 @@
* Pixel-perfect implementation based on Webflow design
*/
interface Props {
title?: string
subtitle?: string
title?: string;
subtitle?: string;
backgroundImage?: {
url?: string;
alt?: string;
};
}
const {
title = '關於恩群數位',
subtitle = 'About Enchun digital',
} = Astro.props
title = "關於恩群數位",
subtitle = "About Enchun digital",
backgroundImage,
} = Astro.props;
// Determine if we have a background image
const hasBackgroundImage = backgroundImage?.url;
const bgImageUrl = backgroundImage?.url || "";
---
<header class="bg-white py-[120px] px-5 lg:py-20 md:py-15 text-center">
<div class="max-w-[1200px] mx-auto">
<div class="flex flex-col items-center">
<h1 class="text-[var(--color-enchunblue)] font-['Noto_Sans_TC','Quicksand'] font-bold text-3xl lg:text-[2.5rem] md:text-2xl leading-tight mb-4">{title}</h1>
<p class="text-[var(--color-enchunblue-dark)] font-['Quicksand','Noto_Sans_TC'] font-normal text-xl lg:text-[1.125rem] md:text-base leading-relaxed">{subtitle}</p>
<section
class:list={[
// Base styles - relative positioning for overlay
"relative",
"flex",
"items-center",
"justify-center",
"overflow-hidden",
"text-center",
"px-5",
// Background color fallback
!hasBackgroundImage && "bg-(--color-dark-blue)",
// Background image styles
hasBackgroundImage && "bg-cover bg-center bg-no-repeat",
// Pull up to counteract layout's pt-20 padding (80px)
"-mt-20",
// Full viewport height
"min-h-dvh",
"z-0",
]}
style={hasBackgroundImage
? `background-image: url('${bgImageUrl}')`
: undefined}
>
{/* Background image overlay for text readability */}
{
hasBackgroundImage && (
<div
class="absolute inset-0 bg-linear-to-b from-black/80 to-transparent z-1"
aria-hidden="true"
/>
)
}
{/* Content container - relative z-index above overlay */}
<div class="relative z-2 max-w-6xl mx-auto">
<!-- Main Title -->
<h1
class="text-white text-shadow-md font-['Noto_Sans_TC',sans-serif]! font-bold leading-[1.2] -mb-1 text-6xl! md:text-5xl"
>
{title}
</h1>
<!-- Subtitle -->
<p
class="text-gray-100 text-shadow-md font-['Quicksand'] font-thin leading-[1.2] max-w-3xl mx-auto text-3xl! md:text-2xl"
>
{subtitle}
</p>
</div>
</div>
</header>
</section>

View File

@@ -16,187 +16,63 @@ const {
// Four features data
const features = [
{
icon: 'location_on',
img: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528bec64942cd5_address-Bro-%E5%9C%A8%E5%9C%B0%E5%8C%96%E5%84%AA%E5%85%88%201.svg',
title: '在地化優先',
description: '上線下結合曝光渠道,整合多方資訊,帶給消費者最佳的使用體驗,展現商家的獨特之處,順利的將潛在使用者帶到你的實際門市。',
description: '上線下結合曝光渠道,整合多方資訊,帶給消費者最佳的使用體驗,展現商家的獨特之處,順利的將潛在使用者帶到你的實際門市。',
},
{
icon: 'account_balance',
img: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b582e942cd6_Banknote-bro-%E9%AB%98%E6%8A%95%E8%B3%87%E5%A0%B1%E9%85%AC%E7%8E%87%201.svg',
title: '高投資轉換率',
description: '你覺得網路行銷很貴嗎?恩群數位善用每一分廣告預算,讓你在網路上發揮最大效益,幫助店家鎖定精準客群,達成目標。',
},
{
icon: 'analytics',
img: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b517f942cd8_Social%20Dashboard-bro-%E6%95%B8%E6%93%9A%E5%84%AA%E5%85%88%201.svg',
title: '數據優先',
description: '想要精準行銷?恩群數位從數據中萃取洞察,根據數據分析廣告成效,更聰明、有策略的幫您省下行銷預算。',
},
{
icon: 'handshake',
img: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b26a1942cd7_Partnership-bro-%E9%97%9C%E4%BF%82%E5%84%AA%E5%85%88%201.svg',
title: '關係優於銷售',
description: '除了幫您拓展網路上的知名度,我們更是每家公司最專業的數位夥伴,你會知道有恩群的存在,事業路上你並不孤單。',
},
]
---
<section class="section_feature" aria-labelledby="feature-heading">
<div class="w-container">
<section class="section_feature bg-white px-4 py-[60px] md:px-5 md:py-[80px]" aria-labelledby="feature-heading">
<div class="w-container mx-auto max-w-4xl">
<!-- Section Header -->
<div class="section_header_w_line">
<h2 id="feature-heading" class="header_subtitle_head">
<div class="section_header_w_line mb-[60px] text-center">
<h2 id="feature-heading" class="header_subtitle_head my-4 text-[1.75rem] font-bold leading-[1.2] text-[var(--color-enchunblue)] md:text-[2.25rem]">
{title}
</h2>
<p class="header_subtitle_paragraph">
<p class="header_subtitle_paragraph mt-2 text-base font-normal text-[#666666]">
{subtitle}
</p>
<div class="divider_line"></div>
<div class="divider_line mx-auto h-[2px] w-[100px] bg-[var(--color-enchunblue)]"></div>
</div>
<!-- Features Grid -->
<div class="feature_grid">
<div class="grid grid-cols-1 gap-1 md:grid-cols-2 md:gap-2 lg:gap-6">
{
features.map((feature) => (
<div class="feature_card">
<div class="feature_card bg-white p-5 transition-all duration-[var(--transition-base)] hover:-translate-y-1 md:p-6 lg:p-8">
<div class="grid grid-cols-[120px_1fr] items-start gap-4 md:grid-cols-[110px_1fr] lg:grid-cols-[190px_1fr] lg:gap-6">
<!-- Icon -->
<div class="feature_image">
<span class="material-icon">{feature.icon}</span>
<div class="flex w-full items-center justify-center">
<img src={feature.img} alt={feature.title} class="h-auto w-full object-contain" />
</div>
<div class="flex flex-col gap-2">
<!-- Title -->
<h3 class="feature_head">{feature.title}</h3>
<h3 class="font-['Noto_Sans_TC'] lg:text-3xl text-2xl font-semibold text-(--color-dark-blue)">{feature.title}</h3>
<!-- Description -->
<p class="feature_description">{feature.description}</p>
<p class="font-['Noto_Sans_TC'] lg:text-base text-sm font-thin leading-[1.6] text-grey-2">{feature.description}</p>
</div>
</div>
</div>
))
}
</div>
</div>
</section>
<style>
/* Feature Section Styles - Pixel-perfect from Webflow */
.section_feature {
background-color: #ffffff;
padding: 80px 20px;
}
.w-container {
max-width: 1200px;
margin: 0 auto;
}
/* Section Header */
.section_header_w_line {
text-align: center;
margin-bottom: 60px;
}
.header_subtitle_head {
color: var(--color-enchunblue);
font-weight: 700;
font-size: 2.25rem;
line-height: 1.2;
margin: 16px 0;
}
.header_subtitle_paragraph {
color: #666666;
font-weight: 400;
font-size: 1rem;
margin-top: 8px;
}
.divider_line {
background-color: var(--color-enchunblue);
height: 2px;
width: 100px;
margin: 0 auto;
}
/* Features Grid */
.feature_grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
}
/* Feature Card */
.feature_card {
background: #ffffff;
border-radius: var(--radius-lg);
padding: 32px;
transition: all var(--transition-base);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.feature_card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
/* Icon */
.feature_image {
width: 80px;
height: 80px;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.material-icon {
font-size: 64px;
color: var(--color-enchunblue);
}
/* Title */
.feature_head {
color: #333333;
font-weight: 600;
font-size: 1.25rem;
margin-bottom: 12px;
}
/* Description */
.feature_description {
color: #666666;
font-weight: 400;
font-size: 1rem;
line-height: 1.6;
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.feature_grid {
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.feature_card {
padding: 24px;
}
}
@media (max-width: 767px) {
.section_feature {
padding: 60px 16px;
}
.feature_grid {
grid-template-columns: 1fr;
gap: 16px;
}
.header_subtitle_head {
font-size: 1.75rem;
}
.feature_card {
padding: 20px;
}
.material-icon {
font-size: 48px;
}
}
</style>

View File

@@ -4,21 +4,73 @@
* Pixel-perfect implementation based on Webflow design
*/
interface Props {
title?: string
subtitle?: string
title?: string;
subtitle?: string;
backgroundImage?: {
url?: string;
alt?: string;
};
}
const {
title = '恩群大本營',
subtitle = 'Team members of Enchun',
} = Astro.props
title = "恩群大本營",
subtitle = "Team members of Enchun",
backgroundImage,
} = Astro.props;
// Determine if we have a background image
const hasBackgroundImage = backgroundImage?.url;
const bgImageUrl = backgroundImage?.url || "";
---
<header class="bg-[var(--color-dark-blue)] pt-[120px] pb-20 px-5 lg:pt-20 lg:pb-15 md:pt-15 md:pb-10 md:px-4 text-center">
<div class="max-w-[1200px] mx-auto">
<div class="flex flex-col items-center">
<h1 class="text-white font-['Noto_Sans_TC'] font-bold text-[3.39em] lg:text-[2.45em] md:text-[7vw] leading-tight mb-4">{title}</h1>
<p class="text-[var(--color-gray-100)] font-['Quicksand','Noto_Sans_TC'] font-normal text-[1.5em] lg:text-[1.15em] md:text-[3.4vw] leading-tight">{subtitle}</p>
<section
class:list={[
// Base styles - relative positioning for overlay
"relative",
"flex",
"items-center",
"justify-center",
"overflow-hidden",
"text-center",
"px-5",
// Background color fallback
!hasBackgroundImage && "bg-(--color-dark-blue)",
// Background image styles
hasBackgroundImage && "bg-size-[120vw] bg-center bg-no-repeat",
// Pull up to counteract layout's pt-20 padding (80px)
"-mt-20",
// Full viewport height
"min-h-dvh",
"z-0",
]}
style={hasBackgroundImage
? `background-image: url('${bgImageUrl}')`
: undefined}
>
{/* Background image overlay for text readability */}
{
hasBackgroundImage && (
<div
class="absolute inset-0 bg-linear-to-b from-black/80 to-transparent z-1"
aria-hidden="true"
/>
)
}
{/* Content container - relative z-index above overlay */}
<div class="relative z-2 max-w-6xl mx-auto">
<!-- Main Title -->
<h1
class="text-white text-shadow-md font-['Noto_Sans_TC',sans-serif]! font-bold leading-[1.2] -mb-1 text-6xl! md:text-5xl"
>
{title}
</h1>
<!-- Subtitle -->
<p
class="text-gray-100 text-shadow-md font-['Quicksand'] font-thin leading-[1.2] max-w-3xl mx-auto text-3xl! md:text-2xl"
>
{subtitle}
</p>
</div>
</div>
</header>
</section>

View File

@@ -16,84 +16,140 @@
============================================ */
/* Primary Colors (主要色) */
--color-primary: #3898ec; /* Primary blue */
--color-primary-dark: #0082f3; /* Deep blue */
--color-primary-light: #67aee1; /* Light blue */
--color-primary-hover: #2895f7; /* Hover blue */
--color-primary: #3898ec;
/* Primary blue */
--color-primary-dark: #0082f3;
/* Deep blue */
--color-primary-light: #67aee1;
/* Light blue */
--color-primary-hover: #2895f7;
/* Hover blue */
/* Secondary Colors (次要色) */
--color-secondary: #f39c12; /* Secondary orange */
--color-secondary-light: #f6c456; /* Light pink */
--color-secondary-dark: #d84038; /* Deep orange */
--color-secondary: #f39c12;
/* Secondary orange */
--color-secondary-light: #f6c456;
/* Light pink */
--color-secondary-dark: #d84038;
/* Deep orange */
/* Accent Colors (強調色) */
--color-accent: #d84038; /* Accent red-orange */
--color-accent-light: #f6c456; /* Light pink */
--color-accent-dark: #ea384c; /* Deep red */
--color-accent-pink: #bb8282; /* Pink */
--color-accent-rose: #c48383; /* Rose */
--color-accent: #d84038;
/* Accent red-orange */
--color-accent-light: #f6c456;
/* Light pink */
--color-accent-dark: #ea384c;
/* Deep red */
--color-accent-pink: #bb8282;
/* Pink */
--color-accent-rose: #c48383;
/* Rose */
/* Link Colors (連結色) */
--color-link: #3083bf; /* Default link */
--color-link-hover: #23608c; /* Link hover */
--color-link: #3083bf;
/* Default link */
--color-link-hover: #23608c;
/* Link hover */
/* Neutral Colors (中性色 - Tailwind Slate mapping) */
--color-white: #ffffff; /* White */
--color-black: #000000; /* Black */
--color-gray-50: #fafafa; /* Surface lightest */
--color-gray-100: #f5f5f5; /* Surface light */
--color-gray-200: #f3f3f3; /* Surface */
--color-gray-300: #eeeeee; /* Border light */
--color-gray-400: #dddddd; /* Border default */
--color-gray-500: #c8c8c8; /* Mid gray */
--color-gray-600: #999999; /* Text muted */
--color-gray-700: #828282; /* Text dark */
--color-gray-800: #758696; /* Dark gray 1 */
--color-gray-900: #5d6c7b; /* Dark gray 2 */
--color-gray-950: #4f4f4f; /* Darkest gray */
--color-white: #ffffff;
/* White */
--color-black: #000000;
/* Black */
--color-gray-50: #fafafa;
/* Surface lightest */
--color-gray-100: #f5f5f5;
/* Surface light */
--color-gray-200: #f3f3f3;
/* Surface */
--color-gray-300: #eeeeee;
/* Border light */
--color-gray-400: #dddddd;
/* Border default */
--color-gray-500: #c8c8c8;
/* Mid gray */
--color-gray-600: #999999;
/* Text muted */
--color-gray-700: #828282;
/* Text dark */
--color-gray-800: #758696;
/* Dark gray 1 */
--color-gray-900: #5d6c7b;
/* Dark gray 2 */
--color-gray-950: #4f4f4f;
/* Darkest gray */
/* Semantic Colors (語意化顏色) */
--color-text-primary: #333333; /* Primary text */
--color-text-secondary: #222222; /* Secondary text */
--color-text-muted: #999999; /* Muted text */
--color-text-light: #758696; /* Light text on dark */
--color-text-inverse: #ffffff; /* Inverse (white) */
--color-text-primary: #333333;
/* Primary text */
--color-text-secondary: #222222;
/* Secondary text */
--color-text-muted: #999999;
/* Muted text */
--color-text-light: #758696;
/* Light text on dark */
--color-text-inverse: #ffffff;
/* Inverse (white) */
/* Backgrounds (背景色) */
--color-background: #f2f2f2; /* Main background - Grey 6 from Webflow */
--color-surface: #fafafa; /* Surface background */
--color-surface2: #f3f3f3; /* Surface elevated */
--color-surface-dark: #2226; /* Dark background */
--color-background: #f2f2f2;
/* Main background - Grey 6 from Webflow */
--color-surface: #fafafa;
/* Surface background */
--color-surface2: #f3f3f3;
/* Surface elevated */
--color-surface-dark: #2226;
/* Dark background */
/* Borders (邊框色) */
--color-border: #e2e8f0; /* Default border */
--color-border-light: #dddddd; /* Light border */
--color-border: #e2e8f0;
/* Default border */
--color-border-light: #dddddd;
/* Light border */
/* Category Colors (文章分類) */
--color-category-google: #67aee1; /* Google小學堂 */
--color-category-meta: #8974de; /* Meta小學堂 */
--color-category-news: #3083bf; /* 行銷時事最前線 */
--color-category-enchun: #3898ec; /* 恩群數位 */
--color-category-google: #67aee1;
/* Google小學堂 */
--color-category-meta: #8974de;
/* Meta小學堂 */
--color-category-news: #3083bf;
/* 行銷時事最前線 */
--color-category-enchun: #3898ec;
/* 恩群數位 */
/* Badge Colors (標籤) */
--color-badge-hot: #ea384c; /* Hot 標籤 (紅) */
--color-badge-new: #67aee1; /* New 標籤 (淡藍) */
--color-badge-hot: #ea384c;
/* Hot 標籤 () */
--color-badge-new: #67aee1;
/* New 標籤 (淡藍) */
/* Webflow Colors - Story 1-4 Global Layout (Verified against original) */
--color-enchunblue: #23608c; /* Enchun Blue - 品牌/主色 */
--color-enchunblue-dark: #3083bf; /* Enchun Blue Dark - 品牌/主色深色 */
--color-tropical-blue: #c7e4fa; /* Tropical Blue - 頁腳背景 (verified: rgb(199, 228, 250)) */
--color-st-tropaz: #5d7285; /* St. Tropaz - 頁腳文字 */
--color-amber: #f6c456; /* Amber - CTA/強調 */
--color-tarawera: #2d3748; /* Tarawera - 深色文字 */
--color-nav-link: var(--color-gray-200); /* Navigation Link - 使用灰色系 */
--color-enchunblue: #23608c;
/* Enchun Blue - 品牌/主色 */
--color-enchunblue-dark: #3083bf;
/* Enchun Blue Dark - 品牌/主色深色 */
--color-tropical-blue: #c7e4fa;
/* Tropical Blue - 頁腳背景 (verified: rgb(199, 228, 250)) */
--color-st-tropaz: #5d7285;
/* St. Tropaz - 頁腳文字 */
--color-amber: #f6c456;
/* Amber - CTA/強調 */
--color-tarawera: #2d3748;
/* Tarawera - 深色文字 */
--color-nav-link: var(--color-gray-200);
/* Navigation Link - 使用灰色系 */
/* Webflow Additional Colors - Story 1-5 Homepage */
--color-notification-red: #d84038; /* Notification Red - CTA Button */
--color-dark-blue: #062841; /* Dark Blue - Headings */
--color-medium-blue: #67aee1; /* Medium Blue - Accents */
--color-grey5: #e0e0e0; /* Grey 5 - Borders */
--color-grey6: #f2f2f2; /* Grey 6 - Backgrounds */
--color-notification-red: #d84038;
/* Notification Red - CTA Button */
--color-dark-blue: #062841;
/* Dark Blue - Headings */
--color-medium-blue: #67aee1;
/* Medium Blue - Accents */
--color-grey5: #e0e0e0;
/* Grey 5 - Borders */
--color-grey6: #f2f2f2;
/* Grey 6 - Backgrounds */
/* ============================================
🔤 TYPOGRAPHY - From Webflow
@@ -111,13 +167,20 @@
📏 SPACING - Based on Tailwind scale
============================================ */
--spacing-xs: 0.25rem; /* 4px */
--spacing-sm: 0.5rem; /* 8px */
--spacing-md: 1rem; /* 16px */
--spacing-lg: 1.5rem; /* 24px */
--spacing-xl: 2rem; /* 32px */
--spacing-2xl: 3rem; /* 48px */
--spacing-3xl: 4rem; /* 64px */
--spacing-xs: 0.25rem;
/* 4px */
--spacing-sm: 0.5rem;
/* 8px */
--spacing-md: 1rem;
/* 16px */
--spacing-lg: 1.5rem;
/* 24px */
--spacing-xl: 2rem;
/* 32px */
--spacing-2xl: 3rem;
/* 48px */
--spacing-3xl: 4rem;
/* 64px */
/* Container */
--container-max-width: 1200px;
@@ -128,14 +191,22 @@
🔲 BORDER RADIUS
============================================ */
--radius-sm: 0.125rem; /* 2px */
--radius: 0.375rem; /* 6px (DEFAULT) */
--radius-md: 0.5rem; /* 8px */
--radius-lg: 0.75rem; /* 12px */
--radius-xl: 1rem; /* 16px */
--radius-2xl: 1.5rem; /* 24px */
--radius-3xl: 2rem; /* 32px */
--radius-full: 9999px; /* Full circle */
--radius-sm: 0.125rem;
/* 2px */
--radius: 0.375rem;
/* 6px (DEFAULT) */
--radius-md: 0.5rem;
/* 8px */
--radius-lg: 0.75rem;
/* 12px */
--radius-xl: 1rem;
/* 16px */
--radius-2xl: 1.5rem;
/* 24px */
--radius-3xl: 2rem;
/* 32px */
--radius-full: 9999px;
/* Full circle */
/* ============================================
💫 SHADOWS
@@ -178,9 +249,12 @@
Small (≤479px): 13px
*/
--html-font-size-desktop: 19px;
--html-font-size-tablet: 19px; /* 991px breakpoint */
--html-font-size-mobile: 16px; /* 767px breakpoint */
--html-font-size-small: 13px; /* 479px breakpoint */
--html-font-size-tablet: 19px;
/* 991px breakpoint */
--html-font-size-mobile: 16px;
/* 767px breakpoint */
--html-font-size-small: 13px;
/* 479px breakpoint */
}
/* ============================================
@@ -252,11 +326,9 @@ body {
/* Text Gradient */
.text-gradient {
background: linear-gradient(
135deg,
background: linear-gradient(135deg,
var(--color-primary),
var(--color-accent)
);
var(--color-accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -275,6 +347,7 @@ body {
from {
opacity: 0;
}
to {
opacity: 1;
}
@@ -285,6 +358,7 @@ body {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
@@ -357,7 +431,7 @@ body {
left: 0;
width: 0;
height: 2px;
background: var(--color-primary);
background: var(--color-secondary);
transition: width var(--transition-fast);
}
@@ -365,7 +439,11 @@ body {
width: 100%;
}
/* Active Navigation Link */
/* Active Navigation Link — position: relative ensures ::after stays within the link */
.nav-active {
position: relative;
}
.nav-active::after {
content: "";
position: absolute;
@@ -402,9 +480,11 @@ body {
.prose-custom h1 {
font-size: 2.25rem;
}
.prose-custom h2 {
font-size: 1.875rem;
}
.prose-custom h3 {
font-size: 1.5rem;
}