From 9c2181f7439b0e725427c5151f7ca8e7bb1bd723 Mon Sep 17 00:00:00 2001 From: pkupuk Date: Wed, 11 Feb 2026 11:50:42 +0800 Subject: [PATCH] feat(frontend): update pages, components and branding Refresh Astro frontend implementation including new pages (Portfolio, Teams, Services), components, and styling updates. --- apps/frontend/Dockerfile | 52 ++ apps/frontend/astro.config.mjs | 6 + apps/frontend/dev.vars | 6 +- apps/frontend/package.json | 2 + apps/frontend/src/components/Footer.astro | 238 +++--- apps/frontend/src/components/Header.astro | 584 +++++++++++---- .../src/components/PortfolioCard.astro | 173 +++++ .../src/components/PortfolioPreviewCard.astro | 89 +++ .../src/components/ServiceFeatureCard.astro | 53 ++ .../src/components/blog/ArticleCard.astro | 215 ++++++ .../src/components/blog/CategoryFilter.astro | 113 +++ .../src/components/blog/RelatedPosts.astro | 195 +++++ apps/frontend/src/layouts/Layout.astro | 70 +- apps/frontend/src/lib/api/blog.ts | 212 ++++++ apps/frontend/src/lib/api/home.ts | 169 +++++ apps/frontend/src/lib/api/portfolio.ts | 139 ++++ .../frontend/src/lib/serializeLexical.spec.ts | 221 ++++++ apps/frontend/src/lib/serializeLexical.ts | 357 +++++++++ apps/frontend/src/pages/about-enchun.astro | 61 +- apps/frontend/src/pages/contact-us.astro | 662 +++++++++++++++-- apps/frontend/src/pages/index.astro | 110 ++- .../src/pages/marketing-solutions.astro | 59 +- apps/frontend/src/pages/news.astro | 304 +++++++- apps/frontend/src/pages/teams.astro | 181 ++++- .../src/pages/web-portfolios/[slug].astro | 345 +++++++++ .../src/pages/web-portfolios/index.astro | 244 +++++++ .../src/pages/webdesign-profolio/[slug].astro | 454 +++++++++++- .../src/pages/website-portfolio.astro | 234 +++++- .../src/pages/wen-zhang-fen-lei/[slug].astro | 268 +++++-- .../pages/xing-xiao-fang-da-jing/[slug].astro | 376 ++++++++-- apps/frontend/src/sections/AboutHero.astro | 84 +++ .../src/sections/BenefitsSection.astro | 309 ++++++++ apps/frontend/src/sections/CTASection.astro | 147 ++++ .../src/sections/ClientCasesSection.astro | 505 +++++++++++++ apps/frontend/src/sections/CompanyStory.astro | 131 ++++ .../src/sections/ComparisonSection.astro | 274 +++++++ .../src/sections/EnvironmentSlider.astro | 442 +++++++++++ .../src/sections/FeatureSection.astro | 202 ++++++ apps/frontend/src/sections/HeroSection.astro | 268 +++++++ .../src/sections/PainpointSection.astro | 369 ++++++++++ .../src/sections/PortfolioPreview.astro | 77 ++ .../src/sections/ServiceFeatures.astro | 70 ++ apps/frontend/src/sections/ServicesList.astro | 373 ++++++++++ .../frontend/src/sections/SolutionsHero.astro | 94 +++ .../src/sections/StatisticsSection.astro | 257 +++++++ apps/frontend/src/sections/TeamsHero.astro | 95 +++ apps/frontend/src/styles/theme.css | 685 +++++++++++------- apps/frontend/tsconfig.json | 1 + apps/frontend/wrangler.jsonc | 23 + 49 files changed, 9699 insertions(+), 899 deletions(-) create mode 100644 apps/frontend/Dockerfile create mode 100644 apps/frontend/src/components/PortfolioCard.astro create mode 100644 apps/frontend/src/components/PortfolioPreviewCard.astro create mode 100644 apps/frontend/src/components/ServiceFeatureCard.astro create mode 100644 apps/frontend/src/components/blog/ArticleCard.astro create mode 100644 apps/frontend/src/components/blog/CategoryFilter.astro create mode 100644 apps/frontend/src/components/blog/RelatedPosts.astro create mode 100644 apps/frontend/src/lib/api/blog.ts create mode 100644 apps/frontend/src/lib/api/home.ts create mode 100644 apps/frontend/src/lib/api/portfolio.ts create mode 100644 apps/frontend/src/lib/serializeLexical.spec.ts create mode 100644 apps/frontend/src/lib/serializeLexical.ts create mode 100644 apps/frontend/src/pages/web-portfolios/[slug].astro create mode 100644 apps/frontend/src/pages/web-portfolios/index.astro create mode 100644 apps/frontend/src/sections/AboutHero.astro create mode 100644 apps/frontend/src/sections/BenefitsSection.astro create mode 100644 apps/frontend/src/sections/CTASection.astro create mode 100644 apps/frontend/src/sections/ClientCasesSection.astro create mode 100644 apps/frontend/src/sections/CompanyStory.astro create mode 100644 apps/frontend/src/sections/ComparisonSection.astro create mode 100644 apps/frontend/src/sections/EnvironmentSlider.astro create mode 100644 apps/frontend/src/sections/FeatureSection.astro create mode 100644 apps/frontend/src/sections/HeroSection.astro create mode 100644 apps/frontend/src/sections/PainpointSection.astro create mode 100644 apps/frontend/src/sections/PortfolioPreview.astro create mode 100644 apps/frontend/src/sections/ServiceFeatures.astro create mode 100644 apps/frontend/src/sections/ServicesList.astro create mode 100644 apps/frontend/src/sections/SolutionsHero.astro create mode 100644 apps/frontend/src/sections/StatisticsSection.astro create mode 100644 apps/frontend/src/sections/TeamsHero.astro create mode 100644 apps/frontend/wrangler.jsonc diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile new file mode 100644 index 0000000..66d4f19 --- /dev/null +++ b/apps/frontend/Dockerfile @@ -0,0 +1,52 @@ +# Use Node.js 18 Alpine for smaller image size +FROM node:18-alpine AS base + +# Install pnpm globally +RUN npm install -g pnpm@10.17.0 + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the application +RUN pnpm build + +# Production stage +FROM node:18-alpine AS production + +# Install pnpm globally +RUN npm install -g pnpm@10.17.0 + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install only production dependencies +RUN pnpm install --frozen-lockfile --prod + +# Copy built application from base stage +COPY --from=base /app/dist ./dist +COPY --from=base /app/src ./src +COPY --from=base /app/astro.config.mjs ./ +COPY --from=base /app/tailwind.config.mjs ./ + +# Expose port 4321 (Astro default) +EXPOSE 4321 + +# Set environment variables +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=4321 + +# Start the application +CMD ["pnpm", "preview"] diff --git a/apps/frontend/astro.config.mjs b/apps/frontend/astro.config.mjs index 2e63b0a..70edc2c 100644 --- a/apps/frontend/astro.config.mjs +++ b/apps/frontend/astro.config.mjs @@ -2,6 +2,7 @@ import { defineConfig } from "astro/config"; import cloudflare from "@astrojs/cloudflare"; import tailwindcss from "@tailwindcss/vite"; +import path from "node:path"; // https://astro.build/config export default defineConfig({ @@ -15,6 +16,11 @@ export default defineConfig({ }), vite: { plugins: [tailwindcss()], + resolve: { + alias: { + '@': path.resolve('./src'), + }, + }, server: { proxy: { '/api': { diff --git a/apps/frontend/dev.vars b/apps/frontend/dev.vars index 49d0087..0dd31aa 100644 --- a/apps/frontend/dev.vars +++ b/apps/frontend/dev.vars @@ -1,3 +1,5 @@ -# Local development environment variables +# Cloudflare Workers local development environment variables # This file is used by wrangler for local development -PAYLOAD_CMS_URL=https://enchun-admin.anlstudio.cc \ No newline at end of file + +# Production Payload CMS URL (for SSR fetch) +PAYLOAD_CMS_URL=https://enchun-admin.anlstudio.cc diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 35627fc..9217836 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -15,6 +15,8 @@ "dependencies": { "@astrojs/cloudflare": "^12.6.12", "@tailwindcss/vite": "^4.1.14", + "agentation": "^2.1.1", + "agentation-mcp": "^1.1.0", "astro": "6.0.0-beta.1", "better-auth": "^1.3.13" }, diff --git a/apps/frontend/src/components/Footer.astro b/apps/frontend/src/components/Footer.astro index 2cac6e0..79e3ca1 100644 --- a/apps/frontend/src/components/Footer.astro +++ b/apps/frontend/src/components/Footer.astro @@ -1,97 +1,157 @@ --- -import { Image } from 'astro:assets'; -// Footer component with client-side data fetching +import { Image } from "astro:assets"; +// Footer component with server-side data fetching + +// 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) + : import.meta.env.PAYLOAD_CMS_URL || "https://enchun-admin.anlstudio.cc"; + +// Fetch footer data from Payload CMS server-side +let footerNavItems: any[] = []; +try { + const response = await fetch( + `${PAYLOAD_CMS_URL}/api/globals/footer?depth=2&draft=false&locale=undefined&trash=false`, + ); + if (response.ok) { + const data = await response.json(); + footerNavItems = data?.navItems || data || []; + } +} catch (error) { + console.error("[Footer SSR] Failed to fetch footer:", error); +} + +// Fetch categories from Payload CMS server-side +let categories: any[] = []; +try { + const response = await fetch( + `${PAYLOAD_CMS_URL}/api/categories?sort=order&limit=6&depth=0&draft=false`, + ); + if (response.ok) { + const data = await response.json(); + categories = (data?.docs || []) + .sort((a: any, b: any) => (a.order || 0) - (b.order || 0)) + .slice(0, 6); + } +} catch (error) { + console.error("[Footer SSR] Failed to fetch categories:", error); +} + +// Get current year for copyright +const currentYear = new Date().getFullYear(); --- - - + footer a:hover { + text-decoration: underline; + } + diff --git a/apps/frontend/src/components/Header.astro b/apps/frontend/src/components/Header.astro index fd01fc9..d0232a5 100644 --- a/apps/frontend/src/components/Header.astro +++ b/apps/frontend/src/components/Header.astro @@ -1,18 +1,82 @@ --- import { Image } from "astro:assets"; -// Header component +// Header component with scroll-based background and enhanced mobile animations + +// 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) + : import.meta.env.PAYLOAD_CMS_URL || "https://enchun-admin.anlstudio.cc"; + +// Fetch navigation data from Payload CMS server-side +let navItems: any[] = []; +try { + const response = await fetch( + `${PAYLOAD_CMS_URL}/api/globals/header?depth=2&draft=false&locale=undefined&trash=false`, + ); + if (response.ok) { + const data = await response.json(); + navItems = data?.navItems || data || []; + } +} catch (error) { + console.error("[Header SSR] Failed to fetch navigation:", error); +} + +// Helper to get link URL +function getLinkUrl(link: any): string { + if (!link) return "#"; + + if (link.type === "custom" && link.url) { + return link.url; + } + + 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}`; + } + } + + return "#"; +} + +// Check if label should have a badge +function getBadgeForLabel(label: string): string { + if (label.includes("行銷方案")) { + return `Hot`; + } + if (label.includes("行銷放大鏡")) { + return `New`; + } + return ""; +} + +// Check if link is active +function isLinkActive(url: string): boolean { + const currentPath = Astro.url.pathname; + return currentPath === url || (url === "/" && currentPath === "/"); +} --- -
- + + +
+
+ +
+

