Compare commits
14 Commits
b1a8006f12
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 959eaa3d9d | |||
| c0f7ad7458 | |||
| cbe44ffd36 | |||
| acc23b13b6 | |||
| 1d84107147 | |||
| f17523fd91 | |||
| e7344fa7fe | |||
| b5a8e9a1e6 | |||
| 03760b23a5 | |||
| 2e32d52133 | |||
| df1efb4881 | |||
| 84b5a498e6 | |||
| 173905ecd3 | |||
| b199f89998 |
10
CLAUDE.md
10
CLAUDE.md
@@ -6,11 +6,11 @@ Astro frontend + Payload CMS backend monorepo for website migration.
|
|||||||
|
|
||||||
| Command | Purpose |
|
| Command | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `pnpm install` | Sync dependencies |
|
| `bun install` | Sync dependencies |
|
||||||
| `pnpm dev` | Start dev server (Astro at :4321) |
|
| `bun dev` | Start dev server (Astro at :4321) |
|
||||||
| `pnpm test:unit` | Run Vitest tests |
|
| `bun test:unit` | Run Vitest tests |
|
||||||
| `pnpm test:e2e` | Run Playwright tests |
|
| `bun test:e2e` | Run Playwright tests |
|
||||||
| `pnpm build` | Production build |
|
| `bun build` | Production build |
|
||||||
|
|
||||||
## Module Locations
|
## Module Locations
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,17 @@
|
|||||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||||
"postbuild": "next-sitemap --config next-sitemap.config.cjs",
|
"postbuild": "next-sitemap --config next-sitemap.config.cjs",
|
||||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --port 3000",
|
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --port 3000",
|
||||||
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && pnpm build && pnpm start",
|
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation rm -rf .next && bun run build && bun run start",
|
||||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||||
"ii": "cross-env NODE_OPTIONS=--no-deprecation pnpm --ignore-workspace install",
|
"ii": "cross-env NODE_OPTIONS=--no-deprecation bun install",
|
||||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||||
"lint:fix": "cross-env NODE_OPTIONS=--no-deprecation next lint --fix",
|
"lint:fix": "cross-env NODE_OPTIONS=--no-deprecation next lint --fix",
|
||||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||||
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
|
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm bun.lockb && bun install",
|
||||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
||||||
"test": "pnpm run test:int && pnpm run test:e2e",
|
"test": "bun run test:int && bun run test:e2e",
|
||||||
"test:e2e": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" pnpm exec playwright test --config=playwright.config.ts",
|
"test:e2e": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" bunx playwright test --config=playwright.config.ts",
|
||||||
"test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts",
|
"test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts",
|
||||||
"test:load": "k6 run tests/k6/public-browsing.js",
|
"test:load": "k6 run tests/k6/public-browsing.js",
|
||||||
"test:load:all": "k6 run tests/k6/public-browsing.js && k6 run tests/k6/api-performance.js",
|
"test:load:all": "k6 run tests/k6/public-browsing.js && k6 run tests/k6/api-performance.js",
|
||||||
@@ -93,9 +93,5 @@
|
|||||||
"typescript": "5.7.3",
|
"typescript": "5.7.3",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "3.2.3"
|
"vitest": "3.2.3"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.20.2 || >=20.9.0",
|
|
||||||
"pnpm": "^9 || ^10"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11203
apps/backend/pnpm-lock.yaml
generated
11203
apps/backend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -16,25 +16,9 @@ import { generateMeta } from '@/utilities/generateMeta'
|
|||||||
import PageClient from './page.client'
|
import PageClient from './page.client'
|
||||||
import { LivePreviewListener } from '@/components/LivePreviewListener'
|
import { LivePreviewListener } from '@/components/LivePreviewListener'
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
// Use dynamic rendering instead of static generation
|
||||||
const payload = await getPayload({ config: configPromise })
|
// This avoids the need for database connection during build time
|
||||||
const posts = await payload.find({
|
export const dynamic = 'force-dynamic'
|
||||||
collection: 'posts',
|
|
||||||
draft: false,
|
|
||||||
limit: 1000,
|
|
||||||
overrideAccess: false,
|
|
||||||
pagination: false,
|
|
||||||
select: {
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const params = posts.docs.map(({ slug }) => {
|
|
||||||
return { slug }
|
|
||||||
})
|
|
||||||
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { getPayload } from 'payload'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PageClient from './page.client'
|
import PageClient from './page.client'
|
||||||
|
|
||||||
export const dynamic = 'force-static'
|
// Use dynamic rendering to avoid database connection during build
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
export const revalidate = 600
|
export const revalidate = 600
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import React from 'react'
|
|||||||
import PageClient from './page.client'
|
import PageClient from './page.client'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
// Use dynamic rendering to avoid database connection during build
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
export const revalidate = 600
|
export const revalidate = 600
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
|||||||
86
apps/backend/src/blocks/ServicesList/config.ts
Normal file
86
apps/backend/src/blocks/ServicesList/config.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const ServicesList: Block = {
|
||||||
|
slug: 'servicesList',
|
||||||
|
interfaceName: 'ServicesListBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'services',
|
||||||
|
type: 'array',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
initCollapsed: true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'category',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'iconType',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'preset',
|
||||||
|
options: [
|
||||||
|
{ label: 'Preset Icon', value: 'preset' },
|
||||||
|
{ label: 'Custom SVG', value: 'svg' },
|
||||||
|
{ label: 'Upload Image', value: 'upload' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'icon',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'Preset icon name: facebook, google, ads, news, youtube, forum, web, video',
|
||||||
|
condition: (_, siblingData) => siblingData?.iconType === 'preset',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'iconSvg',
|
||||||
|
type: 'textarea',
|
||||||
|
admin: {
|
||||||
|
description: 'Paste SVG code directly (e.g., <svg>...</svg>)',
|
||||||
|
condition: (_, siblingData) => siblingData?.iconType === 'svg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'iconImage',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
admin: {
|
||||||
|
description: 'Upload an icon image (SVG, PNG)',
|
||||||
|
condition: (_, siblingData) => siblingData?.iconType === 'upload',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isHot',
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'link',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
labels: {
|
||||||
|
singular: 'Services List',
|
||||||
|
plural: 'Services Lists',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { Archive } from '../../blocks/ArchiveBlock/config'
|
|||||||
import { CallToAction } from '../../blocks/CallToAction/config'
|
import { CallToAction } from '../../blocks/CallToAction/config'
|
||||||
import { Content } from '../../blocks/Content/config'
|
import { Content } from '../../blocks/Content/config'
|
||||||
import { MediaBlock } from '../../blocks/MediaBlock/config'
|
import { MediaBlock } from '../../blocks/MediaBlock/config'
|
||||||
|
import { ServicesList } from '../../blocks/ServicesList/config'
|
||||||
import { hero } from '@/heros/config'
|
import { hero } from '@/heros/config'
|
||||||
import { slugField } from '@/fields/slug'
|
import { slugField } from '@/fields/slug'
|
||||||
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
|
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
|
||||||
@@ -76,7 +77,7 @@ export const Pages: CollectionConfig<'pages'> = {
|
|||||||
{
|
{
|
||||||
name: 'layout',
|
name: 'layout',
|
||||||
type: 'blocks',
|
type: 'blocks',
|
||||||
blocks: [CallToAction, Content, MediaBlock, Archive],
|
blocks: [CallToAction, Content, MediaBlock, Archive, ServicesList],
|
||||||
required: true,
|
required: true,
|
||||||
admin: {
|
admin: {
|
||||||
initCollapsed: true,
|
initCollapsed: true,
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export interface Page {
|
|||||||
| null;
|
| null;
|
||||||
media?: (string | null) | Media;
|
media?: (string | null) | Media;
|
||||||
};
|
};
|
||||||
layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock)[];
|
layout: (CallToActionBlock | ContentBlock | MediaBlock | ArchiveBlock | ServicesListBlock)[];
|
||||||
meta?: {
|
meta?: {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
/**
|
/**
|
||||||
@@ -564,6 +564,37 @@ export interface ArchiveBlock {
|
|||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'archive';
|
blockType: 'archive';
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ServicesListBlock".
|
||||||
|
*/
|
||||||
|
export interface ServicesListBlock {
|
||||||
|
services: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
iconType?: ('preset' | 'svg' | 'upload') | null;
|
||||||
|
/**
|
||||||
|
* Preset icon name: facebook, google, ads, news, youtube, forum, web, video
|
||||||
|
*/
|
||||||
|
icon?: string | null;
|
||||||
|
/**
|
||||||
|
* Paste SVG code directly (e.g., <svg>...</svg>)
|
||||||
|
*/
|
||||||
|
iconSvg?: string | null;
|
||||||
|
/**
|
||||||
|
* Upload an icon image (SVG, PNG)
|
||||||
|
*/
|
||||||
|
iconImage?: (string | null) | Media;
|
||||||
|
isHot?: boolean | null;
|
||||||
|
image?: (string | null) | Media;
|
||||||
|
link?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[];
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'servicesList';
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "portfolio".
|
* via the `definition` "portfolio".
|
||||||
@@ -923,6 +954,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
content?: T | ContentBlockSelect<T>;
|
content?: T | ContentBlockSelect<T>;
|
||||||
mediaBlock?: T | MediaBlockSelect<T>;
|
mediaBlock?: T | MediaBlockSelect<T>;
|
||||||
archive?: T | ArchiveBlockSelect<T>;
|
archive?: T | ArchiveBlockSelect<T>;
|
||||||
|
servicesList?: T | ServicesListBlockSelect<T>;
|
||||||
};
|
};
|
||||||
meta?:
|
meta?:
|
||||||
| T
|
| T
|
||||||
@@ -1011,6 +1043,29 @@ export interface ArchiveBlockSelect<T extends boolean = true> {
|
|||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "ServicesListBlock_select".
|
||||||
|
*/
|
||||||
|
export interface ServicesListBlockSelect<T extends boolean = true> {
|
||||||
|
services?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
description?: T;
|
||||||
|
category?: T;
|
||||||
|
iconType?: T;
|
||||||
|
icon?: T;
|
||||||
|
iconSvg?: T;
|
||||||
|
iconImage?: T;
|
||||||
|
isHot?: T;
|
||||||
|
image?: T;
|
||||||
|
link?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "posts_select".
|
* via the `definition` "posts_select".
|
||||||
|
|||||||
1
apps/frontend/.assetsignore
Normal file
1
apps/frontend/.assetsignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
_worker.js
|
||||||
@@ -13,6 +13,9 @@ export default defineConfig({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
configPath: './wrangler.jsonc',
|
configPath: './wrangler.jsonc',
|
||||||
},
|
},
|
||||||
|
session: {
|
||||||
|
persistence: 'none',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
|
|||||||
@@ -3,3 +3,10 @@
|
|||||||
|
|
||||||
# Production Payload CMS URL (for SSR fetch)
|
# Production Payload CMS URL (for SSR fetch)
|
||||||
PAYLOAD_CMS_URL=https://enchun-admin.anlstudio.cc
|
PAYLOAD_CMS_URL=https://enchun-admin.anlstudio.cc
|
||||||
|
|
||||||
|
# Cloudflare Turnstile
|
||||||
|
CF_TURNSTILE_SITE_KEY=0x4AAAAAACkOUZK2u7Fo8IZ-
|
||||||
|
CF_TURNSTILE_SECRET_KEY=0x4AAAAAACkOUV9TDUOwVdcdLUakEVxJjww
|
||||||
|
|
||||||
|
# n8n webhook
|
||||||
|
N8N_WEBHOOK_URL=https://n8n.anlstudio.cc/webhook/contact
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev --host --port 4321",
|
"dev": "astro dev --host --port 4321",
|
||||||
"dev:pages": "wrangler pages dev --compatibility-date=2024-01-01",
|
"dev:pages": "wrangler pages dev --compatibility-date=2024-01-01",
|
||||||
"build": "astro build",
|
"build": "astro build && cp .assetsignore dist/",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"check": "astro check",
|
"check": "astro check",
|
||||||
"typecheck": "astro check",
|
"typecheck": "astro check",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"agentation": "^2.1.1",
|
"agentation": "^2.1.1",
|
||||||
"agentation-mcp": "^1.1.0",
|
"agentation-mcp": "^1.1.0",
|
||||||
"astro": "6.0.0-beta.1",
|
"astro": "6.0.0-beta.17",
|
||||||
"better-auth": "^1.3.13"
|
"better-auth": "^1.3.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
34
apps/frontend/src/components/CtaSection.astro
Normal file
34
apps/frontend/src/components/CtaSection.astro
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* CTA Section Component
|
||||||
|
*/
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="py-15 px-5 md:py-20 bg-white text-center mx-auto"
|
||||||
|
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>
|
||||||
@@ -42,8 +42,8 @@ try {
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer class="bg-[var(--color-tropical-blue)] py-10 mt-auto relative">
|
<footer class="bg-(--color-tropical-blue) pt-10 mt-auto">
|
||||||
<div class="max-w-5xl mx-auto px-4">
|
<div class="max-w-4xl mx-auto px-12">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8 mb-16">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8 mb-16">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<Image
|
<Image
|
||||||
@@ -56,23 +56,21 @@ const currentYear = new Date().getFullYear();
|
|||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
class="text-[var(--color-st-tropaz)] text-sm font-light leading-relaxed"
|
class="text-(--color-st-tropaz) text-sm pr-0 md:pr-8 font-light leading-relaxed"
|
||||||
>
|
>
|
||||||
恩群數位累積多年廣告行銷操作經驗,擁有全方位行銷人才,讓我們可以為客戶精準的規劃每一分廣告預算,讓你的品牌深入人心。更重要的是恩群的存在,為了成為每家公司最佳數位夥伴,作為彼此最堅強的後盾,你會知道有我們的陪伴
|
恩群數位累積多年廣告行銷操作經驗,擁有全方位行銷人才,讓我們可以為客戶精準的規劃每一分廣告預算,讓你的品牌深入人心。更重要的是恩群的存在,為了成為每家公司最佳數位夥伴,作為彼此最堅強的後盾,你會知道有我們的陪伴
|
||||||
你並不孤單。
|
你並不孤單。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3 class="text-lg font-bold text-(--color-st-tropaz) mb-4">
|
||||||
class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4"
|
|
||||||
>
|
|
||||||
聯絡我們
|
聯絡我們
|
||||||
</h3>
|
</h3>
|
||||||
<a
|
<a
|
||||||
href="https://www.facebook.com/EnChun-Taiwan-100979265112420"
|
href="https://www.facebook.com/EnChun-Taiwan-100979265112420"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="flex items-center mb-2"
|
class="flex items-center mb-2 no-underline hover:underline transition-colors"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/fb-icon.svg"
|
src="/fb-icon.svg"
|
||||||
@@ -84,74 +82,77 @@ const currentYear = new Date().getFullYear();
|
|||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<p class="text-[var(--color-st-tropaz)] mb-2">
|
<p class="text-sm text-(--color-st-tropaz) mb-2">
|
||||||
諮詢電話:<br /> 02 5570 0527
|
諮詢電話:<br /> 02 5570 0527
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="mailto:enchuntaiwan@gmail.com"
|
href="mailto:enchuntaiwan@gmail.com"
|
||||||
class="text-primary hover:text-secondary transition-colors"
|
class="text-xs text-primary hover:text-secondary no-underline hover:underline transition-colors"
|
||||||
>enchuntaiwan@gmail.com</a
|
>enchuntaiwan@gmail.com</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3 class="text-lg font-bold text-(--color-st-tropaz) mb-4">
|
||||||
class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4"
|
|
||||||
>
|
|
||||||
行銷方案
|
行銷方案
|
||||||
</h3>
|
</h3>
|
||||||
<ul class="space-y-2" id="marketing-solutions">
|
<ul
|
||||||
{footerNavItems.length > 0 && footerNavItems[0]?.childNavItems
|
class="text-sm font-thin space-y-2"
|
||||||
? footerNavItems[0].childNavItems.map((item: any) => (
|
id="marketing-solutions"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
footerNavItems.length > 0 &&
|
||||||
|
footerNavItems[0]?.childNavItems ? (
|
||||||
|
footerNavItems[0].childNavItems.map((item: any) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={item.link?.url || "#"}
|
||||||
|
class="text-(--color-st-tropaz) hover:text-(--color-dark-blue) hover:font-light no-underline transition-colors duration-300"
|
||||||
|
>
|
||||||
|
{item.link?.label || "連結"}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<li>
|
<li>
|
||||||
<a href={item.link?.url || "#"} class="font-normal text-[var(--color-st-tropaz)] hover:text-[var(--color-dove-gray)] transition-colors">
|
<span class="text-gray-500">載入中...</span>
|
||||||
{item.link?.label || "連結"}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
))
|
)
|
||||||
: <li><span class="text-gray-500">載入中...</span></li>
|
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3 class="text-lg font-bold text-(--color-st-tropaz) mb-4">
|
||||||
class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4"
|
|
||||||
>
|
|
||||||
行銷放大鏡
|
行銷放大鏡
|
||||||
</h3>
|
</h3>
|
||||||
<ul class="space-y-2" id="marketing-articles">
|
<ul class="text-sm font-thin space-y-2" id="marketing-articles">
|
||||||
{categories.length > 0
|
{
|
||||||
? categories.map((cat: any) => (
|
categories.length > 0 ? (
|
||||||
|
categories.map((cat: any) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={`/blog/category/${cat.slug}`}
|
||||||
|
class="text-(--color-st-tropaz) hover:text-(--color-dark-blue) hover:font-light no-underline transition-colors duration-300"
|
||||||
|
title={cat.nameEn || cat.title}
|
||||||
|
>
|
||||||
|
{cat.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/blog/category/${cat.slug}`}
|
<span class="text-gray-500">暫無分類</span>
|
||||||
class="font-normal text-[var(--color-st-tropaz)] hover:text-[var(--color-dove-gray)] transition-colors"
|
|
||||||
title={cat.nameEn || cat.title}>
|
|
||||||
{cat.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
))
|
)
|
||||||
: <li><span class="text-gray-500">暫無分類</span></li>
|
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
class="absolute inset-x-0 w-screen bg-[var(--color-amber)] py-3 text-center -left-4"
|
<div
|
||||||
>
|
class="w-screen bg-(--color-amber) font-['Quicksand'] text-(--color-tarawera) py-2 text-xs text-center"
|
||||||
<p class="text-[var(--color-tarawera)]">
|
>
|
||||||
copyright © Enchun digital 2018 - {currentYear}
|
<p>
|
||||||
</p>
|
copyright © Enchun digital 2018 - {currentYear}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Footer specific styles */
|
|
||||||
footer a {
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,29 +1,65 @@
|
|||||||
---
|
---
|
||||||
import { Image } from "astro:assets";
|
|
||||||
// Header component with scroll-based background and enhanced mobile animations
|
// 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
|
// Use local backend in development, production URL from dev.vars/wrangler
|
||||||
const isDev = import.meta.env.DEV;
|
const isDev = import.meta.env.DEV;
|
||||||
const PAYLOAD_CMS_URL = isDev
|
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";
|
: import.meta.env.PAYLOAD_CMS_URL || "https://enchun-admin.anlstudio.cc";
|
||||||
|
|
||||||
// Fetch navigation data from Payload CMS server-side
|
// Fetch navigation data from Payload CMS server-side
|
||||||
let navItems: any[] = [];
|
let navItems: NavItem[] = [];
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
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) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
navItems = data?.navItems || data || [];
|
navItems = data?.navItems || [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Header SSR] Failed to fetch navigation:", 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
|
// Helper to get link URL
|
||||||
function getLinkUrl(link: any): string {
|
function getLinkUrl(link: NavLink): string {
|
||||||
if (!link) return "#";
|
if (!link) return "#";
|
||||||
|
|
||||||
if (link.type === "custom" && link.url) {
|
if (link.type === "custom" && link.url) {
|
||||||
@@ -32,10 +68,8 @@ function getLinkUrl(link: any): string {
|
|||||||
|
|
||||||
if (link.type === "reference" && link.reference?.value) {
|
if (link.type === "reference" && link.reference?.value) {
|
||||||
if (typeof link.reference.value === "string") {
|
if (typeof link.reference.value === "string") {
|
||||||
// It's an ID, construct URL based on relationTo
|
|
||||||
return `/${link.reference.relationTo || "pages"}/${link.reference.value}`;
|
return `/${link.reference.relationTo || "pages"}/${link.reference.value}`;
|
||||||
} else if (link.reference.value.slug) {
|
} else if (link.reference.value.slug) {
|
||||||
// It's a populated object with slug
|
|
||||||
return `/${link.reference.value.slug}`;
|
return `/${link.reference.value.slug}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,26 +77,21 @@ function getLinkUrl(link: any): string {
|
|||||||
return "#";
|
return "#";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if label should have a badge
|
// Return badge type for label (data-driven, no HTML strings)
|
||||||
function getBadgeForLabel(label: string): string {
|
function getBadgeType(label: string): "hot" | "new" | null {
|
||||||
if (label.includes("行銷方案")) {
|
if (label.includes("行銷方案")) return "hot";
|
||||||
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 "new";
|
||||||
}
|
return null;
|
||||||
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 "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if link is active
|
// Check if link is active
|
||||||
function isLinkActive(url: string): boolean {
|
function isLinkActive(url: string): boolean {
|
||||||
const currentPath = Astro.url.pathname;
|
return Astro.url.pathname === url;
|
||||||
return currentPath === url || (url === "/" && currentPath === "/");
|
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<header
|
<header
|
||||||
class="fixed top-0 left-0 right-0 z-50 transition-transform duration-300 ease-in-out"
|
class="fixed top-0 left-0 right-0 z-50 transition-all duration-300 ease-in-out will-change-transform"
|
||||||
id="main-header"
|
id="main-header"
|
||||||
>
|
>
|
||||||
<nav
|
<nav
|
||||||
@@ -72,46 +101,51 @@ function isLinkActive(url: string): boolean {
|
|||||||
<ul class="flex items-center justify-between list-none">
|
<ul class="flex items-center justify-between list-none">
|
||||||
<li class="shrink-0">
|
<li class="shrink-0">
|
||||||
<a href="/" class="block">
|
<a href="/" class="block">
|
||||||
<!-- Uses Astro's optimized Image component for the site logo -->
|
<img
|
||||||
<Image
|
|
||||||
src="/enchun-logo.svg"
|
src="/enchun-logo.svg"
|
||||||
alt="Enchun Digital Marketing"
|
alt="Enchun Digital Marketing"
|
||||||
class="w-32 h-auto transition-opacity duration-300 drop-shadow-md"
|
class="w-32 h-auto transition-transform duration-300 drop-shadow-md"
|
||||||
width={919}
|
width="919"
|
||||||
height={201}
|
height="201"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="hidden md:flex items-center space-x-6">
|
{
|
||||||
{
|
navItems.map((item) => {
|
||||||
navItems.map((item) => {
|
const link = item.link;
|
||||||
const link = item.link;
|
const href = getLinkUrl(link);
|
||||||
const href = getLinkUrl(link);
|
const label = link.label || "未命名";
|
||||||
const label = link.label || "未命名";
|
const badge = getBadgeType(label);
|
||||||
const badge = getBadgeForLabel(label);
|
const active = isLinkActive(href);
|
||||||
const activeClass = isLinkActive(href)
|
|
||||||
? "nav-active"
|
|
||||||
: "";
|
|
||||||
const hasBadge = badge ? "relative inline-block" : "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<li class="hidden md:block">
|
||||||
<a
|
<a
|
||||||
href={href}
|
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 && {
|
{...(link.newTab && {
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
rel: "noopener noreferrer",
|
rel: "noopener noreferrer",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{label}
|
{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>
|
</a>
|
||||||
);
|
</li>
|
||||||
})
|
);
|
||||||
}
|
})
|
||||||
</li>
|
}
|
||||||
<!-- Mobile menu button with animated hamburger/X icon -->
|
<!-- Mobile menu button with animated hamburger/X icon -->
|
||||||
<li class="md:hidden">
|
<li class="md:hidden">
|
||||||
<button
|
<button
|
||||||
@@ -154,55 +188,57 @@ function isLinkActive(url: string): boolean {
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- Mobile menu with full-screen overlay -->
|
</nav>
|
||||||
<div
|
<!-- Mobile menu with full-screen overlay (separate nav for semantics) -->
|
||||||
class="md:hidden fixed left-0 right-0 top-20 h-[calc(100vh-80px)] h-[calc(100dvh-80px)] bg-white/20 backdrop-blur-lg opacity-0 invisible transition-all duration-300 ease-in-out"
|
<nav
|
||||||
id="mobile-menu"
|
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"
|
||||||
|
id="mobile-nav"
|
||||||
>
|
>
|
||||||
<ul
|
{
|
||||||
class="flex flex-col items-center justify-center h-full space-y-6 px-4"
|
navItems.map((item, index) => {
|
||||||
id="mobile-nav"
|
const link = item.link;
|
||||||
>
|
const href = getLinkUrl(link);
|
||||||
{
|
const label = link.label || "未命名";
|
||||||
navItems.map((item, index) => {
|
|
||||||
const link = item.link;
|
|
||||||
const href = getLinkUrl(link);
|
|
||||||
const label = link.label || "未命名";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
class={`opacity-0`}
|
class="mobile-nav-item"
|
||||||
style={`animation-delay: ${index * 50}ms`}
|
style={`animation-delay: ${index * 50}ms`}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
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",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<a
|
{label}
|
||||||
href={href}
|
</a>
|
||||||
class="text-2xl text-grey-700 text-shadow-sm font-medium transition-all duration-200 transform hover:scale-105"
|
</li>
|
||||||
{...(link.newTab && {
|
);
|
||||||
target: "_blank",
|
})
|
||||||
rel: "noopener noreferrer",
|
}
|
||||||
})}
|
</ul>
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Scroll-based header with smooth hide/show animation
|
// Scroll-based header with smooth hide/show animation
|
||||||
|
// Uses AbortController for cleanup on re-mount (View Transitions)
|
||||||
let lastScrollY = 0;
|
let lastScrollY = 0;
|
||||||
let ticking = false;
|
let ticking = false;
|
||||||
let isHeaderHidden = false;
|
let isHeaderHidden = false;
|
||||||
|
let abortController: AbortController | null = null;
|
||||||
|
|
||||||
function initScrollEffect() {
|
function initScrollEffect() {
|
||||||
const header = document.getElementById("main-header");
|
const header = document.getElementById("main-header");
|
||||||
const nav = document.getElementById("main-nav");
|
const nav = document.getElementById("main-nav");
|
||||||
const mobileMenu = document.getElementById("mobile-menu");
|
|
||||||
if (!header || !nav) return;
|
if (!header || !nav) return;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@@ -210,90 +246,36 @@ function isLinkActive(url: string): boolean {
|
|||||||
const scrollDelta = scrollY - lastScrollY;
|
const scrollDelta = scrollY - lastScrollY;
|
||||||
const scrollDirection = scrollDelta > 0 ? "down" : "up";
|
const scrollDirection = scrollDelta > 0 ? "down" : "up";
|
||||||
|
|
||||||
// Only update lastScrollY if there's meaningful movement
|
|
||||||
if (Math.abs(scrollDelta) > 1) {
|
if (Math.abs(scrollDelta) > 1) {
|
||||||
lastScrollY = scrollY;
|
lastScrollY = scrollY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header shrinks and gets background on scroll
|
// Toggle single class for scrolled state (CSS handles all visuals)
|
||||||
if (scrollY > 10) {
|
if (scrollY > 10) {
|
||||||
if (!header.classList.contains("header-scrolled")) {
|
header.classList.add("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)]",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (header.classList.contains("header-scrolled")) {
|
header.classList.remove("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)]",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide/show header based on scroll direction - with threshold
|
// Hide/show header based on scroll direction
|
||||||
const hideThreshold = 150; // Higher threshold before hiding
|
const hideThreshold = 150;
|
||||||
const showThreshold = 50; // Lower threshold to show (makes it feel smoother)
|
const showThreshold = 50;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
scrollY > hideThreshold &&
|
scrollY > hideThreshold &&
|
||||||
scrollDirection === "down" &&
|
scrollDirection === "down" &&
|
||||||
!isHeaderHidden
|
!isHeaderHidden
|
||||||
) {
|
) {
|
||||||
// Only hide if scrolling down consistently
|
|
||||||
if (scrollDelta > 5) {
|
if (scrollDelta > 5) {
|
||||||
// Must be scrolling down with some speed
|
|
||||||
header.classList.remove("translate-in");
|
header.classList.remove("translate-in");
|
||||||
header.classList.add("translate-out");
|
header.classList.add("translate-out");
|
||||||
isHeaderHidden = true;
|
isHeaderHidden = true;
|
||||||
}
|
}
|
||||||
} else if (scrollDirection === "up" && isHeaderHidden) {
|
} else if (scrollDirection === "up" && isHeaderHidden) {
|
||||||
// Show immediately when scrolling up
|
|
||||||
header.classList.remove("translate-out");
|
header.classList.remove("translate-out");
|
||||||
header.classList.add("translate-in");
|
header.classList.add("translate-in");
|
||||||
isHeaderHidden = false;
|
isHeaderHidden = false;
|
||||||
} else if (scrollY < showThreshold) {
|
} else if (scrollY < showThreshold) {
|
||||||
// Always show when near top
|
|
||||||
header.classList.remove("translate-out");
|
header.classList.remove("translate-out");
|
||||||
header.classList.add("translate-in");
|
header.classList.add("translate-in");
|
||||||
isHeaderHidden = false;
|
isHeaderHidden = false;
|
||||||
@@ -302,44 +284,23 @@ function isLinkActive(url: string): boolean {
|
|||||||
ticking = false;
|
ticking = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const signal = abortController!.signal;
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"scroll",
|
"scroll",
|
||||||
() => {
|
() => {
|
||||||
if (!ticking) {
|
if (!ticking) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(handleScroll);
|
||||||
handleScroll();
|
|
||||||
ticking = false;
|
|
||||||
});
|
|
||||||
ticking = true;
|
ticking = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ passive: true },
|
{ passive: true, signal },
|
||||||
);
|
);
|
||||||
|
|
||||||
handleScroll();
|
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() {
|
function initMobileMenu() {
|
||||||
if (mobileMenuInitialized) return;
|
|
||||||
|
|
||||||
const button = document.getElementById("mobile-menu-button");
|
const button = document.getElementById("mobile-menu-button");
|
||||||
const menu = document.getElementById("mobile-menu");
|
const menu = document.getElementById("mobile-menu");
|
||||||
const hamburgerIcon = document.getElementById("hamburger-icon");
|
const hamburgerIcon = document.getElementById("hamburger-icon");
|
||||||
@@ -348,88 +309,98 @@ function isLinkActive(url: string): boolean {
|
|||||||
.getElementById("mobile-nav")
|
.getElementById("mobile-nav")
|
||||||
?.querySelectorAll("li");
|
?.querySelectorAll("li");
|
||||||
|
|
||||||
if (!button || !menu || !hamburgerIcon || !closeIcon) {
|
if (!button || !menu || !hamburgerIcon || !closeIcon) return;
|
||||||
// Retry after a delay if elements aren't ready
|
|
||||||
setTimeout(initMobileMenu, 100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.addEventListener("click", () => {
|
const signal = abortController!.signal;
|
||||||
const isMenuOpen = menu.classList.contains("opacity-100");
|
|
||||||
|
|
||||||
if (isMenuOpen) {
|
button.addEventListener(
|
||||||
menu.classList.remove("opacity-100", "visible");
|
"click",
|
||||||
menu.classList.add("opacity-0", "invisible");
|
() => {
|
||||||
hamburgerIcon.classList.remove("opacity-0", "rotate-90");
|
const isMenuOpen = menu.classList.contains("opacity-100");
|
||||||
hamburgerIcon.classList.add("opacity-100");
|
|
||||||
closeIcon.classList.remove("opacity-100");
|
|
||||||
closeIcon.classList.add("opacity-0");
|
|
||||||
button.setAttribute("aria-expanded", "false");
|
|
||||||
|
|
||||||
mobileNavItems?.forEach((item) => {
|
if (isMenuOpen) {
|
||||||
item.classList.remove("opacity-100", "translate-y-0");
|
menu.classList.remove("opacity-100", "visible");
|
||||||
item.classList.add("opacity-0", "translate-y-4");
|
menu.classList.add("opacity-0", "invisible");
|
||||||
});
|
hamburgerIcon.classList.remove("opacity-0", "rotate-90");
|
||||||
|
hamburgerIcon.classList.add("opacity-100");
|
||||||
|
closeIcon.classList.remove("opacity-100");
|
||||||
|
closeIcon.classList.add("opacity-0");
|
||||||
|
button.setAttribute("aria-expanded", "false");
|
||||||
|
|
||||||
document.body.style.overflow = "";
|
mobileNavItems?.forEach((item) => {
|
||||||
} else {
|
item.classList.remove("mobile-nav-visible");
|
||||||
menu.classList.remove("opacity-0", "invisible");
|
item.classList.add("mobile-nav-hidden");
|
||||||
menu.classList.add("opacity-100", "visible");
|
});
|
||||||
hamburgerIcon.classList.remove("opacity-100");
|
|
||||||
hamburgerIcon.classList.add("opacity-0", "rotate-90");
|
|
||||||
closeIcon.classList.remove("opacity-0");
|
|
||||||
closeIcon.classList.add("opacity-100");
|
|
||||||
button.setAttribute("aria-expanded", "true");
|
|
||||||
|
|
||||||
mobileNavItems?.forEach((item, index) => {
|
document.documentElement.classList.remove(
|
||||||
setTimeout(() => {
|
"mobile-menu-open",
|
||||||
item.classList.remove("opacity-0", "translate-y-4");
|
);
|
||||||
item.classList.add(
|
} else {
|
||||||
"opacity-100",
|
menu.classList.remove("opacity-0", "invisible");
|
||||||
"translate-y-0",
|
menu.classList.add("opacity-100", "visible");
|
||||||
"transition-all",
|
hamburgerIcon.classList.remove("opacity-100");
|
||||||
"duration-300",
|
hamburgerIcon.classList.add("opacity-0", "rotate-90");
|
||||||
"ease-out",
|
closeIcon.classList.remove("opacity-0");
|
||||||
);
|
closeIcon.classList.add("opacity-100");
|
||||||
}, index * 50);
|
button.setAttribute("aria-expanded", "true");
|
||||||
});
|
|
||||||
|
|
||||||
document.body.style.overflow = "hidden";
|
mobileNavItems?.forEach((item, index) => {
|
||||||
}
|
setTimeout(() => {
|
||||||
});
|
item.classList.remove("mobile-nav-hidden");
|
||||||
|
item.classList.add("mobile-nav-visible");
|
||||||
|
}, index * 50);
|
||||||
|
});
|
||||||
|
|
||||||
menu.addEventListener("click", (e) => {
|
document.documentElement.classList.add("mobile-menu-open");
|
||||||
if (e.target === menu) {
|
}
|
||||||
button.click();
|
},
|
||||||
}
|
{ signal },
|
||||||
});
|
);
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
menu.addEventListener(
|
||||||
if (e.key === "Escape" && menu.classList.contains("opacity-100")) {
|
"click",
|
||||||
button.click();
|
(e) => {
|
||||||
}
|
if (e.target === menu) {
|
||||||
});
|
button.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
mobileMenuInitialized = true;
|
document.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
(e) => {
|
||||||
|
if (
|
||||||
|
e.key === "Escape" &&
|
||||||
|
menu.classList.contains("opacity-100")
|
||||||
|
) {
|
||||||
|
button.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 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();
|
initScrollEffect();
|
||||||
initMobileMenu();
|
initMobileMenu();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
document.addEventListener("astro:after-swap", init);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#main-header {
|
/* JS-controlled animation states */
|
||||||
transition:
|
|
||||||
background-color 0.3s ease-in-out,
|
|
||||||
backdrop-filter 0.3s ease-in-out,
|
|
||||||
box-shadow 0.3s ease-in-out,
|
|
||||||
transform 0.3s ease-in-out;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header translate animation - hide completely above viewport */
|
|
||||||
#main-header.translate-out {
|
#main-header.translate-out {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
@@ -438,21 +409,46 @@ function isLinkActive(url: string): boolean {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nav padding transition for shrink effect */
|
/* --- Scrolled state (all visual changes via single CSS class) --- */
|
||||||
#main-nav {
|
#main-header.header-scrolled {
|
||||||
transition: padding 0.3s ease-in-out;
|
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 */
|
/* Logo shrinks when header is scrolled */
|
||||||
#main-header.header-scrolled img {
|
#main-header.header-scrolled img {
|
||||||
transform: scale(0.85);
|
transform: scale(0.85);
|
||||||
transition: transform 0.3s ease-in-out;
|
}
|
||||||
|
|
||||||
|
/* 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 */
|
/* Navigation links */
|
||||||
#main-nav a {
|
#main-nav a {
|
||||||
color: var(--color-nav-link);
|
color: var(--color-nav-link);
|
||||||
position: relative;
|
|
||||||
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
|
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,28 +456,53 @@ function isLinkActive(url: string): boolean {
|
|||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Nav link underline animation */
|
||||||
|
.nav-link {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
/* Active state */
|
/* Active state */
|
||||||
.nav-active {
|
.nav-active {
|
||||||
|
position: relative;
|
||||||
color: var(--color-enchunblue) !important;
|
color: var(--color-enchunblue) !important;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Badge positioning and styling */
|
/* Badge positioning for nav */
|
||||||
#main-nav a[class*="relative"] .absolute {
|
.badge-nav {
|
||||||
|
position: absolute;
|
||||||
top: -4px;
|
top: -4px;
|
||||||
right: -8px;
|
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 styles */
|
.badge-nav.badge-hot {
|
||||||
#mobile-menu {
|
background-color: var(--color-badge-hot, #ea384c);
|
||||||
transition:
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
opacity 0.3s ease-in-out,
|
|
||||||
visibility 0.3s ease-in-out,
|
|
||||||
top 0.3s ease-in-out;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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) {
|
@media (max-width: 767px) {
|
||||||
#mobile-menu {
|
#mobile-menu {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -502,12 +523,30 @@ function isLinkActive(url: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth transition for all nav elements */
|
/* Mobile nav item animation states (CSS-driven) */
|
||||||
#main-nav a,
|
.mobile-nav-item {
|
||||||
#mobile-nav a {
|
opacity: 0;
|
||||||
|
transform: translateY(1rem);
|
||||||
transition:
|
transition:
|
||||||
color 0.2s ease-in-out,
|
opacity 0.3s ease-out,
|
||||||
text-shadow 0.2s ease-in-out,
|
transform 0.3s ease-out;
|
||||||
background-color 0.2s ease-in-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>
|
</style>
|
||||||
|
|||||||
10
apps/frontend/src/components/HeaderBg.astro
Normal file
10
apps/frontend/src/components/HeaderBg.astro
Normal 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>
|
||||||
@@ -5,68 +5,77 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
interface PortfolioItem {
|
interface PortfolioItem {
|
||||||
slug: string
|
slug: string;
|
||||||
title: string
|
title: string;
|
||||||
description: string
|
description: string;
|
||||||
image?: string
|
image?: string;
|
||||||
tags?: string[]
|
tags?: string[];
|
||||||
externalUrl?: string
|
externalUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: PortfolioItem
|
item: PortfolioItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { item } = Astro.props
|
const { item } = Astro.props;
|
||||||
|
|
||||||
const imageUrl = item.image || '/placeholder-portfolio.jpg'
|
const imageUrl = item.image || "/placeholder-portfolio.jpg";
|
||||||
const title = item.title || 'Untitled'
|
const title = item.title || "Untitled";
|
||||||
const description = item.description || ''
|
const description = item.description || "";
|
||||||
const tags = item.tags || []
|
const tags = item.tags || [];
|
||||||
const hasExternalLink = !!item.externalUrl
|
const hasExternalLink = !!item.externalUrl;
|
||||||
const linkHref = hasExternalLink ? item.externalUrl : `/website-portfolio/${item.slug}`
|
const linkHref = hasExternalLink
|
||||||
const linkTarget = hasExternalLink ? '_blank' : '_self'
|
? item.externalUrl
|
||||||
const linkRel = hasExternalLink ? 'noopener noreferrer' : ''
|
: `/website-portfolio/${item.slug}`;
|
||||||
|
const linkTarget = hasExternalLink ? "_blank" : "_self";
|
||||||
|
const linkRel = hasExternalLink ? "noopener noreferrer" : "";
|
||||||
---
|
---
|
||||||
|
|
||||||
<li class="portfolio-card-item">
|
<li class="list-none">
|
||||||
<a
|
<a
|
||||||
href={linkHref}
|
href={linkHref}
|
||||||
target={linkTarget}
|
target={linkTarget}
|
||||||
rel={linkRel}
|
rel={linkRel}
|
||||||
class="portfolio-card"
|
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"
|
||||||
>
|
>
|
||||||
<!-- Image Wrapper -->
|
|
||||||
<div class="portfolio-image-wrapper">
|
|
||||||
<img
|
|
||||||
src={imageUrl}
|
|
||||||
alt={title}
|
|
||||||
class="portfolio-image"
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
width="800"
|
|
||||||
height="450"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="portfolio-content">
|
<div class="p-2 md:p-3">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h3 class="portfolio-title">{title}</h3>
|
<h3
|
||||||
|
class="font-['Noto_Sans_TC'] text-xl md:text-2xl font-semibold text-[var(--color-tarawera)] mb-2"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
{
|
{
|
||||||
description && (
|
description && (
|
||||||
<p class="portfolio-description">{description}</p>
|
<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 rounded-lg aspect-video mb-4 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt={title}
|
||||||
|
class="absolute inset-0 w-full h-full object-cover transition-transform duration-300 ease-in-out group-hover:scale-105"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
width="800"
|
||||||
|
height="450"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
{
|
{
|
||||||
tags.length > 0 && (
|
tags.length > 0 && (
|
||||||
<div class="portfolio-tags">
|
<div class="flex flex-wrap gap-2">
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<span class="portfolio-tag">{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>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -74,100 +83,3 @@ const linkRel = hasExternalLink ? 'noopener noreferrer' : ''
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Portfolio Card Styles - Pixel-perfect from Webflow */
|
|
||||||
.portfolio-card-item {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-card {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
||||||
transition: all 300ms ease-in-out;
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Image Wrapper */
|
|
||||||
.portfolio-image-wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: 56.25%; /* 16:9 */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-image {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 300ms ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-card:hover .portfolio-image {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content */
|
|
||||||
.portfolio-content {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-title {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-tarawera, #2d3748);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-description {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-gray-600, #666666);
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tags */
|
|
||||||
.portfolio-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-tag {
|
|
||||||
padding: 4px 10px;
|
|
||||||
background-color: var(--color-gray-100, #f2f2f2);
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-gray-700, #4a5568);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.portfolio-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-description {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
44
apps/frontend/src/components/SectionHeader.astro
Normal file
44
apps/frontend/src/components/SectionHeader.astro
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* SectionHeader - Reusable section header with divider lines
|
||||||
|
* Pixel-perfect implementation based on Webflow design
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
class?: string;
|
||||||
|
sectionBg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
class: className,
|
||||||
|
sectionBg: classNameBg,
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={classNameBg}>
|
||||||
|
<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 pt-2 pb-10 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-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>
|
||||||
|
</section>
|
||||||
@@ -52,27 +52,27 @@ const imageAlt = post.heroImage?.alt || post.title
|
|||||||
const displayDate = post.publishedAt || post.createdAt
|
const displayDate = post.publishedAt || post.createdAt
|
||||||
---
|
---
|
||||||
|
|
||||||
<a href={`/xing-xiao-fang-da-jing/${post.slug}`} class="article-card group">
|
<a href={`/xing-xiao-fang-da-jing/${post.slug}`} class="block no-underline text-inherit group">
|
||||||
<div class="article-card-inner">
|
<div class="bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 h-full flex flex-col group-hover:-translate-y-1">
|
||||||
<!-- Featured Image -->
|
<!-- Featured Image -->
|
||||||
<div class="article-image-wrapper">
|
<div class="aspect-video overflow-hidden bg-[var(--color-gray-100)]">
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={imageAlt}
|
alt={imageAlt}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="article-image"
|
class="w-full h-full object-cover transition-transform duration-400 group-hover:scale-105"
|
||||||
width="800"
|
width="800"
|
||||||
height="450"
|
height="450"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Content -->
|
<!-- Card Content -->
|
||||||
<div class="article-content">
|
<div class="p-6 md:p-5 flex flex-col flex-1">
|
||||||
<!-- Category Badge -->
|
<!-- Category Badge -->
|
||||||
{
|
{
|
||||||
category && (
|
category && (
|
||||||
<span
|
<span
|
||||||
class="category-badge"
|
class="inline-block px-3 py-1.5 rounded-3xl text-[0.8125rem] font-semibold mb-3 self-start"
|
||||||
style={`background-color: ${category.backgroundColor || 'var(--color-enchunblue)'}; color: ${category.textColor || '#fff'}`}
|
style={`background-color: ${category.backgroundColor || 'var(--color-enchunblue)'}; color: ${category.textColor || '#fff'}`}
|
||||||
>
|
>
|
||||||
{category.title}
|
{category.title}
|
||||||
@@ -81,14 +81,14 @@ const displayDate = post.publishedAt || post.createdAt
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h3 class="article-title">
|
<h3 class="font-['Noto_Sans_TC'] text-xl md:text-lg font-bold text-[var(--color-dark-blue)] leading-snug mb-3 line-clamp-2">
|
||||||
{post.title}
|
{post.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Excerpt -->
|
<!-- Excerpt -->
|
||||||
{
|
{
|
||||||
post.excerpt && (
|
post.excerpt && (
|
||||||
<p class="article-excerpt">
|
<p class="font-['Noto_Sans_TC'] text-[0.9375rem] md:text-sm text-[var(--color-gray-600)] leading-relaxed mb-4 line-clamp-2 flex-1">
|
||||||
{post.excerpt.length > 150 ? post.excerpt.slice(0, 150) + '...' : post.excerpt}
|
{post.excerpt.length > 150 ? post.excerpt.slice(0, 150) + '...' : post.excerpt}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
@@ -97,7 +97,7 @@ const displayDate = post.publishedAt || post.createdAt
|
|||||||
<!-- Published Date -->
|
<!-- Published Date -->
|
||||||
{
|
{
|
||||||
displayDate && (
|
displayDate && (
|
||||||
<time class="article-date" datetime={displayDate}>
|
<time class="font-['Quicksand','Noto_Sans_TC'] text-sm text-[var(--color-gray-500)] mt-auto" datetime={displayDate}>
|
||||||
{formatDate(displayDate)}
|
{formatDate(displayDate)}
|
||||||
</time>
|
</time>
|
||||||
)
|
)
|
||||||
@@ -105,111 +105,3 @@ const displayDate = post.publishedAt || post.createdAt
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<style>
|
|
||||||
.article-card {
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-card-inner {
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-card:hover .article-card-inner {
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-card:hover .article-image {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Image Section */
|
|
||||||
.article-image-wrapper {
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--color-gray-100, #f5f5f5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content Section */
|
|
||||||
.article-content {
|
|
||||||
padding: 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.375rem 0.875rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-title {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-dark-blue, #1a1a1a);
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-excerpt {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
color: var(--color-gray-600, #666);
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-date {
|
|
||||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-gray-500, #999);
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.article-content {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-excerpt {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -24,12 +24,11 @@ const { categories, activeCategory, baseUrl = '/news' } = Astro.props
|
|||||||
const filteredCategories = categories.filter(c => c.slug !== 'wen-zhang-fen-lei')
|
const filteredCategories = categories.filter(c => c.slug !== 'wen-zhang-fen-lei')
|
||||||
---
|
---
|
||||||
|
|
||||||
<nav class="category-filter" aria-label="文章分類篩選">
|
<nav class="py-4 mb-8" aria-label="文章分類篩選">
|
||||||
<div class="filter-container">
|
<div class="flex flex-wrap justify-center gap-3 md:gap-2 max-w-[1200px] mx-auto">
|
||||||
<a
|
<a
|
||||||
href={baseUrl}
|
href={baseUrl}
|
||||||
class:filter-button-active={!activeCategory}
|
class:list={["inline-flex items-center justify-center px-5 py-2.5 md:px-4 md:py-2 border-2 border-[var(--color-gray-300)] rounded-3xl bg-white text-[var(--color-gray-700)] font-['Noto_Sans_TC'] text-[0.9375rem] md:text-sm font-medium no-underline whitespace-nowrap transition-all duration-[250ms] hover:border-[var(--color-enchunblue)] hover:text-[var(--color-enchunblue)] hover:bg-[rgba(35,96,140,0.05)]", { "bg-[var(--color-enchunblue)] border-[var(--color-enchunblue)] text-white hover:bg-[var(--color-enchunblue-hover)] hover:border-[var(--color-enchunblue-hover)]": !activeCategory }]}
|
||||||
class="filter-button"
|
|
||||||
>
|
>
|
||||||
全部文章
|
全部文章
|
||||||
</a>
|
</a>
|
||||||
@@ -38,8 +37,7 @@ const filteredCategories = categories.filter(c => c.slug !== 'wen-zhang-fen-lei'
|
|||||||
filteredCategories.map((category) => (
|
filteredCategories.map((category) => (
|
||||||
<a
|
<a
|
||||||
href={`/wen-zhang-fen-lei/${category.slug}`}
|
href={`/wen-zhang-fen-lei/${category.slug}`}
|
||||||
class:filter-button-active={activeCategory === category.slug}
|
class:list={["inline-flex items-center justify-center px-5 py-2.5 md:px-4 md:py-2 border-2 border-[var(--color-gray-300)] rounded-3xl bg-white text-[var(--color-gray-700)] font-['Noto_Sans_TC'] text-[0.9375rem] md:text-sm font-medium no-underline whitespace-nowrap transition-all duration-[250ms] hover:border-[var(--color-enchunblue)] hover:text-[var(--color-enchunblue)] hover:bg-[rgba(35,96,140,0.05)]", { "bg-[var(--color-enchunblue)] border-[var(--color-enchunblue)] text-white hover:bg-[var(--color-enchunblue-hover)] hover:border-[var(--color-enchunblue-hover)]": activeCategory === category.slug }]}
|
||||||
class="filter-button"
|
|
||||||
aria-label={`篩選 ${category.title} 類別文章`}
|
aria-label={`篩選 ${category.title} 類別文章`}
|
||||||
>
|
>
|
||||||
{category.title}
|
{category.title}
|
||||||
@@ -48,66 +46,3 @@ const filteredCategories = categories.filter(c => c.slug !== 'wen-zhang-fen-lei'
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<style>
|
|
||||||
.category-filter {
|
|
||||||
padding: 1rem 0;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
border: 2px solid var(--color-gray-300, #e0e0e0);
|
|
||||||
border-radius: 24px;
|
|
||||||
background: #ffffff;
|
|
||||||
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;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-button:hover {
|
|
||||||
border-color: var(--color-enchunblue, #23608c);
|
|
||||||
color: var(--color-enchunblue, #23608c);
|
|
||||||
background: rgba(35, 96, 140, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-button-active {
|
|
||||||
background: var(--color-enchunblue, #23608c);
|
|
||||||
border-color: var(--color-enchunblue, #23608c);
|
|
||||||
color: #ffffff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-button-active:hover {
|
|
||||||
background: var(--color-enchunblue-hover, #1a4d6e);
|
|
||||||
border-color: var(--color-enchunblue-hover, #1a4d6e);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.filter-container {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-button {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -40,42 +40,43 @@ const { posts, title = '相關文章' } = Astro.props
|
|||||||
|
|
||||||
{
|
{
|
||||||
posts && posts.length > 0 && (
|
posts && posts.length > 0 && (
|
||||||
<section class="related-posts-section" aria-labelledby="related-posts-heading">
|
<section class="py-16 md:py-12 px-6 bg-[var(--color-gray-50)]" aria-labelledby="related-posts-heading">
|
||||||
<div class="container">
|
<div class="max-w-[1200px] mx-auto">
|
||||||
<h2 id="related-posts-heading" class="related-posts-title">
|
<h2 id="related-posts-heading" class="font-['Noto_Sans_TC'] text-[1.75rem] md:text-2xl font-bold text-[var(--color-enchunblue)] text-center mb-10 md:mb-8">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="related-posts-grid">
|
<div class="grid grid-cols-1 md:grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-6 md:gap-4">
|
||||||
{
|
{
|
||||||
posts.slice(0, 4).map((post) => (
|
posts.slice(0, 4).map((post) => (
|
||||||
<a href={`/blog/${post.slug}`} class="related-post-card">
|
<a href={`/blog/${post.slug}`} class="flex flex-col bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 no-underline text-inherit hover:-translate-y-1 group">
|
||||||
<!-- Post Image -->
|
<!-- Post Image -->
|
||||||
<div class="related-post-image">
|
<div class="aspect-video overflow-hidden bg-[var(--color-gray-200)]">
|
||||||
<img
|
<img
|
||||||
src={post.heroImage?.url || '/placeholder-blog.jpg'}
|
src={post.heroImage?.url || '/placeholder-blog.jpg'}
|
||||||
alt={post.heroImage?.alt || post.title}
|
alt={post.heroImage?.alt || post.title}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
class="w-full h-full object-cover transition-transform duration-400 group-hover:scale-105"
|
||||||
width="400"
|
width="400"
|
||||||
height="225"
|
height="225"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Post Content -->
|
<!-- Post Content -->
|
||||||
<div class="related-post-content">
|
<div class="p-5 flex flex-col gap-3">
|
||||||
<!-- Category Badge -->
|
<!-- Category Badge -->
|
||||||
{
|
{
|
||||||
post.categories && post.categories[0] && (
|
post.categories && post.categories[0] && (
|
||||||
<span
|
<span
|
||||||
class="related-category-badge"
|
class="inline-block px-3 py-1 rounded-2xl text-xs font-semibold self-start"
|
||||||
style={`background-color: ${post.categories[0].backgroundColor || 'var(--color-enchunblue)'}; color: ${post.categories[0].textColor || '#fff'}`}
|
style={`background-color: ${post.categories[0].backgroundColor || 'var(--color-enchunblue)'}; color: ${post.categories[0].textColor || '#fff'}`}
|
||||||
>
|
>
|
||||||
{post.categories[0].title}
|
{post.categories[0].title}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<!-- Post Title -->
|
<!-- Post Title -->
|
||||||
<h3 class="related-post-title">
|
<h3 class="font-['Noto_Sans_TC'] text-[1.0625rem] font-semibold text-[var(--color-dark-blue)] leading-snug line-clamp-2">
|
||||||
{post.title}
|
{post.title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,109 +88,3 @@ const { posts, title = '相關文章' } = Astro.props
|
|||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<style>
|
|
||||||
.related-posts-section {
|
|
||||||
padding: 4rem 1.5rem;
|
|
||||||
background: var(--color-gray-50, #f9fafb);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-posts-title {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-enchunblue, #23608c);
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-posts-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-post-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-post-card:hover {
|
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-post-card:hover .related-post-image img {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-post-image {
|
|
||||||
aspect-ratio: 16 / 9;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--color-gray-200, #e5e5e5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-post-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-post-content {
|
|
||||||
padding: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-category-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-post-title {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 1.0625rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-dark-blue, #1a1a1a);
|
|
||||||
line-height: 1.4;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.related-posts-section {
|
|
||||||
padding: 3rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-posts-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-posts-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
208
apps/frontend/src/lib/api/marketing-solution.ts
Normal file
208
apps/frontend/src/lib/api/marketing-solution.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* Marketing Solutions API utilities
|
||||||
|
* Helper functions for fetching marketing solutions page data from Payload CMS
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const CMS_BASE_URL = 'https://enchun-cms.anlstudio.cc'
|
||||||
|
const PAYLOAD_API_URL = `${CMS_BASE_URL}/api`
|
||||||
|
export const MARKETING_SOLUTIONS_SLUG = 'marketing-solutions'
|
||||||
|
export const PLACEHOLDER_IMAGE = '/placeholder-service.jpg'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert relative URL to absolute URL with CMS base URL
|
||||||
|
*/
|
||||||
|
function getFullUrl(url: string | undefined): string | undefined {
|
||||||
|
if (!url) return undefined
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) return url
|
||||||
|
return `${CMS_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceItem {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
category: string
|
||||||
|
iconType?: 'preset' | 'svg' | 'upload'
|
||||||
|
icon?: string
|
||||||
|
iconSvg?: string
|
||||||
|
iconImage?: {
|
||||||
|
url?: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
isHot?: boolean
|
||||||
|
image?: {
|
||||||
|
url?: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
link?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageHero {
|
||||||
|
type?: string
|
||||||
|
richText?: {
|
||||||
|
root?: {
|
||||||
|
children?: Array<{
|
||||||
|
type: string
|
||||||
|
children?: Array<{
|
||||||
|
text: string
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
media?: {
|
||||||
|
url?: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageLayout {
|
||||||
|
blockType: string
|
||||||
|
services?: ServiceItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageData {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
hero?: PageHero
|
||||||
|
layout?: PageLayout[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract plain text from Lexical richText structure
|
||||||
|
*/
|
||||||
|
function extractTextFromRichText(richText: PageHero['richText']): string | undefined {
|
||||||
|
if (!richText?.root?.children) return undefined
|
||||||
|
|
||||||
|
const texts: string[] = []
|
||||||
|
for (const child of richText.root.children) {
|
||||||
|
if (child.children) {
|
||||||
|
for (const textNode of child.children) {
|
||||||
|
if (textNode.text) {
|
||||||
|
texts.push(textNode.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return texts.length > 0 ? texts.join(' ') : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch page by slug from Payload CMS
|
||||||
|
*/
|
||||||
|
export async function getPageBySlug(slug: string): Promise<PageData | null> {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('where[slug][equals]', slug)
|
||||||
|
params.append('where[_status][equals]', 'published')
|
||||||
|
params.append('depth', '2')
|
||||||
|
|
||||||
|
const response = await fetch(`${PAYLOAD_API_URL}/pages?${params}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch page: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
return data.docs?.[0] || null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching page by slug:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Marketing Solutions page data
|
||||||
|
* Returns hero title/subtitle/image and services list
|
||||||
|
*/
|
||||||
|
export async function getMarketingSolutionsPage(): Promise<{
|
||||||
|
heroTitle?: string
|
||||||
|
heroSubtitle?: string
|
||||||
|
heroImage?: {
|
||||||
|
url?: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
services: ServiceItem[]
|
||||||
|
} | null> {
|
||||||
|
try {
|
||||||
|
const page = await getPageBySlug(MARKETING_SOLUTIONS_SLUG)
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract hero content
|
||||||
|
const heroTitle = page.hero?.type === 'highImpact'
|
||||||
|
? page.title
|
||||||
|
: extractTextFromRichText(page.hero?.richText)
|
||||||
|
|
||||||
|
// Find hero subtitle from hero richText (second paragraph if exists)
|
||||||
|
function extractText(node: any): string {
|
||||||
|
if (!node) return ''
|
||||||
|
if (node.type === 'text') return node.text || ''
|
||||||
|
if (Array.isArray(node.children)) {
|
||||||
|
return node.children.map(extractText).join(' ')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let heroSubtitle: string | undefined
|
||||||
|
|
||||||
|
const paragraphs = page.hero?.richText?.root?.children?.filter(
|
||||||
|
(child: any) => child.type === 'paragraph'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (paragraphs?.length) {
|
||||||
|
heroSubtitle = extractText(paragraphs[0]).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract hero image if available
|
||||||
|
const heroImage = page.hero?.media
|
||||||
|
? {
|
||||||
|
url: getFullUrl(page.hero.media.url),
|
||||||
|
alt: page.hero.media.alt || page.title,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// Find ServicesList block in layout
|
||||||
|
const servicesBlock = page.layout?.find(
|
||||||
|
(block) => block.blockType === 'servicesList'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transform services data
|
||||||
|
const services: ServiceItem[] = servicesBlock?.services?.map((service: any) => ({
|
||||||
|
id: service.id,
|
||||||
|
title: service.title,
|
||||||
|
description: service.description,
|
||||||
|
category: service.category,
|
||||||
|
iconType: service.iconType || 'preset',
|
||||||
|
icon: service.icon,
|
||||||
|
iconSvg: service.iconSvg,
|
||||||
|
iconImage: service.iconImage
|
||||||
|
? {
|
||||||
|
url: getFullUrl(service.iconImage.url),
|
||||||
|
alt: service.iconImage.alt || service.title,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
isHot: service.isHot,
|
||||||
|
image: service.image
|
||||||
|
? {
|
||||||
|
url: getFullUrl(service.image.url),
|
||||||
|
alt: service.image.alt || service.title,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
link: service.link,
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
return {
|
||||||
|
heroTitle,
|
||||||
|
heroSubtitle,
|
||||||
|
heroImage,
|
||||||
|
services,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching marketing solutions page:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
41
apps/frontend/src/lib/sanitize.ts
Normal file
41
apps/frontend/src/lib/sanitize.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* SVG sanitization utilities
|
||||||
|
* Prevents XSS attacks from user-provided SVG content
|
||||||
|
*
|
||||||
|
* Note: Uses basic regex-based sanitization for Cloudflare Workers compatibility
|
||||||
|
* DOMPurify doesn't work in Workers due to lack of DOM APIs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SVG sanitization - removes dangerous tags and attributes
|
||||||
|
* Suitable for Cloudflare Workers environment
|
||||||
|
*/
|
||||||
|
export const sanitizeSvg = (svg: string): string => {
|
||||||
|
if (!svg || typeof svg !== 'string') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let sanitized = svg
|
||||||
|
|
||||||
|
// Remove script tags and their content
|
||||||
|
sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||||
|
|
||||||
|
// Remove dangerous tags and their content
|
||||||
|
sanitized = sanitized.replace(/<(iframe|object|embed|style|link|meta|base)[^>]*>.*?<\/\1>/gi, '')
|
||||||
|
sanitized = sanitized.replace(/<(iframe|object|embed|style|link|meta|base)[^>]*\/?>/gi, '')
|
||||||
|
|
||||||
|
// Remove event handlers (onclick, onload, onerror, etc.)
|
||||||
|
sanitized = sanitized.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
|
||||||
|
sanitized = sanitized.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '')
|
||||||
|
|
||||||
|
// Remove javascript: URLs
|
||||||
|
sanitized = sanitized.replace(/javascript\s*:/gi, '')
|
||||||
|
|
||||||
|
// Remove data: URLs (except for images which are safe in SVGs)
|
||||||
|
sanitized = sanitized.replace(/(?<!image\/)data\s*:/gi, '')
|
||||||
|
|
||||||
|
// Remove style attributes that could contain expressions
|
||||||
|
sanitized = sanitized.replace(/\s+style\s*=\s*["'][^"']*expression[^"']*["']/gi, '')
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
@@ -3,20 +3,30 @@
|
|||||||
* About Page - 關於恩群數位
|
* About Page - 關於恩群數位
|
||||||
* 展示公司特色、服務優勢和與其他公司的差異
|
* 展示公司特色、服務優勢和與其他公司的差異
|
||||||
*/
|
*/
|
||||||
import Layout from '../layouts/Layout.astro'
|
import Layout from "../layouts/Layout.astro";
|
||||||
import AboutHero from '../sections/AboutHero.astro'
|
import AboutHero from "../sections/AboutHero.astro";
|
||||||
import FeatureSection from '../sections/FeatureSection.astro'
|
import FeatureSection from "../sections/FeatureSection.astro";
|
||||||
import ComparisonSection from '../sections/ComparisonSection.astro'
|
import ComparisonSection from "../sections/ComparisonSection.astro";
|
||||||
import CTASection from '../sections/CTASection.astro'
|
import CTASection from "../sections/CTASection.astro";
|
||||||
|
import SectionHeader from "../components/SectionHeader.astro";
|
||||||
|
import CtaSection from "@/components/CtaSection.astro";
|
||||||
|
|
||||||
// Metadata for SEO
|
// Metadata for SEO
|
||||||
const title = '關於恩群數位 | 專業數位行銷服務團隊'
|
const title = "關於恩群數位 | 專業數位行銷服務團隊";
|
||||||
const description = '恩群數位行銷成立於2018年,提供全方位數位行銷服務。我們在地化優先、數據驅動,是您最可信赖的數位行銷夥伴。'
|
const description =
|
||||||
|
"恩群數位行銷成立於2018年,提供全方位數位行銷服務。我們在地化優先、數據驅動,是您最可信赖的數位行銷夥伴。";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={title} description={description}>
|
<Layout title={title} description={description}>
|
||||||
<!-- Hero Section -->
|
<!-- 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: "關於恩群數位的背景圖",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Service Features Section -->
|
<!-- Service Features Section -->
|
||||||
<FeatureSection />
|
<FeatureSection />
|
||||||
@@ -25,14 +35,5 @@ const description = '恩群數位行銷成立於2018年,提供全方位數位
|
|||||||
<ComparisonSection />
|
<ComparisonSection />
|
||||||
|
|
||||||
<!-- CTA Section -->
|
<!-- CTA Section -->
|
||||||
<CTASection
|
<CtaSection />
|
||||||
homeData={{
|
|
||||||
ctaSection: {
|
|
||||||
headline: '準備好開始新的旅程了嗎',
|
|
||||||
description: '讓我們一起為您的品牌打造獨特的數位行銷策略',
|
|
||||||
buttonText: '聯絡我們',
|
|
||||||
buttonLink: '/contact-us',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
82
apps/frontend/src/pages/api/contact.ts
Normal file
82
apps/frontend/src/pages/api/contact.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
|
// Your Cloudflare Turnstile Secret Key
|
||||||
|
// In production, this should be set in environment variables
|
||||||
|
const TURNSTILE_SECRET_KEY = import.meta.env.CF_TURNSTILE_SECRET_KEY || import.meta.env.TURNSTILE_SECRET_KEY;
|
||||||
|
// The n8n webhook URL
|
||||||
|
const WEBHOOK_URL = import.meta.env.N8N_WEBHOOK_URL || "https://n8n.anlstudio.cc/webhook/contact";
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { name, phone, email, message, "cf-turnstile-response": token } = body;
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!name || !phone || !email || !message) {
|
||||||
|
return new Response(JSON.stringify({ error: "Missing required fields" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return new Response(JSON.stringify({ error: "[Turnstile] No token provided" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Turnstile token
|
||||||
|
if (TURNSTILE_SECRET_KEY) {
|
||||||
|
const turnstileVerify = await fetch(
|
||||||
|
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
secret: TURNSTILE_SECRET_KEY,
|
||||||
|
response: token,
|
||||||
|
}).toString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const turnstileResult = await turnstileVerify.json();
|
||||||
|
|
||||||
|
if (!turnstileResult.success) {
|
||||||
|
return new Response(JSON.stringify({ error: "[Turnstile] Token verification failed" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("TURNSTILE_SECRET_KEY is not defined. Skipping verification.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// After successful verification, send data to n8n
|
||||||
|
const n8nResponse = await fetch(WEBHOOK_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name, phone, email, message }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!n8nResponse.ok) {
|
||||||
|
throw new Error(`n8n webhook failed with status ${n8nResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API proxy error:", error);
|
||||||
|
return new Response(JSON.stringify({ error: "Internal server error" }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,622 +4,387 @@
|
|||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
* Includes form validation, submission handling, and responsive layout
|
* Includes form validation, submission handling, and responsive layout
|
||||||
*/
|
*/
|
||||||
import Layout from '../layouts/Layout.astro'
|
import Layout from "../layouts/Layout.astro";
|
||||||
|
import HeaderBg from "../components/HeaderBg.astro";
|
||||||
|
|
||||||
// Metadata for SEO
|
// Metadata for SEO
|
||||||
const title = '聯絡我們 | 恩群數位行銷'
|
const title = "聯絡我們 | 恩群數位行銷";
|
||||||
const description = '有任何問題嗎?歡迎聯絡恩群數位行銷,我們的專業團隊將竭誠為您服務。電話: 02 5570 0527,Email: enchuntaiwan@gmail.com'
|
const description =
|
||||||
|
"有任何問題嗎?歡迎聯絡恩群數位行銷,我們的專業團隊將竭誠為您服務。電話: 02 5570 0527,Email: enchuntaiwan@gmail.com";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={title} description={description}>
|
<Layout title={title} description={description}>
|
||||||
<section class="contact-section" id="contact">
|
<HeaderBg />
|
||||||
<div class="contactus_wrapper">
|
<section class="py-20 bg-[#f4f5f7] scroll-mt-20 lg:py-10" id="contact">
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-2 gap-16 items-start max-w-3xl mx-auto px-5 lg:grid-cols-2 lg:gap-10"
|
||||||
|
>
|
||||||
|
<!-- Contact Image Side -->
|
||||||
|
<div class="flex justify-center items-center w-full lg:order-1 pt-4">
|
||||||
|
<img
|
||||||
|
src="https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b532e942d09_Contact%20us-bro%201.svg"
|
||||||
|
alt="聯絡我們插圖"
|
||||||
|
class="w-full h-auto object-contain drop-shadow-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Contact Form Side -->
|
<!-- Contact Form Side -->
|
||||||
<div class="contact_form_wrapper">
|
<div class="w-full lg:order-2">
|
||||||
<h1 class="contact_head">聯絡我們</h1>
|
<h2
|
||||||
<p class="contact_parafraph">
|
class="text-4xl font-bold leading-snug text-[#2a5b83] mb-3 lg:text-3xl"
|
||||||
有任何問題嗎?歡迎聯絡我們,我們將竭誠為您服務。
|
>
|
||||||
|
與我們聯絡
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
class="text-lg font-medium text-[#2a5b83] mb-2 tracking-wide lg:text-base"
|
||||||
|
>
|
||||||
|
任何關於行銷的相關訊息或是詢價,歡迎與我們聯絡
|
||||||
</p>
|
</p>
|
||||||
<p class="contact_reminder">
|
<p class="text-sm italic text-slate-500 mb-8">
|
||||||
* 標註欄位為必填
|
有星號的地方 (*) 是必填欄位
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Contact Form -->
|
<!-- Contact Form -->
|
||||||
<form id="contact-form" class="contact_form" novalidate>
|
<form id="contact-form" class="w-full flex flex-col gap-6" novalidate>
|
||||||
<!-- Success Message -->
|
<!-- Success Message -->
|
||||||
<div id="form-success" class="w-form-done" style="display: none;">
|
<div
|
||||||
|
id="form-success"
|
||||||
|
class="p-4 px-6 rounded-lg text-center font-medium bg-[#d4edda] text-[#155724] border border-[#c3e6cb] animate-[slideUp_0.3s_ease-out]"
|
||||||
|
style="display: none;"
|
||||||
|
>
|
||||||
感謝您的留言!我們會盡快回覆您。
|
感謝您的留言!我們會盡快回覆您。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
<div id="form-error" class="w-form-fail" style="display: none;">
|
<div
|
||||||
|
id="form-error"
|
||||||
|
class="p-4 px-6 rounded-lg text-center font-medium bg-[#f8d7da] text-[#721c24] border border-[#f5c6cb] animate-[slideUp_0.3s_ease-out]"
|
||||||
|
style="display: none;"
|
||||||
|
>
|
||||||
送出失敗,請稍後再試或直接來電。
|
送出失敗,請稍後再試或直接來電。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Fields -->
|
<!-- Name Field -->
|
||||||
<div class="contact-form-grid">
|
<div class="flex flex-col">
|
||||||
<!-- Name Field -->
|
<label
|
||||||
<div class="contact_field_wrapper">
|
for="Name"
|
||||||
<label for="Name" class="contact_field_name">
|
class="text-[0.95rem] font-medium text-slate-500 mb-2 block"
|
||||||
姓名 <span>*</span>
|
>
|
||||||
</label>
|
姓名*
|
||||||
<input
|
</label>
|
||||||
type="text"
|
<input
|
||||||
id="Name"
|
type="text"
|
||||||
name="Name"
|
id="Name"
|
||||||
class="input_field"
|
name="Name"
|
||||||
required
|
class="w-full px-4 py-2.5 border border-[#4a90e2] rounded-lg text-base bg-white focus:outline-none focus:ring-2 focus:ring-[#4a90e2]/20 transition-all shadow-sm [&.error]:border-[#dc3545]"
|
||||||
minlength="2"
|
required
|
||||||
maxlength="256"
|
minlength="2"
|
||||||
placeholder="請輸入您的姓名"
|
maxlength="256"
|
||||||
/>
|
/>
|
||||||
<span class="error-message" id="Name-error"></span>
|
<span
|
||||||
</div>
|
class="error-message text-[#dc3545] text-sm mt-1 hidden"
|
||||||
|
id="Name-error"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Phone Field -->
|
<!-- Phone Field -->
|
||||||
<div class="contact_field_wrapper">
|
<div class="flex flex-col">
|
||||||
<label for="Phone" class="contact_field_name">
|
<label
|
||||||
聯絡電話 <span>*</span>
|
for="Phone"
|
||||||
</label>
|
class="text-[0.95rem] font-medium text-slate-500 mb-2 block"
|
||||||
<input
|
>
|
||||||
type="tel"
|
聯絡電話*
|
||||||
id="Phone"
|
</label>
|
||||||
name="Phone"
|
<input
|
||||||
class="input_field"
|
type="tel"
|
||||||
required
|
id="Phone"
|
||||||
placeholder="請輸入您的電話號碼"
|
name="Phone"
|
||||||
/>
|
class="w-full px-4 py-2.5 border border-white shadow-sm rounded-lg text-base bg-white focus:outline-none focus:border-[#4a90e2] focus:ring-2 focus:ring-[#4a90e2]/20 transition-all [&.error]:border-[#dc3545]"
|
||||||
<span class="error-message" id="Phone-error"></span>
|
required
|
||||||
</div>
|
/>
|
||||||
|
<span
|
||||||
|
class="error-message text-[#dc3545] text-sm mt-1 hidden"
|
||||||
|
id="Phone-error"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email Field -->
|
<!-- Email Field -->
|
||||||
<div class="contact_field_wrapper">
|
<div class="flex flex-col">
|
||||||
<label for="Email" class="contact_field_name">
|
<label
|
||||||
Email <span>*</span>
|
for="Email"
|
||||||
|
class="text-[0.95rem] font-medium text-slate-500 mb-2 block"
|
||||||
|
>
|
||||||
|
Email *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="Email"
|
id="Email"
|
||||||
name="Email"
|
name="Email"
|
||||||
class="input_field"
|
class="w-full px-4 py-2.5 border border-white shadow-sm rounded-lg text-base bg-white focus:outline-none focus:border-[#4a90e2] focus:ring-2 focus:ring-[#4a90e2]/20 transition-all [&.error]:border-[#dc3545]"
|
||||||
required
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Message Field -->
|
<!-- Message Field -->
|
||||||
<div class="contact_field_wrapper">
|
<div class="flex flex-col">
|
||||||
<label for="Message" class="contact_field_name">
|
<label
|
||||||
聯絡訊息 <span>*</span>
|
for="Message"
|
||||||
|
class="text-[0.95rem] font-medium text-slate-500 mb-2 block"
|
||||||
|
>
|
||||||
|
聯絡訊息
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="Message"
|
id="Message"
|
||||||
name="Message"
|
name="Message"
|
||||||
class="input_field"
|
class="w-full px-4 py-3 border border-white shadow-sm rounded-lg text-base bg-white focus:outline-none focus:border-[#4a90e2] focus:ring-2 focus:ring-[#4a90e2]/20 transition-all resize-y min-h-[120px] [&.error]:border-[#dc3545]"
|
||||||
minlength="10"
|
|
||||||
maxlength="5000"
|
|
||||||
required
|
required
|
||||||
placeholder="請輸入您的訊息(至少 10 個字元)"
|
minlength="10"
|
||||||
></textarea>
|
maxlength="5000"></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>
|
||||||
|
|
||||||
|
<!-- Turnstile Widget -->
|
||||||
|
<div class="flex flex-col mt-2">
|
||||||
|
<div
|
||||||
|
class="cf-turnstile"
|
||||||
|
data-sitekey={import.meta.env.PUBLIC_TURNSTILE_SITE_KEY ||
|
||||||
|
"0x4AAAAAACkOUZK2u7Fo8IZ-"}
|
||||||
|
data-theme="light"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="error-message text-[#dc3545] text-sm mt-1 hidden"
|
||||||
|
id="turnstile-error"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<button type="submit" class="submit-button" id="submit-btn">
|
<div class="flex justify-end mt-2">
|
||||||
<span class="button-text">送出訊息</span>
|
<button
|
||||||
<span class="button-loading" style="display: none;">送出中...</span>
|
type="submit"
|
||||||
</button>
|
class="bg-[#2a5b83] text-white border-none rounded-lg px-10 py-[0.6rem] text-[1.05rem] font-medium cursor-pointer transition-all duration-200 hover:bg-[#1f4666] shadow-sm transform hover:-translate-y-0.5 min-w-[120px] disabled:bg-slate-400 disabled:cursor-not-allowed disabled:transform-none"
|
||||||
|
id="submit-btn"
|
||||||
|
>
|
||||||
|
<span class="button-text">送出</span>
|
||||||
|
<span class="button-loading" style="display: none;"
|
||||||
|
>送出中...</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact Image Side -->
|
|
||||||
<div class="contact-image">
|
|
||||||
<div class="image-wrapper">
|
|
||||||
<img
|
|
||||||
src="/placeholder-contact.jpg"
|
|
||||||
alt="聯絡恩群數位"
|
|
||||||
width="600"
|
|
||||||
height="400"
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer
|
||||||
/* Contact Section Styles - Pixel-perfect from Webflow */
|
></script>
|
||||||
.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>
|
<script>
|
||||||
// Form validation and submission handler
|
// Form validation and submission handler
|
||||||
function initContactForm() {
|
function initContactForm() {
|
||||||
const form = document.getElementById('contact-form') as HTMLFormElement
|
const form = document.getElementById("contact-form") as HTMLFormElement;
|
||||||
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement
|
const submitBtn = document.getElementById(
|
||||||
const successMsg = document.getElementById('form-success') as HTMLElement
|
"submit-btn",
|
||||||
const errorMsg = document.getElementById('form-error') as HTMLElement
|
) as HTMLButtonElement;
|
||||||
|
const successMsg = document.getElementById("form-success") as HTMLElement;
|
||||||
|
const errorMsg = document.getElementById("form-error") as HTMLElement;
|
||||||
|
const turnstileError = document.getElementById(
|
||||||
|
"turnstile-error",
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
if (!form) return
|
if (!form) return;
|
||||||
|
|
||||||
// Validation patterns
|
// Validation patterns
|
||||||
const patterns = {
|
const patterns = {
|
||||||
Name: /^[\u4e00-\u9fa5a-zA-Z\s]{2,256}$/,
|
Name: /^[\u4e00-\u9fa5a-zA-Z\s]{2,256}$/,
|
||||||
Phone: /^[0-9\-\s\+]{6,20}$/,
|
Phone: /^[0-9\-\s\+]{6,20}$/,
|
||||||
Email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
Email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
||||||
Message: /^.{10,5000}$/
|
Message: /^.{10,5000}$/,
|
||||||
}
|
};
|
||||||
|
|
||||||
// Validation function
|
// Validation function
|
||||||
function validateField(input: HTMLInputElement | HTMLTextAreaElement): boolean {
|
function validateField(
|
||||||
const name = input.name
|
input: HTMLInputElement | HTMLTextAreaElement,
|
||||||
const value = input.value.trim()
|
): boolean {
|
||||||
const errorSpan = document.getElementById(`${name}-error`) as HTMLElement
|
const name = input.name;
|
||||||
|
const value = input.value.trim();
|
||||||
|
const errorSpan = document.getElementById(`${name}-error`) as HTMLElement;
|
||||||
|
|
||||||
if (!input.hasAttribute('required') && !value) {
|
if (!input.hasAttribute("required") && !value) {
|
||||||
clearError(input, errorSpan)
|
clearError(input, errorSpan);
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let isValid = true
|
let isValid = true;
|
||||||
let errorMessage = ''
|
let errorMessage = "";
|
||||||
|
|
||||||
// Required check
|
// Required check
|
||||||
if (input.hasAttribute('required') && !value) {
|
if (input.hasAttribute("required") && !value) {
|
||||||
isValid = false
|
isValid = false;
|
||||||
errorMessage = '此欄位為必填'
|
errorMessage = "此欄位為必填";
|
||||||
}
|
}
|
||||||
// Pattern validation
|
// Pattern validation
|
||||||
else if (patterns[name as keyof typeof patterns] && !patterns[name as keyof typeof patterns].test(value)) {
|
else if (
|
||||||
isValid = false
|
patterns[name as keyof typeof patterns] &&
|
||||||
|
!patterns[name as keyof typeof patterns].test(value)
|
||||||
|
) {
|
||||||
|
isValid = false;
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'Name':
|
case "Name":
|
||||||
errorMessage = '請輸入有效的姓名(至少 2 個字元)'
|
errorMessage = "請輸入有效的姓名(至少 2 個字元)";
|
||||||
break
|
break;
|
||||||
case 'Phone':
|
case "Phone":
|
||||||
errorMessage = '請輸入有效的電話號碼'
|
errorMessage = "請輸入有效的電話號碼";
|
||||||
break
|
break;
|
||||||
case 'Email':
|
case "Email":
|
||||||
errorMessage = '請輸入有效的 Email 格式'
|
errorMessage = "請輸入有效的 Email 格式";
|
||||||
break
|
break;
|
||||||
case 'Message':
|
case "Message":
|
||||||
errorMessage = '訊息至少需要 10 個字元'
|
errorMessage = "訊息至少需要 10 個字元";
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide error
|
// Show/hide error
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
input.classList.add('error')
|
input.classList.add("error");
|
||||||
if (errorSpan) {
|
if (errorSpan) {
|
||||||
errorSpan.textContent = errorMessage
|
errorSpan.textContent = errorMessage;
|
||||||
errorSpan.style.display = 'block'
|
errorSpan.style.display = "block";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
clearError(input, errorSpan)
|
clearError(input, errorSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isValid
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearError(input: HTMLInputElement | HTMLTextAreaElement, errorSpan: HTMLElement) {
|
function clearError(
|
||||||
input.classList.remove('error')
|
input: HTMLInputElement | HTMLTextAreaElement,
|
||||||
|
errorSpan: HTMLElement,
|
||||||
|
) {
|
||||||
|
input.classList.remove("error");
|
||||||
if (errorSpan) {
|
if (errorSpan) {
|
||||||
errorSpan.textContent = ''
|
errorSpan.textContent = "";
|
||||||
errorSpan.style.display = 'none'
|
errorSpan.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Real-time validation on blur
|
// Real-time validation on blur
|
||||||
form.querySelectorAll('input, textarea').forEach((field) => {
|
form.querySelectorAll("input, textarea").forEach((field) => {
|
||||||
field.addEventListener('blur', () => {
|
field.addEventListener("blur", () => {
|
||||||
validateField(field as HTMLInputElement | HTMLTextAreaElement)
|
validateField(field as HTMLInputElement | HTMLTextAreaElement);
|
||||||
})
|
});
|
||||||
|
|
||||||
field.addEventListener('input', () => {
|
field.addEventListener("input", () => {
|
||||||
const input = field as HTMLInputElement | HTMLTextAreaElement
|
const input = field as HTMLInputElement | HTMLTextAreaElement;
|
||||||
if (input.classList.contains('error')) {
|
if (input.classList.contains("error")) {
|
||||||
validateField(input)
|
validateField(input);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
// Form submission
|
// Form submission
|
||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
|
|
||||||
// Validate all fields
|
// Validate all fields
|
||||||
const inputs = form.querySelectorAll('input, textarea') as NodeListOf<HTMLInputElement | HTMLTextAreaElement>
|
const inputs = form.querySelectorAll("input, textarea") as NodeListOf<
|
||||||
let isFormValid = true
|
HTMLInputElement | HTMLTextAreaElement
|
||||||
|
>;
|
||||||
|
let isFormValid = true;
|
||||||
|
|
||||||
inputs.forEach((input) => {
|
inputs.forEach((input) => {
|
||||||
if (!validateField(input)) {
|
if (!validateField(input)) {
|
||||||
isFormValid = false
|
isFormValid = false;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// Collect form data to check turnstile token
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const turnstileToken = formData.get("cf-turnstile-response");
|
||||||
|
|
||||||
|
// Validate Turnstile
|
||||||
|
if (!turnstileToken) {
|
||||||
|
isFormValid = false;
|
||||||
|
if (turnstileError) {
|
||||||
|
turnstileError.textContent = "請完成驗證";
|
||||||
|
turnstileError.style.display = "block";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (turnstileError) {
|
||||||
|
turnstileError.textContent = "";
|
||||||
|
turnstileError.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFormValid) {
|
if (!isFormValid) {
|
||||||
// Scroll to first error
|
// Scroll to first error
|
||||||
const firstError = form.querySelector('.input_field.error') as HTMLElement
|
const firstError = form.querySelector(
|
||||||
|
".input_field.error, #turnstile-error[style*='display: block']",
|
||||||
|
) as HTMLElement;
|
||||||
if (firstError) {
|
if (firstError) {
|
||||||
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
firstError.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
submitBtn.disabled = true
|
submitBtn.disabled = true;
|
||||||
const buttonText = submitBtn.querySelector('.button-text') as HTMLElement
|
const buttonText = submitBtn.querySelector(".button-text") as HTMLElement;
|
||||||
const buttonLoading = submitBtn.querySelector('.button-loading') as HTMLElement
|
const buttonLoading = submitBtn.querySelector(
|
||||||
if (buttonText) buttonText.style.display = 'none'
|
".button-loading",
|
||||||
if (buttonLoading) buttonLoading.style.display = 'inline'
|
) as HTMLElement;
|
||||||
|
if (buttonText) buttonText.style.display = "none";
|
||||||
|
if (buttonLoading) buttonLoading.style.display = "inline";
|
||||||
|
|
||||||
// Hide previous messages
|
// Hide previous messages
|
||||||
successMsg.style.display = 'none'
|
successMsg.style.display = "none";
|
||||||
errorMsg.style.display = 'none'
|
errorMsg.style.display = "none";
|
||||||
|
|
||||||
// Collect form data
|
|
||||||
const formData = new FormData(form)
|
|
||||||
const data = {
|
const data = {
|
||||||
name: formData.get('Name'),
|
name: formData.get("Name"),
|
||||||
phone: formData.get('Phone'),
|
phone: formData.get("Phone"),
|
||||||
email: formData.get('Email'),
|
email: formData.get("Email"),
|
||||||
message: formData.get('Message')
|
message: formData.get("Message"),
|
||||||
}
|
"cf-turnstile-response": turnstileToken,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Submit to backend (via API proxy)
|
// Submit to API proxy
|
||||||
const response = await fetch('/api/contact', {
|
const response = await fetch("/api/contact", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data),
|
||||||
})
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Success
|
// Success
|
||||||
successMsg.style.display = 'block'
|
successMsg.style.display = "block";
|
||||||
form.reset()
|
form.reset();
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Submission failed')
|
throw new Error("Submission failed");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Form submission error:', error)
|
console.error("Form submission error:", error);
|
||||||
errorMsg.style.display = 'block'
|
errorMsg.style.display = "block";
|
||||||
} finally {
|
} finally {
|
||||||
// Reset button state
|
// Reset button state
|
||||||
submitBtn.disabled = false
|
submitBtn.disabled = false;
|
||||||
if (buttonText) buttonText.style.display = 'inline'
|
if (buttonText) buttonText.style.display = "inline";
|
||||||
if (buttonLoading) buttonLoading.style.display = 'none'
|
if (buttonLoading) buttonLoading.style.display = "none";
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize when DOM is ready
|
// Initialize when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', initContactForm)
|
document.addEventListener("DOMContentLoaded", initContactForm);
|
||||||
if (document.readyState !== 'loading') {
|
if (document.readyState !== "loading") {
|
||||||
initContactForm()
|
initContactForm();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,23 +2,46 @@
|
|||||||
/**
|
/**
|
||||||
* Marketing Solutions Page - 行銷方案頁面
|
* Marketing Solutions Page - 行銷方案頁面
|
||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
|
* Data fetched from Payload CMS Pages Collection API
|
||||||
*/
|
*/
|
||||||
import Layout from '../layouts/Layout.astro'
|
import Layout from "../layouts/Layout.astro";
|
||||||
import SolutionsHero from '../sections/SolutionsHero.astro'
|
import SolutionsHero from "../sections/SolutionsHero.astro";
|
||||||
import ServicesList from '../sections/ServicesList.astro'
|
import ServicesList from "../sections/ServicesList.astro";
|
||||||
|
import { getMarketingSolutionsPage } from "../lib/api/marketing-solution";
|
||||||
|
import SectionHeader from "../components/SectionHeader.astro";
|
||||||
|
import CtaSection from "../components/CtaSection.astro";
|
||||||
|
|
||||||
|
// Fetch page data from CMS
|
||||||
|
const pageData = await getMarketingSolutionsPage();
|
||||||
|
|
||||||
|
// Use CMS data or fallback to defaults
|
||||||
|
const heroTitle = pageData?.heroTitle || "行銷解決方案";
|
||||||
|
const heroSubtitle =
|
||||||
|
pageData?.heroSubtitle ||
|
||||||
|
"提供全方位的數位行銷服務,協助您的品牌在數位時代脫穎而出";
|
||||||
|
const heroImage = pageData?.heroImage;
|
||||||
|
const services = pageData?.services || [];
|
||||||
|
|
||||||
// Metadata for SEO
|
// Metadata for SEO
|
||||||
const title = '行銷解決方案 | 恩群數位行銷'
|
const title = "行銷解決方案 | 恩群數位行銷";
|
||||||
const description = '恩群數位行銷提供全方位的數位行銷服務,包括 Google Ads、社群代操、論壇行銷、網紅行銷、網站設計等,協助您的品牌在數位時代脫穎而出。'
|
const description =
|
||||||
|
"恩群數位行銷提供全方位的數位行銷服務,包括 Google Ads、社群代操、論壇行銷、網紅行銷、網站設計等,協助您的品牌在數位時代脫穎而出。";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={title} description={description}>
|
<Layout title={title} description={description}>
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<SolutionsHero
|
<SolutionsHero
|
||||||
title="行銷解決方案"
|
title={heroTitle}
|
||||||
subtitle="提供全方位的數位行銷服務,協助您的品牌在數位時代脫穎而出"
|
subtitle={heroSubtitle}
|
||||||
|
backgroundImage={heroImage}
|
||||||
|
/>
|
||||||
|
<!-- Section Header -->
|
||||||
|
<SectionHeader
|
||||||
|
title="行銷方案"
|
||||||
|
subtitle="Marketing solutions"
|
||||||
|
sectionBg="bg-white"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Services List -->
|
<!-- Services List -->
|
||||||
<ServicesList />
|
<ServicesList services={services} />
|
||||||
|
<CtaSection />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -39,29 +39,29 @@ const validCategories = categories.filter(c =>
|
|||||||
|
|
||||||
<Layout title={title} description={description}>
|
<Layout title={title} description={description}>
|
||||||
<!-- Blog Hero Section -->
|
<!-- Blog Hero Section -->
|
||||||
<section class="blog-hero" aria-labelledby="blog-heading">
|
<section class="py-[60px] px-5 bg-white lg:py-10 md:py-10 md:px-4" aria-labelledby="blog-heading">
|
||||||
<div class="container">
|
<div class="max-w-[1200px] mx-auto">
|
||||||
<div class="section_header_w_line">
|
<div class="flex items-center justify-center gap-4 md:flex-wrap md:gap-3">
|
||||||
<div class="divider_line"></div>
|
<div class="w-10 h-0.5 bg-link-hover md:w-[30px]"></div>
|
||||||
<div class="header_subtitle">
|
<div class="text-center">
|
||||||
<h2 id="blog-heading" class="header_subtitle_head">行銷放大鏡</h2>
|
<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="header_subtitle_paragraph">Marketing Blog</p>
|
<p class="text-base text-slate-600 font-accent md:text-[0.9375rem]">Marketing Blog</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider_line"></div>
|
<div class="w-10 h-0.5 bg-link-hover md:w-[30px]"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Category Filter -->
|
<!-- Category Filter -->
|
||||||
<section class="filter-section">
|
<section class="bg-white pb-5">
|
||||||
<div class="container">
|
<div class="max-w-[1200px] mx-auto">
|
||||||
<CategoryFilter categories={validCategories} />
|
<CategoryFilter categories={validCategories} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Blog Posts Grid -->
|
<!-- Blog Posts Grid -->
|
||||||
<section class="blog-section" aria-label="文章列表">
|
<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="container">
|
<div class="max-w-[1200px] mx-auto">
|
||||||
{
|
{
|
||||||
posts.length > 0 ? (
|
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">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div class="empty-state">
|
<div class="text-center py-20 px-5">
|
||||||
<p class="empty-text">暫無文章</p>
|
<p class="text-lg text-slate-500">暫無文章</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -79,13 +79,13 @@ const validCategories = categories.filter(c =>
|
|||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{
|
{
|
||||||
totalPages > 1 && (
|
totalPages > 1 && (
|
||||||
<nav class="pagination" aria-label="分頁導航">
|
<nav class="pt-5" aria-label="分頁導航">
|
||||||
<div class="pagination-container">
|
<div class="flex items-center justify-center gap-6 md:gap-4">
|
||||||
{
|
{
|
||||||
hasPreviousPage && (
|
hasPreviousPage && (
|
||||||
<a
|
<a
|
||||||
href={`?page=${page - 1}`}
|
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="上一頁"
|
aria-label="上一頁"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
<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">
|
<div class="flex items-center gap-2 font-accent text-base text-slate-600">
|
||||||
<span class="current-page">{page}</span>
|
<span class="font-bold text-link-hover">{page}</span>
|
||||||
<span class="page-separator">/</span>
|
<span class="text-slate-400">/</span>
|
||||||
<span class="total-pages">{totalPages}</span>
|
<span>{totalPages}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
hasNextPage && (
|
hasNextPage && (
|
||||||
<a
|
<a
|
||||||
href={`?page=${page + 1}`}
|
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="下一頁"
|
aria-label="下一頁"
|
||||||
>
|
>
|
||||||
下一頁
|
下一頁
|
||||||
@@ -124,166 +124,3 @@ const validCategories = categories.filter(c =>
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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>
|
|
||||||
|
|||||||
@@ -4,178 +4,48 @@
|
|||||||
* 展示工作環境、公司故事和員工福利
|
* 展示工作環境、公司故事和員工福利
|
||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
*/
|
*/
|
||||||
import Layout from '../layouts/Layout.astro'
|
import Layout from "../layouts/Layout.astro";
|
||||||
import TeamsHero from '../sections/TeamsHero.astro'
|
import TeamsHero from "../sections/TeamsHero.astro";
|
||||||
import EnvironmentSlider from '../sections/EnvironmentSlider.astro'
|
import EnvironmentSlider from "../sections/EnvironmentSlider.astro";
|
||||||
import CompanyStory from '../sections/CompanyStory.astro'
|
import CompanyStory from "../sections/CompanyStory.astro";
|
||||||
import BenefitsSection from '../sections/BenefitsSection.astro'
|
import BenefitsSection from "../sections/BenefitsSection.astro";
|
||||||
|
import SectionHeader from "../components/SectionHeader.astro";
|
||||||
|
import CtaHrCompoents from "../sections/Cta-Hr-compoents.astro";
|
||||||
// Metadata for SEO
|
// Metadata for SEO
|
||||||
const title = '恩群大本營 | 恩群數位行銷'
|
const title = "恩群大本營 | 恩群數位行銷";
|
||||||
const description = '加入恩群數位的團隊,享受優質的工作環境與完善的員工福利。我們重視個人的特質發揮,歡迎樂於學習、善於建立關係的你加入我們。'
|
const description =
|
||||||
|
"加入恩群數位的團隊,享受優質的工作環境與完善的員工福利。我們重視個人的特質發揮,歡迎樂於學習、善於建立關係的你加入我們。";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={title} description={description}>
|
<Layout title={title} description={description}>
|
||||||
<!-- Hero Section -->
|
<!-- 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 -->
|
<!-- Environment Slider Section -->
|
||||||
|
|
||||||
<EnvironmentSlider />
|
<EnvironmentSlider />
|
||||||
|
|
||||||
<!-- Company Story Section -->
|
<!-- Company Story Section -->
|
||||||
<CompanyStory />
|
<CompanyStory />
|
||||||
|
|
||||||
<!-- Benefits Section -->
|
<!-- Benefits Section -->
|
||||||
|
|
||||||
<BenefitsSection />
|
<BenefitsSection />
|
||||||
|
|
||||||
<!-- CTA Section -->
|
<!-- CTA Section -->
|
||||||
<section class="section-call4action" aria-labelledby="cta-heading">
|
<CtaHrCompoents
|
||||||
<div class="container w-container">
|
title="以人的成長為優先<br />創造人的最大價值"
|
||||||
<div class="c4a-grid">
|
description="在恩群數位裡我們重視個人的特質能夠完全發揮,只要你樂於學習、善於跟人建立關係,並且重要的是你有一個善良的心,恩群數位歡迎你的加入"
|
||||||
<div class="c4a-content">
|
image={{
|
||||||
<h3 id="cta-heading" class="career-c4a-heading">
|
url: "https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b4723942d01_工作環境-銘言底圖-1400x659.jpg",
|
||||||
以人的成長為優先<br />
|
alt: "工作環境",
|
||||||
創造人的最大價值
|
}}
|
||||||
</h3>
|
/>
|
||||||
<p class="career-c4a-paragraph">
|
|
||||||
在恩群數位裡我們重視個人的特質能夠完全發揮,只要你樂於學習、善於跟人建立關係,並且重要的是你有一個善良的心,恩群數位歡迎你的加入
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="https://www.104.com.tw/company/1a2x6bkoaj?jobsource=joblist_r_cust"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="c4a-button"
|
|
||||||
>
|
|
||||||
立刻申請面試
|
|
||||||
</a>
|
|
||||||
<div class="c4a-bg"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Layout>
|
</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>
|
|
||||||
|
|||||||
@@ -1,345 +0,0 @@
|
|||||||
---
|
|
||||||
/**
|
|
||||||
* Portfolio Detail Page - 作品詳情頁
|
|
||||||
* Displays full project information with live website link
|
|
||||||
* Pixel-perfect implementation based on Webflow design
|
|
||||||
*/
|
|
||||||
import Layout from '../../layouts/Layout.astro'
|
|
||||||
import { fetchPortfolioBySlug, getWebsiteTypeLabel } from '../../lib/api/portfolio'
|
|
||||||
|
|
||||||
// Get slug from params
|
|
||||||
const { slug } = Astro.params
|
|
||||||
|
|
||||||
// Validate slug
|
|
||||||
if (!slug) {
|
|
||||||
return Astro.redirect('/404')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch portfolio item
|
|
||||||
const portfolio = await fetchPortfolioBySlug(slug)
|
|
||||||
|
|
||||||
// Handle 404 for non-existent portfolio
|
|
||||||
if (!portfolio) {
|
|
||||||
return Astro.redirect('/404')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get website type label
|
|
||||||
const typeLabel = getWebsiteTypeLabel(portfolio.websiteType)
|
|
||||||
|
|
||||||
// Get tags
|
|
||||||
const tags = portfolio.tags?.map(t => t.tag) || []
|
|
||||||
|
|
||||||
// SEO metadata
|
|
||||||
const title = `${portfolio.title} | 恩群數位案例分享`
|
|
||||||
const description = portfolio.description || `瀏覽 ${portfolio.title} 專案詳情`
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={title} description={description}>
|
|
||||||
<article class="portfolio-detail">
|
|
||||||
<div class="container">
|
|
||||||
<!-- Back Link -->
|
|
||||||
<a href="/portfolio" class="back-link">
|
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
||||||
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
|
|
||||||
</svg>
|
|
||||||
返回作品列表
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Project Header -->
|
|
||||||
<header class="project-header">
|
|
||||||
<div class="project-meta">
|
|
||||||
<span class="project-type-badge">{typeLabel}</span>
|
|
||||||
</div>
|
|
||||||
<h1 class="project-title">{portfolio.title}</h1>
|
|
||||||
|
|
||||||
{
|
|
||||||
portfolio.description && (
|
|
||||||
<p class="project-description">{portfolio.description}</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Tags -->
|
|
||||||
{
|
|
||||||
tags.length > 0 && (
|
|
||||||
<div class="project-tags">
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<span class="tag">{tag}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Hero Image -->
|
|
||||||
{
|
|
||||||
portfolio.image?.url && (
|
|
||||||
<div class="project-hero-image">
|
|
||||||
<img
|
|
||||||
src={portfolio.image.url}
|
|
||||||
alt={portfolio.image.alt || portfolio.title}
|
|
||||||
loading="eager"
|
|
||||||
width="1200"
|
|
||||||
height="675"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Project Content -->
|
|
||||||
<div class="project-content">
|
|
||||||
<div class="content-section">
|
|
||||||
<h2>專案介紹</h2>
|
|
||||||
<p>
|
|
||||||
此專案展示了我們在{typeLabel}領域的專業能力。
|
|
||||||
我們致力於為客戶打造符合品牌定位、使用體驗優良的數位產品。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content-section">
|
|
||||||
<h2>專案連結</h2>
|
|
||||||
{
|
|
||||||
portfolio.url ? (
|
|
||||||
<a
|
|
||||||
href={portfolio.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="btn-primary"
|
|
||||||
>
|
|
||||||
前往網站
|
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
||||||
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<p class="text-muted">此專案暫無公開連結</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CTA Section -->
|
|
||||||
<div class="detail-cta">
|
|
||||||
<h3>喜歡這個作品嗎?</h3>
|
|
||||||
<p>讓我們一起為您的品牌打造獨特的數位體驗</p>
|
|
||||||
<a href="/contact-us" class="cta-button">
|
|
||||||
聯絡我們
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Portfolio Detail */
|
|
||||||
.portfolio-detail {
|
|
||||||
padding: 60px 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Back Link */
|
|
||||||
.back-link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: var(--color-enchunblue, #23608c);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: color 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link:hover {
|
|
||||||
color: var(--color-enchunblue-hover, #1a4d6e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Project Header */
|
|
||||||
.project-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-meta {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-type-badge {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--color-enchunblue, #23608c);
|
|
||||||
color: white;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-title {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-dark-blue, #1a1a1a);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-description {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
color: var(--color-gray-600, #666);
|
|
||||||
max-width: 700px;
|
|
||||||
margin: 0 auto 1.5rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
background: var(--color-gray-100, #f5f5f5);
|
|
||||||
color: var(--color-gray-700, #4a5568);
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero Image */
|
|
||||||
.project-hero-image {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
border-radius: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-hero-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Project Content */
|
|
||||||
.project-content {
|
|
||||||
display: grid;
|
|
||||||
gap: 2.5rem;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section h2 {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-dark-blue, #1a1a1a);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section p {
|
|
||||||
color: var(--color-gray-600, #666);
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
background: var(--color-enchunblue, #23608c);
|
|
||||||
color: white;
|
|
||||||
padding: 0.875rem 1.75rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: var(--color-enchunblue-hover, #1a4d6e);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-muted {
|
|
||||||
color: var(--color-gray-500, #999);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Detail CTA */
|
|
||||||
.detail-cta {
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem 2rem;
|
|
||||||
background: var(--color-gray-50, #f9fafb);
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-cta h3 {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-enchunblue, #23608c);
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-cta p {
|
|
||||||
color: var(--color-gray-600, #666);
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--color-enchunblue, #23608c);
|
|
||||||
color: white;
|
|
||||||
padding: 14px 28px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button:hover {
|
|
||||||
background: var(--color-enchunblue-hover, #1a4d6e);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.portfolio-detail {
|
|
||||||
padding: 50px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.portfolio-detail {
|
|
||||||
padding: 40px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-title {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-description {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-cta {
|
|
||||||
padding: 2rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-cta h3 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
---
|
|
||||||
/**
|
|
||||||
* Portfolio Listing Page - 案例分享列表頁
|
|
||||||
* Displays all portfolio items in a 2-column grid
|
|
||||||
* Pixel-perfect implementation based on Webflow design
|
|
||||||
*/
|
|
||||||
import Layout from '../../layouts/Layout.astro'
|
|
||||||
import PortfolioCard from '../../components/PortfolioCard.astro'
|
|
||||||
import { fetchPortfolios } from '../../lib/api/portfolio'
|
|
||||||
|
|
||||||
// Metadata for SEO
|
|
||||||
const title = '案例分享 | 恩群數位行銷'
|
|
||||||
const description = '瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。'
|
|
||||||
|
|
||||||
// Fetch portfolios from Payload CMS
|
|
||||||
const portfoliosData = await fetchPortfolios(1, 100)
|
|
||||||
const portfolios = portfoliosData.docs
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={title} description={description}>
|
|
||||||
<!-- Portfolio Header -->
|
|
||||||
<section class="portfolio-header" aria-labelledby="portfolio-heading">
|
|
||||||
<div class="container">
|
|
||||||
<div class="section_header_w_line">
|
|
||||||
<div class="divider_line"></div>
|
|
||||||
<div class="header_subtitle">
|
|
||||||
<h2 id="portfolio-heading" class="header_subtitle_head">案例分享</h2>
|
|
||||||
<p class="header_subtitle_paragraph">Our Works</p>
|
|
||||||
</div>
|
|
||||||
<div class="divider_line"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Portfolio Grid -->
|
|
||||||
<section class="portfolio-grid-section" aria-label="作品列表">
|
|
||||||
<div class="container">
|
|
||||||
{
|
|
||||||
portfolios.length > 0 ? (
|
|
||||||
<ul class="portfolio-grid">
|
|
||||||
{
|
|
||||||
portfolios.map((item) => (
|
|
||||||
<PortfolioCard
|
|
||||||
item={{
|
|
||||||
slug: item.slug,
|
|
||||||
title: item.title,
|
|
||||||
description: item.description || '',
|
|
||||||
image: item.image?.url,
|
|
||||||
tags: item.tags?.map(t => t.tag) || [],
|
|
||||||
externalUrl: item.url || undefined,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<div class="empty-state">
|
|
||||||
<p class="empty-text">暫無作品資料</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Portfolio Header */
|
|
||||||
.portfolio-header {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty State */
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 80px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
color: var(--color-gray-500, #999);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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, #23608c);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-description {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--color-gray-600, #666);
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: var(--color-enchunblue, #23608c);
|
|
||||||
color: white;
|
|
||||||
padding: 16px 32px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
|
|
||||||
background-color: var(--color-enchunblue-hover, #1a4d6e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.portfolio-header {
|
|
||||||
padding: 50px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_head {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portfolio-grid {
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.portfolio-header {
|
|
||||||
padding: 40px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_head {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_paragraph {
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-description {
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
padding: 14px 24px;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,442 +0,0 @@
|
|||||||
---
|
|
||||||
/**
|
|
||||||
* Portfolio Detail Page - 案例詳情頁
|
|
||||||
* Pixel-perfect implementation based on Webflow design
|
|
||||||
*/
|
|
||||||
import Layout from '../../layouts/Layout.astro'
|
|
||||||
|
|
||||||
// Get portfolio items
|
|
||||||
const portfolioItems: Record<string, {
|
|
||||||
title: string
|
|
||||||
client?: string
|
|
||||||
date?: string
|
|
||||||
category?: string
|
|
||||||
description: string
|
|
||||||
images?: string[]
|
|
||||||
externalUrl?: string
|
|
||||||
}> = {
|
|
||||||
'corporate-website-1': {
|
|
||||||
title: '企業官網設計案例',
|
|
||||||
client: '知名製造業公司',
|
|
||||||
date: '2024年1月',
|
|
||||||
category: '企業官網',
|
|
||||||
description: `
|
|
||||||
<p>這是一個為知名製造業公司打造的現代化企業官網專案。客戶希望建立一個能夠有效展示產品系列、公司形象以及最新消息的專業網站。</p>
|
|
||||||
|
|
||||||
<h3>專案背景</h3>
|
|
||||||
<p>客戶原有的網站設計過時,難以在手機設備上正常瀏覽,且內容管理不夠靈活。他們需要一個響應式設計的現代化網站,能夠自動適應各種設備,並方便內容團隊更新。</p>
|
|
||||||
|
|
||||||
<h3>解決方案</h3>
|
|
||||||
<p>我們採用了最新的網頁技術,打造了一個完全響應式的企業官網。網站架構清晰,導航直觀,產品展示頁面設計精美,同時整合了新聞發佈功能,讓客戶能夠快速分享公司動態。</p>
|
|
||||||
|
|
||||||
<h3>專案成果</h3>
|
|
||||||
<p>網站上線後,客戶反映使用者停留時間增加了 40%,詢問量也顯著提升。響應式設計讓行動用戶也能獲得良好的瀏覽體驗,整體滿意度非常高。</p>
|
|
||||||
`,
|
|
||||||
images: ['/placeholder-portfolio-1.jpg'],
|
|
||||||
},
|
|
||||||
'ecommerce-site-1': {
|
|
||||||
title: '電商平台建置',
|
|
||||||
client: '精品品牌',
|
|
||||||
date: '2024年2月',
|
|
||||||
category: '電商網站',
|
|
||||||
description: `
|
|
||||||
<p>這是一個為精品品牌打造的 B2C 電商網站專案。客戶需要一個功能完整、設計精緻的線上購物平台。</p>
|
|
||||||
|
|
||||||
<h3>專案背景</h3>
|
|
||||||
<p>客戶希望拓展線上銷售渠道,建立一個能夠完整呈現品牌調性的電商網站。需求包含會員系統、購物車、金流整合、庫存管理等完整功能。</p>
|
|
||||||
|
|
||||||
<h3>解決方案</h3>
|
|
||||||
<p>我們設計了一個簡潔優雅的電商平台,整合了完整的購物流程。從商品瀏覽、加入購物車、結帳到訂單追蹤,整個流程流暢無縫。同時整合了主流金流與物流服務。</p>
|
|
||||||
|
|
||||||
<h3>專案成果</h3>
|
|
||||||
<p>網站上線後首月即達到預期銷售目標,客戶反應購物體驗流暢,設計風格也獲得用戶高度評價。</p>
|
|
||||||
`,
|
|
||||||
images: ['/placeholder-portfolio-2.jpg'],
|
|
||||||
},
|
|
||||||
'brand-website-1': {
|
|
||||||
title: '品牌形象網站',
|
|
||||||
client: '新創品牌',
|
|
||||||
date: '2024年3月',
|
|
||||||
category: '品牌網站',
|
|
||||||
description: `
|
|
||||||
<p>這是一個為新創品牌打造的以視覺故事為核心的品牌網站專案。</p>
|
|
||||||
|
|
||||||
<h3>專案背景</h3>
|
|
||||||
<p>客戶是一個新創品牌,需要一個能夠有效傳達品牌故事、價值理念的網站。他們希望網站不只是資訊展示,更能成為品牌與用戶情感連結的橋樑。</p>
|
|
||||||
|
|
||||||
<h3>解決方案</h3>
|
|
||||||
<p>我們以視覺故事為設計核心,運用大型視覺元素、動畫效果和互動設計,打造了一個充滿故事性的品牌網站。每一個區塊都精心設計,引導用戶深入了解品牌。</p>
|
|
||||||
|
|
||||||
<h3>專案成果</h3>
|
|
||||||
<p>網站成功建立了強烈的品牌印象,訪客平均停留時間超過 3 分鐘,品牌認知度顯著提升。</p>
|
|
||||||
`,
|
|
||||||
images: ['/placeholder-portfolio-3.jpg'],
|
|
||||||
},
|
|
||||||
'landing-page-1': {
|
|
||||||
title: '活動行銷頁面',
|
|
||||||
client: '活動主辦單位',
|
|
||||||
date: '2024年4月',
|
|
||||||
category: '活動頁面',
|
|
||||||
description: `
|
|
||||||
<p>這是一個為大型活動設計的高轉換率行銷頁面專案。</p>
|
|
||||||
|
|
||||||
<h3>專案背景</h3>
|
|
||||||
<p>客戶需要一個能夠有效吸引報名、轉換率高的活動頁面。頁面需要能夠清楚傳達活動資訊、吸引目光,並引導用戶完成報名流程。</p>
|
|
||||||
|
|
||||||
<h3>解決方案</h3>
|
|
||||||
<p>我們設計了一個視覺衝擊力強的活動頁面,CTA 按鈕配置經過精心規劃,報名流程簡化到最少步驟。同時加入倒數計時、名額顯示等促進轉換的元素。</p>
|
|
||||||
|
|
||||||
<h3>專案成果</h3>
|
|
||||||
<p>頁面上線後,報名轉換率達到 15%,遠超過預期目標,活動順利達到滿檔狀態。</p>
|
|
||||||
`,
|
|
||||||
images: ['/placeholder-portfolio-4.jpg'],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const slugs = Object.keys(portfolioItems)
|
|
||||||
|
|
||||||
return slugs.map((slug) => ({
|
|
||||||
params: { slug },
|
|
||||||
props: { slug }
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const { slug } = Astro.props
|
|
||||||
const project = portfolioItems[slug] || {
|
|
||||||
title: '作品詳情',
|
|
||||||
description: '<p>暫無內容</p>',
|
|
||||||
images: ['/placeholder-portfolio.jpg'],
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata for SEO
|
|
||||||
const title = `${project.title} | 恩群數位案例`
|
|
||||||
const description = project.description.replace(/<[^>]*>/g, '').slice(0, 160)
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={title} description={description}>
|
|
||||||
<!-- Breadcrumb -->
|
|
||||||
<nav class="breadcrumb" aria-label="麵包屑導航">
|
|
||||||
<div class="container">
|
|
||||||
<ol class="breadcrumb-list">
|
|
||||||
<li><a href="/">首頁</a></li>
|
|
||||||
<li><a href="/website-portfolio">案例分享</a></li>
|
|
||||||
<li aria-current="page">{project.title}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Portfolio Detail -->
|
|
||||||
<article class="portfolio-detail">
|
|
||||||
<div class="container">
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="portfolio-detail-header">
|
|
||||||
<h1 class="portfolio-detail-title">{project.title}</h1>
|
|
||||||
|
|
||||||
<div class="portfolio-detail-meta">
|
|
||||||
{
|
|
||||||
project.client && (
|
|
||||||
<span class="meta-item">
|
|
||||||
<strong>客戶:</strong>{project.client}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
project.date && (
|
|
||||||
<span class="meta-item">
|
|
||||||
<strong>日期:</strong>{project.date}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
project.category && (
|
|
||||||
<span class="meta-item">
|
|
||||||
<strong>類別:</strong>{project.category}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Featured Image -->
|
|
||||||
{
|
|
||||||
project.images && project.images.length > 0 && (
|
|
||||||
<div class="portfolio-detail-image-wrapper">
|
|
||||||
<img
|
|
||||||
src={project.images[0]}
|
|
||||||
alt={project.title}
|
|
||||||
class="portfolio-detail-image"
|
|
||||||
loading="eager"
|
|
||||||
width="1000"
|
|
||||||
height="562"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<div class="portfolio-detail-content">
|
|
||||||
<div class="description-wrapper" set:html={project.description} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
<div class="portfolio-detail-cta">
|
|
||||||
<h3 class="cta-heading">有專案想要討論嗎?</h3>
|
|
||||||
<p class="cta-subheading">我們很樂意聽聽您的需求</p>
|
|
||||||
<a href="/contact-us" class="cta-button">
|
|
||||||
聯絡我們
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Related Projects -->
|
|
||||||
<div class="related-projects">
|
|
||||||
<h3 class="related-title">更多案例</h3>
|
|
||||||
<a href="/website-portfolio" class="view-all-link">
|
|
||||||
查看全部案例 →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Portfolio Detail Styles - Pixel-perfect from Webflow */
|
|
||||||
|
|
||||||
/* Breadcrumb */
|
|
||||||
.breadcrumb {
|
|
||||||
padding: 16px 20px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-gray-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-list a {
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-list a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb-list li:not(:last-child)::after {
|
|
||||||
content: '/';
|
|
||||||
margin-left: 8px;
|
|
||||||
color: var(--color-gray-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Detail Section */
|
|
||||||
.portfolio-detail {
|
|
||||||
padding: 60px 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.portfolio-detail-header {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -3,223 +3,78 @@
|
|||||||
* Portfolio Listing Page - 案例分享列表頁
|
* Portfolio Listing Page - 案例分享列表頁
|
||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
*/
|
*/
|
||||||
import Layout from '../layouts/Layout.astro'
|
import Layout from "../layouts/Layout.astro";
|
||||||
import PortfolioCard from '../components/PortfolioCard.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
|
// Metadata for SEO
|
||||||
const title = '案例分享 | 恩群數位行銷'
|
const title = "案例分享 | 恩群數位行銷";
|
||||||
const description = '瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。'
|
const description =
|
||||||
|
"瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。";
|
||||||
|
|
||||||
// Portfolio items - can be fetched from Payload CMS
|
// Portfolio items - can be fetched from Payload CMS
|
||||||
const portfolioItems = [
|
const portfolioItems = [
|
||||||
{
|
{
|
||||||
slug: 'corporate-website-1',
|
slug: "corporate-website-1",
|
||||||
title: '企業官網設計案例',
|
title: "Saas 一頁式頁面",
|
||||||
description: '為知名製造業打造的現代化企業官網,整合產品展示與新聞發佈功能。',
|
description:
|
||||||
image: '/placeholder-portfolio-1.jpg',
|
"現代SaaS一頁式網站,吸引目標客戶並提升轉化率而設計。採用React、Tailwind CSS和TypeScript等先進技術構建,響應式流暢導航、視覺效果和性能。非常適合初創公司和科技新創",
|
||||||
tags: ['企業官網', '響應式設計'],
|
image:
|
||||||
|
"https://enchun-cms.anlstudio.cc/api/media/file/67a99c71f1be7be0b6fb5b6b_saas_website_design-p-1080.webp",
|
||||||
|
tags: ["SaaS", "一頁式頁面"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'ecommerce-site-1',
|
slug: "ecommerce-site-1",
|
||||||
title: '電商平台建置',
|
title: "BioNova銷售頁",
|
||||||
description: 'B2C 電商網站,包含會員系統、購物車、金流整合等完整功能。',
|
description:
|
||||||
image: '/placeholder-portfolio-2.jpg',
|
"本網站以簡潔、專業、易於操作的介面,向受眾傳達 BioNova 超能複方菁萃膠囊的產品特色、優勢及功效,並鼓勵消費者立即購買。",
|
||||||
tags: ['電商網站', '金流整合'],
|
image:
|
||||||
|
"https://enchun-cms.anlstudio.cc/api/media/file/67a9a0aa4ef074c15c2b4409_landingpage_website_design-p-1080.webp",
|
||||||
|
tags: ["電商網站", "金流整合"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'brand-website-1',
|
slug: "brand-website-1",
|
||||||
title: '品牌形象網站',
|
title: "美髮業預約頁面",
|
||||||
description: '以視覺故事為核心的品牌網站,展現品牌獨特價值與理念。',
|
description:
|
||||||
image: '/placeholder-portfolio-3.jpg',
|
"美髮官網線上預約系統,透過便捷高效的線上預約系統已成為吸引顧客、提升服務品質的關鍵。一個專業、時尚、易於操作的線上平台,讓顧客隨時隨地輕鬆預約美髮服務,同時展示美髮店的專業形象、服務特色及最新資訊。",
|
||||||
tags: ['品牌網站', '視覺設計'],
|
image:
|
||||||
|
"https://enchun-cms.anlstudio.cc/api/media/file/67aacde54fbce6ce004c18d2_booking_website_design_alt-p-1080.webp",
|
||||||
|
tags: ["品牌網站", "視覺設計"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'landing-page-1',
|
slug: "landing-page-1",
|
||||||
title: '活動行銷頁面',
|
title: "物流業官方網站",
|
||||||
description: '高轉換率的活動頁面設計,有效的 CTA 配置與使用者體驗規劃。',
|
description:
|
||||||
image: '/placeholder-portfolio-4.jpg',
|
"專業、高效的物流網站已成為必然趨勢,透過設計出操作便捷的物流網站,以滿足現代物流業務的需求。提供線上客服功能,解答客戶疑問提供幫助。",
|
||||||
tags: ['活動頁面', '行銷'],
|
image:
|
||||||
|
"https://enchun-cms.anlstudio.cc/api/media/file/67a9acb0737c25d4ba894081_freight_website_design-p-1080.webp",
|
||||||
|
tags: ["活動頁面", "行銷"],
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={title} description={description}>
|
<Layout title={title} description={description}>
|
||||||
<!-- Portfolio Header -->
|
<HeaderBg />
|
||||||
<section class="portfolio-header" aria-labelledby="portfolio-heading">
|
<SectionHeader
|
||||||
<div class="container">
|
title="案例分享"
|
||||||
<h1 id="portfolio-heading" class="portfolio-title">案例分享</h1>
|
subtitle="Selected Works"
|
||||||
<p class="portfolio-subtitle">Selected Works</p>
|
sectionBg="bg-white"
|
||||||
<div class="divider-line"></div>
|
/>
|
||||||
<div class="divider-line"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Portfolio Grid -->
|
<!-- Portfolio Grid -->
|
||||||
<section class="portfolio-grid-section" aria-label="作品列表">
|
<section
|
||||||
<ul class="portfolio-grid">
|
class="py-0 px-5 pb-20 bg-white lg:px-4 lg:pb-10"
|
||||||
{
|
aria-label="作品列表"
|
||||||
portfolioItems.map((item) => (
|
>
|
||||||
<PortfolioCard item={item} />
|
<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>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA Section -->
|
<!-- CTA Section -->
|
||||||
<section class="portfolio-cta" aria-labelledby="cta-heading">
|
<CtaSection />
|
||||||
<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>
|
|
||||||
</Layout>
|
</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>
|
|
||||||
|
|||||||
@@ -4,81 +4,73 @@
|
|||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
*/
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string
|
title?: string;
|
||||||
subtitle?: string
|
subtitle?: string;
|
||||||
|
backgroundImage?: {
|
||||||
|
url?: string;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title = '關於恩群數位',
|
title = "關於恩群數位",
|
||||||
subtitle = 'About Enchun digital',
|
subtitle = "About Enchun digital",
|
||||||
} = Astro.props
|
backgroundImage,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
// Determine if we have a background image
|
||||||
|
const hasBackgroundImage = backgroundImage?.url;
|
||||||
|
const bgImageUrl = backgroundImage?.url || "";
|
||||||
---
|
---
|
||||||
|
|
||||||
<header class="hero-overlay-about">
|
<section
|
||||||
<div class="w-container">
|
class:list={[
|
||||||
<div class="div-block">
|
// Base styles - relative positioning for overlay
|
||||||
<h1 class="hero_title_head-about">{title}</h1>
|
"relative",
|
||||||
<p class="hero_sub_paragraph-about">{subtitle}</p>
|
"flex",
|
||||||
</div>
|
"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>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* About Hero Styles - Pixel-perfect from Webflow */
|
|
||||||
.hero-overlay-about {
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 120px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_title_head-about {
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
font-family: "Noto Sans TC", "Quicksand", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 3rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_sub_paragraph-about {
|
|
||||||
color: var(--color-enchunblue-dark);
|
|
||||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.hero-overlay-about {
|
|
||||||
padding: 80px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_title_head-about {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_sub_paragraph-about {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.hero-overlay-about {
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_title_head-about {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_sub_paragraph-about {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -6,304 +6,93 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
interface BenefitItem {
|
interface BenefitItem {
|
||||||
title: string
|
title: string;
|
||||||
icon: string
|
img: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
benefits?: BenefitItem[]
|
benefits?: BenefitItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBenefits: BenefitItem[] = [
|
const defaultBenefits: BenefitItem[] = [
|
||||||
{
|
{
|
||||||
title: '高績效、高獎金\n新人開張獎金',
|
title: "高績效、高獎金\n新人開張獎金",
|
||||||
icon: 'bonus',
|
img: "/api/media/file/61f24aa108528b79b2942d05_Make%20it%20rain-bro-%E6%96%B0%E4%BA%BA%E9%96%8B%E5%BC%B5%E7%8D%8E%E9%87%91.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '生日慶生、電影日\n員工下午茶',
|
title: "生日慶生、電影日\n員工下午茶",
|
||||||
icon: 'birthday',
|
img: "/api/media/file/61f24aa108528be590942d06_Blowing%20out%20Birthday%20candles-bro-%E7%94%9F%E6%97%A5%E6%85%B6%E7%94%9F.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '教育訓練補助',
|
title: "教育訓練補助",
|
||||||
icon: 'education',
|
img: "/api/media/file/61f24aa108528be22a942d03_Online%20learning-bro-%E6%95%99%E8%82%B2%E8%A8%93%E7%B7%B4%E8%A3%9C%E5%8A%A9.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '寬敞的工作空間',
|
title: "寬敞的工作空間",
|
||||||
icon: 'workspace',
|
img: "/api/media/file/61f24aa108528be064942d08_Shared%20workspace-bro-%E5%AF%AC%E6%95%9E%E7%9A%84%E5%B7%A5%E4%BD%9C%E7%A9%BA%E9%96%93.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '員工國內外旅遊\n部門聚餐、年終活動',
|
title: "員工國內外旅遊\n部門聚餐、年終活動",
|
||||||
icon: 'travel',
|
img: "/api/media/file/61f24aa108528b0960942d04_Flight%20Booking-bro-%E5%93%A1%E5%B7%A5%E6%97%85%E9%81%8A.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '入職培訓及團隊建設',
|
title: "入職培訓及團隊建設",
|
||||||
icon: 'training',
|
img: "/api/media/file/61f24aa108528bf90b942d02_Brainstorming-bro-%E5%85%A5%E8%81%B7%E5%9F%B9%E8%A8%93.svg",
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
const benefits = Astro.props.benefits || defaultBenefits
|
const benefits = Astro.props.benefits || defaultBenefits;
|
||||||
|
import SectionHeader from "../components/SectionHeader.astro";
|
||||||
// Icon SVG components (placeholder for now, replace with actual SVG)
|
|
||||||
const getIconSVG = (iconType: string) => {
|
|
||||||
const icons: Record<string, string> = {
|
|
||||||
bonus: `<svg viewBox="0 0 200 200" class="benefit-icon-svg">
|
|
||||||
<circle cx="100" cy="70" r="40" fill="#23608c" opacity="0.2"/>
|
|
||||||
<rect x="60" y="100" width="80" height="60" rx="8" fill="#23608c"/>
|
|
||||||
<text x="100" y="140" text-anchor="middle" fill="white" font-size="36">💰</text>
|
|
||||||
</svg>`,
|
|
||||||
birthday: `<svg viewBox="0 0 200 200" class="benefit-icon-svg">
|
|
||||||
<circle cx="100" cy="70" r="40" fill="#23608c" opacity="0.2"/>
|
|
||||||
<rect x="60" y="100" width="80" height="60" rx="8" fill="#23608c"/>
|
|
||||||
<text x="100" y="140" text-anchor="middle" fill="white" font-size="36">🎂</text>
|
|
||||||
</svg>`,
|
|
||||||
education: `<svg viewBox="0 0 200 200" class="benefit-icon-svg">
|
|
||||||
<circle cx="100" cy="70" r="40" fill="#23608c" opacity="0.2"/>
|
|
||||||
<rect x="60" y="100" width="80" height="60" rx="8" fill="#23608c"/>
|
|
||||||
<text x="100" y="140" text-anchor="middle" fill="white" font-size="36">📚</text>
|
|
||||||
</svg>`,
|
|
||||||
workspace: `<svg viewBox="0 0 200 200" class="benefit-icon-svg">
|
|
||||||
<circle cx="100" cy="70" r="40" fill="#23608c" opacity="0.2"/>
|
|
||||||
<rect x="60" y="100" width="80" height="60" rx="8" fill="#23608c"/>
|
|
||||||
<text x="100" y="140" text-anchor="middle" fill="white" font-size="36">🏢</text>
|
|
||||||
</svg>`,
|
|
||||||
travel: `<svg viewBox="0 0 200 200" class="benefit-icon-svg">
|
|
||||||
<circle cx="100" cy="70" r="40" fill="#23608c" opacity="0.2"/>
|
|
||||||
<rect x="60" y="100" width="80" height="60" rx="8" fill="#23608c"/>
|
|
||||||
<text x="100" y="140" text-anchor="middle" fill="white" font-size="36">✈️</text>
|
|
||||||
</svg>`,
|
|
||||||
training: `<svg viewBox="0 0 200 200" class="benefit-icon-svg">
|
|
||||||
<circle cx="100" cy="70" r="40" fill="#23608c" opacity="0.2"/>
|
|
||||||
<rect x="60" y="100" width="80" height="60" rx="8" fill="#23608c"/>
|
|
||||||
<text x="100" y="140" text-anchor="middle" fill="white" font-size="36">🤝</text>
|
|
||||||
</svg>`,
|
|
||||||
}
|
|
||||||
return icons[iconType] || icons.bonus
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="section-benefit" aria-labelledby="benefits-heading">
|
<section
|
||||||
<div class="container w-container">
|
class="py-15 px-5 bg-white md:py-10 md:px-4"
|
||||||
|
aria-labelledby="benefits-heading"
|
||||||
|
>
|
||||||
|
<div class="max-w-6xl mx-auto">
|
||||||
<!-- Section Header -->
|
<!-- Section Header -->
|
||||||
<div class="section_header_w_line">
|
<SectionHeader
|
||||||
<div class="divider_line"></div>
|
title="工作福利"
|
||||||
<div class="header_subtitle">
|
subtitle="Benefit Packages"
|
||||||
<h2 id="benefits-heading" class="header_subtitle_head">工作福利</h2>
|
sectionBg="bg-white"
|
||||||
<p class="header_subtitle_paragraph">Benefit Package</p>
|
/>
|
||||||
</div>
|
|
||||||
<div class="divider_line"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Benefits Grid -->
|
<!-- Benefits Grid: 2 cards per row -->
|
||||||
<div class="benefit-grid-wrapper">
|
<div
|
||||||
|
class="max-w-md md:max-w-3xl mx-auto grid grid-cols-1 gap-x-2 gap-y-2 md:gap-y-6 md:grid-cols-2 py-18"
|
||||||
|
>
|
||||||
{
|
{
|
||||||
benefits.map((benefit, index) => (
|
benefits.map((benefit, index) => {
|
||||||
<div class={`benefit-card ${index % 2 === 0 ? 'benefit-card' : 'benefit-card-opposite'}`}>
|
const isLeft = index % 2 === 0;
|
||||||
<!-- Odd: Icon on right, Even: Icon on left -->
|
return (
|
||||||
{
|
<div class="grid grid-cols-2 gap-2 items-center">
|
||||||
index % 2 === 0 ? (
|
<div
|
||||||
<>
|
class:list={[
|
||||||
<div class="benefit-content">
|
isLeft ? "order-1 text-right" : "order-2 text-left",
|
||||||
<h3 class="benefit-title-text">{benefit.title}</h3>
|
]}
|
||||||
</div>
|
>
|
||||||
<div class="benefit-image-right" set:html={getIconSVG(benefit.icon)} />
|
<h3 class="text-xl md:text-base lg:text-xl font-semibold text-(--color-enchunblue) whitespace-pre-wrap leading-snug">
|
||||||
</>
|
{benefit.title}
|
||||||
) : (
|
</h3>
|
||||||
<>
|
</div>
|
||||||
<div class="benefit-image-left" set:html={getIconSVG(benefit.icon)} />
|
<div
|
||||||
<div class="benefit-content">
|
class:list={[
|
||||||
<h3 class="benefit-title-text">{benefit.title}</h3>
|
"flex items-center",
|
||||||
</div>
|
isLeft ? "order-2 justify-start" : "order-1 justify-end",
|
||||||
</>
|
]}
|
||||||
)
|
>
|
||||||
}
|
<img
|
||||||
</div>
|
src={`https://enchun-cms.anlstudio.cc${benefit.img}`}
|
||||||
))
|
alt={benefit.title}
|
||||||
|
class="size-35 object-contain"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Benefits Section Styles - Pixel-perfect from Webflow */
|
|
||||||
.section-benefit {
|
|
||||||
padding: 60px 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section Header */
|
|
||||||
.section_header_w_line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_head {
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_paragraph {
|
|
||||||
color: var(--color-gray-600);
|
|
||||||
font-family: "Quicksand", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider_line {
|
|
||||||
width: 40px;
|
|
||||||
height: 2px;
|
|
||||||
background-color: var(--color-enchunblue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Benefits Grid */
|
|
||||||
.benefit-grid-wrapper {
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Benefit Card */
|
|
||||||
.benefit-card,
|
|
||||||
.benefit-card-opposite {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 40px;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Odd cards: icon on right */
|
|
||||||
.benefit-card {
|
|
||||||
grid-template-areas: "content image";
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-card .benefit-content {
|
|
||||||
grid-area: content;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-card .benefit-image-right {
|
|
||||||
grid-area: image;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Even cards: icon on left */
|
|
||||||
.benefit-card-opposite {
|
|
||||||
grid-template-areas: "image content";
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-card-opposite .benefit-content {
|
|
||||||
grid-area: content;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-card-opposite .benefit-image-left {
|
|
||||||
grid-area: image;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Benefit Title */
|
|
||||||
.benefit-title-text {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-tarawera, #23608c);
|
|
||||||
white-space: pre-line;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Benefit Icon */
|
|
||||||
.benefit-icon-svg {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-image-right,
|
|
||||||
.benefit-image-left {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-image-right svg,
|
|
||||||
.benefit-image-left svg {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.benefit-card,
|
|
||||||
.benefit-card-opposite {
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-title-text {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.section-benefit {
|
|
||||||
padding: 40px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section_header_w_line {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-card,
|
|
||||||
.benefit-card-opposite {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-areas: "image" "content" !important;
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-content {
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-image-right,
|
|
||||||
.benefit-image-left {
|
|
||||||
order: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-title-text {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-icon-svg {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-image-right svg,
|
|
||||||
.benefit-image-left svg {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -4,128 +4,36 @@
|
|||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
*/
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string
|
title?: string;
|
||||||
subtitle?: string
|
subtitle?: string;
|
||||||
content?: string
|
content?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title = '恩群數位的故事',
|
title = "恩群數位的故事",
|
||||||
subtitle = 'Something About Enchun Digital',
|
subtitle = "Something About Enchun Digital",
|
||||||
content = '恩群數位是由一群年輕果斷、敢冒險的年輕人聚在一起,共同為台灣在地經營努力不懈的商家老闆們建立品牌的知名度。而商家的經營本身就不是一件容易的事情,早在恩群成立之前,我們便一直聚焦在與不同行業的老闆們建立起信賴可靠的關係,從一家又一家的合作關係當中,聚集了在行銷領域裡的頂尖好手,培育了許多優秀行銷顧問。讓每個辛苦經營的商家老闆可以獲得最佳的服務,讓每次的行銷需求可以透過有效的互動與聆聽,達到彼此心目中的預期目標。數字的確會說話,但是每一個有溫度的服務才是在恩群裡最重視的地方。',
|
content = "恩群數位是由一群年輕果斷、敢冒險的年輕人聚在一起,共同為台灣在地經營努力不懈的商家老闆們建立品牌的知名度。而商家的經營本身就不是一件容易的事情,早在恩群成立之前,我們便一直聚焦在與不同行業的老闆們建立起信賴可靠的關係,從一家又一家的合作關係當中,聚集了在行銷領域裡的頂尖好手,培育了許多優秀行銷顧問。讓每個辛苦經營的商家老闆可以獲得最佳的服務,讓每次的行銷需求可以透過有效的互動與聆聽,達到彼此心目中的預期目標。數字的確會說話,但是每一個有溫度的服務才是在恩群裡最重視的地方。",
|
||||||
} = Astro.props
|
} = Astro.props;
|
||||||
|
import SectionHeader from "../components/SectionHeader.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="section-story" aria-labelledby="story-heading">
|
<section
|
||||||
<div class="container w-container">
|
class="section-story py-20 md:py-[60px] md:px-4 max-md:py-10 max-md:px-4 text-center bg-white"
|
||||||
|
aria-labelledby="story-heading"
|
||||||
|
>
|
||||||
|
<div class="container w-container max-w-4xl mx-auto">
|
||||||
<!-- Section Header -->
|
<!-- Section Header -->
|
||||||
<div class="section_header_w_line">
|
<SectionHeader
|
||||||
<div class="divider_line"></div>
|
title="恩群數位的故事"
|
||||||
<div class="header_subtitle">
|
subtitle="Something About Enchun Digital"
|
||||||
<h2 id="story-heading" class="header_subtitle_head">{title}</h2>
|
sectionBg="bg-white"
|
||||||
<p class="header_subtitle_paragraph">{subtitle}</p>
|
/>
|
||||||
</div>
|
|
||||||
<div class="divider_line"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Story Content -->
|
<!-- Story Content -->
|
||||||
<p class="story-paragraph">{content}</p>
|
<p
|
||||||
|
class="story-paragraph max-w-3xl mx-auto text-base md:text-lg font-thin leading-[1.8] max-md:leading-[1.7] text-(--color-text-secondary)/80 font-[Noto_Sans_TC,sans-serif]"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Company Story Styles - Pixel-perfect from Webflow */
|
|
||||||
.section-story {
|
|
||||||
padding: 80px 20px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section Header */
|
|
||||||
.section_header_w_line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_head {
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_paragraph {
|
|
||||||
color: var(--color-gray-600);
|
|
||||||
font-family: "Quicksand", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider_line {
|
|
||||||
width: 40px;
|
|
||||||
height: 2px;
|
|
||||||
background-color: var(--color-enchunblue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Story Content */
|
|
||||||
.story-paragraph {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.section-story {
|
|
||||||
padding: 60px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_head {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-paragraph {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.section-story {
|
|
||||||
padding: 40px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section_header_w_line {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_head {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-paragraph {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,274 +1,139 @@
|
|||||||
---
|
---
|
||||||
|
import SectionHeader from "@/components/SectionHeader.astro";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ComparisonSection - 恩群數位 vs 其他行銷公司 對比表格
|
* ComparisonSection - 恩群數位 vs 其他行銷公司 對比表格
|
||||||
* Pixel-perfect implementation based on Webflow design
|
* Custom layout based on new design specs with Tailwind CSS
|
||||||
*/
|
*/
|
||||||
interface ComparisonItem {
|
|
||||||
feature: string
|
|
||||||
enchun: string
|
|
||||||
others: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const comparisonItems: ComparisonItem[] = [
|
const otherCompanyItems = [
|
||||||
{
|
"缺乏經驗",
|
||||||
feature: '服務範圍',
|
"沒有成效保證",
|
||||||
enchun: '全方位數位行銷服務,從策略到執行一條龍',
|
"售後無服務",
|
||||||
others: '單一服務項目,缺乏整合性',
|
"沒有策略",
|
||||||
},
|
"不了解客戶需求",
|
||||||
{
|
"沒有接受客戶反饋",
|
||||||
feature: '數據分析',
|
];
|
||||||
enchun: '專業數據分析團隊,精準追蹤 ROI',
|
|
||||||
others: '基礎報告,缺乏深度分析',
|
const enchunItems = [
|
||||||
},
|
"實際執行經驗豐富",
|
||||||
{
|
"實際成效",
|
||||||
feature: '在地化經驗',
|
"售後服務架構完善",
|
||||||
enchun: '深耕台灣市場,了解本地消費者習性',
|
"行銷策略有方",
|
||||||
others: '通用策略,缺乏在地化調整',
|
"熟悉客戶需求",
|
||||||
},
|
"最多客戶回饋",
|
||||||
{
|
];
|
||||||
feature: '客戶服務',
|
|
||||||
enchun: '一對一專人服務,快速響應',
|
|
||||||
others: '標準化流程,回應較慢',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
feature: '價格透明',
|
|
||||||
enchun: '明確報價,無隱藏費用',
|
|
||||||
others: '複雜收費結構,容易超支',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="section-comparison" aria-labelledby="comparison-heading">
|
<section
|
||||||
<div class="w-container">
|
class="bg-[#f8f9fa] py-10 px-3 md:py-[60px] md:px-4 lg:py-20 lg:px-5"
|
||||||
|
aria-labelledby="comparison-heading"
|
||||||
|
>
|
||||||
|
<div class="max-w-[1000px] mx-auto">
|
||||||
<!-- Section Header -->
|
<!-- Section Header -->
|
||||||
<div class="section-header-w-line">
|
<SectionHeader
|
||||||
<h2 id="comparison-heading" class="header-subtitle-head">
|
title="恩群與其他公司有什麼不同"
|
||||||
為什麼選擇恩群數位
|
subtitle="What make us different from others"
|
||||||
</h2>
|
/>
|
||||||
<div class="divider-line"></div>
|
|
||||||
|
<!-- Comparison Cards Layout -->
|
||||||
|
<div
|
||||||
|
class="relative bg-white rounded-2xl shadow-xl overflow-hidden mt-10 md:mt-12 max-w-4xl mx-auto border border-gray-100"
|
||||||
|
>
|
||||||
|
<!-- Winning Medal (Absolute Positioned) -->
|
||||||
|
<div
|
||||||
|
class="absolute -top-2 right-6 md:right-12 w-16 h-16 md:w-20 md:h-20 z-10 drop-shadow-lg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://cdn.prod.website-files.com/61f24aa108528b1962942c95/61f24aa108528b7eab942cd9_winning%20medal.svg"
|
||||||
|
loading="lazy"
|
||||||
|
alt="Winning Medal"
|
||||||
|
class="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||||
|
<!-- Left Column: Other Companies -->
|
||||||
|
<div
|
||||||
|
class="p-8 md:p-12 border-b md:border-b-0 md:border-r border-gray-200 bg-gray-50 flex flex-col gap-6"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="text-2xl md:text-3xl font-bold text-gray-800 tracking-tight font-['Noto_Sans_TC',sans-serif]"
|
||||||
|
>
|
||||||
|
其他行銷公司
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
class="text-gray-500 text-sm md:text-base leading-relaxed h-[60px]"
|
||||||
|
>
|
||||||
|
市場上每間行銷公司想要讓自己的公司服務可以被看見,有時候客戶只能成為待宰羔羊
|
||||||
|
</p>
|
||||||
|
<div class="h-px w-full bg-gray-200 my-2"></div>
|
||||||
|
<ul role="list" class="flex flex-col gap-4 md:gap-5 mt-2">
|
||||||
|
{
|
||||||
|
otherCompanyItems.map((item) => (
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="text-gray-700 font-medium text-base md:text-lg flex-1">
|
||||||
|
{item}
|
||||||
|
</span>
|
||||||
|
<div class="flex-shrink-0 flex items-center justify-center w-6 h-6 rounded-full bg-red-50 relative top-1">
|
||||||
|
<img
|
||||||
|
src="https://cdn.prod.website-files.com/61f24aa108528b1962942c95/61f24aa108528b9634942cdb_wrong.svg"
|
||||||
|
loading="lazy"
|
||||||
|
alt="Cross Icon"
|
||||||
|
class="w-3 h-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Enchun Digital -->
|
||||||
|
<div class="p-8 md:p-12 flex flex-col gap-6 relative bg-white">
|
||||||
|
<h3
|
||||||
|
class="text-2xl md:text-3xl font-bold text-[var(--color-enchunblue)] tracking-tight font-['Noto_Sans_TC',sans-serif]"
|
||||||
|
>
|
||||||
|
恩群數位
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
class="text-gray-600 text-sm md:text-base leading-relaxed h-[60px]"
|
||||||
|
>
|
||||||
|
在恩群數位每個客戶都是我們最重視的拍檔,不論合作的項目大小,我們珍惜與客戶的合作關係
|
||||||
|
</p>
|
||||||
|
<div class="h-px w-full bg-blue-100 my-2"></div>
|
||||||
|
<ul role="list" class="flex flex-col gap-4 md:gap-5 mt-2">
|
||||||
|
{
|
||||||
|
enchunItems.map((item) => (
|
||||||
|
<li class="flex items-center gap-3">
|
||||||
|
<span class="text-gray-800 font-medium text-base md:text-lg flex-1">
|
||||||
|
{item}
|
||||||
|
</span>
|
||||||
|
<div class="flex-shrink-0 flex items-center justify-center w-6 h-6 rounded-full bg-green-50 relative top-1">
|
||||||
|
<img
|
||||||
|
src="https://cdn.prod.website-files.com/61f24aa108528b1962942c95/61f24aa108528b7a79942cda_checked.svg"
|
||||||
|
loading="lazy"
|
||||||
|
alt="Check Icon"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comparison Table -->
|
<!-- CTA Button -->
|
||||||
<div class="comparison-table-wrapper">
|
<div class="mt-12 flex justify-center">
|
||||||
<table class="comparison-table">
|
<a
|
||||||
<thead>
|
href="https://heyform.itslouis.cc/form/7mYtUNjA"
|
||||||
<tr>
|
target="_blank"
|
||||||
<th class="th-feature">比較項目</th>
|
class="inline-flex items-center justify-center bg-[var(--color-enchunblue)] text-white font-bold text-lg px-10 py-4 rounded-full shadow-[0_8px_20px_-6px_rgba(43,83,186,0.5)] hover:shadow-[0_12px_24px_-8px_rgba(43,83,186,0.6)] hover:opacity-95 transition-all duration-300 transform hover:-translate-y-1"
|
||||||
<th class="th-enchun">
|
>
|
||||||
<span class="enchun-badge">恩群數位</span>
|
<div class="tracking-wide">跟行銷顧問聊聊</div>
|
||||||
</th>
|
</a>
|
||||||
<th class="th-others">其他行銷公司</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{
|
|
||||||
comparisonItems.map((item, index) => (
|
|
||||||
<tr class={index % 2 === 0 ? 'row-even' : 'row-odd'}>
|
|
||||||
<td class="td-feature">{item.feature}</td>
|
|
||||||
<td class="td-enchun">
|
|
||||||
<span class="enchun-icon">✓</span>
|
|
||||||
{item.enchun}
|
|
||||||
</td>
|
|
||||||
<td class="td-others">{item.others}</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CTA Note -->
|
|
||||||
<div class="comparison-note">
|
|
||||||
<p class="note-text">
|
|
||||||
選擇恩群數位,讓您的品牌在數位時代脫穎而出!
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Comparison Section Styles - Pixel-perfect from Webflow */
|
|
||||||
.section-comparison {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
padding: 80px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-container {
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section Header */
|
|
||||||
.section-header-w-line {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-subtitle-head {
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
font-family: "Noto Sans TC", "Quicksand", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider-line {
|
|
||||||
background-color: var(--color-enchunblue);
|
|
||||||
height: 2px;
|
|
||||||
width: 60px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table Wrapper */
|
|
||||||
.comparison-table-wrapper {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Comparison Table */
|
|
||||||
.comparison-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table Header */
|
|
||||||
.comparison-table thead {
|
|
||||||
background-color: var(--color-enchunblue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-table th {
|
|
||||||
color: white;
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.th-feature {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.th-enchun {
|
|
||||||
width: 45%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.th-others {
|
|
||||||
width: 35%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enchun Badge */
|
|
||||||
.enchun-badge {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: white;
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
padding: 4px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table Body */
|
|
||||||
.comparison-table tbody tr {
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-table tbody tr:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-even {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-odd {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-table td {
|
|
||||||
padding: 20px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-feature {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-enchun {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.enchun-icon {
|
|
||||||
display: inline-block;
|
|
||||||
color: #22c55e;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-others {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Comparison Note */
|
|
||||||
.comparison-note {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-text {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.section-comparison {
|
|
||||||
padding: 60px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-table th,
|
|
||||||
.comparison-table td {
|
|
||||||
padding: 16px 12px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-subtitle-head {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
/* Convert to card layout on mobile */
|
|
||||||
.comparison-table-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-table {
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-comparison {
|
|
||||||
padding: 40px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-subtitle-head {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comparison-table th,
|
|
||||||
.comparison-table td {
|
|
||||||
padding: 12px 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.enchun-badge {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding: 2px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
62
apps/frontend/src/sections/Cta-Hr-compoents.astro
Normal file
62
apps/frontend/src/sections/Cta-Hr-compoents.astro
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* Cta-Hr-compoents - HR CTA Section
|
||||||
|
* 招募行動呼籲區塊,引導求職者申請面試
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image?: { url: string; alt: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, image } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="relative py-20 px-5 bg-slate-100 bg-cover bg-center overflow-hidden"
|
||||||
|
aria-labelledby="cta-heading"
|
||||||
|
style={image ? `background-image: url('${image.url}')` : undefined}
|
||||||
|
>
|
||||||
|
{/* Dark overlay when image is present */}
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-linear-to-b from-transparent to-black/70 z-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="relative z-10 max-w-3xl md:max-w-3xl mx-auto">
|
||||||
|
<div
|
||||||
|
class="grid grid-rows-[1fr_auto] gap-8 items-center lg:grid-cols-[1fr_auto]"
|
||||||
|
>
|
||||||
|
<div class="text-center md:text-left">
|
||||||
|
<h3
|
||||||
|
id="cta-heading"
|
||||||
|
class:list={[
|
||||||
|
"text-4xl md:text-4xl font-semibold mb-4 leading-snug",
|
||||||
|
image ? "text-white" : "text-[#23608c]",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Fragment set:html={title} />
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
class:list={[
|
||||||
|
"text-sm md:text-base leading-relaxed text-balance max-w-xl lg:max-w-full",
|
||||||
|
image ? "text-white/80" : "text-slate-600",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://www.104.com.tw/company/1a2x6bkoaj?jobsource=joblist_r_cust"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center justify-center mx-auto bg-(--color-pale-purple) text-white px-8 py-4 rounded-md font-semibold text-xl transition-all duration-200 whitespace-nowrap hover:-translate-y-0.5 hover:shadow-md hover:bg-(--color-pale-purple)/90 w-full max-w-3xs md:w-full lg:max-w-[300px]"
|
||||||
|
>
|
||||||
|
立刻申請面試
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -6,56 +6,88 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
interface SlideImage {
|
interface SlideImage {
|
||||||
src: string
|
src: string;
|
||||||
alt: string
|
alt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
slides?: SlideImage[]
|
slides?: SlideImage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSlides: SlideImage[] = [
|
const defaultSlides: SlideImage[] = [
|
||||||
{ src: '/placeholder-environment-1.jpg', alt: '恩群環境照片 1' },
|
{
|
||||||
{ src: '/placeholder-environment-2.jpg', alt: '恩群環境照片 2' },
|
src: "https://enchun-cms.anlstudio.cc/api/media/file/61f76b4962117e2d84363174_%E6%81%A9%E7%BE%A4%E7%92%B0%E5%A2%83%20%E7%85%A7%E7%89%871.jpg",
|
||||||
{ src: '/placeholder-environment-3.jpg', alt: '恩群環境照片 3' },
|
alt: "恩群環境照片 1",
|
||||||
{ src: '/placeholder-environment-4.jpg', alt: '恩群環境照片 4' },
|
},
|
||||||
{ src: '/placeholder-environment-5.jpg', alt: '恩群環境照片 5' },
|
{
|
||||||
{ src: '/placeholder-environment-6.jpg', alt: '恩群環境照片 6' },
|
src: "https://enchun-cms.anlstudio.cc/api/media/file/61f76b49558b7e5b0e81de8f_%E6%81%A9%E7%BE%A4%E7%92%B0%E5%A2%83%20%E7%85%A7%E7%89%871-7.jpg",
|
||||||
{ src: '/placeholder-environment-7.jpg', alt: '恩群環境照片 7' },
|
alt: "恩群環境照片 2",
|
||||||
{ src: '/placeholder-environment-8.jpg', alt: '恩群環境照片 8' },
|
},
|
||||||
]
|
{
|
||||||
|
src: "https://enchun-cms.anlstudio.cc/api/media/file/61f76b4962117e2d84363174_%E6%81%A9%E7%BE%A4%E7%92%B0%E5%A2%83%20%E7%85%A7%E7%89%871-1.jpg",
|
||||||
|
alt: "恩群環境照片 3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://enchun-cms.anlstudio.cc/api/media/file/61f76c511895ed028da1c7f0_%E6%81%A9%E7%BE%A4%E7%92%B0%E5%A2%83%20%E7%85%A7%E7%89%871-3.jpg",
|
||||||
|
alt: "恩群環境照片 4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://enchun-cms.anlstudio.cc/api/media/file/61f76b48558b7e072a81de8e_%E6%81%A9%E7%BE%A4%E7%92%B0%E5%A2%83%20%E7%85%A7%E7%89%871-4.jpg",
|
||||||
|
alt: "恩群環境照片 5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://enchun-cms.anlstudio.cc/api/media/file/61f76b483504a25babe537ef_%E6%81%A9%E7%BE%A4%E7%92%B0%E5%A2%83%20%E7%85%A7%E7%89%871-1.jpg",
|
||||||
|
alt: "恩群環境照片 6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://enchun-cms.anlstudio.cc/api/media/file/61f76b48ef5754e17a8c1676_%E6%81%A9%E7%BE%A4%E7%92%B0%E5%A2%83%20%E7%85%A7%E7%89%871-6.jpg",
|
||||||
|
alt: "恩群環境照片 7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "https://enchun-cms.anlstudio.cc/api/media/file/620639d7a68fef44f6569b23_%E6%81%A9%E7%BE%A4%E7%92%B0%E5%A2%83%20%E7%85%A7%E7%89%879.jpg",
|
||||||
|
alt: "恩群環境照片 8",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const slides = Astro.props.slides || defaultSlides
|
const slides = Astro.props.slides || defaultSlides;
|
||||||
|
|
||||||
|
import SectionHeader from "../components/SectionHeader.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="section-video" aria-label="工作環境照片">
|
<section
|
||||||
<div class="container spacer8 w-container">
|
class="section-video py-10 md:py-20 md:px-5 px-4 bg-white"
|
||||||
|
aria-label="工作環境照片"
|
||||||
|
>
|
||||||
|
<div class="container w-container max-w-3xl mx-auto">
|
||||||
<!-- Section Header -->
|
<!-- Section Header -->
|
||||||
<div class="section_header_w_line">
|
<SectionHeader
|
||||||
<div class="divider_line"></div>
|
title="在恩群工作的環境"
|
||||||
<div class="header_subtitle">
|
subtitle="Working Enviroment"
|
||||||
<h2 class="header_subtitle_head">在恩群工作的環境</h2>
|
sectionBg="bg-white"
|
||||||
<p class="header_subtitle_paragraph">Working Enviroment</p>
|
/>
|
||||||
</div>
|
|
||||||
<div class="divider_line"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Environment Slider -->
|
<!-- Environment Slider -->
|
||||||
<div
|
<div
|
||||||
class="environment-slider"
|
class="environment-slider relative w-full max-w-4xl mx-auto"
|
||||||
id="env-slider-{Math.random().toString(36).slice(2, 8)}"
|
id="env-slider-{Math.random().toString(36).slice(2, 8)}"
|
||||||
>
|
>
|
||||||
<!-- Slides Container -->
|
<!-- Slides Container -->
|
||||||
<div class="slides-container">
|
<div
|
||||||
|
class="slides-container flex overflow-x-auto snap-x snap-mandatory scroll-smooth overscroll-x-contain [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden cursor-grab"
|
||||||
|
>
|
||||||
{
|
{
|
||||||
slides.map((slide, index) => (
|
slides.map((slide, index) => (
|
||||||
<div class="environment-slide" data-index={index}>
|
<div
|
||||||
|
class="environment-slide flex-none w-full snap-start aspect-video overflow-hidden rounded-xl"
|
||||||
|
data-index={index}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={slide.src}
|
src={slide.src}
|
||||||
alt={slide.alt}
|
alt={slide.alt}
|
||||||
loading={index === 0 ? 'eager' : 'lazy'}
|
loading={index === 0 ? "eager" : "lazy"}
|
||||||
width="800"
|
width="800"
|
||||||
height="450"
|
height="450"
|
||||||
|
class="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -63,23 +95,33 @@ const slides = Astro.props.slides || defaultSlides
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Arrow Navigation -->
|
<!-- Arrow Navigation -->
|
||||||
<button class="slider-arrow slider-arrow-left" aria-label="上一張">
|
<button
|
||||||
|
class="slider-arrow slider-arrow-left absolute top-1/2 -translate-y-1/2 w-12 h-12 md:w-12 md:h-12 w-10 h-10 bg-white/80 rounded-full flex items-center justify-center cursor-pointer transition-all duration-200 ease-in-out z-10 hover:bg-white hover:scale-110 left-4 md:left-4 left-2"
|
||||||
|
aria-label="上一張"
|
||||||
|
>
|
||||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
<path fill="currentColor" d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="slider-arrow slider-arrow-right" aria-label="下一張">
|
<button
|
||||||
|
class="slider-arrow slider-arrow-right absolute top-1/2 -translate-y-1/2 w-12 h-12 md:w-12 md:h-12 w-10 h-10 bg-white/80 rounded-full flex items-center justify-center cursor-pointer transition-all duration-200 ease-in-out z-10 hover:bg-white hover:scale-110 right-4 md:right-4 right-2"
|
||||||
|
aria-label="下一張"
|
||||||
|
>
|
||||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||||
<path fill="currentColor" d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dot Navigation -->
|
<!-- Dot Navigation -->
|
||||||
<div class="slider-dots">
|
<div class="slider-dots flex justify-center gap-2 mt-4">
|
||||||
{
|
{
|
||||||
slides.map((_, index) => (
|
slides.map((_, index) => (
|
||||||
<button
|
<button
|
||||||
class={`slider-dot ${index === 0 ? 'active' : ''}`}
|
class={`slider-dot w-3 h-3 rounded-full cursor-pointer transition-all duration-200 ease-in-out ${index === 0 ? "active bg-(--color-enchunblue) w-8 rounded-[6px]" : "bg-(--color-enchunblue)/30"}`}
|
||||||
data-index={index}
|
data-index={index}
|
||||||
aria-label={`顯示第 ${index + 1} 張照片`}
|
aria-label={`顯示第 ${index + 1} 張照片`}
|
||||||
/>
|
/>
|
||||||
@@ -90,353 +132,183 @@ const slides = Astro.props.slides || defaultSlides
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Environment Slider Styles - Pixel-perfect from Webflow */
|
|
||||||
.section-video {
|
|
||||||
padding: 80px 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer8 {
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section Header */
|
|
||||||
.section_header_w_line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_head {
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_paragraph {
|
|
||||||
color: var(--color-gray-600);
|
|
||||||
font-family: "Quicksand", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider_line {
|
|
||||||
width: 40px;
|
|
||||||
height: 2px;
|
|
||||||
background-color: var(--color-enchunblue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Environment Slider */
|
|
||||||
.environment-slider {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slides-container {
|
|
||||||
display: flex;
|
|
||||||
overflow-x: auto;
|
|
||||||
scroll-snap-type: x mandatory;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slides-container::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.environment-slide {
|
|
||||||
flex: 0 0 100%;
|
|
||||||
scroll-snap-align: start;
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.environment-slide img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Arrow Navigation */
|
|
||||||
.slider-arrow {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-arrow:hover {
|
|
||||||
background-color: white;
|
|
||||||
transform: translateY(-50%) scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-arrow-left {
|
|
||||||
left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-arrow-right {
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dot Navigation */
|
|
||||||
.slider-dots {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-dot {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(35, 96, 140, 0.3);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-dot.active {
|
|
||||||
background-color: var(--color-enchunblue);
|
|
||||||
width: 32px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.environment-slider {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.environment-slider {
|
|
||||||
max-width: 550px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section_header_w_line {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.section-video {
|
|
||||||
padding: 60px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.environment-slider {
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-arrow {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-arrow-left {
|
|
||||||
left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-arrow-right {
|
|
||||||
right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_subtitle_head {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Environment Slider functionality
|
// Environment Slider functionality
|
||||||
function initEnvironmentSlider() {
|
function initEnvironmentSlider() {
|
||||||
const sliders = document.querySelectorAll('.environment-slider')
|
const sliders = document.querySelectorAll(".environment-slider");
|
||||||
|
|
||||||
sliders.forEach((slider) => {
|
sliders.forEach((slider) => {
|
||||||
const container = slider.querySelector('.slides-container') as HTMLElement
|
const container = slider.querySelector(
|
||||||
const slides = slider.querySelectorAll('.environment-slide')
|
".slides-container",
|
||||||
const dots = slider.querySelectorAll('.slider-dot')
|
) as HTMLElement;
|
||||||
const prevBtn = slider.querySelector('.slider-arrow-left') as HTMLButtonElement
|
const slides = slider.querySelectorAll(".environment-slide");
|
||||||
const nextBtn = slider.querySelector('.slider-arrow-right') as HTMLButtonElement
|
const dots = slider.querySelectorAll(".slider-dot");
|
||||||
|
const prevBtn = slider.querySelector(
|
||||||
|
".slider-arrow-left",
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const nextBtn = slider.querySelector(
|
||||||
|
".slider-arrow-right",
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
if (!container || slides.length === 0) return
|
if (!container || slides.length === 0) return;
|
||||||
|
|
||||||
let currentIndex = 0
|
let currentIndex = 0;
|
||||||
const totalSlides = slides.length
|
const totalSlides = slides.length;
|
||||||
let isDragging = false
|
let isDragging = false;
|
||||||
let startPos = 0
|
let startPos = 0;
|
||||||
let currentTranslate = 0
|
let currentTranslate = 0;
|
||||||
let prevTranslate = 0
|
let prevTranslate = 0;
|
||||||
let animationID: number
|
let animationID: number;
|
||||||
|
|
||||||
// Update slider position
|
// Update slider position
|
||||||
const updateSlider = () => {
|
const updateSlider = () => {
|
||||||
container.scrollTo({
|
container.scrollTo({
|
||||||
left: currentIndex * container.offsetWidth,
|
left: currentIndex * container.offsetWidth,
|
||||||
behavior: 'smooth'
|
behavior: "smooth",
|
||||||
})
|
});
|
||||||
updateDots()
|
updateDots();
|
||||||
}
|
};
|
||||||
|
|
||||||
// Update dots
|
// Update dots
|
||||||
const updateDots = () => {
|
const updateDots = () => {
|
||||||
dots.forEach((dot, index) => {
|
dots.forEach((dot, index) => {
|
||||||
if (index === currentIndex) {
|
if (index === currentIndex) {
|
||||||
dot.classList.add('active')
|
dot.classList.add(
|
||||||
|
"active",
|
||||||
|
"w-8",
|
||||||
|
"rounded-[6px]",
|
||||||
|
"bg-(--color-enchunblue)",
|
||||||
|
);
|
||||||
|
dot.classList.remove("w-3", "bg-(--color-enchunblue)/30");
|
||||||
} else {
|
} else {
|
||||||
dot.classList.remove('active')
|
dot.classList.remove(
|
||||||
|
"active",
|
||||||
|
"w-8",
|
||||||
|
"rounded-[6px]",
|
||||||
|
"bg-(--color-enchunblue)",
|
||||||
|
);
|
||||||
|
dot.classList.add("w-3", "bg-(--color-enchunblue)/30");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// Go to specific slide
|
// Go to specific slide
|
||||||
const goToSlide = (index: number) => {
|
const goToSlide = (index: number) => {
|
||||||
if (index < 0) index = totalSlides - 1
|
if (index < 0) index = totalSlides - 1;
|
||||||
if (index >= totalSlides) index = 0
|
if (index >= totalSlides) index = 0;
|
||||||
currentIndex = index
|
currentIndex = index;
|
||||||
updateSlider()
|
updateSlider();
|
||||||
}
|
};
|
||||||
|
|
||||||
// Previous slide
|
// Previous slide
|
||||||
prevBtn?.addEventListener('click', () => goToSlide(currentIndex - 1))
|
prevBtn?.addEventListener("click", () => goToSlide(currentIndex - 1));
|
||||||
|
|
||||||
// Next slide
|
// Next slide
|
||||||
nextBtn?.addEventListener('click', () => goToSlide(currentIndex + 1))
|
nextBtn?.addEventListener("click", () => goToSlide(currentIndex + 1));
|
||||||
|
|
||||||
// Dot navigation
|
// Dot navigation
|
||||||
dots.forEach((dot, index) => {
|
dots.forEach((dot, index) => {
|
||||||
dot.addEventListener('click', () => goToSlide(index))
|
dot.addEventListener("click", () => goToSlide(index));
|
||||||
})
|
});
|
||||||
|
|
||||||
// Scroll snap detection
|
// Scroll snap detection
|
||||||
container.addEventListener('scroll', () => {
|
container.addEventListener("scroll", () => {
|
||||||
const slideIndex = Math.round(container.scrollLeft / container.offsetWidth)
|
const slideIndex = Math.round(
|
||||||
|
container.scrollLeft / container.offsetWidth,
|
||||||
|
);
|
||||||
if (slideIndex !== currentIndex) {
|
if (slideIndex !== currentIndex) {
|
||||||
currentIndex = slideIndex
|
currentIndex = slideIndex;
|
||||||
updateDots()
|
updateDots();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// Touch/swipe support
|
// Touch/swipe support
|
||||||
const touchStart = (_index: number) => {
|
const touchStart = (_index: number) => {
|
||||||
return function(event: TouchEvent) {
|
return function (event: TouchEvent) {
|
||||||
isDragging = true
|
isDragging = true;
|
||||||
startPos = event.touches[0].clientX
|
startPos = event.touches[0].clientX;
|
||||||
animationID = requestAnimationFrame(animation)
|
animationID = requestAnimationFrame(animation);
|
||||||
container.style.cursor = 'grabbing'
|
container.style.cursor = "grabbing";
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const touchEnd = () => {
|
const touchEnd = () => {
|
||||||
isDragging = false
|
isDragging = false;
|
||||||
cancelAnimationFrame(animationID)
|
cancelAnimationFrame(animationID);
|
||||||
container.style.cursor = 'grab'
|
container.style.cursor = "grab";
|
||||||
|
|
||||||
const movedBy = currentTranslate - prevTranslate
|
const movedBy = currentTranslate - prevTranslate;
|
||||||
|
|
||||||
if (movedBy < -50 && currentIndex < totalSlides - 1) {
|
if (movedBy < -50 && currentIndex < totalSlides - 1) {
|
||||||
currentIndex += 1
|
currentIndex += 1;
|
||||||
} else if (movedBy > 50 && currentIndex > 0) {
|
} else if (movedBy > 50 && currentIndex > 0) {
|
||||||
currentIndex -= 1
|
currentIndex -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
goToSlide(currentIndex)
|
goToSlide(currentIndex);
|
||||||
}
|
};
|
||||||
|
|
||||||
const touchMove = (event: TouchEvent) => {
|
const touchMove = (event: TouchEvent) => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
const currentPosition = event.touches[0].clientX
|
const currentPosition = event.touches[0].clientX;
|
||||||
currentTranslate = prevTranslate + currentPosition - startPos
|
currentTranslate = prevTranslate + currentPosition - startPos;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const animation = () => {
|
const animation = () => {
|
||||||
if (isDragging) requestAnimationFrame(animation)
|
if (isDragging) requestAnimationFrame(animation);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Mouse events for desktop
|
// Mouse events for desktop
|
||||||
let mouseStartPos = 0
|
let mouseStartPos = 0;
|
||||||
let isMouseDown = false
|
let isMouseDown = false;
|
||||||
|
|
||||||
container.addEventListener('mousedown', (e: MouseEvent) => {
|
container.addEventListener("mousedown", (e: MouseEvent) => {
|
||||||
isMouseDown = true
|
isMouseDown = true;
|
||||||
mouseStartPos = e.clientX
|
mouseStartPos = e.clientX;
|
||||||
container.style.cursor = 'grabbing'
|
container.style.cursor = "grabbing";
|
||||||
})
|
});
|
||||||
|
|
||||||
container.addEventListener('mouseup', (e: MouseEvent) => {
|
container.addEventListener("mouseup", (e: MouseEvent) => {
|
||||||
if (!isMouseDown) return
|
if (!isMouseDown) return;
|
||||||
isMouseDown = false
|
isMouseDown = false;
|
||||||
container.style.cursor = 'grab'
|
container.style.cursor = "grab";
|
||||||
|
|
||||||
const movedBy = e.clientX - mouseStartPos
|
const movedBy = e.clientX - mouseStartPos;
|
||||||
if (movedBy < -50 && currentIndex < totalSlides - 1) {
|
if (movedBy < -50 && currentIndex < totalSlides - 1) {
|
||||||
currentIndex += 1
|
currentIndex += 1;
|
||||||
} else if (movedBy > 50 && currentIndex > 0) {
|
} else if (movedBy > 50 && currentIndex > 0) {
|
||||||
currentIndex -= 1
|
currentIndex -= 1;
|
||||||
}
|
}
|
||||||
goToSlide(currentIndex)
|
goToSlide(currentIndex);
|
||||||
})
|
});
|
||||||
|
|
||||||
container.addEventListener('mouseleave', () => {
|
container.addEventListener("mouseleave", () => {
|
||||||
isMouseDown = false
|
isMouseDown = false;
|
||||||
container.style.cursor = 'grab'
|
container.style.cursor = "grab";
|
||||||
})
|
});
|
||||||
|
|
||||||
// Touch events
|
// Touch events
|
||||||
container.addEventListener('touchstart', touchStart(currentIndex))
|
container.addEventListener("touchstart", touchStart(currentIndex));
|
||||||
container.addEventListener('touchend', touchEnd)
|
container.addEventListener("touchend", touchEnd);
|
||||||
container.addEventListener('touchmove', touchMove)
|
container.addEventListener("touchmove", touchMove);
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
slider.addEventListener('keydown', (e) => {
|
slider.addEventListener("keydown", (e) => {
|
||||||
if (e instanceof KeyboardEvent) {
|
if (e instanceof KeyboardEvent) {
|
||||||
if (e.key === 'ArrowLeft') goToSlide(currentIndex - 1)
|
if (e.key === "ArrowLeft") goToSlide(currentIndex - 1);
|
||||||
if (e.key === 'ArrowRight') goToSlide(currentIndex + 1)
|
if (e.key === "ArrowRight") goToSlide(currentIndex + 1);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// Set initial state
|
// Set initial state
|
||||||
container.style.cursor = 'grab'
|
container.style.cursor = "grab";
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize when DOM is ready
|
// Initialize when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', initEnvironmentSlider)
|
document.addEventListener("DOMContentLoaded", initEnvironmentSlider);
|
||||||
if (document.readyState !== 'loading') {
|
if (document.readyState !== "loading") {
|
||||||
initEnvironmentSlider()
|
initEnvironmentSlider();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,187 +16,60 @@ const {
|
|||||||
// Four features data
|
// Four features data
|
||||||
const features = [
|
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: '在地化優先',
|
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: '高投資轉換率',
|
title: '高投資轉換率',
|
||||||
description: '你覺得網路行銷很貴嗎?恩群數位善用每一分廣告預算,讓你在網路上發揮最大效益,幫助店家鎖定精準客群,達成目標。',
|
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: '數據優先',
|
title: '數據優先',
|
||||||
description: '想要精準行銷?恩群數位從數據中萃取洞察,根據數據分析廣告成效,更聰明、有策略的幫您省下行銷預算。',
|
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: '關係優於銷售',
|
title: '關係優於銷售',
|
||||||
description: '除了幫您拓展網路上的知名度,我們更是每家公司最專業的數位夥伴,你會知道有恩群的存在,事業路上你並不孤單。',
|
description: '除了幫您拓展網路上的知名度,我們更是每家公司最專業的數位夥伴,你會知道有恩群的存在,事業路上你並不孤單。',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
import SectionHeader from '../components/SectionHeader.astro'
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="section_feature" aria-labelledby="feature-heading">
|
<section class="section_feature bg-white px-4 py-16 md:px-5 md:py-20" aria-labelledby="feature-heading">
|
||||||
<div class="w-container">
|
<div class="w-container mx-auto max-w-4xl">
|
||||||
<!-- Section Header -->
|
<!-- Section Header -->
|
||||||
<div class="section_header_w_line">
|
<SectionHeader
|
||||||
<h2 id="feature-heading" class="header_subtitle_head">
|
title="恩群服務特色"
|
||||||
{title}
|
subtitle="Why you can trust us"
|
||||||
</h2>
|
sectionBg="bg-white"
|
||||||
<p class="header_subtitle_paragraph">
|
/>
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
<div class="divider_line"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Features Grid -->
|
<!-- Features Grid -->
|
||||||
<div class="feature_grid">
|
<div class="grid grid-cols-2 gap-1 md:grid-cols-2 md:gap-2 lg:gap-6">
|
||||||
{
|
{
|
||||||
features.map((feature) => (
|
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">
|
||||||
<!-- Icon -->
|
<div class="flex flex-col items-center gap-4 text-center md:flex-row md:items-start md:text-left lg:gap-6">
|
||||||
<div class="feature_image">
|
<!-- Icon -->
|
||||||
<span class="material-icon">{feature.icon}</span>
|
<div class="flex w-full max-w-[100px] shrink-0 items-center justify-center md:max-w-[110px] lg:max-w-[190px]">
|
||||||
|
<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="font-['Noto_Sans_TC'] text-2xl font-semibold text-(--color-dark-blue) lg:text-3xl">{feature.title}</h3>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="font-['Noto_Sans_TC'] text-sm font-thin leading-[1.6] text-grey-2 lg:text-base">{feature.description}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<h3 class="feature_head">{feature.title}</h3>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<p class="feature_description">{feature.description}</p>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const painpoints = [
|
|||||||
{
|
{
|
||||||
id: 'burning',
|
id: 'burning',
|
||||||
title: '廣告行銷像燒錢',
|
title: '廣告行銷像燒錢',
|
||||||
icon: '💸',
|
img: '💸',
|
||||||
description: '廣告預算投入很多,但看不到實際效果?感覺像在燒錢?',
|
description: '廣告預算投入很多,但看不到實際效果?感覺像在燒錢?',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,14 +4,23 @@
|
|||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { sanitizeSvg } from '../lib/sanitize'
|
||||||
|
import { PLACEHOLDER_IMAGE } from '../lib/api/marketing-solution'
|
||||||
|
|
||||||
interface ServicesListItem {
|
interface ServicesListItem {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
category: string
|
category: string
|
||||||
|
iconType?: 'preset' | 'svg' | 'upload'
|
||||||
icon?: string
|
icon?: string
|
||||||
|
iconSvg?: string
|
||||||
|
iconImage?: {
|
||||||
|
url?: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
isHot?: boolean
|
isHot?: boolean
|
||||||
image?: string
|
image?: string | { url?: string; alt?: string }
|
||||||
link?: string
|
link?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,345 +38,197 @@ const defaultServices: ServicesListItem[] = [
|
|||||||
id: 'social-media',
|
id: 'social-media',
|
||||||
title: '社群經營代操',
|
title: '社群經營代操',
|
||||||
category: '海洋專案',
|
category: '海洋專案',
|
||||||
|
iconType: 'preset',
|
||||||
icon: 'facebook',
|
icon: 'facebook',
|
||||||
isHot: true,
|
isHot: false,
|
||||||
image: '/placeholder-service-1.jpg',
|
image: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b3ab4942cf1_%E7%A4%BE%E7%BE%A4%E7%B6%93%E7%87%9F%E4%BB%A3%E6%93%8D.svg',
|
||||||
description: '專業社群媒體經營團隊,從內容策劃、社群經營到數據分析,提供一站式社群代操服務。我們擅長經營 Facebook、Instagram 等主流平台,幫助品牌建立強大的社群影響力。',
|
description: '專業社群媒體經營團隊,從內容策劃、社群經營到數據分析,提供一站式社群代操服務。我們擅長經營 Facebook、Instagram 等主流平台,幫助品牌建立強大的社群影響力。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'google-business',
|
id: 'google-business',
|
||||||
title: 'Google 商家關鍵字',
|
title: 'Google 商家關鍵字',
|
||||||
category: 'Google',
|
category: 'Google',
|
||||||
|
iconType: 'preset',
|
||||||
icon: 'google',
|
icon: 'google',
|
||||||
isHot: true,
|
isHot: false,
|
||||||
image: '/placeholder-service-2.jpg',
|
image: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b5a4c942cec_%E5%95%86%E5%AE%B6%E9%97%9C%E9%8D%B5%E5%AD%97.svg',
|
||||||
description: '優化 Google 商家列表,提升在地搜尋排名。透過關鍵字策略、評論管理和商家資訊優化,讓您的商家在 Google 地圖和搜尋結果中脫穎而出。',
|
description: '優化 Google 商家列表,提升在地搜尋排名。透過關鍵字策略、評論管理和商家資訊優化,讓您的商家在 Google 地圖和搜尋結果中脫穎而出。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'google-ads',
|
id: 'google-ads',
|
||||||
title: 'Google Ads 關鍵字',
|
title: 'Google Ads 關鍵字',
|
||||||
category: 'Google',
|
category: 'Google',
|
||||||
|
iconType: 'preset',
|
||||||
icon: 'ads',
|
icon: 'ads',
|
||||||
isHot: false,
|
isHot: false,
|
||||||
image: '/placeholder-service-3.jpg',
|
image: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b3898942cde_Google%20ADS.svg',
|
||||||
description: '專業的 Google Ads 投放服務,從關鍵字研究、廣告文案撰寫到出價策略優化,精準觸達目標受眾,最大化廣告投資報酬率。',
|
description: '專業的 Google Ads 投放服務,從關鍵字研究、廣告文案撰寫到出價策略優化,精準觸達目標受眾,最大化廣告投資報酬率。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'news-media',
|
id: 'news-media',
|
||||||
title: '網路新聞媒體',
|
title: '網路新聞媒體',
|
||||||
category: '媒體行銷',
|
category: '媒體行銷',
|
||||||
|
iconType: 'preset',
|
||||||
icon: 'news',
|
icon: 'news',
|
||||||
isHot: false,
|
isHot: false,
|
||||||
image: '/placeholder-service-4.jpg',
|
image: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b0b57942cee_%E7%B6%B2%E8%B7%AF%E6%96%B0%E8%81%9E%E5%AA%92%E9%AB%94.svg',
|
||||||
description: '與各大新聞媒體合作,提供新聞發佈、媒體採訪、品牌曝光等服務。透過專業的新聞行銷策略,提升品牌知名度和公信力。',
|
description: '與各大新聞媒體合作,提供新聞發佈、媒體採訪、品牌曝光等服務。透過專業的新聞行銷策略,提升品牌知名度和公信力。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'influencer',
|
id: 'influencer',
|
||||||
title: '網紅行銷專案',
|
title: '網紅行銷專案',
|
||||||
category: '口碑行銷',
|
category: '口碑行銷',
|
||||||
|
iconType: 'preset',
|
||||||
icon: 'youtube',
|
icon: 'youtube',
|
||||||
isHot: true,
|
isHot: false,
|
||||||
image: '/placeholder-service-5.jpg',
|
image: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b2267942cf2_%E7%B6%B2%E7%B4%85%E8%A1%8C%E9%8A%B7%E5%B0%88%E6%A1%88.svg',
|
||||||
description: '連結品牌與網紅/KOL,打造影響者行銷活動。從網紅篩選、活動策劃到內容製作,提供完整的網紅行銷解決方案,快速建立品牌口碑。',
|
description: '連結品牌與網紅/KOL,打造影響者行銷活動。從網紅篩選、活動策劃到內容製作,提供完整的網紅行銷解決方案,快速建立品牌口碑。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'forum',
|
id: 'forum',
|
||||||
title: '論壇行銷專案',
|
title: '論壇行銷專案',
|
||||||
category: '口碑行銷',
|
category: '口碑行銷',
|
||||||
|
iconType: 'preset',
|
||||||
icon: 'forum',
|
icon: 'forum',
|
||||||
isHot: false,
|
isHot: false,
|
||||||
image: '/placeholder-service-6.jpg',
|
image: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528be8e0942cf0_%E8%AB%96%E5%A3%87%E8%A1%8C%E9%8A%B7%E5%B0%88%E6%A1%88.svg',
|
||||||
description: '深耕各大論壇社群,包括 Dcard、PTT 等平台。透過專業的論壇行銷策略,建立品牌口碑,提升用戶信任度和互動參與度。',
|
description: '深耕各大論壇社群,包括 Dcard、PTT 等平台。透過專業的論壇行銷策略,建立品牌口碑,提升用戶信任度和互動參與度。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'website-design',
|
id: 'website-design',
|
||||||
title: '形象網站設計',
|
title: '形象網站設計',
|
||||||
category: '品牌行銷',
|
category: '品牌行銷',
|
||||||
|
iconType: 'preset',
|
||||||
icon: 'web',
|
icon: 'web',
|
||||||
isHot: false,
|
isHot: false,
|
||||||
image: '/placeholder-service-7.jpg',
|
image: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528bc016942cef_%E5%BD%A2%E8%B1%A1%E7%B6%B2%E7%AB%99%E8%A8%AD%E8%A8%88.svg',
|
||||||
description: '現代化響應式網站設計服務,結合美學與功能,打造獨特的品牌形象。從 UI/UX 設計到前端開發,提供完整的網站解決方案。',
|
description: '現代化響應式網站設計服務,結合美學與功能,打造獨特的品牌形象。從 UI/UX 設計到前端開發,提供完整的網站解決方案。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'brand-video',
|
id: 'brand-video',
|
||||||
title: '品牌形象影片',
|
title: '品牌形象影片',
|
||||||
category: '品牌行銷',
|
category: '品牌行銷',
|
||||||
|
iconType: 'preset',
|
||||||
icon: 'video',
|
icon: 'video',
|
||||||
isHot: false,
|
isHot: false,
|
||||||
image: '/placeholder-service-8.jpg',
|
image: 'https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b678d942ced_%E5%93%81%E7%89%8C%E5%BD%A2%E8%B1%A1%E5%BD%B1%E7%89%87.svg',
|
||||||
description: '專業影片製作團隊,從腳本創作、拍攝到後製剪輯,打造高品質的品牌形象影片。透過視覺故事說,傳達品牌核心價值。',
|
description: '專業影片製作團隊,從腳本創作、拍攝到後製剪輯,打造高品質的品牌形象影片。透過視覺故事說,傳達品牌核心價值。',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const servicesList = services && services.length > 0 ? services : defaultServices
|
const servicesList = services && services.length > 0 ? services : defaultServices
|
||||||
|
|
||||||
// Icon SVG components
|
// Helper to get image URL from string or object
|
||||||
const icons = {
|
const getImageUrl = (image: ServicesListItem['image']) => {
|
||||||
facebook: `<svg viewBox="0 0 24 24" class="w-8 h-8"><path fill="currentColor" d="M12 2C6.477 2 2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.879V14.89h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.989C18.343 21.129 22 16.99 22 12c0-5.523-4.477-10-10-10z"/></svg>`,
|
if (!image) return PLACEHOLDER_IMAGE
|
||||||
google: `<svg viewBox="0 0 24 24" class="w-8 h-8"><path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
|
return typeof image === 'string' ? image : image.url || PLACEHOLDER_IMAGE
|
||||||
ads: `<svg viewBox="0 0 24 24" class="w-8 h-8"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>`,
|
}
|
||||||
news: `<svg viewBox="0 0 24 24" class="w-8 h-8"><path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>`,
|
|
||||||
youtube: `<svg viewBox="0 0 24 24" class="w-8 h-8"><path fill="currentColor" d="M23.5 6.188c-1.258-2.667-4.636-4.764-8.148-5.36-.734-.12-3.32-.264-3.352-.264-3.032 0-2.618.144-3.352.264-3.512.596-6.89 2.693-8.148 5.36-.438.928-.796 2.622-1.028 5.578l-.01.232c0 .046-.004.136-.004.232 0 .096.004.186.004.232l.01.232c.232 2.956.59 4.65 1.028 5.578 1.258 2.667 4.636 4.764 8.148 5.36.734.12 3.32.264 3.352.264 3.032 0 2.618-.144 3.352-.264 3.512-.596 6.89-2.693 8.148-5.36.438-.928.796-2.622 1.028-5.578l.01-.232c0-.046.004-.136.004-.232 0-.096-.004-.186-.004-.232l-.01-.232c-.232-2.956-.59-4.65-1.028-5.578zM9.5 15.5v-7l6 3.5-6 3.5z"/></svg>`,
|
// Preset icon SVG components
|
||||||
forum: `<svg viewBox="0 0 24 24" class="w-8 h-8"><path fill="currentColor" d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>`,
|
const presetIcons: Record<string, string> = {
|
||||||
web: `<svg viewBox="0 0 24 24" class="w-8 h-8"><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-2zm-5 14H4v-4h11v4zm0-5H4V9h11v4zm5 5h-4V9h4v9z"/></svg>`,
|
facebook: `<svg viewBox="0 0 24 24" class="w-6 h-6" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.879V14.89h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.989C18.343 21.129 22 16.99 22 12c0-5.523-4.477-10-10-10z"/></svg>`,
|
||||||
video: `<svg viewBox="0 0 24 24" class="w-8 h-8"><path fill="currentColor" d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4h-3l-2 4h-3l2-4h-2z"/></svg>`,
|
google: `<svg viewBox="0 0 24 24" class="w-6 h-6"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
|
||||||
|
ads: `<svg viewBox="0 0 24 24" class="w-6 h-6" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>`,
|
||||||
|
news: `<svg viewBox="0 0 24 24" class="w-6 h-6" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>`,
|
||||||
|
youtube: `<svg viewBox="0 0 24 24" class="w-6 h-6" fill="currentColor"><path d="M23.5 6.188c-1.258-2.667-4.636-4.764-8.148-5.36-.734-.12-3.32-.264-3.352-.264-3.032 0-2.618.144-3.352.264-3.512.596-6.89 2.693-8.148 5.36-.438.928-.796 2.622-1.028 5.578l-.01.232c0 .046-.004.136-.004.232 0 .096.004.186.004.232l.01.232c.232 2.956.59 4.65 1.028 5.578 1.258 2.667 4.636 4.764 8.148 5.36.734.12 3.32.264 3.352.264 3.032 0 2.618-.144 3.352-.264 3.512-.596 6.89-2.693 8.148-5.36.438-.928.796-2.622 1.028-5.578l.01-.232c0-.046.004-.136.004-.232 0-.096-.004-.186-.004-.232l-.01-.232c-.232-2.956-.59-4.65-1.028-5.578zM9.5 15.5v-7l6 3.5-6 3.5z"/></svg>`,
|
||||||
|
forum: `<svg viewBox="0 0 24 24" class="w-6 h-6" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4l4 4 4-4h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>`,
|
||||||
|
web: `<svg viewBox="0 0 24 24" class="w-6 h-6" fill="currentColor"><path 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-2zm-5 14H4v-4h11v4zm0-5H4V9h11v4zm5 5h-4V9h4v9z"/></svg>`,
|
||||||
|
video: `<svg viewBox="0 0 24 24" class="w-6 h-6" fill="currentColor"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4h-3l-2 4h-3l2-4h-2z"/></svg>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to render icon based on type with XSS protection
|
||||||
|
const renderIcon = (service: ServicesListItem): string | null => {
|
||||||
|
const iconType = service.iconType || 'preset'
|
||||||
|
|
||||||
|
if (iconType === 'svg' && service.iconSvg) {
|
||||||
|
// Sanitize user-provided SVG to prevent XSS attacks
|
||||||
|
return sanitizeSvg(service.iconSvg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iconType === 'upload' && service.iconImage?.url) {
|
||||||
|
return `<img src="${service.iconImage.url}" alt="${service.iconImage.alt || service.title}" class="w-6 h-6 object-contain" />`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iconType === 'preset' && service.icon && presetIcons[service.icon]) {
|
||||||
|
return presetIcons[service.icon]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="section_service-list" aria-labelledby="services-heading">
|
<section class="bg-white py-[60px] px-5 md:py-[60px] md:px-5 lg:py-[60px] lg:px-5" aria-labelledby="services-heading">
|
||||||
<div class="services-container">
|
<div class="max-w-3xl mx-auto flex flex-col gap-6">
|
||||||
{
|
{
|
||||||
servicesList.map((service, index) => (
|
servicesList.map((service, index) => {
|
||||||
<a
|
const isOdd = index % 2 === 0
|
||||||
href={service.link || '#'}
|
const iconHtml = renderIcon(service)
|
||||||
class={`service-item ${index % 2 === 0 ? 'odd' : 'even'}`}
|
return (
|
||||||
aria-labelledby={`service-${service.id}-title`}
|
<a
|
||||||
>
|
href={service.link || '#'}
|
||||||
<!-- Content Side -->
|
class={`
|
||||||
<div class="service-item-content">
|
grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-6 items-center mb-[60px] md:mb-10 relative
|
||||||
<!-- Category Tag -->
|
transition-transform duration-300 hover:-translate-y-[2px]
|
||||||
<span class="service-category-tag">
|
no-underline
|
||||||
{service.category}
|
group
|
||||||
</span>
|
`}
|
||||||
|
aria-labelledby={`service-${service.id}-title`}
|
||||||
|
>
|
||||||
|
<!-- Content Side -->
|
||||||
|
<div class={`service-item-content order-2 ${isOdd ? 'md:order-1' : 'md:order-2'}`}>
|
||||||
|
<!-- Category Tag with Icon -->
|
||||||
|
<div class="flex flex-col-reverse items-start gap-2 mb-4">
|
||||||
|
<span class="inline-block text-(--color-enchunblue) text-3xl font-light">
|
||||||
|
{service.category}
|
||||||
|
</span>
|
||||||
|
{/* Icon displayed next to category */}
|
||||||
|
{
|
||||||
|
iconHtml && (
|
||||||
|
<span class="inline-flex items-center justify-start w-8 h-8 ml-2 scale-150 text-(--color-enchunblue)" set:html={iconHtml} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h2
|
<h2
|
||||||
id={`service-${service.id}-title`}
|
id={`service-${service.id}-title`}
|
||||||
class="service-title"
|
class="font-['Noto_Sans_TC',sans-serif] font-bold text-2xl md:text-xl text-(--color-dark-blue) mb-4 leading-[1.3] transition-colors duration-300 group-hover:text-(--color-enchunblue)"
|
||||||
>
|
>
|
||||||
{service.title}
|
{service.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<div class="service-divider"></div>
|
<div class="w-fill h-[1px] bg-(--color-enchunblue) mb-4"></div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p class="service-description">
|
<p class="font-['Quicksand',_'Noto_Sans_TC',sans-serif] font-thin text-2xl md:text-xl text-gray-700 leading-[1.6]">
|
||||||
{service.description}
|
{service.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Icon (if provided) -->
|
|
||||||
{
|
|
||||||
service.icon && icons[service.icon as keyof typeof icons] && (
|
|
||||||
<div class="service-icon" set:html={icons[service.icon as keyof typeof icons]} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Image Side -->
|
|
||||||
<div class="service-item-image">
|
|
||||||
<div class="image-wrapper">
|
|
||||||
<img
|
|
||||||
src={service.image || '/placeholder-service.jpg'}
|
|
||||||
alt={service.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hot Badge -->
|
<!-- Image Side -->
|
||||||
{
|
<div class={`service-item-image order-1 relative ${isOdd ? 'md:order-2' : 'md:order-1'}`}>
|
||||||
service.isHot && (
|
<div class="aspect-[4/3] rounded-[12px] overflow-hidden bg-[var(--color-white)]">
|
||||||
<span class="service-hot-badge">HOT</span>
|
<img
|
||||||
)
|
src={getImageUrl(service.image)}
|
||||||
}
|
alt={service.title}
|
||||||
</div>
|
loading="lazy"
|
||||||
</a>
|
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-[1.05]"
|
||||||
))
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hot Badge -->
|
||||||
|
{
|
||||||
|
service.isHot && (
|
||||||
|
<span class="absolute top-4 right-4 md:top-3 md:right-3 bg-[var(--color-notification-red)] text-white px-[12px] py-[6px] md:px-[10px] md:py-[4px] rounded-[20px] text-[0.75rem] md:text-[0.7rem] font-bold uppercase">
|
||||||
|
HOT
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Services List Styles - Pixel-perfect from Webflow */
|
|
||||||
.section_service-list {
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Service Item - Zig-zag Layout */
|
|
||||||
.service-item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 40px;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 60px;
|
|
||||||
text-decoration: none;
|
|
||||||
position: relative;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Odd items - content on left */
|
|
||||||
.service-item.odd {
|
|
||||||
grid-template-areas: "content image";
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.odd .service-item-content {
|
|
||||||
grid-area: content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.odd .service-item-image {
|
|
||||||
grid-area: image;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Even items - content on right */
|
|
||||||
.service-item.even {
|
|
||||||
grid-template-areas: "image content";
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.even .service-item-content {
|
|
||||||
grid-area: content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item.even .service-item-image {
|
|
||||||
grid-area: image;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover Effects */
|
|
||||||
.service-item:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item:hover .service-title {
|
|
||||||
color: var(--color-enchunblue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Category Tag */
|
|
||||||
.service-category-tag {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 6px 14px;
|
|
||||||
background-color: var(--color-enchunblue);
|
|
||||||
color: white;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Title */
|
|
||||||
.service-title {
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
color: var(--color-dark-blue);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
line-height: 1.3;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Divider */
|
|
||||||
.service-divider {
|
|
||||||
width: 60px;
|
|
||||||
height: 2px;
|
|
||||||
background-color: var(--color-enchunblue);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Description */
|
|
||||||
.service-description {
|
|
||||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--color-gray-700);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Image Side */
|
|
||||||
.service-item-image {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-wrapper {
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--color-gray-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-wrapper img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item:hover .image-wrapper img {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hot Badge */
|
|
||||||
.service-hot-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
background-color: var(--color-notification-red);
|
|
||||||
color: white;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Service Icon - shown by default when content exists */
|
|
||||||
.service-item-content .service-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show icon only if icon content exists (not hidden) */
|
|
||||||
.service-item-content:has(.service-icon) .service-icon {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.service-item {
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.section_service-list {
|
|
||||||
padding: 40px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-areas: "image" "content" !important;
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item-content {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item-image {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-title {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-description {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-hot-badge {
|
|
||||||
top: 12px;
|
|
||||||
right: 12px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -2,93 +2,71 @@
|
|||||||
/**
|
/**
|
||||||
* SolutionsHero - Hero section for Solutions/Services page
|
* SolutionsHero - Hero section for Solutions/Services page
|
||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
|
* Supports optional background image from CMS
|
||||||
|
* Styled with Tailwind CSS
|
||||||
*/
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string
|
title?: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
|
backgroundImage?: {
|
||||||
|
url?: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title = '行銷解決方案',
|
title = '行銷解決方案',
|
||||||
subtitle = '提供全方位的數位行銷服務,協助您的品牌在數位時代脫穎而出',
|
subtitle = '提供全方位的數位行銷服務,協助您的品牌在數位時代脫穎而出',
|
||||||
|
backgroundImage,
|
||||||
} = Astro.props
|
} = Astro.props
|
||||||
|
|
||||||
|
// Determine if we have a background image
|
||||||
|
const hasBackgroundImage = backgroundImage?.url
|
||||||
|
const bgImageUrl = backgroundImage?.url || ''
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="hero-overlay-solution">
|
<section
|
||||||
<div class="max-w-6xl mx-auto">
|
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 -->
|
<!-- Main Title -->
|
||||||
<h1 class="hero_title_head-solution">
|
<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}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Subtitle -->
|
<!-- Subtitle -->
|
||||||
<p class="hero_sub_paragraph-solution">
|
<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}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Solutions Hero Styles - Pixel-perfect from Webflow */
|
|
||||||
.hero-overlay-solution {
|
|
||||||
background-color: var(--color-dark-blue);
|
|
||||||
max-height: 63.5vh;
|
|
||||||
padding: 120px 20px 80px;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Title - 3.39em (64px at 19px base) */
|
|
||||||
.hero_title_head-solution {
|
|
||||||
color: #ffffff;
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 3.39em;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Subtitle - 1.56em (30px at 19px base) */
|
|
||||||
.hero_sub_paragraph-solution {
|
|
||||||
color: var(--color-gray-100);
|
|
||||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.56em;
|
|
||||||
line-height: 1.2;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.hero-overlay-solution {
|
|
||||||
max-height: none;
|
|
||||||
padding: 80px 20px 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_title_head-solution {
|
|
||||||
font-size: 2.45em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_sub_paragraph-solution {
|
|
||||||
font-size: 1.15em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.hero-overlay-solution {
|
|
||||||
padding: 60px 16px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_title_head-solution {
|
|
||||||
font-size: 7vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_sub_paragraph-solution {
|
|
||||||
font-size: 3.4vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -4,92 +4,73 @@
|
|||||||
* Pixel-perfect implementation based on Webflow design
|
* Pixel-perfect implementation based on Webflow design
|
||||||
*/
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string
|
title?: string;
|
||||||
subtitle?: string
|
subtitle?: string;
|
||||||
|
backgroundImage?: {
|
||||||
|
url?: string;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title = '恩群大本營',
|
title = "恩群大本營",
|
||||||
subtitle = 'Team members of Enchun',
|
subtitle = "Team members of Enchun",
|
||||||
} = Astro.props
|
backgroundImage,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
// Determine if we have a background image
|
||||||
|
const hasBackgroundImage = backgroundImage?.url;
|
||||||
|
const bgImageUrl = backgroundImage?.url || "";
|
||||||
---
|
---
|
||||||
|
|
||||||
<header class="hero-overlay-team">
|
<section
|
||||||
<div class="centered-container w-container">
|
class:list={[
|
||||||
<div class="div-block">
|
// Base styles - relative positioning for overlay
|
||||||
<h1 class="hero_title_head-team">{title}</h1>
|
"relative",
|
||||||
<p class="hero_sub_paragraph-team">{subtitle}</p>
|
"flex",
|
||||||
</div>
|
"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>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Teams Hero Styles - Pixel-perfect from Webflow */
|
|
||||||
.hero-overlay-team {
|
|
||||||
background-color: var(--color-dark-blue);
|
|
||||||
padding: 120px 20px 80px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.centered-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.div-block {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_title_head-team {
|
|
||||||
color: #ffffff;
|
|
||||||
font-family: "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 3.39em;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_sub_paragraph-team {
|
|
||||||
color: var(--color-gray-100);
|
|
||||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.5em;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.hero-overlay-team {
|
|
||||||
padding: 80px 20px 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_title_head-team {
|
|
||||||
font-size: 2.45em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_sub_paragraph-team {
|
|
||||||
font-size: 1.15em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
.hero-overlay-team {
|
|
||||||
padding: 60px 16px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_title_head-team {
|
|
||||||
font-size: 7vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero_sub_paragraph-team {
|
|
||||||
font-size: 3.4vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -16,84 +16,142 @@
|
|||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
/* Primary Colors (主要色) */
|
/* Primary Colors (主要色) */
|
||||||
--color-primary: #3898ec; /* Primary blue */
|
--color-primary: #3898ec;
|
||||||
--color-primary-dark: #0082f3; /* Deep blue */
|
/* Primary blue */
|
||||||
--color-primary-light: #67aee1; /* Light blue */
|
--color-primary-dark: #0082f3;
|
||||||
--color-primary-hover: #2895f7; /* Hover blue */
|
/* Deep blue */
|
||||||
|
--color-primary-light: #67aee1;
|
||||||
|
/* Light blue */
|
||||||
|
--color-primary-hover: #2895f7;
|
||||||
|
/* Hover blue */
|
||||||
|
|
||||||
/* Secondary Colors (次要色) */
|
/* Secondary Colors (次要色) */
|
||||||
--color-secondary: #f39c12; /* Secondary orange */
|
--color-secondary: #f39c12;
|
||||||
--color-secondary-light: #f6c456; /* Light pink */
|
/* Secondary orange */
|
||||||
--color-secondary-dark: #d84038; /* Deep orange */
|
--color-secondary-light: #f6c456;
|
||||||
|
/* Light pink */
|
||||||
|
--color-secondary-dark: #d84038;
|
||||||
|
/* Deep orange */
|
||||||
|
|
||||||
/* Accent Colors (強調色) */
|
/* Accent Colors (強調色) */
|
||||||
--color-accent: #d84038; /* Accent red-orange */
|
--color-accent: #d84038;
|
||||||
--color-accent-light: #f6c456; /* Light pink */
|
/* Accent red-orange */
|
||||||
--color-accent-dark: #ea384c; /* Deep red */
|
--color-accent-light: #f6c456;
|
||||||
--color-accent-pink: #bb8282; /* Pink */
|
/* Light pink */
|
||||||
--color-accent-rose: #c48383; /* Rose */
|
--color-accent-dark: #ea384c;
|
||||||
|
/* Deep red */
|
||||||
|
--color-accent-pink: #bb8282;
|
||||||
|
/* Pink */
|
||||||
|
--color-accent-rose: #c48383;
|
||||||
|
/* Rose */
|
||||||
|
|
||||||
/* Link Colors (連結色) */
|
/* Link Colors (連結色) */
|
||||||
--color-link: #3083bf; /* Default link */
|
--color-link: #3083bf;
|
||||||
--color-link-hover: #23608c; /* Link hover */
|
/* Default link */
|
||||||
|
--color-link-hover: #23608c;
|
||||||
|
/* Link hover */
|
||||||
|
|
||||||
/* Neutral Colors (中性色 - Tailwind Slate mapping) */
|
/* Neutral Colors (中性色 - Tailwind Slate mapping) */
|
||||||
--color-white: #ffffff; /* White */
|
--color-white: #ffffff;
|
||||||
--color-black: #000000; /* Black */
|
/* White */
|
||||||
--color-gray-50: #fafafa; /* Surface lightest */
|
--color-black: #000000;
|
||||||
--color-gray-100: #f5f5f5; /* Surface light */
|
/* Black */
|
||||||
--color-gray-200: #f3f3f3; /* Surface */
|
--color-gray-50: #fafafa;
|
||||||
--color-gray-300: #eeeeee; /* Border light */
|
/* Surface lightest */
|
||||||
--color-gray-400: #dddddd; /* Border default */
|
--color-gray-100: #f5f5f5;
|
||||||
--color-gray-500: #c8c8c8; /* Mid gray */
|
/* Surface light */
|
||||||
--color-gray-600: #999999; /* Text muted */
|
--color-gray-200: #f3f3f3;
|
||||||
--color-gray-700: #828282; /* Text dark */
|
/* Surface */
|
||||||
--color-gray-800: #758696; /* Dark gray 1 */
|
--color-gray-300: #eeeeee;
|
||||||
--color-gray-900: #5d6c7b; /* Dark gray 2 */
|
/* Border light */
|
||||||
--color-gray-950: #4f4f4f; /* Darkest gray */
|
--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 (語意化顏色) */
|
/* Semantic Colors (語意化顏色) */
|
||||||
--color-text-primary: #333333; /* Primary text */
|
--color-text-primary: #333333;
|
||||||
--color-text-secondary: #222222; /* Secondary text */
|
/* Primary text */
|
||||||
--color-text-muted: #999999; /* Muted text */
|
--color-text-secondary: #222222;
|
||||||
--color-text-light: #758696; /* Light text on dark */
|
/* Secondary text */
|
||||||
--color-text-inverse: #ffffff; /* Inverse (white) */
|
--color-text-muted: #999999;
|
||||||
|
/* Muted text */
|
||||||
|
--color-text-light: #758696;
|
||||||
|
/* Light text on dark */
|
||||||
|
--color-text-inverse: #ffffff;
|
||||||
|
/* Inverse (white) */
|
||||||
|
|
||||||
/* Backgrounds (背景色) */
|
/* Backgrounds (背景色) */
|
||||||
--color-background: #f2f2f2; /* Main background - Grey 6 from Webflow */
|
--color-background: #f2f2f2;
|
||||||
--color-surface: #fafafa; /* Surface background */
|
/* Main background - Grey 6 from Webflow */
|
||||||
--color-surface2: #f3f3f3; /* Surface elevated */
|
--color-surface: #fafafa;
|
||||||
--color-surface-dark: #2226; /* Dark background */
|
/* Surface background */
|
||||||
|
--color-surface2: #f3f3f3;
|
||||||
|
/* Surface elevated */
|
||||||
|
--color-surface-dark: #2226;
|
||||||
|
/* Dark background */
|
||||||
|
|
||||||
/* Borders (邊框色) */
|
/* Borders (邊框色) */
|
||||||
--color-border: #e2e8f0; /* Default border */
|
--color-border: #e2e8f0;
|
||||||
--color-border-light: #dddddd; /* Light border */
|
/* Default border */
|
||||||
|
--color-border-light: #dddddd;
|
||||||
|
/* Light border */
|
||||||
|
|
||||||
/* Category Colors (文章分類) */
|
/* Category Colors (文章分類) */
|
||||||
--color-category-google: #67aee1; /* Google小學堂 */
|
--color-category-google: #67aee1;
|
||||||
--color-category-meta: #8974de; /* Meta小學堂 */
|
/* Google小學堂 */
|
||||||
--color-category-news: #3083bf; /* 行銷時事最前線 */
|
--color-category-meta: #8974de;
|
||||||
--color-category-enchun: #3898ec; /* 恩群數位 */
|
/* Meta小學堂 */
|
||||||
|
--color-category-news: #3083bf;
|
||||||
|
/* 行銷時事最前線 */
|
||||||
|
--color-category-enchun: #3898ec;
|
||||||
|
/* 恩群數位 */
|
||||||
|
|
||||||
/* Badge Colors (標籤) */
|
/* Badge Colors (標籤) */
|
||||||
--color-badge-hot: #ea384c; /* Hot 標籤 (紅) */
|
--color-badge-hot: #ea384c;
|
||||||
--color-badge-new: #67aee1; /* New 標籤 (淡藍) */
|
/* Hot 標籤 (紅) */
|
||||||
|
--color-badge-new: #67aee1;
|
||||||
|
/* New 標籤 (淡藍) */
|
||||||
|
|
||||||
/* Webflow Colors - Story 1-4 Global Layout (Verified against original) */
|
/* Webflow Colors - Story 1-4 Global Layout (Verified against original) */
|
||||||
--color-enchunblue: #23608c; /* Enchun Blue - 品牌/主色 */
|
--color-enchunblue: #23608c;
|
||||||
--color-enchunblue-dark: #3083bf; /* Enchun Blue Dark - 品牌/主色深色 */
|
/* Enchun Blue - 品牌/主色 */
|
||||||
--color-tropical-blue: #c7e4fa; /* Tropical Blue - 頁腳背景 (verified: rgb(199, 228, 250)) */
|
--color-enchunblue-dark: #3083bf;
|
||||||
--color-st-tropaz: #5d7285; /* St. Tropaz - 頁腳文字 */
|
/* Enchun Blue Dark - 品牌/主色深色 */
|
||||||
--color-amber: #f6c456; /* Amber - CTA/強調 */
|
--color-tropical-blue: #c7e4fa;
|
||||||
--color-tarawera: #2d3748; /* Tarawera - 深色文字 */
|
/* Tropical Blue - 頁腳背景 (verified: rgb(199, 228, 250)) */
|
||||||
--color-nav-link: var(--color-gray-200); /* Navigation Link - 使用灰色系 */
|
--color-st-tropaz: #5d7285;
|
||||||
|
/* St. Tropaz - 頁腳文字 */
|
||||||
|
--color-amber: #f6c456;
|
||||||
|
/* Amber - CTA/強調 */
|
||||||
|
--color-tarawera: #2d3748;
|
||||||
|
/* Tarawera - 深色文字 */
|
||||||
|
--color-pale-purple: oklch(62.664% 0.15547 290.298);
|
||||||
|
/* Pale Purple - CTA/強調 */
|
||||||
|
--color-nav-link: var(--color-gray-200);
|
||||||
|
/* Navigation Link - 使用灰色系 */
|
||||||
|
|
||||||
/* Webflow Additional Colors - Story 1-5 Homepage */
|
/* Webflow Additional Colors - Story 1-5 Homepage */
|
||||||
--color-notification-red: #d84038; /* Notification Red - CTA Button */
|
--color-notification-red: #d84038;
|
||||||
--color-dark-blue: #062841; /* Dark Blue - Headings */
|
/* Notification Red - CTA Button */
|
||||||
--color-medium-blue: #67aee1; /* Medium Blue - Accents */
|
--color-dark-blue: #062841;
|
||||||
--color-grey5: #e0e0e0; /* Grey 5 - Borders */
|
/* Dark Blue - Headings */
|
||||||
--color-grey6: #f2f2f2; /* Grey 6 - Backgrounds */
|
--color-medium-blue: #67aee1;
|
||||||
|
/* Medium Blue - Accents */
|
||||||
|
--color-grey5: #e0e0e0;
|
||||||
|
/* Grey 5 - Borders */
|
||||||
|
--color-grey6: #f2f2f2;
|
||||||
|
/* Grey 6 - Backgrounds */
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
🔤 TYPOGRAPHY - From Webflow
|
🔤 TYPOGRAPHY - From Webflow
|
||||||
@@ -101,19 +159,30 @@
|
|||||||
|
|
||||||
--font-family-sans: "Noto Sans TC", "Quicksand", Arial, sans-serif;
|
--font-family-sans: "Noto Sans TC", "Quicksand", Arial, sans-serif;
|
||||||
--font-family-heading: "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;
|
|
||||||
|
/* Rich Text Block Typography */
|
||||||
|
--rtb-heading: 1.44rem;
|
||||||
|
--rtb-body: 1rem;
|
||||||
|
--rtb-blockquote: 0.9rem;
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
📏 SPACING - Based on Tailwind scale
|
📏 SPACING - Based on Tailwind scale
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
--spacing-xs: 0.25rem; /* 4px */
|
--spacing-xs: 0.25rem;
|
||||||
--spacing-sm: 0.5rem; /* 8px */
|
/* 4px */
|
||||||
--spacing-md: 1rem; /* 16px */
|
--spacing-sm: 0.5rem;
|
||||||
--spacing-lg: 1.5rem; /* 24px */
|
/* 8px */
|
||||||
--spacing-xl: 2rem; /* 32px */
|
--spacing-md: 1rem;
|
||||||
--spacing-2xl: 3rem; /* 48px */
|
/* 16px */
|
||||||
--spacing-3xl: 4rem; /* 64px */
|
--spacing-lg: 1.5rem;
|
||||||
|
/* 24px */
|
||||||
|
--spacing-xl: 2rem;
|
||||||
|
/* 32px */
|
||||||
|
--spacing-2xl: 3rem;
|
||||||
|
/* 48px */
|
||||||
|
--spacing-3xl: 4rem;
|
||||||
|
/* 64px */
|
||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
--container-max-width: 1200px;
|
--container-max-width: 1200px;
|
||||||
@@ -124,14 +193,22 @@
|
|||||||
🔲 BORDER RADIUS
|
🔲 BORDER RADIUS
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
--radius-sm: 0.125rem; /* 2px */
|
--radius-sm: 0.125rem;
|
||||||
--radius: 0.375rem; /* 6px (DEFAULT) */
|
/* 2px */
|
||||||
--radius-md: 0.5rem; /* 8px */
|
--radius: 0.375rem;
|
||||||
--radius-lg: 0.75rem; /* 12px */
|
/* 6px (DEFAULT) */
|
||||||
--radius-xl: 1rem; /* 16px */
|
--radius-md: 0.5rem;
|
||||||
--radius-2xl: 1.5rem; /* 24px */
|
/* 8px */
|
||||||
--radius-3xl: 2rem; /* 32px */
|
--radius-lg: 0.75rem;
|
||||||
--radius-full: 9999px; /* Full circle */
|
/* 12px */
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
/* 16px */
|
||||||
|
--radius-2xl: 1.5rem;
|
||||||
|
/* 24px */
|
||||||
|
--radius-3xl: 2rem;
|
||||||
|
/* 32px */
|
||||||
|
--radius-full: 9999px;
|
||||||
|
/* Full circle */
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
💫 SHADOWS
|
💫 SHADOWS
|
||||||
@@ -174,9 +251,12 @@
|
|||||||
Small (≤479px): 13px
|
Small (≤479px): 13px
|
||||||
*/
|
*/
|
||||||
--html-font-size-desktop: 19px;
|
--html-font-size-desktop: 19px;
|
||||||
--html-font-size-tablet: 19px; /* 991px breakpoint */
|
--html-font-size-tablet: 19px;
|
||||||
--html-font-size-mobile: 16px; /* 767px breakpoint */
|
/* 991px breakpoint */
|
||||||
--html-font-size-small: 13px; /* 479px breakpoint */
|
--html-font-size-mobile: 16px;
|
||||||
|
/* 767px breakpoint */
|
||||||
|
--html-font-size-small: 13px;
|
||||||
|
/* 479px breakpoint */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
@@ -248,11 +328,9 @@ body {
|
|||||||
|
|
||||||
/* Text Gradient */
|
/* Text Gradient */
|
||||||
.text-gradient {
|
.text-gradient {
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg,
|
||||||
135deg,
|
var(--color-primary),
|
||||||
var(--color-primary),
|
var(--color-accent));
|
||||||
var(--color-accent)
|
|
||||||
);
|
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -271,6 +349,7 @@ body {
|
|||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -281,6 +360,7 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(1rem);
|
transform: translateY(1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -353,7 +433,7 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--color-primary);
|
background: var(--color-secondary);
|
||||||
transition: width var(--transition-fast);
|
transition: width var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +441,11 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Active Navigation Link */
|
/* Active Navigation Link — position: relative ensures ::after stays within the link */
|
||||||
|
.nav-active {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-active::after {
|
.nav-active::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -398,9 +482,11 @@ body {
|
|||||||
.prose-custom h1 {
|
.prose-custom h1 {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose-custom h2 {
|
.prose-custom h2 {
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose-custom h3 {
|
.prose-custom h3 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -544,4 +630,4 @@ body {
|
|||||||
.badge-category-enchun {
|
.badge-category-enchun {
|
||||||
background-color: var(--color-category-enchun);
|
background-color: var(--color-category-enchun);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,33 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||||
"name": "enchun-frontend",
|
"name": "website-enchun-mgr",
|
||||||
"main": "./dist/_worker.js/index.js",
|
"main": "./dist/_worker.js/index.js",
|
||||||
"compatibility_date": "2025-01-19",
|
"compatibility_date": "2025-01-19",
|
||||||
"compatibility_flags": [
|
"compatibility_flags": [
|
||||||
"nodejs_compat"
|
"nodejs_compat"
|
||||||
],
|
],
|
||||||
"assets": {
|
"assets": {
|
||||||
"directory": "./dist"
|
"directory": "./dist",
|
||||||
|
"binding": "ASSETS"
|
||||||
},
|
},
|
||||||
|
"kv_namespaces": [
|
||||||
|
{
|
||||||
|
"binding": "SESSION",
|
||||||
|
"id": "0de94ad20be24add94c1f8fe9c4a0440"
|
||||||
|
}
|
||||||
|
],
|
||||||
"vars": {
|
"vars": {
|
||||||
"PAYLOAD_CMS_URL": "https://enchun-admin.anlstudio.cc"
|
"PAYLOAD_CMS_URL": "https://enchun-admin.anlstudio.cc"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"production": {
|
"production": {
|
||||||
"name": "enchun-frontend-production",
|
"name": "website-enchun-mgr-production",
|
||||||
|
"kv_namespaces": [
|
||||||
|
{
|
||||||
|
"binding": "SESSION",
|
||||||
|
"id": "0de94ad20be24add94c1f8fe9c4a0440"
|
||||||
|
}
|
||||||
|
],
|
||||||
"vars": {
|
"vars": {
|
||||||
"PAYLOAD_CMS_URL": "https://enchun-admin.anlstudio.cc"
|
"PAYLOAD_CMS_URL": "https://enchun-admin.anlstudio.cc"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
name = "enchun-frontend"
|
|
||||||
compatibility_date = "2024-01-01"
|
|
||||||
pages_build_output_dir = "dist"
|
|
||||||
|
|
||||||
[vars]
|
|
||||||
PAYLOAD_CMS_URL = "https://enchun-admin.anlstudio.cc"
|
|
||||||
|
|
||||||
[env.production.vars]
|
|
||||||
PAYLOAD_CMS_URL = "https://enchun-admin.anlstudio.cc"
|
|
||||||
13
bunfig.toml
Normal file
13
bunfig.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Bunfig - Bun 套件優化配置
|
||||||
|
# https://bun.sh/docs/runtime/bunfig
|
||||||
|
|
||||||
|
[install]
|
||||||
|
# 使用精確版本,避免意外升級
|
||||||
|
exact = true
|
||||||
|
|
||||||
|
# 生產環境使用 lockfile
|
||||||
|
frozenLockfile = true
|
||||||
|
|
||||||
|
[install.lockfile]
|
||||||
|
# 保存 lockfile
|
||||||
|
save = true
|
||||||
16
package.json
16
package.json
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "enchun-monorepo",
|
"name": "enchun-monorepo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.17.0",
|
"packageManager": "bun@1.3.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo run dev --parallel",
|
"dev": "turbo run dev --parallel",
|
||||||
"dev:stop": "echo 'Stopping dev servers...' && pkill -f 'astro.js dev' && pkill -f 'next dev' && pkill -f 'pnpm dev' && echo 'Dev servers stopped' || echo 'No dev servers were running'",
|
"dev:stop": "echo 'Stopping dev servers...' && pkill -f 'astro dev' && pkill -f 'next dev' && echo 'Dev servers stopped' || echo 'No dev servers were running'",
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"test": "turbo run test",
|
"test": "turbo run test",
|
||||||
@@ -17,11 +17,9 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"turbo": "^2.0.5"
|
"turbo": "^2.0.5"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"workspaces": [
|
||||||
"onlyBuiltDependencies": [
|
"apps/frontend",
|
||||||
"sharp",
|
"apps/backend",
|
||||||
"esbuild",
|
"packages/*"
|
||||||
"unrs-resolver"
|
]
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
22
packages/shared/pnpm-lock.yaml
generated
22
packages/shared/pnpm-lock.yaml
generated
@@ -1,22 +0,0 @@
|
|||||||
lockfileVersion: '9.0'
|
|
||||||
|
|
||||||
settings:
|
|
||||||
autoInstallPeers: true
|
|
||||||
excludeLinksFromLockfile: false
|
|
||||||
|
|
||||||
importers:
|
|
||||||
|
|
||||||
.:
|
|
||||||
devDependencies:
|
|
||||||
tailwindcss:
|
|
||||||
specifier: ^4.1.14
|
|
||||||
version: 4.1.14
|
|
||||||
|
|
||||||
packages:
|
|
||||||
|
|
||||||
tailwindcss@4.1.14:
|
|
||||||
resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==}
|
|
||||||
|
|
||||||
snapshots:
|
|
||||||
|
|
||||||
tailwindcss@4.1.14: {}
|
|
||||||
16304
pnpm-lock.yaml
generated
16304
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
|||||||
packages:
|
|
||||||
- "apps/frontend"
|
|
||||||
- "apps/backend"
|
|
||||||
- packages/*
|
|
||||||
Reference in New Issue
Block a user