Files
website-enchun-mgr/apps/frontend/src/pages/contact-us.astro
pkupuk 173905ecd3 Extract generic UI components
Reduces duplication across marketing pages by converting sections into
reusable components like CtaSection and HeaderBg. Consolidates styling
patterns to improve maintainability and consistency of the user interface.
2026-02-28 04:55:25 +08:00

318 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
/**
* Contact Page - 聯絡我們頁面
* Pixel-perfect implementation based on Webflow design
* Includes form validation, submission handling, and responsive layout
*/
import Layout from '../layouts/Layout.astro'
// Metadata for SEO
const title = '聯絡我們 | 恩群數位行銷'
const description = '有任何問題嗎?歡迎聯絡恩群數位行銷,我們的專業團隊將竭誠為您服務。電話: 02 5570 0527Email: enchuntaiwan@gmail.com'
---
<Layout title={title} description={description}>
<section class="py-16 bg-background scroll-mt-20 lg:py-8 md:py-8" id="contact">
<div class="grid grid-cols-2 gap-12 items-center max-w-[1200px] mx-auto px-5 lg:grid-cols-1 lg:gap-8">
<!-- Contact Form Side -->
<div class="w-full">
<h1 class="text-[2.5rem] font-bold leading-tight text-text-primary mb-4 lg:text-2xl md:text-[1.5rem]">聯絡我們</h1>
<p class="text-lg font-normal leading-relaxed text-slate-600 mb-2">
有任何問題嗎?歡迎聯絡我們,我們將竭誠為您服務。
</p>
<p class="text-sm italic text-text-muted mb-8">
* 標註欄位為必填
</p>
<!-- Contact Form -->
<form id="contact-form" class="p-8 bg-surface rounded-lg shadow-lg lg:p-6 md:p-4" novalidate>
<!-- Success Message -->
<div id="form-success" class="p-4 px-6 rounded-md mt-4 text-center font-medium bg-[#d4edda] text-[#155724] border border-[#c3e6cb] animate-[slideUp_0.3s_ease-out]" style="display: none;">
感謝您的留言!我們會盡快回覆您。
</div>
<!-- Error Message -->
<div id="form-error" class="p-4 px-6 rounded-md mt-4 text-center font-medium bg-[#f8d7da] text-[#721c24] border border-[#f5c6cb] animate-[slideUp_0.3s_ease-out]" style="display: none;">
送出失敗,請稍後再試或直接來電。
</div>
<!-- Form Fields -->
<div class="grid grid-cols-2 gap-6 mb-6 md:grid-cols-1 md:gap-4">
<!-- Name Field -->
<div class="flex flex-col mb-6">
<label for="Name" class="text-sm font-semibold text-text-primary mb-2 block">
姓名 <span class="text-primary">*</span>
</label>
<input
type="text"
id="Name"
name="Name"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]"
required
minlength="2"
maxlength="256"
placeholder="請輸入您的姓名"
/>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Name-error"></span>
</div>
<!-- Phone Field -->
<div class="flex flex-col mb-6">
<label for="Phone" class="text-sm font-semibold text-text-primary mb-2 block">
聯絡電話 <span class="text-primary">*</span>
</label>
<input
type="tel"
id="Phone"
name="Phone"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]"
required
placeholder="請輸入您的電話號碼"
/>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Phone-error"></span>
</div>
</div>
<!-- Email Field -->
<div class="flex flex-col mb-6">
<label for="Email" class="text-sm font-semibold text-text-primary mb-2 block">
Email <span class="text-primary">*</span>
</label>
<input
type="email"
id="Email"
name="Email"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]"
required
placeholder="請輸入您的 Email"
/>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Email-error"></span>
</div>
<!-- Message Field -->
<div class="flex flex-col mb-8">
<label for="Message" class="text-sm font-semibold text-text-primary mb-2 block">
聯絡訊息 <span class="text-primary">*</span>
</label>
<textarea
id="Message"
name="Message"
class="w-full px-4 py-3 border border-border rounded-md text-base leading-normal bg-background transition-all duration-150 font-sans text-text-primary focus:outline-none focus:border-primary focus:shadow-[0_0_0_3px_rgba(56,152,236,0.1)] focus:-translate-y-0.5 placeholder:text-text-muted min-h-[120px] resize-y [&.error]:border-[#dc3545] [&.error]:bg-[#fff5f5]"
minlength="10"
maxlength="5000"
required
placeholder="請輸入您的訊息(至少 10 個字元)"
></textarea>
<span class="error-message text-[#dc3545] text-sm mt-1 hidden" id="Message-error"></span>
</div>
<!-- Submit Button -->
<button type="submit" class="bg-primary text-white border-none rounded-md px-8 py-[0.875rem] text-base font-semibold cursor-pointer transition-all duration-150 text-center inline-flex items-center justify-center min-w-[200px] w-full hover:bg-primary-hover hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 disabled:bg-slate-500 disabled:cursor-not-allowed disabled:opacity-80" id="submit-btn">
<span class="button-text">送出訊息</span>
<span class="button-loading" style="display: none;">送出中...</span>
</button>
</form>
</div>
<!-- Contact Image Side -->
<div class="flex flex-col gap-8 justify-center items-center">
<div class="w-full rounded-lg overflow-hidden shadow-md bg-slate-100 aspect-[3/2]">
<img
src="/placeholder-contact.jpg"
alt="聯絡恩群數位"
width="600"
height="400"
class="w-full h-full object-cover"
/>
</div>
<!-- Contact Info Card -->
<div class="w-full p-6 bg-white rounded-lg shadow-lg">
<h3 class="text-xl font-semibold text-text-primary mb-4">聯絡資訊</h3>
<div class="flex items-center gap-3 mb-4 text-[0.95rem] text-text-secondary">
<svg class="w-5 h-5 flex-shrink-0 text-primary" viewBox="0 0 24 24"><path fill="currentColor" d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47-1.14.75-1.02L2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02L2.05 21.05zM19.95 2.95c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36.11.74-.47-1.14-.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.02.75 1.02L2.05 21.05c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36-.11-.74.47-1.14.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11-.74.47 1.14.75 1.02L2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02z"/></svg>
<span>諮詢電話: 02 5570 0527</span>
</div>
<div class="flex items-center gap-3 mb-4 text-[0.95rem] text-text-secondary">
<svg class="w-5 h-5 flex-shrink-0 text-primary" viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
<span>Email: <a href="mailto:enchuntaiwan@gmail.com" class="text-primary no-underline hover:underline">enchuntaiwan@gmail.com</a></span>
</div>
</div>
</div>
</div>
</section>
</Layout>
<script>
// Form validation and submission handler
function initContactForm() {
const form = document.getElementById('contact-form') as HTMLFormElement
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement
const successMsg = document.getElementById('form-success') as HTMLElement
const errorMsg = document.getElementById('form-error') as HTMLElement
if (!form) return
// Validation patterns
const patterns = {
Name: /^[\u4e00-\u9fa5a-zA-Z\s]{2,256}$/,
Phone: /^[0-9\-\s\+]{6,20}$/,
Email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
Message: /^.{10,5000}$/
}
// Validation function
function validateField(input: HTMLInputElement | HTMLTextAreaElement): boolean {
const name = input.name
const value = input.value.trim()
const errorSpan = document.getElementById(`${name}-error`) as HTMLElement
if (!input.hasAttribute('required') && !value) {
clearError(input, errorSpan)
return true
}
let isValid = true
let errorMessage = ''
// Required check
if (input.hasAttribute('required') && !value) {
isValid = false
errorMessage = '此欄位為必填'
}
// Pattern validation
else if (patterns[name as keyof typeof patterns] && !patterns[name as keyof typeof patterns].test(value)) {
isValid = false
switch (name) {
case 'Name':
errorMessage = '請輸入有效的姓名(至少 2 個字元)'
break
case 'Phone':
errorMessage = '請輸入有效的電話號碼'
break
case 'Email':
errorMessage = '請輸入有效的 Email 格式'
break
case 'Message':
errorMessage = '訊息至少需要 10 個字元'
break
}
}
// Show/hide error
if (!isValid) {
input.classList.add('error')
if (errorSpan) {
errorSpan.textContent = errorMessage
errorSpan.style.display = 'block'
}
} else {
clearError(input, errorSpan)
}
return isValid
}
function clearError(input: HTMLInputElement | HTMLTextAreaElement, errorSpan: HTMLElement) {
input.classList.remove('error')
if (errorSpan) {
errorSpan.textContent = ''
errorSpan.style.display = 'none'
}
}
// Real-time validation on blur
form.querySelectorAll('input, textarea').forEach((field) => {
field.addEventListener('blur', () => {
validateField(field as HTMLInputElement | HTMLTextAreaElement)
})
field.addEventListener('input', () => {
const input = field as HTMLInputElement | HTMLTextAreaElement
if (input.classList.contains('error')) {
validateField(input)
}
})
})
// Form submission
form.addEventListener('submit', async (e) => {
e.preventDefault()
// Validate all fields
const inputs = form.querySelectorAll('input, textarea') as NodeListOf<HTMLInputElement | HTMLTextAreaElement>
let isFormValid = true
inputs.forEach((input) => {
if (!validateField(input)) {
isFormValid = false
}
})
if (!isFormValid) {
// Scroll to first error
const firstError = form.querySelector('.input_field.error') as HTMLElement
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
return
}
// Show loading state
submitBtn.disabled = true
const buttonText = submitBtn.querySelector('.button-text') as HTMLElement
const buttonLoading = submitBtn.querySelector('.button-loading') as HTMLElement
if (buttonText) buttonText.style.display = 'none'
if (buttonLoading) buttonLoading.style.display = 'inline'
// Hide previous messages
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')
}
try {
// Submit to backend (via API proxy)
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
if (response.ok) {
// Success
successMsg.style.display = 'block'
form.reset()
window.scrollTo({ top: 0, behavior: 'smooth' })
} else {
throw new Error('Submission failed')
}
} catch (error) {
console.error('Form submission error:', error)
errorMsg.style.display = 'block'
} finally {
// Reset button state
submitBtn.disabled = false
if (buttonText) buttonText.style.display = 'inline'
if (buttonLoading) buttonLoading.style.display = 'none'
}
})
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initContactForm)
if (document.readyState !== 'loading') {
initContactForm()
}
</script>