{project.title}

+ +
+ { + project.client && ( + + 客戶:{project.client} + + ) + } + { + project.date && ( + + 日期:{project.date} + + ) + } + { + project.category && ( + + 類別:{project.category} + + ) + } +
+
+ + + { + project.images && project.images.length > 0 && ( +
+ {project.title} +
+ ) + } + + +
+
+
+ + +
+

有專案想要討論嗎?

+

我們很樂意聽聽您的需求

+ + 聯絡我們 + +
+ + + +
+
\ No newline at end of file + + .portfolio-detail-title { + font-family: "Noto Sans TC", sans-serif; + font-size: 2.5rem; + font-weight: 700; + color: var(--color-tarawera, #2d3748); + margin-bottom: 24px; + line-height: 1.2; + } + + .portfolio-detail-meta { + display: flex; + justify-content: center; + gap: 24px; + font-size: 0.875rem; + color: var(--color-gray-600); + flex-wrap: wrap; + } + + .meta-item strong { + color: var(--color-text-primary); + } + + /* Featured Image */ + .portfolio-detail-image-wrapper { + margin-bottom: 40px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + } + + .portfolio-detail-image { + width: 100%; + height: auto; + display: block; + } + + /* Content */ + .portfolio-detail-content { + margin-bottom: 60px; + } + + .description-wrapper { + font-family: "Noto Sans TC", sans-serif; + font-size: 1.125rem; + line-height: 1.8; + color: var(--color-text-primary); + } + + .description-wrapper :global(p) { + margin-bottom: 20px; + } + + .description-wrapper :global(h3) { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-enchunblue); + margin-top: 32px; + margin-bottom: 16px; + } + + /* CTA Section */ + .portfolio-detail-cta { + text-align: center; + padding: 60px; + background: linear-gradient(135deg, rgba(35, 96, 140, 0.05) 0%, rgba(35, 96, 140, 0.02) 100%); + border-radius: 12px; + margin-bottom: 60px; + } + + .cta-heading { + font-family: "Noto Sans TC", sans-serif; + font-size: 1.75rem; + font-weight: 600; + color: var(--color-enchunblue); + margin-bottom: 8px; + } + + .cta-subheading { + font-size: 1rem; + color: var(--color-gray-600); + margin-bottom: 24px; + } + + .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); + } + + /* Related Projects */ + .related-projects { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 32px; + border-top: 1px solid var(--color-border, #e5e7eb); + } + + .related-title { + font-family: "Noto Sans TC", sans-serif; + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text-primary); + } + + .view-all-link { + color: var(--color-enchunblue); + text-decoration: none; + font-weight: 500; + transition: color var(--transition-fast, 0.2s ease); + } + + .view-all-link:hover { + color: var(--color-enchunblue-hover, #1a4d6e); + } + + /* Responsive Adjustments */ + @media (max-width: 991px) { + .portfolio-detail { + padding: 50px 16px; + } + + .portfolio-detail-title { + font-size: 2rem; + } + + .portfolio-detail-cta { + padding: 40px; + } + + .cta-heading { + font-size: 1.5rem; + } + } + + @media (max-width: 767px) { + .breadcrumb { + padding: 12px 16px; + } + + .breadcrumb-list { + font-size: 0.8125rem; + } + + .portfolio-detail { + padding: 40px 16px; + } + + .portfolio-detail-title { + font-size: 1.75rem; + } + + .portfolio-detail-meta { + flex-direction: column; + gap: 8px; + } + + .portfolio-detail-cta { + padding: 32px 20px; + } + + .related-projects { + flex-direction: column; + gap: 16px; + align-items: flex-start; + } + + .description-wrapper { + font-size: 1rem; + } + + .description-wrapper :global(h3) { + font-size: 1.25rem; + } + } + diff --git a/apps/frontend/src/pages/website-portfolio.astro b/apps/frontend/src/pages/website-portfolio.astro index b752c37..6236137 100644 --- a/apps/frontend/src/pages/website-portfolio.astro +++ b/apps/frontend/src/pages/website-portfolio.astro @@ -1,57 +1,225 @@ --- -import Layout from '../layouts/Layout.astro'; +/** + * Portfolio Listing Page - 案例分享列表頁 + * Pixel-perfect implementation based on Webflow design + */ +import Layout from '../layouts/Layout.astro' +import PortfolioCard from '../components/PortfolioCard.astro' -// Placeholder portfolios -const portfolios = [ - { slug: 'web-design-project-2', title: 'Project 2', description: 'Description...' }, - // Add more -]; +// Metadata for SEO +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: 'ecommerce-site-1', + title: '電商平台建置', + description: 'B2C 電商網站,包含會員系統、購物車、金流整合等完整功能。', + image: '/placeholder-portfolio-2.jpg', + tags: ['電商網站', '金流整合'], + }, + { + slug: 'brand-website-1', + title: '品牌形象網站', + description: '以視覺故事為核心的品牌網站,展現品牌獨特價值與理念。', + image: '/placeholder-portfolio-3.jpg', + tags: ['品牌網站', '視覺設計'], + }, + { + slug: 'landing-page-1', + title: '活動行銷頁面', + description: '高轉換率的活動頁面設計,有效的 CTA 配置與使用者體驗規劃。', + image: '/placeholder-portfolio-4.jpg', + tags: ['活動頁面', '行銷'], + }, +] --- - -
+ + +
-

網站設計作品

-
- {portfolios.map(item => ( -
-

{item.title}

-

{item.description}

-
- ))} -
+

案例分享

+

Selected Works

+
+
+
+
+ + +
+
    + { + portfolioItems.map((item) => ( + + )) + } +
+
+ + +
+
+

+ 有興趣與我們合作嗎? +

+

+ 讓我們一起為您的品牌打造獨特的數位體驗 +

+ + 聯絡我們 +
\ No newline at end of file + + /* 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; + } + } + diff --git a/apps/frontend/src/pages/wen-zhang-fen-lei/[slug].astro b/apps/frontend/src/pages/wen-zhang-fen-lei/[slug].astro index 3e28036..55cbff3 100644 --- a/apps/frontend/src/pages/wen-zhang-fen-lei/[slug].astro +++ b/apps/frontend/src/pages/wen-zhang-fen-lei/[slug].astro @@ -1,79 +1,233 @@ --- -import Layout from '../../layouts/Layout.astro'; +/** + * Category Page - 文章分類 + * URL: /wen-zhang-fen-lei/[slug] + * Displays posts filtered by category from Payload CMS + */ +import Layout from '../../layouts/Layout.astro' +import ArticleCard from '../../components/blog/ArticleCard.astro' +import { fetchCategoryBySlug, fetchPosts, formatDate } from '../../lib/api/blog' -export async function getStaticPaths() { - // Category slugs - const slugs = [ - 'en-qun-shu-wei-zui-xin-gong-gao', - 'xing-xiao-shi-shi-zui-qian-xian', - 'meta-xiao-xue-tang', - 'google-xiao-xue-tang' - ]; +const { slug } = Astro.params - return slugs.map(slug => ({ - params: { slug }, - props: { slug } - })); +// Fetch category by slug +const category = await fetchCategoryBySlug(slug) + +// Handle 404 for non-existent category +if (!category) { + return Astro.redirect('/404') } -const { slug } = Astro.props; +// Fetch posts for this category (limit 100 for category pages) +const PAGE_SIZE = 100 +const postsData = await fetchPosts(1, PAGE_SIZE, slug) +const posts = postsData.docs -// Placeholder - would fetch category and posts from CMS -const category = { - name: 'Category Name', - posts: [ - { slug: 'post1', title: 'Post 1', date: '2023-01-01' } - ] -}; +// Format date function +const formatDateTW = (dateStr: string | null | undefined): string => { + if (!dateStr) return '' + const date = new Date(dateStr) + return date.toLocaleDateString('zh-TW', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) +} --- - -
+ + +
-

{category.name}

-
- {category.posts.map(post => ( - - ))} -
+ ← 回到文章列表 +

{category.title}

+ { + category.nameEn && ( +

{category.nameEn}

+ ) + } +
+
+ + +
+
+ { + posts.length > 0 ? ( + + ) : ( +
+

此分類暫無文章

+
+ ) + }
\ No newline at end of file + + .category-subtitle { + font-family: "Quicksand", "Noto Sans TC", sans-serif; + font-size: 1rem; + color: var(--color-gray-600, #666); + } + + .category-posts { + padding: 40px 20px 60px; + background-color: #f8f9fa; + min-height: 60vh; + } + + .posts-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + } + + .post-item { + background: #ffffff; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transition: box-shadow 0.3s ease; + } + + .post-item:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + } + + .post-link { + display: block; + text-decoration: none; + color: inherit; + } + + .post-image { + aspect-ratio: 16 / 9; + overflow: hidden; + background: var(--color-gray-100, #f5f5f5); + } + + .post-image img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .post-content { + padding: 20px; + } + + .post-title { + font-family: "Noto Sans TC", sans-serif; + font-size: 1.125rem; + font-weight: 600; + color: var(--color-dark-blue, #1a1a1a); + margin-bottom: 12px; + line-height: 1.4; + } + + .post-date { + font-size: 0.875rem; + color: var(--color-gray-500, #999); + margin-bottom: 12px; + } + + .post-excerpt { + font-size: 0.9375rem; + color: var(--color-gray-600, #666); + line-height: 1.6; + } + + .empty-state { + text-align: center; + padding: 80px 20px; + color: var(--color-gray-500, #999); + } + + /* Responsive */ + @media (max-width: 991px) { + .posts-grid { + grid-template-columns: repeat(2, 1fr); + gap: 20px; + } + + .category-title { + font-size: 1.75rem; + } + } + + @media (max-width: 767px) { + .category-hero { + padding: 40px 16px 30px; + } + + .category-title { + font-size: 1.5rem; + } + + .posts-grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .category-posts { + padding: 30px 16px 50px; + } + } + diff --git a/apps/frontend/src/pages/xing-xiao-fang-da-jing/[slug].astro b/apps/frontend/src/pages/xing-xiao-fang-da-jing/[slug].astro index 2197baa..ade330f 100644 --- a/apps/frontend/src/pages/xing-xiao-fang-da-jing/[slug].astro +++ b/apps/frontend/src/pages/xing-xiao-fang-da-jing/[slug].astro @@ -1,66 +1,356 @@ --- -import Layout from '../../layouts/Layout.astro'; +/** + * Blog Article Detail Page - 行銷放大鏡文章詳情 + * URL: /xing-xiao-fang-da-jing/[slug] + * Fetches from Payload CMS and renders with Lexical content + */ +import Layout from '../../layouts/Layout.astro' +import RelatedPosts from '../../components/blog/RelatedPosts.astro' +import { fetchPostBySlug, fetchRelatedPosts, formatDate } from '../../lib/api/blog' +import { serializeLexical } from '../../lib/serializeLexical' -export async function getStaticPaths() { - // Placeholder slugs - would fetch from CMS - const slugs = [ - '2-zhao-yao-kong-xiao-fei-zhe-de-xin', - '2022-jie-qing-xing-xiao-quan-gong-lue', - // Add all from sitemap - ]; +// Get slug from params +const { slug } = Astro.params - return slugs.map(slug => ({ - params: { slug }, - props: { slug } - })); +// Validate slug +if (!slug) { + return Astro.redirect('/404') } -const { slug } = Astro.props; +// Fetch post by slug +const post = await fetchPostBySlug(slug) -// Placeholder content - would fetch from CMS -const post = { - title: 'Sample Post Title', - date: 'January 20, 2022', - content: 'Sample content...' -}; +// Handle 404 for non-existent or unpublished posts +if (!post) { + return Astro.redirect('/404') +} + +// Fetch related posts +const categorySlugs = post.categories?.map(c => c.slug) || [] +const relatedPosts = await fetchRelatedPosts(post.id, categorySlugs, 4) + +// Format date +const displayDate = formatDate(post.publishedAt) + +// Get primary category +const category = post.categories?.[0] + +// Serialize Lexical content to HTML (must await the async function) +const contentHtml = await serializeLexical(post.content) + +// SEO metadata +const metaTitle = post.meta?.title || post.title +const metaDescription = post.meta?.description || post.excerpt || '' +const metaImage = post.meta?.image?.url || post.ogImage?.url || post.heroImage?.url || '' --- - -
-
- 回到文章列表 -
-

{post.title}

- -
-

{post.content}

- + +
+ +
+
+ + { + category && ( + + ) + } + + +

+ {post.title} +

+ + + { + displayDate && ( + + ) + } +
+
+ + + { + post.heroImage?.url && ( +
+ {post.heroImage.alt
-
+ ) + } + + +
+
+
+ +
+
+
-
+ + +
+ +
+ + + +
- \ No newline at end of file + + @media (max-width: 767px) { + .article-header { + padding: 40px 16px 30px; + } + + .article-title { + font-size: 1.75rem; + } + + .article-hero-image { + aspect-ratio: 4 / 3; + } + + .article-content { + padding: 40px 16px; + } + + .content-wrapper { + padding: 0; + } + + .prose :global(h1) { font-size: 1.75rem; } + .prose :global(h2) { font-size: 1.5rem; } + .prose :global(h3) { font-size: 1.25rem; } + .prose :global(p) { + font-size: 1rem; + } + } + diff --git a/apps/frontend/src/sections/AboutHero.astro b/apps/frontend/src/sections/AboutHero.astro new file mode 100644 index 0000000..4a33836 --- /dev/null +++ b/apps/frontend/src/sections/AboutHero.astro @@ -0,0 +1,84 @@ +--- +/** + * AboutHero - Hero section for About page + * Pixel-perfect implementation based on Webflow design + */ +interface Props { + title?: string + subtitle?: string +} + +const { + title = '關於恩群數位', + subtitle = 'About Enchun digital', +} = Astro.props +--- + +
+
+
+

{title}

+

{subtitle}

+
+
+
+ + diff --git a/apps/frontend/src/sections/BenefitsSection.astro b/apps/frontend/src/sections/BenefitsSection.astro new file mode 100644 index 0000000..65aee33 --- /dev/null +++ b/apps/frontend/src/sections/BenefitsSection.astro @@ -0,0 +1,309 @@ +--- +/** + * BenefitsSection - Work benefits section for Teams page + * Pixel-perfect implementation based on Webflow design + * Features 6 benefit cards with alternating layout + */ + +interface BenefitItem { + title: string + icon: string +} + +interface Props { + benefits?: BenefitItem[] +} + +const defaultBenefits: BenefitItem[] = [ + { + title: '高績效、高獎金\n新人開張獎金', + icon: 'bonus', + }, + { + title: '生日慶生、電影日\n員工下午茶', + icon: 'birthday', + }, + { + title: '教育訓練補助', + icon: 'education', + }, + { + title: '寬敞的工作空間', + icon: 'workspace', + }, + { + title: '員工國內外旅遊\n部門聚餐、年終活動', + icon: 'travel', + }, + { + title: '入職培訓及團隊建設', + icon: 'training', + }, +] + +const benefits = Astro.props.benefits || defaultBenefits + +// Icon SVG components (placeholder for now, replace with actual SVG) +const getIconSVG = (iconType: string) => { + const icons: Record = { + bonus: ` + + + 💰 + `, + birthday: ` + + + 🎂 + `, + education: ` + + + 📚 + `, + workspace: ` + + + 🏢 + `, + travel: ` + + + ✈️ + `, + training: ` + + + 🤝 + `, + } + return icons[iconType] || icons.bonus +} +--- + +
+
+ +
+
+
+

工作福利

+

Benefit Package

+
+
+
+ + +
+ { + benefits.map((benefit, index) => ( +
+ + { + index % 2 === 0 ? ( + <> +
+

{benefit.title}

+
+
+ + ) : ( + <> +
+
+

{benefit.title}

+
+ + ) + } +
+ )) + } +
+
+
+ + diff --git a/apps/frontend/src/sections/CTASection.astro b/apps/frontend/src/sections/CTASection.astro new file mode 100644 index 0000000..6ec36aa --- /dev/null +++ b/apps/frontend/src/sections/CTASection.astro @@ -0,0 +1,147 @@ +--- +/** + * CTASection - Call to action section (Pixel-perfect from Webflow) + * Features: 2-column grid, notification-red button, hover effects + */ +import type { HomeData } from '@/lib/api/home' + +interface Props { + homeData?: HomeData | null +} + +const { homeData } = Astro.props + +// Get CTA config from CMS or use defaults - matching reference HTML +const ctaSection = homeData?.ctaSection +const headline = ctaSection?.headline || '準備好開始新的旅程了嗎 歡迎與我們聯絡' +const description = ctaSection?.description || '' // Reference has no description +const buttonText = ctaSection?.buttonText || '預約諮詢 phone_callback' +const buttonLink = ctaSection?.buttonLink || 'https://heyform.itslouis.cc/form/7mYtUNjA' +--- + +
+
+
+ +
+

+ {headline} +

+ { + description && ( +

+ {description} +

+ ) + } +
+ + + + + {buttonText} + + +
+
+
+ + diff --git a/apps/frontend/src/sections/ClientCasesSection.astro b/apps/frontend/src/sections/ClientCasesSection.astro new file mode 100644 index 0000000..307c9ca --- /dev/null +++ b/apps/frontend/src/sections/ClientCasesSection.astro @@ -0,0 +1,505 @@ +--- +/** + * ClientCasesSection - Client testimonials carousel + * Features: Auto-play carousel, 2-column grid, responsive + */ +import type { HomeData } from '@/lib/api/home' + +interface Props { + homeData?: HomeData | null +} + +const { homeData } = Astro.props + +// Client cases data - matching reference HTML exactly +const clientCases = [ + { + id: 1, + name: '落日精品咖啡', + title: '王孟梵 老闆', + company: '', + image: '/placeholder-client-1.jpg', + review: '業務專業、能反應需求、良好溝通一起努力達成目標,比起自己下廣告,合作更能將每分錢用在刀口上。', + }, + { + id: 2, + name: 'Gee hair salon', + title: 'Ivan 店長', + company: '', + image: '/placeholder-client-2.jpg', + review: '與恩群合作多年,疫情時期,不指定、網路指定客數量不減反增,讓在店周圍客人們就進更能找到合適自己的髮型師。', + }, + { + id: 3, + name: '高雄欣興租車', + title: 'ANDY 店長', + company: '', + image: '/placeholder-client-3.jpg', + review: '感謝團隊的專業指導與耐心的溝通,讓商家的理念可以轉換成實際的成果,發揮廣告效應最大化!特別是後勤團隊高效的執行能力,深受店家放心😊', + }, + { + id: 4, + name: '這健小事運動工作室', + title: 'Nash嘉慶 老闆', + company: '', + image: '/placeholder-client-4.jpg', + review: '對於我們預算有限的自營小工作室的人真的最佳選擇,謝謝你們,讓更多需要運動的人,看見我們', + }, + { + id: 5, + name: '即刻體能運動空間', + title: 'Peter 老闆', + company: '', + image: '/placeholder-client-5.jpg', + review: '在疫情期間開業的我們,很慶幸有恩群的協助,讓喜愛健身的客人能搜尋到我們、並給我們機會。感謝恩群團隊,讓我們感受到滿滿的服務熱忱及設身處地為客戶著想的心,非常推薦!', + }, +] +--- + +
+
+ +
+

+ 客戶案例 +

+

clients who work with us

+

+ 恩群數位的客戶真實示範,增加流量就是這麼簡單! +

+
+ + + +
+
+ + + + diff --git a/apps/frontend/src/sections/CompanyStory.astro b/apps/frontend/src/sections/CompanyStory.astro new file mode 100644 index 0000000..be1877c --- /dev/null +++ b/apps/frontend/src/sections/CompanyStory.astro @@ -0,0 +1,131 @@ +--- +/** + * CompanyStory - Company story section for Teams page + * Pixel-perfect implementation based on Webflow design + */ +interface Props { + title?: string + subtitle?: string + content?: string +} + +const { + title = '恩群數位的故事', + subtitle = 'Something About Enchun Digital', + content = '恩群數位是由一群年輕果斷、敢冒險的年輕人聚在一起,共同為台灣在地經營努力不懈的商家老闆們建立品牌的知名度。而商家的經營本身就不是一件容易的事情,早在恩群成立之前,我們便一直聚焦在與不同行業的老闆們建立起信賴可靠的關係,從一家又一家的合作關係當中,聚集了在行銷領域裡的頂尖好手,培育了許多優秀行銷顧問。讓每個辛苦經營的商家老闆可以獲得最佳的服務,讓每次的行銷需求可以透過有效的互動與聆聽,達到彼此心目中的預期目標。數字的確會說話,但是每一個有溫度的服務才是在恩群裡最重視的地方。', +} = Astro.props +--- + +
+
+ +
+
+
+

{title}

+

{subtitle}

+
+
+
+ + +

{content}

+
+
+ + diff --git a/apps/frontend/src/sections/ComparisonSection.astro b/apps/frontend/src/sections/ComparisonSection.astro new file mode 100644 index 0000000..b285d66 --- /dev/null +++ b/apps/frontend/src/sections/ComparisonSection.astro @@ -0,0 +1,274 @@ +--- +/** + * ComparisonSection - 恩群數位 vs 其他行銷公司 對比表格 + * Pixel-perfect implementation based on Webflow design + */ +interface ComparisonItem { + feature: string + enchun: string + others: string +} + +const comparisonItems: ComparisonItem[] = [ + { + feature: '服務範圍', + enchun: '全方位數位行銷服務,從策略到執行一條龍', + others: '單一服務項目,缺乏整合性', + }, + { + feature: '數據分析', + enchun: '專業數據分析團隊,精準追蹤 ROI', + others: '基礎報告,缺乏深度分析', + }, + { + feature: '在地化經驗', + enchun: '深耕台灣市場,了解本地消費者習性', + others: '通用策略,缺乏在地化調整', + }, + { + feature: '客戶服務', + enchun: '一對一專人服務,快速響應', + others: '標準化流程,回應較慢', + }, + { + feature: '價格透明', + enchun: '明確報價,無隱藏費用', + others: '複雜收費結構,容易超支', + }, +] +--- + +
+
+ +
+

+ 為什麼選擇恩群數位 +

+
+
+ + +
+ + + + + + + + + + { + comparisonItems.map((item, index) => ( + + + + + + )) + } + +
比較項目 + 恩群數位 + 其他行銷公司
{item.feature} + + {item.enchun} + {item.others}
+
+ + +
+

+ 選擇恩群數位,讓您的品牌在數位時代脫穎而出! +

+
+
+
+ + diff --git a/apps/frontend/src/sections/EnvironmentSlider.astro b/apps/frontend/src/sections/EnvironmentSlider.astro new file mode 100644 index 0000000..928f18f --- /dev/null +++ b/apps/frontend/src/sections/EnvironmentSlider.astro @@ -0,0 +1,442 @@ +--- +/** + * EnvironmentSlider - Photo slider for work environment + * Pixel-perfect implementation based on Webflow design + * Features: 8 photos, arrow navigation, dot navigation, touch swipe support + */ + +interface SlideImage { + src: string + alt: string +} + +interface Props { + slides?: SlideImage[] +} + +const defaultSlides: SlideImage[] = [ + { src: '/placeholder-environment-1.jpg', alt: '恩群環境照片 1' }, + { src: '/placeholder-environment-2.jpg', alt: '恩群環境照片 2' }, + { src: '/placeholder-environment-3.jpg', alt: '恩群環境照片 3' }, + { src: '/placeholder-environment-4.jpg', alt: '恩群環境照片 4' }, + { src: '/placeholder-environment-5.jpg', alt: '恩群環境照片 5' }, + { src: '/placeholder-environment-6.jpg', alt: '恩群環境照片 6' }, + { src: '/placeholder-environment-7.jpg', alt: '恩群環境照片 7' }, + { src: '/placeholder-environment-8.jpg', alt: '恩群環境照片 8' }, +] + +const slides = Astro.props.slides || defaultSlides +--- + +
+
+ +
+
+
+

在恩群工作的環境

+

Working Enviroment

+
+
+
+ + +
+ +
+ { + slides.map((slide, index) => ( +
+ {slide.alt} +
+ )) + } +
+ + + + + + +
+ { + slides.map((_, index) => ( +
+
+
+
+ + + + diff --git a/apps/frontend/src/sections/FeatureSection.astro b/apps/frontend/src/sections/FeatureSection.astro new file mode 100644 index 0000000..251acaf --- /dev/null +++ b/apps/frontend/src/sections/FeatureSection.astro @@ -0,0 +1,202 @@ +--- +/** + * FeatureSection - Service features section with 4 cards + * Pixel-perfect implementation based on Webflow design + */ +interface Props { + title?: string + subtitle?: string +} + +const { + title = '我們的服務特色', + subtitle = '為什麼選擇恩群數位?', +} = Astro.props + +// Four features data +const features = [ + { + icon: 'location_on', + title: '在地化優先', + description: '緊上線下結合曝光渠道,整合多方資訊,帶給消費者最佳的使用體驗,展現商家的獨特之處,順利的將潛在使用者帶到你的實際門市。', + }, + { + icon: 'account_balance', + title: '高投資轉換率', + description: '你覺得網路行銷很貴嗎?恩群數位善用每一分廣告預算,讓你在網路上發揮最大效益,幫助店家鎖定精準客群,達成目標。', + }, + { + icon: 'analytics', + title: '數據優先', + description: '想要精準行銷?恩群數位從數據中萃取洞察,根據數據分析廣告成效,更聰明、有策略的幫您省下行銷預算。', + }, + { + icon: 'handshake', + title: '關係優於銷售', + description: '除了幫您拓展網路上的知名度,我們更是每家公司最專業的數位夥伴,你會知道有恩群的存在,事業路上你並不孤單。', + }, +] +--- + +
+
+ +
+

+ {title} +

+

+ {subtitle} +

+
+
+ + +
+ { + features.map((feature) => ( +
+ +
+ {feature.icon} +
+ + +

{feature.title}

+ + +

{feature.description}

+
+ )) + } +
+
+
+ + diff --git a/apps/frontend/src/sections/HeroSection.astro b/apps/frontend/src/sections/HeroSection.astro new file mode 100644 index 0000000..fb30259 --- /dev/null +++ b/apps/frontend/src/sections/HeroSection.astro @@ -0,0 +1,268 @@ +--- +/** + * HeroSection - Pixel-perfect Hero component matching Webflow design + * Supports video background with fallback image and gradient overlay + */ +import { Image } from 'astro:assets' +import type { HomeData } from '@/lib/api/home' + +interface Props { + homeData?: HomeData | null + fallbackDesktopVideo?: string + fallbackMobileVideo?: string +} + +const { + homeData, + fallbackDesktopVideo = '/video/enchun-hero-background-video.mp4', + fallbackMobileVideo = '/video/enchun-hero-background-video.webm', +} = Astro.props + +// Extract hero content from CMS data or use defaults +const heroHeadline = homeData?.heroHeadline || '創造企業更多發展的可能性\n是我們的使命' +const heroSubheadline = homeData?.heroSubheadline || "It's our destiny to create possibilities for your business." + +// Get video URLs from CMS or fallback +const desktopVideoUrl = homeData?.heroDesktopVideo?.url || fallbackDesktopVideo +const mobileVideoUrl = homeData?.heroMobileVideo?.url || fallbackMobileVideo + +// Get fallback image +const fallbackImageUrl = homeData?.heroFallbackImage?.url + +// Get logo +const logoUrl = homeData?.heroLogo?.url || '/enchun-logo-full.svg' + +// Helper to get base path without extension +const getBasePath = (path: string) => path.replace(/\.(mp4|webm)$/i, '') + +// Generate sources for video elements +const getSources = (inputPath: string) => { + const basePath = getBasePath(inputPath) + return [ + { src: `${basePath}.webm`, type: 'video/webm' }, + { src: `${basePath}.mp4`, type: 'video/mp4' }, + ] +} +--- + + +{ + fallbackImageUrl && ( +
+ Hero background +
+
+ ) +} + + + + + + + + +
+ +
+ +
+
+ + + + +
+ +

+ {heroHeadline} +

+ + +

+ {heroSubheadline} +

+
+
+
+
+ + + + diff --git a/apps/frontend/src/sections/PainpointSection.astro b/apps/frontend/src/sections/PainpointSection.astro new file mode 100644 index 0000000..ad0eab8 --- /dev/null +++ b/apps/frontend/src/sections/PainpointSection.astro @@ -0,0 +1,369 @@ +--- +/** + * PainpointSection - Interactive tab section showing customer pain points + * Features: Tab switching, icon display, hover effects + */ +import type { HomeData } from '@/lib/api/home' + +interface Props { + homeData?: HomeData | null +} + +const { homeData } = Astro.props + +// Pain points data - matching reference HTML exactly +const painpoints = [ + { + id: 'company', + title: '行銷公司難找', + icon: '🔍', + description: '市場上有太多行銷公司了,每家都跟我說他們家的服務可以掛保證。但是我應該要找誰合作呢?', + }, + { + id: 'methods', + title: '宣傳方法太多難選擇', + icon: '🤔', + description: '網路行銷方法這麼多,Google、FB、IG、YT...哪種才適合我的產品?', + }, + { + id: 'digital', + title: '數位轉型太難', + icon: '💻', + description: '想要做數位轉型,但不知道從哪裡開始?技術門檻太高?', + }, + { + id: 'burning', + title: '廣告行銷像燒錢', + icon: '💸', + description: '廣告預算投入很多,但看不到實際效果?感覺像在燒錢?', + }, +] +--- + +
+
+ +
+

+ 你可能會遇到的煩惱 +

+
+
+ + +
+ +
+ { + painpoints.map((point, index) => ( + + )) + } +
+ + +
+ { + painpoints.map((point, index) => ( + + )) + } +
+
+
+
+ + + + diff --git a/apps/frontend/src/sections/PortfolioPreview.astro b/apps/frontend/src/sections/PortfolioPreview.astro new file mode 100644 index 0000000..c568e44 --- /dev/null +++ b/apps/frontend/src/sections/PortfolioPreview.astro @@ -0,0 +1,77 @@ +--- +/** + * PortfolioPreview - Portfolio preview section + */ +import PortfolioPreviewCard from '@/components/PortfolioPreviewCard.astro' +import type { HomeData } from '@/lib/api/home' +import type { PortfolioItem } from '@/lib/api/home' + +interface Props { + homeData?: HomeData | null + portfolioItems?: PortfolioItem[] +} + +const { homeData, portfolioItems = [] } = Astro.props + +// Get section config from CMS or use defaults +const portfolioSection = homeData?.portfolioSection +const headline = portfolioSection?.headline || '精選案例' +const subheadline = portfolioSection?.subheadline || '探索我們為客戶打造的優質網站' + +// Show section only if we have items +const showSection = portfolioItems.length > 0 +--- + +{ + showSection && ( +
+
+ +
+

+ {headline} +

+

+ {subheadline} +

+
+ + +
    + { + portfolioItems.map((item) => ( + + )) + } +
+ + + +
+
+ ) +} diff --git a/apps/frontend/src/sections/ServiceFeatures.astro b/apps/frontend/src/sections/ServiceFeatures.astro new file mode 100644 index 0000000..6beec67 --- /dev/null +++ b/apps/frontend/src/sections/ServiceFeatures.astro @@ -0,0 +1,70 @@ +--- +/** + * ServiceFeatures - Service features grid section + */ +import ServiceFeatureCard from '@/components/ServiceFeatureCard.astro' +import type { HomeData, ServiceFeature } from '@/lib/api/home' + +interface Props { + homeData?: HomeData | null +} + +const { homeData } = Astro.props + +// Default service features if not provided by CMS +const defaultFeatures: ServiceFeature[] = [ + { + icon: '🎯', + title: 'Google 商家關鍵字', + description: '專業的關鍵字優化服務,提升您在 Google 搜尋結果的排名,讓客戶更容易找到您。', + }, + { + icon: '📱', + title: '社群代操', + description: '全方位社群媒體經營,從內容策劃到數據分析,提供一站式社群行銷服務。', + }, + { + icon: '💬', + title: '論壇行銷', + description: '深耕各大論壇社群,建立品牌口碑,提升用戶信任度與互動參與度。', + }, + { + icon: '🎨', + title: '網站設計', + description: '現代化響應式網站設計,結合美學與功能,打造獨特的品牌形象和優質用戶體驗。', + }, +] + +const serviceFeatures: ServiceFeature[] = (homeData && homeData.serviceFeatures && homeData.serviceFeatures.length > 0) + ? homeData.serviceFeatures + : defaultFeatures +--- + +
+
+ +
+

+ 我們的服務 +

+

+ 提供全方位的數位行銷解決方案,協助您的品牌在數位時代脫穎而出 +

+
+ + +
    + { + serviceFeatures.map((feature) => ( + + )) + } +
+
+
diff --git a/apps/frontend/src/sections/ServicesList.astro b/apps/frontend/src/sections/ServicesList.astro new file mode 100644 index 0000000..e859a4a --- /dev/null +++ b/apps/frontend/src/sections/ServicesList.astro @@ -0,0 +1,373 @@ +--- +/** + * ServicesList - Services list with zig-zag alternating layout + * Pixel-perfect implementation based on Webflow design + */ + +interface ServicesListItem { + id: string + title: string + description: string + category: string + icon?: string + isHot?: boolean + image?: string + link?: string +} + +interface Props { + services?: ServicesListItem[] +} + +const { + services, +} = Astro.props + +// Default services data (can be fetched from CMS) +const defaultServices: ServicesListItem[] = [ + { + id: 'social-media', + title: '社群經營代操', + category: '海洋專案', + icon: 'facebook', + isHot: true, + image: '/placeholder-service-1.jpg', + description: '專業社群媒體經營團隊,從內容策劃、社群經營到數據分析,提供一站式社群代操服務。我們擅長經營 Facebook、Instagram 等主流平台,幫助品牌建立強大的社群影響力。', + }, + { + id: 'google-business', + title: 'Google 商家關鍵字', + category: 'Google', + icon: 'google', + isHot: true, + image: '/placeholder-service-2.jpg', + description: '優化 Google 商家列表,提升在地搜尋排名。透過關鍵字策略、評論管理和商家資訊優化,讓您的商家在 Google 地圖和搜尋結果中脫穎而出。', + }, + { + id: 'google-ads', + title: 'Google Ads 關鍵字', + category: 'Google', + icon: 'ads', + isHot: false, + image: '/placeholder-service-3.jpg', + description: '專業的 Google Ads 投放服務,從關鍵字研究、廣告文案撰寫到出價策略優化,精準觸達目標受眾,最大化廣告投資報酬率。', + }, + { + id: 'news-media', + title: '網路新聞媒體', + category: '媒體行銷', + icon: 'news', + isHot: false, + image: '/placeholder-service-4.jpg', + description: '與各大新聞媒體合作,提供新聞發佈、媒體採訪、品牌曝光等服務。透過專業的新聞行銷策略,提升品牌知名度和公信力。', + }, + { + id: 'influencer', + title: '網紅行銷專案', + category: '口碑行銷', + icon: 'youtube', + isHot: true, + image: '/placeholder-service-5.jpg', + description: '連結品牌與網紅/KOL,打造影響者行銷活動。從網紅篩選、活動策劃到內容製作,提供完整的網紅行銷解決方案,快速建立品牌口碑。', + }, + { + id: 'forum', + title: '論壇行銷專案', + category: '口碑行銷', + icon: 'forum', + isHot: false, + image: '/placeholder-service-6.jpg', + description: '深耕各大論壇社群,包括 Dcard、PTT 等平台。透過專業的論壇行銷策略,建立品牌口碑,提升用戶信任度和互動參與度。', + }, + { + id: 'website-design', + title: '形象網站設計', + category: '品牌行銷', + icon: 'web', + isHot: false, + image: '/placeholder-service-7.jpg', + description: '現代化響應式網站設計服務,結合美學與功能,打造獨特的品牌形象。從 UI/UX 設計到前端開發,提供完整的網站解決方案。', + }, + { + id: 'brand-video', + title: '品牌形象影片', + category: '品牌行銷', + icon: 'video', + isHot: false, + image: '/placeholder-service-8.jpg', + description: '專業影片製作團隊,從腳本創作、拍攝到後製剪輯,打造高品質的品牌形象影片。透過視覺故事說,傳達品牌核心價值。', + }, +] + +const servicesList = services && services.length > 0 ? services : defaultServices + +// Icon SVG components +const icons = { + facebook: ``, + google: ``, + ads: ``, + news: ``, + youtube: ``, + forum: ``, + web: ``, + video: ``, +} +--- + +
+
+ + diff --git a/apps/frontend/src/sections/SolutionsHero.astro b/apps/frontend/src/sections/SolutionsHero.astro new file mode 100644 index 0000000..c0e5e80 --- /dev/null +++ b/apps/frontend/src/sections/SolutionsHero.astro @@ -0,0 +1,94 @@ +--- +/** + * SolutionsHero - Hero section for Solutions/Services page + * Pixel-perfect implementation based on Webflow design + */ +interface Props { + title?: string + subtitle?: string +} + +const { + title = '行銷解決方案', + subtitle = '提供全方位的數位行銷服務,協助您的品牌在數位時代脫穎而出', +} = Astro.props +--- + +
+
+ +

+ {title} +

+ + +

+ {subtitle} +

+
+
+ + diff --git a/apps/frontend/src/sections/StatisticsSection.astro b/apps/frontend/src/sections/StatisticsSection.astro new file mode 100644 index 0000000..2a9957a --- /dev/null +++ b/apps/frontend/src/sections/StatisticsSection.astro @@ -0,0 +1,257 @@ +--- +/** + * StatisticsSection - Data showcase with countup animation + * Features: 4 statistics, countup animation (3s), responsive grid + */ +import type { HomeData } from '@/lib/api/home' + +interface Props { + homeData?: HomeData | null +} + +const { homeData } = Astro.props + +// Statistics data - matching reference HTML exactly +const statistics = [ + { + number: 500, + suffix: '', + percentage: '', + description: '至今協助商家數', + }, + { + number: 150, + suffix: '.0', + percentage: '%', + description: '提升成長率', + }, + { + number: 98, + suffix: '.0', + percentage: '%', + description: '客戶滿意度', + }, + { + number: 95, + suffix: '.0', + percentage: '%', + description: '客戶續約率', + }, +] +--- + + +
+ +
+

+ 數據會說話 +

+

+ Statistics reveal the truth +

+
+ + +
+ { + statistics.map((stat) => ( +
+ +
+ 0 + {stat.suffix} +
+ + + { + stat.percentage && ( + {stat.percentage} + ) + } + + +

{stat.description}

+
+ )) + } +
+
+ + + + diff --git a/apps/frontend/src/sections/TeamsHero.astro b/apps/frontend/src/sections/TeamsHero.astro new file mode 100644 index 0000000..183135b --- /dev/null +++ b/apps/frontend/src/sections/TeamsHero.astro @@ -0,0 +1,95 @@ +--- +/** + * TeamsHero - Hero section for Teams page + * Pixel-perfect implementation based on Webflow design + */ +interface Props { + title?: string + subtitle?: string +} + +const { + title = '恩群大本營', + subtitle = 'Team members of Enchun', +} = Astro.props +--- + +
+
+
+

{title}

+

{subtitle}

+
+
+
+ + diff --git a/apps/frontend/src/styles/theme.css b/apps/frontend/src/styles/theme.css index 9e46b39..0a9a121 100644 --- a/apps/frontend/src/styles/theme.css +++ b/apps/frontend/src/styles/theme.css @@ -1,204 +1,385 @@ -/* Theme CSS Variables and Custom Styles */ +/** + * Theme CSS Variables and Custom Styles + * 恩群數位行銷 - Enchun Digital Marketing + * + * Design tokens extracted from Webflow CSS + * Last Updated: 2026-01-31 + */ + +/* ============================================ + CSS CUSTOM PROPERTIES - DESIGN TOKENS + ============================================ */ -/* CSS Custom Properties for Theme */ :root { - /* Color Palette */ - --color-primary: #1F3A93; - --color-secondary: #F39C12; - --color-accent: #16A085; - --color-enchunblue: #3083BF; - --color-background: #FFFFFF; - --color-surface: #F7FAFC; - --color-text: #1A202C; - --color-text-muted: #718096; - --color-border: #E2E8F0; - /* - Purpose: - Define extended Enchun brand color palette as CSS custom properties, all prefixed with --color- for consistency and semantic clarity. - This ensures all color variables are easily discoverable and maintainable across the codebase. - */ - --color-alabaster: #fafafa; - --color-alto: #d1d1d1; - --color-amber: #ffc107; - --color-black: #000000; - --color-boston-blue: #3083bf; - --color-concrete: #f2f2f2; - --color-cream-can: #f6c456; - --color-dove-gray: #6b6b6b; - --color-dusty-gray: #999999; - --color-emperor: #4f4f4f; - --color-gray: #878787; - --color-killarney: #3c6f50; - --color-lucky-point: #171c61; - --color-manatee: #939494; - --color-mercury: #e3e3e3; - --color-mine-shaft: #333333; - --color-mine-shaft-60: #222222; - --color-nobel: #b6b6b6; - --color-oslo-gray: #939494; - --color-pomegranate: #f44336; - --color-silver: #bdbdbd; - --color-silver-chalice: #acacac; - --color-st-tropaz: #2b618f; - --color-tarawera: #062841; - --color-tropical-blue: #c7e4fa; - --color-tundora: #4d4d4d; - --color-turbo: #ffef00; - --color-valencia: #d84038; - --color-viking: #67aee1; - --color-white: #ffffff; - --color-wild-sand: #f6f6f6; + /* ============================================ + 🎨 COLORS - Extracted from Webflow CSS + ============================================ */ - /* Typography */ - --font-family-sans: 'Noto Sans CJK TC', 'Inter', system-ui, -apple-system, sans-serif; - --font-family-heading: 'Noto Sans CJK TC', 'Inter', system-ui, -apple-system, sans-serif; + /* 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 */ - /* Spacing */ - --spacing-xs: 0.25rem; - --spacing-sm: 0.5rem; - --spacing-md: 1rem; - --spacing-lg: 1.5rem; - --spacing-xl: 2rem; - --spacing-2xl: 3rem; + /* Secondary Colors (次要色) */ + --color-secondary: #f39c12; /* Secondary orange */ + --color-secondary-light: #f6c456; /* Light pink */ + --color-secondary-dark: #d84038; /* Deep orange */ - /* Border Radius */ - --radius-sm: 0.25rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; + /* 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 */ - /* Shadows */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + /* Link Colors (連結色) */ + --color-link: #3083bf; /* Default link */ + --color-link-hover: #23608c; /* Link hover */ - /* Transitions */ - --transition-fast: 150ms ease-in-out; - --transition-normal: 250ms ease-in-out; - --transition-slow: 350ms ease-in-out; + /* 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 */ + + /* 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) */ + + /* 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 */ + + /* Borders (邊框色) */ + --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; /* 恩群數位 */ + + /* Badge Colors (標籤) */ + --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 - 使用灰色系 */ + + /* 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 */ + + /* ============================================ + 🔤 TYPOGRAPHY - From Webflow + ============================================ */ + + --font-family-sans: "Noto Sans TC", "Quicksand", Arial, sans-serif; + --font-family-heading: "Noto Sans TC", "Quicksand", Arial, sans-serif; + --font-family-accent: "Quicksand", "Noto Sans TC", sans-serif; + + /* ============================================ + 📏 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 */ + + /* Container */ + --container-max-width: 1200px; + --container-padding: 1.5rem; + --container-padding-lg: 2rem; + + /* ============================================ + 🔲 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 */ + + /* ============================================ + 💫 SHADOWS + ============================================ */ + + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06); + --shadow-md: + 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: + 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + + /* ============================================ + ⏱ TRANSITIONS + ============================================ */ + + --transition-fast: 150ms ease-in-out; + --transition-base: 200ms ease-in-out; + --transition-normal: 250ms ease-in-out; + --transition-slow: 350ms ease-in-out; + + /* ============================================ + 🎯 Z-INDEX LAYERS + ============================================ */ + + --z-dropdown: 1000; + --z-sticky: 1020; + --z-modal: 1040; + --z-popover: 1060; + --z-tooltip: 1080; + + /* ============================================ + 📱 RESPONSIVE - Font Size Adjustments (Webflow) + ============================================ */ + + /* Webflow breakpoints: + Desktop default: 19px + Tablet (≤991px): 19px + Mobile (≤767px): 16px + 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 */ } -/* Dark Theme (if needed in future) */ -@media (prefers-color-scheme: dark) { - :root { - --color-background: #1A202C; - --color-surface: #2D3748; - --color-text: #F7FAFC; - --color-text-muted: #A0AEC0; - --color-border: #4A5568; - } -} +/* ============================================ + 🌙 DARK MODE (Optional - for future use) + ============================================ */ + +/*@media (prefers-color-scheme: dark) { + :root { + --color-background: #1a202c; + --color-surface: #2d3748; + --color-surface2: #1a202c; + --color-text: #f7fafc; + --color-text-muted: #a0aec0; + --color-text-secondary: #e2e8f0; + --color-border: #4a5568; + } +}*/ + +/* ============================================ + 📐 BASE STYLES + ============================================ */ -/* Base Styles */ * { - box-sizing: border-box; + box-sizing: border-box; } html { - font-family: var(--font-family-sans); - line-height: 1.6; - color: var(--color-text); - background-color: var(--color-background); + font-family: var(--font-family-sans); + line-height: 1.5; + color: var(--color-text-primary); + background-color: var(--color-background); + font-size: var(--html-font-size-desktop); } body { - margin: 0; - padding: 0; - font-family: inherit; - line-height: inherit; - color: inherit; - background-color: inherit; + margin: 0; + padding: 0; + font-family: inherit; + line-height: inherit; + color: inherit; + background-color: inherit; } -/* Typography Classes */ +/* ============================================ + 📱 RESPONSIVE FONT SIZE ADJUSTMENTS + ============================================ */ + +@media (max-width: 991px) { + html { + font-size: var(--html-font-size-tablet); + } +} + +@media (max-width: 767px) { + html { + font-size: var(--html-font-size-mobile); + } +} + +@media (max-width: 479px) { + html { + font-size: var(--html-font-size-small); + } +} + +/* ============================================ + 🎨 UTILITY CLASSES + ============================================ */ + +/* Text Gradient */ .text-gradient { - background: linear-gradient(135deg, var(--color-primary), var(--color-accent)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + background: linear-gradient( + 135deg, + var(--color-primary), + var(--color-accent) + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } -/* Animation Classes */ +/* Animations */ .fade-in { - animation: fadeIn var(--transition-normal) ease-in-out; + animation: fadeIn var(--transition-normal) ease-in-out; } .slide-up { - animation: slideUp var(--transition-normal) ease-in-out; + animation: slideUp var(--transition-normal) ease-in-out; } @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { + opacity: 0; + } + to { + opacity: 1; + } } @keyframes slideUp { - from { - opacity: 0; - transform: translateY(1rem); - } - to { - opacity: 1; - transform: translateY(0); - } + from { + opacity: 0; + transform: translateY(1rem); + } + to { + opacity: 1; + transform: translateY(0); + } } -/* Utility Classes */ +/* Glass Effect */ .glass-effect { - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); } +/* Shadow Custom */ .shadow-custom { - box-shadow: var(--shadow-lg); + box-shadow: var(--shadow-lg); } -/* Component Specific Styles */ +/* ============================================ + 🔘 BUTTON STYLES + ============================================ */ + +.btn-primary { + background: var(--color-primary); + color: white; + padding: var(--spacing-sm) var(--spacing-lg); + border-radius: var(--radius); + border: none; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-primary:hover { + background: var(--color-primary-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background: var(--color-secondary); + color: white; + padding: var(--spacing-sm) var(--spacing-lg); + border-radius: var(--radius); + border: none; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-secondary:hover { + background: var(--color-secondary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +/* ============================================ + 🧭 NAVIGATION STYLES + ============================================ */ + .nav-link { - position: relative; - transition: color var(--transition-fast); + position: relative; + transition: color var(--transition-fast); } .nav-link::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - width: 0; - height: 2px; - background: var(--color-primary); - transition: width var(--transition-fast); + content: ""; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background: var(--color-primary); + transition: width var(--transition-fast); } .nav-link:hover::after { - width: 100%; -} - -/* Active Navigation Link Indicator */ -.nav-active { - position: relative; + width: 100%; } +/* Active Navigation Link */ .nav-active::after { - content: ''; - position: absolute; - bottom: -2px; - left: 50%; - transform: translateX(-50%); - width: 70%; - height: 2px; - background: var(--color-secondary); + content: ""; + position: absolute; + bottom: -2px; + left: 50%; + transform: translateX(-50%); + width: 70%; + height: 2px; + background: var(--color-secondary); } -/* Prose/Markdown Styles */ +/* ============================================ + 📝 PROSE / MARKDOWN STYLES + ============================================ */ + .prose-custom { - color: var(--color-text); - font-family: var(--font-family-sans); + color: var(--color-text-primary); + font-family: var(--font-family-sans); } .prose-custom h1, @@ -207,158 +388,160 @@ body { .prose-custom h4, .prose-custom h5, .prose-custom h6 { - color: var(--color-text); - font-weight: 700; - line-height: 1.2; - margin-top: var(--spacing-xl); - margin-bottom: var(--spacing-md); + color: var(--color-text-primary); + font-weight: 700; + line-height: 1.2; + margin-top: var(--spacing-xl); + margin-bottom: var(--spacing-md); } .prose-custom h1 { - font-size: 2.25rem; + font-size: 2.25rem; } - .prose-custom h2 { - font-size: 1.875rem; + font-size: 1.875rem; } - .prose-custom h3 { - font-size: 1.5rem; + font-size: 1.5rem; } .prose-custom p { - margin-bottom: var(--spacing-md); - line-height: 1.7; + margin-bottom: var(--spacing-md); + line-height: 1.7; } .prose-custom a { - color: var(--color-primary); - text-decoration: none; - transition: color var(--transition-fast); + color: var(--color-link); + text-decoration: none; + transition: color var(--transition-fast); } .prose-custom a:hover { - color: #1a2f7a; - text-decoration: underline; + color: var(--color-link-hover); + text-decoration: underline; } .prose-custom strong { - color: var(--color-text); - font-weight: 600; + color: var(--color-text-primary); + font-weight: 600; } .prose-custom em { - color: var(--color-text-muted); -} - -.prose-custom ul, -.prose-custom ol { - margin-bottom: var(--spacing-md); - padding-left: var(--spacing-lg); -} - -.prose-custom li { - margin-bottom: var(--spacing-xs); - line-height: 1.6; -} - -.prose-custom blockquote { - border-left: 4px solid var(--color-primary); - padding-left: var(--spacing-md); - margin: var(--spacing-lg) 0; - color: var(--color-text-muted); - font-style: italic; - background: var(--color-surface); - padding: var(--spacing-md); - border-radius: var(--radius-md); + color: var(--color-text-muted); } .prose-custom code { - background: var(--color-surface); - color: var(--color-text); - padding: 0.125rem 0.25rem; - border-radius: var(--radius-sm); - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.875em; + background: var(--color-surface); + color: var(--color-text-primary); + padding: 0.125rem 0.25rem; + border-radius: var(--radius-sm); + font-family: "Monaco", "Menlo", monospace; + font-size: 0.875em; } .prose-custom pre { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - padding: var(--spacing-md); - overflow-x: auto; - margin: var(--spacing-lg) 0; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--spacing-md); + overflow-x: auto; + margin: var(--spacing-lg) 0; } -.prose-custom pre code { - background: transparent; - padding: 0; - border-radius: 0; +.prose-custom blockquote { + border-left: 4px solid var(--color-primary); + padding-left: var(--spacing-md); + margin: var(--spacing-lg) 0; + color: var(--color-text-muted); + font-style: italic; + background: var(--color-surface); + padding: var(--spacing-md); + border-radius: var(--radius-md); } -.prose-custom hr { - border: 0; - border-top: 1px solid var(--color-border); - margin: var(--spacing-2xl) 0; -} +/* ============================================ + 📊 TABLE STYLES + ============================================ */ .prose-custom table { - width: 100%; - border-collapse: collapse; - margin: var(--spacing-lg) 0; + width: 100%; + border-collapse: collapse; + margin: var(--spacing-lg) 0; } .prose-custom th, .prose-custom td { - border: 1px solid var(--color-border); - padding: var(--spacing-sm) var(--spacing-md); - text-align: left; + border: 1px solid var(--color-border); + padding: var(--spacing-sm) var(--spacing-md); + text-align: left; } .prose-custom th { - background: var(--color-surface); - font-weight: 600; + background: var(--color-surface); + font-weight: 600; } +/* ============================================ + 🖼️ IMAGE STYLES + ============================================ */ + .prose-custom img { - max-width: 100%; - height: auto; - border-radius: var(--radius-md); - margin: var(--spacing-md) 0; + max-width: 100%; + height: auto; + border-radius: var(--radius-md); + margin: var(--spacing-md) 0; } -/* Button Styles */ -.btn-primary { - background: var(--color-primary); - color: white; - padding: var(--spacing-sm) var(--spacing-lg); - border-radius: var(--radius-md); - border: none; - font-weight: 600; - cursor: pointer; - transition: all var(--transition-fast); +/* ============================================ + 📏 HR (DIVIDER) STYLES + ============================================ */ + +.prose-custom hr { + border: 0; + border-top: 1px solid var(--color-border); + margin: var(--spacing-2xl) 0; } -.btn-primary:hover { - background: #1a2f7a; - transform: translateY(-1px); - box-shadow: var(--shadow-md); +/* ============================================ + 🏷️ CATEGORY BADGES + ============================================ */ + +.badge { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; } -.btn-secondary { - background: var(--color-secondary); - color: white; - padding: var(--spacing-sm) var(--spacing-lg); - border-radius: var(--radius-md); - border: none; - font-weight: 600; - cursor: pointer; - transition: all var(--transition-fast); +.badge-hot { + background-color: var(--color-badge-hot); + color: white; } -.btn-secondary:hover { - background: #e08e0b; - transform: translateY(-1px); - box-shadow: var(--shadow-md); -} \ No newline at end of file +.badge-new { + background-color: var(--color-badge-new); + color: white; +} + +/* Category Badge Colors */ +.badge-category-google { + background-color: var(--color-category-google); + color: white; +} + +.badge-category-meta { + background-color: var(--color-category-meta); + color: white; +} + +.badge-category-news { + background-color: var(--color-category-news); + color: white; +} + +.badge-category-enchun { + background-color: var(--color-category-enchun); + color: white; +} diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json index 040b811..3e4839f 100644 --- a/apps/frontend/tsconfig.json +++ b/apps/frontend/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { + "@/*": ["./src/*"], "@shared/*": ["../packages/shared/src/*"] } }, diff --git a/apps/frontend/wrangler.jsonc b/apps/frontend/wrangler.jsonc new file mode 100644 index 0000000..32d1bcd --- /dev/null +++ b/apps/frontend/wrangler.jsonc @@ -0,0 +1,23 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "enchun-frontend", + "main": "./dist/_worker.js/index.js", + "compatibility_date": "2025-01-19", + "compatibility_flags": [ + "nodejs_compat" + ], + "assets": { + "directory": "./dist" + }, + "vars": { + "PAYLOAD_CMS_URL": "https://enchun-admin.anlstudio.cc" + }, + "env": { + "production": { + "name": "enchun-frontend-production", + "vars": { + "PAYLOAD_CMS_URL": "https://enchun-admin.anlstudio.cc" + } + } + } +} \ No newline at end of file