Compare commits
2 Commits
84b5a498e6
...
001-users-
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e32d52133 | |||
| df1efb4881 |
@@ -3,3 +3,10 @@
|
||||
|
||||
# Production Payload CMS URL (for SSR fetch)
|
||||
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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
---
|
||||
|
||||
<section
|
||||
class="py-20 px-5 bg-white text-center mx-auto lg:py-[60px] lg:px-5 md:py-10"
|
||||
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">
|
||||
|
||||
@@ -42,8 +42,8 @@ try {
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="bg-[var(--color-tropical-blue)] pt-10 mt-auto relative">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<footer class="bg-(--color-tropical-blue) pt-10 mt-auto">
|
||||
<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="col-span-2">
|
||||
<Image
|
||||
@@ -56,16 +56,14 @@ const currentYear = new Date().getFullYear();
|
||||
decoding="async"
|
||||
/>
|
||||
<p
|
||||
class="text-[var(--color-st-tropaz)] text-sm pr-8 font-light leading-relaxed"
|
||||
class="text-(--color-st-tropaz) text-sm pr-0 md:pr-8 font-light leading-relaxed"
|
||||
>
|
||||
恩群數位累積多年廣告行銷操作經驗,擁有全方位行銷人才,讓我們可以為客戶精準的規劃每一分廣告預算,讓你的品牌深入人心。更重要的是恩群的存在,為了成為每家公司最佳數位夥伴,作為彼此最堅強的後盾,你會知道有我們的陪伴
|
||||
你並不孤單。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4"
|
||||
>
|
||||
<h3 class="text-lg font-bold text-(--color-st-tropaz) mb-4">
|
||||
聯絡我們
|
||||
</h3>
|
||||
<a
|
||||
@@ -84,7 +82,7 @@ const currentYear = new Date().getFullYear();
|
||||
decoding="async"
|
||||
/>
|
||||
</a>
|
||||
<p class="text-sm text-[var(--color-st-tropaz)] mb-2">
|
||||
<p class="text-sm text-(--color-st-tropaz) mb-2">
|
||||
諮詢電話:<br /> 02 5570 0527
|
||||
</p>
|
||||
<a
|
||||
@@ -94,9 +92,7 @@ const currentYear = new Date().getFullYear();
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4"
|
||||
>
|
||||
<h3 class="text-lg font-bold text-(--color-st-tropaz) mb-4">
|
||||
行銷方案
|
||||
</h3>
|
||||
<ul
|
||||
@@ -125,9 +121,7 @@ const currentYear = new Date().getFullYear();
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4"
|
||||
>
|
||||
<h3 class="text-lg font-bold text-(--color-st-tropaz) mb-4">
|
||||
行銷放大鏡
|
||||
</h3>
|
||||
<ul class="text-sm font-thin space-y-2" id="marketing-articles">
|
||||
@@ -155,7 +149,7 @@ const currentYear = new Date().getFullYear();
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-screen bg-[var(--color-amber)] font-['Quicksand'] text-[var(--color-tarawera)] py-2 text-xs text-center"
|
||||
class="w-screen bg-(--color-amber) font-['Quicksand'] text-(--color-tarawera) py-2 text-xs text-center"
|
||||
>
|
||||
<p>
|
||||
copyright © Enchun digital 2018 - {currentYear}
|
||||
|
||||
@@ -22,7 +22,7 @@ const {
|
||||
<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 py-12 mx-auto",
|
||||
"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,
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -28,12 +28,6 @@ const description =
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Section Header -->
|
||||
<SectionHeader
|
||||
title="關於恩群"
|
||||
subtitle="About Enchun"
|
||||
sectionBg="bg-white"
|
||||
/>
|
||||
<!-- Service Features Section -->
|
||||
<FeatureSection />
|
||||
|
||||
|
||||
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" },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
* Includes form validation, submission handling, and responsive layout
|
||||
*/
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import HeaderBg from "../components/HeaderBg.astro";
|
||||
|
||||
// Metadata for SEO
|
||||
const title = "聯絡我們 | 恩群數位行銷";
|
||||
@@ -13,6 +14,7 @@ const description =
|
||||
---
|
||||
|
||||
<Layout title={title} description={description}>
|
||||
<HeaderBg />
|
||||
<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"
|
||||
@@ -144,6 +146,20 @@ const description =
|
||||
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>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="flex justify-end mt-2">
|
||||
<button
|
||||
@@ -163,6 +179,8 @@ const description =
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer
|
||||
></script>
|
||||
<script>
|
||||
// Form validation and submission handler
|
||||
function initContactForm() {
|
||||
@@ -172,6 +190,9 @@ const description =
|
||||
) 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;
|
||||
|
||||
@@ -281,10 +302,28 @@ const description =
|
||||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
// Scroll to first error
|
||||
const firstError = form.querySelector(
|
||||
".input_field.error",
|
||||
".input_field.error, #turnstile-error[style*='display: block']",
|
||||
) as HTMLElement;
|
||||
if (firstError) {
|
||||
firstError.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
@@ -305,17 +344,16 @@ const description =
|
||||
successMsg.style.display = "none";
|
||||
errorMsg.style.display = "none";
|
||||
|
||||
// Collect form data
|
||||
const formData = new FormData(form);
|
||||
const data = {
|
||||
name: formData.get("Name"),
|
||||
phone: formData.get("Phone"),
|
||||
email: formData.get("Email"),
|
||||
message: formData.get("Message"),
|
||||
"cf-turnstile-response": turnstileToken,
|
||||
};
|
||||
|
||||
try {
|
||||
// Submit to backend (via API proxy)
|
||||
// Submit to API proxy
|
||||
const response = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -4,24 +4,28 @@
|
||||
* Pixel-perfect implementation based on Webflow design
|
||||
* Data fetched from Payload CMS Pages Collection API
|
||||
*/
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
import SolutionsHero from '../sections/SolutionsHero.astro'
|
||||
import ServicesList from '../sections/ServicesList.astro'
|
||||
import { getMarketingSolutionsPage } from '../lib/api/marketing-solution'
|
||||
import SectionHeader from '../components/SectionHeader.astro'
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import SolutionsHero from "../sections/SolutionsHero.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()
|
||||
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 || []
|
||||
const heroTitle = pageData?.heroTitle || "行銷解決方案";
|
||||
const heroSubtitle =
|
||||
pageData?.heroSubtitle ||
|
||||
"提供全方位的數位行銷服務,協助您的品牌在數位時代脫穎而出";
|
||||
const heroImage = pageData?.heroImage;
|
||||
const services = pageData?.services || [];
|
||||
|
||||
// Metadata for SEO
|
||||
const title = '行銷解決方案 | 恩群數位行銷'
|
||||
const description = '恩群數位行銷提供全方位的數位行銷服務,包括 Google Ads、社群代操、論壇行銷、網紅行銷、網站設計等,協助您的品牌在數位時代脫穎而出。'
|
||||
const title = "行銷解決方案 | 恩群數位行銷";
|
||||
const description =
|
||||
"恩群數位行銷提供全方位的數位行銷服務,包括 Google Ads、社群代操、論壇行銷、網紅行銷、網站設計等,協助您的品牌在數位時代脫穎而出。";
|
||||
---
|
||||
|
||||
<Layout title={title} description={description}>
|
||||
@@ -39,4 +43,5 @@ const description = '恩群數位行銷提供全方位的數位行銷服務,
|
||||
/>
|
||||
<!-- Services List -->
|
||||
<ServicesList services={services} />
|
||||
<CtaSection />
|
||||
</Layout>
|
||||
|
||||
@@ -9,7 +9,8 @@ import TeamsHero from "../sections/TeamsHero.astro";
|
||||
import EnvironmentSlider from "../sections/EnvironmentSlider.astro";
|
||||
import CompanyStory from "../sections/CompanyStory.astro";
|
||||
import BenefitsSection from "../sections/BenefitsSection.astro";
|
||||
|
||||
import SectionHeader from "../components/SectionHeader.astro";
|
||||
import CtaHrCompoents from "../sections/Cta-Hr-compoents.astro";
|
||||
// Metadata for SEO
|
||||
const title = "恩群大本營 | 恩群數位行銷";
|
||||
const description =
|
||||
@@ -28,50 +29,23 @@ const description =
|
||||
/>
|
||||
|
||||
<!-- Environment Slider Section -->
|
||||
|
||||
<EnvironmentSlider />
|
||||
|
||||
<!-- Company Story Section -->
|
||||
<CompanyStory />
|
||||
|
||||
<!-- Benefits Section -->
|
||||
|
||||
<BenefitsSection />
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section
|
||||
class="py-20 px-5 bg-slate-100 text-center lg:py-[60px] lg:px-4 md:py-10"
|
||||
aria-labelledby="cta-heading"
|
||||
>
|
||||
<div class="max-w-[1200px] mx-auto">
|
||||
<div
|
||||
class="grid grid-cols-[1fr_auto] gap-8 items-center relative lg:grid-cols-1 lg:text-center"
|
||||
>
|
||||
<div class="text-left lg:text-center">
|
||||
<h3
|
||||
id="cta-heading"
|
||||
class="text-[1.75rem] font-semibold text-[#23608c] mb-4 leading-snug lg:text-[1.5rem] md:text-[1.5rem]"
|
||||
>
|
||||
以人的成長為優先<br />
|
||||
創造人的最大價值
|
||||
</h3>
|
||||
<p
|
||||
class="text-base text-slate-600 leading-relaxed max-w-[500px] lg:max-w-full md:text-[0.95rem]"
|
||||
>
|
||||
在恩群數位裡我們重視個人的特質能夠完全發揮,只要你樂於學習、善於跟人建立關係,並且重要的是你有一個善良的心,恩群數位歡迎你的加入
|
||||
</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 bg-link-hover text-white px-8 py-4 rounded-md font-semibold text-base transition-all duration-200 whitespace-nowrap hover:-translate-y-0.5 hover:shadow-md hover:bg-[#1a4d6e] lg:w-full lg:max-w-[300px] md:py-[14px] md:px-6 md:text-[0.95rem]"
|
||||
>
|
||||
立刻申請面試
|
||||
</a>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-[rgba(35,96,140,0.05)] to-[rgba(35,96,140,0.02)] rounded-lg -z-10 lg:hidden"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<CtaHrCompoents
|
||||
title="以人的成長為優先<br />創造人的最大價值"
|
||||
description="在恩群數位裡我們重視個人的特質能夠完全發揮,只要你樂於學習、善於跟人建立關係,並且重要的是你有一個善良的心,恩群數位歡迎你的加入"
|
||||
image={{
|
||||
url: "https://enchun-cms.anlstudio.cc/api/media/file/61f24aa108528b4723942d01_工作環境-銘言底圖-1400x659.jpg",
|
||||
alt: "工作環境",
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
@@ -6,304 +6,93 @@
|
||||
*/
|
||||
|
||||
interface BenefitItem {
|
||||
title: string
|
||||
icon: string
|
||||
title: string;
|
||||
img: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
benefits?: BenefitItem[]
|
||||
benefits?: BenefitItem[];
|
||||
}
|
||||
|
||||
const defaultBenefits: BenefitItem[] = [
|
||||
{
|
||||
title: '高績效、高獎金\n新人開張獎金',
|
||||
icon: 'bonus',
|
||||
title: "高績效、高獎金\n新人開張獎金",
|
||||
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員工下午茶',
|
||||
icon: 'birthday',
|
||||
title: "生日慶生、電影日\n員工下午茶",
|
||||
img: "/api/media/file/61f24aa108528be590942d06_Blowing%20out%20Birthday%20candles-bro-%E7%94%9F%E6%97%A5%E6%85%B6%E7%94%9F.svg",
|
||||
},
|
||||
{
|
||||
title: '教育訓練補助',
|
||||
icon: 'education',
|
||||
title: "教育訓練補助",
|
||||
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: '寬敞的工作空間',
|
||||
icon: 'workspace',
|
||||
title: "寬敞的工作空間",
|
||||
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部門聚餐、年終活動',
|
||||
icon: 'travel',
|
||||
title: "員工國內外旅遊\n部門聚餐、年終活動",
|
||||
img: "/api/media/file/61f24aa108528b0960942d04_Flight%20Booking-bro-%E5%93%A1%E5%B7%A5%E6%97%85%E9%81%8A.svg",
|
||||
},
|
||||
{
|
||||
title: '入職培訓及團隊建設',
|
||||
icon: 'training',
|
||||
title: "入職培訓及團隊建設",
|
||||
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
|
||||
|
||||
// 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
|
||||
}
|
||||
const benefits = Astro.props.benefits || defaultBenefits;
|
||||
import SectionHeader from "../components/SectionHeader.astro";
|
||||
---
|
||||
|
||||
<section class="section-benefit" aria-labelledby="benefits-heading">
|
||||
<div class="container w-container">
|
||||
<section
|
||||
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 -->
|
||||
<div class="section_header_w_line">
|
||||
<div class="divider_line"></div>
|
||||
<div class="header_subtitle">
|
||||
<h2 id="benefits-heading" class="header_subtitle_head">工作福利</h2>
|
||||
<p class="header_subtitle_paragraph">Benefit Package</p>
|
||||
</div>
|
||||
<div class="divider_line"></div>
|
||||
</div>
|
||||
<SectionHeader
|
||||
title="工作福利"
|
||||
subtitle="Benefit Packages"
|
||||
sectionBg="bg-white"
|
||||
/>
|
||||
|
||||
<!-- Benefits Grid -->
|
||||
<div class="benefit-grid-wrapper">
|
||||
<!-- Benefits Grid: 2 cards per row -->
|
||||
<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) => (
|
||||
<div class={`benefit-card ${index % 2 === 0 ? 'benefit-card' : 'benefit-card-opposite'}`}>
|
||||
<!-- Odd: Icon on right, Even: Icon on left -->
|
||||
{
|
||||
index % 2 === 0 ? (
|
||||
<>
|
||||
<div class="benefit-content">
|
||||
<h3 class="benefit-title-text">{benefit.title}</h3>
|
||||
</div>
|
||||
<div class="benefit-image-right" set:html={getIconSVG(benefit.icon)} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div class="benefit-image-left" set:html={getIconSVG(benefit.icon)} />
|
||||
<div class="benefit-content">
|
||||
<h3 class="benefit-title-text">{benefit.title}</h3>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
benefits.map((benefit, index) => {
|
||||
const isLeft = index % 2 === 0;
|
||||
return (
|
||||
<div class="grid grid-cols-2 gap-2 items-center">
|
||||
<div
|
||||
class:list={[
|
||||
isLeft ? "order-1 text-right" : "order-2 text-left",
|
||||
]}
|
||||
>
|
||||
<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:list={[
|
||||
"flex items-center",
|
||||
isLeft ? "order-2 justify-start" : "order-1 justify-end",
|
||||
]}
|
||||
>
|
||||
<img
|
||||
src={`https://enchun-cms.anlstudio.cc${benefit.img}`}
|
||||
alt={benefit.title}
|
||||
class="size-35 object-contain"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
*/
|
||||
interface Props {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
content?: string
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = '恩群數位的故事',
|
||||
subtitle = 'Something About Enchun Digital',
|
||||
content = '恩群數位是由一群年輕果斷、敢冒險的年輕人聚在一起,共同為台灣在地經營努力不懈的商家老闆們建立品牌的知名度。而商家的經營本身就不是一件容易的事情,早在恩群成立之前,我們便一直聚焦在與不同行業的老闆們建立起信賴可靠的關係,從一家又一家的合作關係當中,聚集了在行銷領域裡的頂尖好手,培育了許多優秀行銷顧問。讓每個辛苦經營的商家老闆可以獲得最佳的服務,讓每次的行銷需求可以透過有效的互動與聆聽,達到彼此心目中的預期目標。數字的確會說話,但是每一個有溫度的服務才是在恩群裡最重視的地方。',
|
||||
} = Astro.props
|
||||
title = "恩群數位的故事",
|
||||
subtitle = "Something About Enchun Digital",
|
||||
content = "恩群數位是由一群年輕果斷、敢冒險的年輕人聚在一起,共同為台灣在地經營努力不懈的商家老闆們建立品牌的知名度。而商家的經營本身就不是一件容易的事情,早在恩群成立之前,我們便一直聚焦在與不同行業的老闆們建立起信賴可靠的關係,從一家又一家的合作關係當中,聚集了在行銷領域裡的頂尖好手,培育了許多優秀行銷顧問。讓每個辛苦經營的商家老闆可以獲得最佳的服務,讓每次的行銷需求可以透過有效的互動與聆聽,達到彼此心目中的預期目標。數字的確會說話,但是每一個有溫度的服務才是在恩群裡最重視的地方。",
|
||||
} = Astro.props;
|
||||
import SectionHeader from "../components/SectionHeader.astro";
|
||||
---
|
||||
|
||||
<section class="section-story" aria-labelledby="story-heading">
|
||||
<div class="container w-container">
|
||||
<section
|
||||
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 -->
|
||||
<div class="section_header_w_line">
|
||||
<div class="divider_line"></div>
|
||||
<div class="header_subtitle">
|
||||
<h2 id="story-heading" class="header_subtitle_head">{title}</h2>
|
||||
<p class="header_subtitle_paragraph">{subtitle}</p>
|
||||
</div>
|
||||
<div class="divider_line"></div>
|
||||
</div>
|
||||
<SectionHeader
|
||||
title="恩群數位的故事"
|
||||
subtitle="Something About Enchun Digital"
|
||||
sectionBg="bg-white"
|
||||
/>
|
||||
|
||||
<!-- 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>
|
||||
</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 其他行銷公司 對比表格
|
||||
* 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[] = [
|
||||
{
|
||||
feature: '服務範圍',
|
||||
enchun: '全方位數位行銷服務,從策略到執行一條龍',
|
||||
others: '單一服務項目,缺乏整合性',
|
||||
},
|
||||
{
|
||||
feature: '數據分析',
|
||||
enchun: '專業數據分析團隊,精準追蹤 ROI',
|
||||
others: '基礎報告,缺乏深度分析',
|
||||
},
|
||||
{
|
||||
feature: '在地化經驗',
|
||||
enchun: '深耕台灣市場,了解本地消費者習性',
|
||||
others: '通用策略,缺乏在地化調整',
|
||||
},
|
||||
{
|
||||
feature: '客戶服務',
|
||||
enchun: '一對一專人服務,快速響應',
|
||||
others: '標準化流程,回應較慢',
|
||||
},
|
||||
{
|
||||
feature: '價格透明',
|
||||
enchun: '明確報價,無隱藏費用',
|
||||
others: '複雜收費結構,容易超支',
|
||||
},
|
||||
]
|
||||
const otherCompanyItems = [
|
||||
"缺乏經驗",
|
||||
"沒有成效保證",
|
||||
"售後無服務",
|
||||
"沒有策略",
|
||||
"不了解客戶需求",
|
||||
"沒有接受客戶反饋",
|
||||
];
|
||||
|
||||
const enchunItems = [
|
||||
"實際執行經驗豐富",
|
||||
"實際成效",
|
||||
"售後服務架構完善",
|
||||
"行銷策略有方",
|
||||
"熟悉客戶需求",
|
||||
"最多客戶回饋",
|
||||
];
|
||||
---
|
||||
|
||||
<section class="section-comparison" aria-labelledby="comparison-heading">
|
||||
<div class="w-container">
|
||||
<section
|
||||
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 -->
|
||||
<div class="section-header-w-line">
|
||||
<h2 id="comparison-heading" class="header-subtitle-head">
|
||||
為什麼選擇恩群數位
|
||||
</h2>
|
||||
<div class="divider-line"></div>
|
||||
<SectionHeader
|
||||
title="恩群與其他公司有什麼不同"
|
||||
subtitle="What make us different from others"
|
||||
/>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Comparison Table -->
|
||||
<div class="comparison-table-wrapper">
|
||||
<table class="comparison-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="th-feature">比較項目</th>
|
||||
<th class="th-enchun">
|
||||
<span class="enchun-badge">恩群數位</span>
|
||||
</th>
|
||||
<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>
|
||||
<!-- CTA Button -->
|
||||
<div class="mt-12 flex justify-center">
|
||||
<a
|
||||
href="https://heyform.itslouis.cc/form/7mYtUNjA"
|
||||
target="_blank"
|
||||
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"
|
||||
>
|
||||
<div class="tracking-wide">跟行銷顧問聊聊</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</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,54 +6,88 @@
|
||||
*/
|
||||
|
||||
interface SlideImage {
|
||||
src: string
|
||||
alt: string
|
||||
src: string;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
slides?: SlideImage[]
|
||||
slides?: SlideImage[];
|
||||
}
|
||||
|
||||
const defaultSlides: SlideImage[] = [
|
||||
{ src: '/placeholder-environment-1.jpg', alt: '恩群環境照片 1' },
|
||||
{ src: '/placeholder-environment-2.jpg', alt: '恩群環境照片 2' },
|
||||
{ src: '/placeholder-environment-3.jpg', alt: '恩群環境照片 3' },
|
||||
{ src: '/placeholder-environment-4.jpg', alt: '恩群環境照片 4' },
|
||||
{ src: '/placeholder-environment-5.jpg', alt: '恩群環境照片 5' },
|
||||
{ src: '/placeholder-environment-6.jpg', alt: '恩群環境照片 6' },
|
||||
{ src: '/placeholder-environment-7.jpg', alt: '恩群環境照片 7' },
|
||||
{ src: '/placeholder-environment-8.jpg', alt: '恩群環境照片 8' },
|
||||
]
|
||||
{
|
||||
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",
|
||||
alt: "恩群環境照片 1",
|
||||
},
|
||||
{
|
||||
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",
|
||||
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-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'
|
||||
import SectionHeader from "../components/SectionHeader.astro";
|
||||
---
|
||||
|
||||
<section class="section-video" aria-label="工作環境照片">
|
||||
<div class="container spacer8 w-container">
|
||||
<section
|
||||
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 -->
|
||||
<SectionHeader
|
||||
title="在恩群工作的環境"
|
||||
subtitle="Working Enviroment"
|
||||
sectionBg="bg-white"
|
||||
/>
|
||||
|
||||
<!-- Environment Slider -->
|
||||
<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)}"
|
||||
>
|
||||
<!-- 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) => (
|
||||
<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
|
||||
src={slide.src}
|
||||
alt={slide.alt}
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
loading={index === 0 ? "eager" : "lazy"}
|
||||
width="800"
|
||||
height="450"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
@@ -61,23 +95,33 @@ import SectionHeader from '../components/SectionHeader.astro'
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
</button>
|
||||
|
||||
<!-- Dot Navigation -->
|
||||
<div class="slider-dots">
|
||||
<div class="slider-dots flex justify-center gap-2 mt-4">
|
||||
{
|
||||
slides.map((_, index) => (
|
||||
<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}
|
||||
aria-label={`顯示第 ${index + 1} 張照片`}
|
||||
/>
|
||||
@@ -88,313 +132,183 @@ import SectionHeader from '../components/SectionHeader.astro'
|
||||
</div>
|
||||
</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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Environment Slider functionality
|
||||
function initEnvironmentSlider() {
|
||||
const sliders = document.querySelectorAll('.environment-slider')
|
||||
const sliders = document.querySelectorAll(".environment-slider");
|
||||
|
||||
sliders.forEach((slider) => {
|
||||
const container = slider.querySelector('.slides-container') as HTMLElement
|
||||
const slides = slider.querySelectorAll('.environment-slide')
|
||||
const dots = slider.querySelectorAll('.slider-dot')
|
||||
const prevBtn = slider.querySelector('.slider-arrow-left') as HTMLButtonElement
|
||||
const nextBtn = slider.querySelector('.slider-arrow-right') as HTMLButtonElement
|
||||
const container = slider.querySelector(
|
||||
".slides-container",
|
||||
) as HTMLElement;
|
||||
const slides = slider.querySelectorAll(".environment-slide");
|
||||
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
|
||||
const totalSlides = slides.length
|
||||
let isDragging = false
|
||||
let startPos = 0
|
||||
let currentTranslate = 0
|
||||
let prevTranslate = 0
|
||||
let animationID: number
|
||||
let currentIndex = 0;
|
||||
const totalSlides = slides.length;
|
||||
let isDragging = false;
|
||||
let startPos = 0;
|
||||
let currentTranslate = 0;
|
||||
let prevTranslate = 0;
|
||||
let animationID: number;
|
||||
|
||||
// Update slider position
|
||||
const updateSlider = () => {
|
||||
container.scrollTo({
|
||||
left: currentIndex * container.offsetWidth,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
updateDots()
|
||||
}
|
||||
behavior: "smooth",
|
||||
});
|
||||
updateDots();
|
||||
};
|
||||
|
||||
// Update dots
|
||||
const updateDots = () => {
|
||||
dots.forEach((dot, index) => {
|
||||
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 {
|
||||
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
|
||||
const goToSlide = (index: number) => {
|
||||
if (index < 0) index = totalSlides - 1
|
||||
if (index >= totalSlides) index = 0
|
||||
currentIndex = index
|
||||
updateSlider()
|
||||
}
|
||||
if (index < 0) index = totalSlides - 1;
|
||||
if (index >= totalSlides) index = 0;
|
||||
currentIndex = index;
|
||||
updateSlider();
|
||||
};
|
||||
|
||||
// Previous slide
|
||||
prevBtn?.addEventListener('click', () => goToSlide(currentIndex - 1))
|
||||
prevBtn?.addEventListener("click", () => goToSlide(currentIndex - 1));
|
||||
|
||||
// Next slide
|
||||
nextBtn?.addEventListener('click', () => goToSlide(currentIndex + 1))
|
||||
nextBtn?.addEventListener("click", () => goToSlide(currentIndex + 1));
|
||||
|
||||
// Dot navigation
|
||||
dots.forEach((dot, index) => {
|
||||
dot.addEventListener('click', () => goToSlide(index))
|
||||
})
|
||||
dot.addEventListener("click", () => goToSlide(index));
|
||||
});
|
||||
|
||||
// Scroll snap detection
|
||||
container.addEventListener('scroll', () => {
|
||||
const slideIndex = Math.round(container.scrollLeft / container.offsetWidth)
|
||||
container.addEventListener("scroll", () => {
|
||||
const slideIndex = Math.round(
|
||||
container.scrollLeft / container.offsetWidth,
|
||||
);
|
||||
if (slideIndex !== currentIndex) {
|
||||
currentIndex = slideIndex
|
||||
updateDots()
|
||||
currentIndex = slideIndex;
|
||||
updateDots();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Touch/swipe support
|
||||
const touchStart = (_index: number) => {
|
||||
return function(event: TouchEvent) {
|
||||
isDragging = true
|
||||
startPos = event.touches[0].clientX
|
||||
animationID = requestAnimationFrame(animation)
|
||||
container.style.cursor = 'grabbing'
|
||||
}
|
||||
}
|
||||
return function (event: TouchEvent) {
|
||||
isDragging = true;
|
||||
startPos = event.touches[0].clientX;
|
||||
animationID = requestAnimationFrame(animation);
|
||||
container.style.cursor = "grabbing";
|
||||
};
|
||||
};
|
||||
|
||||
const touchEnd = () => {
|
||||
isDragging = false
|
||||
cancelAnimationFrame(animationID)
|
||||
container.style.cursor = 'grab'
|
||||
isDragging = false;
|
||||
cancelAnimationFrame(animationID);
|
||||
container.style.cursor = "grab";
|
||||
|
||||
const movedBy = currentTranslate - prevTranslate
|
||||
const movedBy = currentTranslate - prevTranslate;
|
||||
|
||||
if (movedBy < -50 && currentIndex < totalSlides - 1) {
|
||||
currentIndex += 1
|
||||
currentIndex += 1;
|
||||
} else if (movedBy > 50 && currentIndex > 0) {
|
||||
currentIndex -= 1
|
||||
currentIndex -= 1;
|
||||
}
|
||||
|
||||
goToSlide(currentIndex)
|
||||
}
|
||||
goToSlide(currentIndex);
|
||||
};
|
||||
|
||||
const touchMove = (event: TouchEvent) => {
|
||||
if (isDragging) {
|
||||
const currentPosition = event.touches[0].clientX
|
||||
currentTranslate = prevTranslate + currentPosition - startPos
|
||||
const currentPosition = event.touches[0].clientX;
|
||||
currentTranslate = prevTranslate + currentPosition - startPos;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const animation = () => {
|
||||
if (isDragging) requestAnimationFrame(animation)
|
||||
}
|
||||
if (isDragging) requestAnimationFrame(animation);
|
||||
};
|
||||
|
||||
// Mouse events for desktop
|
||||
let mouseStartPos = 0
|
||||
let isMouseDown = false
|
||||
let mouseStartPos = 0;
|
||||
let isMouseDown = false;
|
||||
|
||||
container.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
isMouseDown = true
|
||||
mouseStartPos = e.clientX
|
||||
container.style.cursor = 'grabbing'
|
||||
})
|
||||
container.addEventListener("mousedown", (e: MouseEvent) => {
|
||||
isMouseDown = true;
|
||||
mouseStartPos = e.clientX;
|
||||
container.style.cursor = "grabbing";
|
||||
});
|
||||
|
||||
container.addEventListener('mouseup', (e: MouseEvent) => {
|
||||
if (!isMouseDown) return
|
||||
isMouseDown = false
|
||||
container.style.cursor = 'grab'
|
||||
container.addEventListener("mouseup", (e: MouseEvent) => {
|
||||
if (!isMouseDown) return;
|
||||
isMouseDown = false;
|
||||
container.style.cursor = "grab";
|
||||
|
||||
const movedBy = e.clientX - mouseStartPos
|
||||
const movedBy = e.clientX - mouseStartPos;
|
||||
if (movedBy < -50 && currentIndex < totalSlides - 1) {
|
||||
currentIndex += 1
|
||||
currentIndex += 1;
|
||||
} else if (movedBy > 50 && currentIndex > 0) {
|
||||
currentIndex -= 1
|
||||
currentIndex -= 1;
|
||||
}
|
||||
goToSlide(currentIndex)
|
||||
})
|
||||
goToSlide(currentIndex);
|
||||
});
|
||||
|
||||
container.addEventListener('mouseleave', () => {
|
||||
isMouseDown = false
|
||||
container.style.cursor = 'grab'
|
||||
})
|
||||
container.addEventListener("mouseleave", () => {
|
||||
isMouseDown = false;
|
||||
container.style.cursor = "grab";
|
||||
});
|
||||
|
||||
// Touch events
|
||||
container.addEventListener('touchstart', touchStart(currentIndex))
|
||||
container.addEventListener('touchend', touchEnd)
|
||||
container.addEventListener('touchmove', touchMove)
|
||||
container.addEventListener("touchstart", touchStart(currentIndex));
|
||||
container.addEventListener("touchend", touchEnd);
|
||||
container.addEventListener("touchmove", touchMove);
|
||||
|
||||
// Keyboard navigation
|
||||
slider.addEventListener('keydown', (e) => {
|
||||
slider.addEventListener("keydown", (e) => {
|
||||
if (e instanceof KeyboardEvent) {
|
||||
if (e.key === 'ArrowLeft') goToSlide(currentIndex - 1)
|
||||
if (e.key === 'ArrowRight') goToSlide(currentIndex + 1)
|
||||
if (e.key === "ArrowLeft") goToSlide(currentIndex - 1);
|
||||
if (e.key === "ArrowRight") goToSlide(currentIndex + 1);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Set initial state
|
||||
container.style.cursor = 'grab'
|
||||
})
|
||||
container.style.cursor = "grab";
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initEnvironmentSlider)
|
||||
if (document.readyState !== 'loading') {
|
||||
initEnvironmentSlider()
|
||||
document.addEventListener("DOMContentLoaded", initEnvironmentSlider);
|
||||
if (document.readyState !== "loading") {
|
||||
initEnvironmentSlider();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,38 +36,35 @@ const features = [
|
||||
description: '除了幫您拓展網路上的知名度,我們更是每家公司最專業的數位夥伴,你會知道有恩群的存在,事業路上你並不孤單。',
|
||||
},
|
||||
]
|
||||
import SectionHeader from '../components/SectionHeader.astro'
|
||||
---
|
||||
|
||||
<section class="section_feature bg-white px-4 py-[60px] md:px-5 md:py-[80px]" 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 mx-auto max-w-4xl">
|
||||
<!-- Section Header -->
|
||||
<div class="section_header_w_line mb-[60px] text-center">
|
||||
<h2 id="feature-heading" class="header_subtitle_head my-4 text-[1.75rem] font-bold leading-[1.2] text-[var(--color-enchunblue)] md:text-[2.25rem]">
|
||||
{title}
|
||||
</h2>
|
||||
<p class="header_subtitle_paragraph mt-2 text-base font-normal text-[#666666]">
|
||||
{subtitle}
|
||||
</p>
|
||||
<div class="divider_line mx-auto h-[2px] w-[100px] bg-[var(--color-enchunblue)]"></div>
|
||||
</div>
|
||||
<SectionHeader
|
||||
title="恩群服務特色"
|
||||
subtitle="Why you can trust us"
|
||||
sectionBg="bg-white"
|
||||
/>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<div class="grid grid-cols-1 gap-1 md:grid-cols-2 md:gap-2 lg:gap-6">
|
||||
<div class="grid grid-cols-2 gap-1 md:grid-cols-2 md:gap-2 lg:gap-6">
|
||||
{
|
||||
features.map((feature) => (
|
||||
<div class="feature_card bg-white p-5 transition-all duration-[var(--transition-base)] hover:-translate-y-1 md:p-6 lg:p-8">
|
||||
<div class="grid grid-cols-[120px_1fr] items-start gap-4 md:grid-cols-[110px_1fr] lg:grid-cols-[190px_1fr] lg:gap-6">
|
||||
<div class="flex flex-col items-center gap-4 text-center md:flex-row md:items-start md:text-left lg:gap-6">
|
||||
<!-- Icon -->
|
||||
<div class="flex w-full items-center justify-center">
|
||||
<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'] lg:text-3xl text-2xl font-semibold text-(--color-dark-blue)">{feature.title}</h3>
|
||||
<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'] lg:text-base text-sm font-thin leading-[1.6] text-grey-2">{feature.description}</p>
|
||||
<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>
|
||||
|
||||
@@ -34,7 +34,7 @@ const painpoints = [
|
||||
{
|
||||
id: 'burning',
|
||||
title: '廣告行銷像燒錢',
|
||||
icon: '💸',
|
||||
img: '💸',
|
||||
description: '廣告預算投入很多,但看不到實際效果?感覺像在燒錢?',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -158,7 +158,7 @@ const renderIcon = (service: ServicesListItem): string | null => {
|
||||
---
|
||||
|
||||
<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="max-w-3xl mx-auto">
|
||||
<div class="max-w-3xl mx-auto flex flex-col gap-6">
|
||||
{
|
||||
servicesList.map((service, index) => {
|
||||
const isOdd = index % 2 === 0
|
||||
@@ -177,14 +177,14 @@ const renderIcon = (service: ServicesListItem): string | null => {
|
||||
<!-- Content Side -->
|
||||
<div class={`service-item-content order-2 ${isOdd ? 'md:order-1' : 'md:order-2'}`}>
|
||||
<!-- Category Tag with Icon -->
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<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-center w-8 h-8 text-(--color-enchunblue)" set:html={iconHtml} />
|
||||
<span class="inline-flex items-center justify-start w-8 h-8 ml-2 scale-150 text-(--color-enchunblue)" set:html={iconHtml} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ const bgImageUrl = backgroundImage?.url || "";
|
||||
// Background color fallback
|
||||
!hasBackgroundImage && "bg-(--color-dark-blue)",
|
||||
// Background image styles
|
||||
hasBackgroundImage && "bg-size-[120vw] bg-center bg-no-repeat",
|
||||
hasBackgroundImage && "bg-cover bg-center bg-no-repeat",
|
||||
// Pull up to counteract layout's pt-20 padding (80px)
|
||||
"-mt-20",
|
||||
// Full viewport height
|
||||
|
||||
@@ -136,6 +136,8 @@
|
||||
/* 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 - 使用灰色系 */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user