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.
318 lines
14 KiB
Plaintext
318 lines
14 KiB
Plaintext
---
|
||
/**
|
||
* 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 0527,Email: 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>
|