feat(frontend): update pages, components and branding

Refresh Astro frontend implementation including new pages (Portfolio, Teams, Services), components, and styling updates.
This commit is contained in:
2026-02-11 11:50:42 +08:00
parent be7fc902fb
commit 9c2181f743
49 changed files with 9699 additions and 899 deletions

View File

@@ -1,85 +1,625 @@
---
import Layout from '../layouts/Layout.astro';
/**
* 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>
<section class="contact-section">
<div class="container">
<h1>聯絡我們</h1>
<form id="contact-form">
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" required />
<Layout title={title} description={description}>
<section class="contact-section" id="contact">
<div class="contactus_wrapper">
<!-- Contact Form Side -->
<div class="contact_form_wrapper">
<h1 class="contact_head">聯絡我們</h1>
<p class="contact_parafraph">
有任何問題嗎?歡迎聯絡我們,我們將竭誠為您服務。
</p>
<p class="contact_reminder">
* 標註欄位為必填
</p>
<!-- Contact Form -->
<form id="contact-form" class="contact_form" novalidate>
<!-- Success Message -->
<div id="form-success" class="w-form-done" style="display: none;">
感謝您的留言!我們會盡快回覆您。
</div>
<!-- Error Message -->
<div id="form-error" class="w-form-fail" style="display: none;">
送出失敗,請稍後再試或直接來電。
</div>
<!-- Form Fields -->
<div class="contact-form-grid">
<!-- Name Field -->
<div class="contact_field_wrapper">
<label for="Name" class="contact_field_name">
姓名 <span>*</span>
</label>
<input
type="text"
id="Name"
name="Name"
class="input_field"
required
minlength="2"
maxlength="256"
placeholder="請輸入您的姓名"
/>
<span class="error-message" id="Name-error"></span>
</div>
<!-- Phone Field -->
<div class="contact_field_wrapper">
<label for="Phone" class="contact_field_name">
聯絡電話 <span>*</span>
</label>
<input
type="tel"
id="Phone"
name="Phone"
class="input_field"
required
placeholder="請輸入您的電話號碼"
/>
<span class="error-message" id="Phone-error"></span>
</div>
</div>
<!-- Email Field -->
<div class="contact_field_wrapper">
<label for="Email" class="contact_field_name">
Email <span>*</span>
</label>
<input
type="email"
id="Email"
name="Email"
class="input_field"
required
placeholder="請輸入您的 Email"
/>
<span class="error-message" id="Email-error"></span>
</div>
<!-- Message Field -->
<div class="contact_field_wrapper">
<label for="Message" class="contact_field_name">
聯絡訊息 <span>*</span>
</label>
<textarea
id="Message"
name="Message"
class="input_field"
minlength="10"
maxlength="5000"
required
placeholder="請輸入您的訊息(至少 10 個字元)"
></textarea>
<span class="error-message" id="Message-error"></span>
</div>
<!-- Submit Button -->
<button type="submit" class="submit-button" id="submit-btn">
<span class="button-text">送出訊息</span>
<span class="button-loading" style="display: none;">送出中...</span>
</button>
</form>
</div>
<!-- Contact Image Side -->
<div class="contact-image">
<div class="image-wrapper">
<img
src="/placeholder-contact.jpg"
alt="聯絡恩群數位"
width="600"
height="400"
/>
</div>
<div class="form-group">
<label for="email">電子郵件</label>
<input type="email" id="email" name="email" required />
<!-- Contact Info Card -->
<div class="contact-info-card">
<h3>聯絡資訊</h3>
<div class="info-item">
<svg class="info-icon" viewBox="0 0 24 24"><path fill="currentColor" d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47-1.14.75-1.02zM2.05 21.05c.15.35.48.59.84.59l2.2.73c.36.12.74.12 1.06-.05.13-.07.22-.17.28-.28l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53.36-.11.74.47 1.14.75 1.02L2.05 21.05zM19.95 2.95c-.15-.35-.48-.59-.84-.59l-2.2-.73c-.36-.12-.74-.12-1.06.05-.13.07-.22.17-.28.28l-2.2 2.2c-.27.27-.36.67-.24 1.02.37 1.12.57 2.33.57 3.57 0 .55-.17 1.45-.5 2.53-.36.11-.74-.47-1.14-.75l1.02-1.02 2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1.45-.17 2.53-.5.36-.11.74-.47 1.02-.75l2.2-2.2c.27-.27.36-.67.24-1.02-.37-1.12-.57-2.33-.57-3.57 0-.55.17-1.45.5-2.53z"/></svg>
<span>諮詢電話: 02 5570 0527</span>
</div>
<div class="info-item">
<svg class="info-icon" viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
<span>Email: <a href="mailto:enchuntaiwan@gmail.com">enchuntaiwan@gmail.com</a></span>
</div>
</div>
<div class="form-group">
<label for="message">訊息</label>
<textarea id="message" name="message" required></textarea>
</div>
<button type="submit">送出</button>
</form>
<div class="contact-info">
<p>諮詢電話: 02 5570 0527</p>
<p>電子郵件: <a href="mailto:enchuntaiwan@gmail.com">enchuntaiwan@gmail.com</a></p>
</div>
</div>
</section>
</Layout>
<script>
// Basic form handler - would integrate with Cloudflare Worker
document.getElementById('contact-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
// Submit to Cloudflare Worker
alert('Form submitted (placeholder)');
});
</script>
<style>
/* Contact Section Styles - Pixel-perfect from Webflow */
.contact-section {
padding: 40px 0;
padding: 4rem 0;
background: var(--color-background);
scroll-margin-top: 80px;
}
.container {
max-width: 800px;
.contactus_wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input, textarea {
/* Form Side */
.contact_form_wrapper {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
textarea {
height: 150px;
/* Headings */
.contact_head {
font-size: 2.5rem;
font-weight: 700;
line-height: 1.2;
color: var(--color-text-primary);
margin-bottom: 1rem;
}
button {
background: #007bff;
.contact_parafraph {
font-size: 1.125rem;
font-weight: 400;
line-height: 1.6;
color: var(--color-gray-600);
margin-bottom: 0.5rem;
}
.contact_reminder {
font-size: 0.875rem;
font-style: italic;
color: var(--color-text-muted);
margin-bottom: 2rem;
}
/* Form Container */
.contact_form {
padding: 2rem;
background: var(--color-surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
/* Form Grid */
.contact-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
/* Field Wrapper */
.contact_field_wrapper {
display: flex;
flex-direction: column;
margin-bottom: 1.5rem;
}
.contact_field_wrapper:last-child {
margin-bottom: 2rem;
}
/* Field Label */
.contact_field_name {
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 0.5rem;
display: block;
}
.contact_field_name span {
color: var(--color-primary);
}
/* Input Fields */
.input_field {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-size: 1rem;
line-height: 1.5;
background: var(--color-background);
transition: all var(--transition-fast);
font-family: inherit;
color: var(--color-text-primary);
}
.input_field:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(56, 152, 236, 0.1);
transform: translateY(-1px);
}
.input_field::placeholder {
color: var(--color-text-muted);
}
.input_field.error {
border-color: #dc3545 !important;
background-color: #fff5f5;
}
#Message {
min-height: 120px;
resize: vertical;
}
/* Error Message */
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
display: none;
}
.error-message:not(:empty) {
display: block;
}
/* Submit Button */
.submit-button {
background: var(--color-primary);
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
border-radius: var(--radius);
padding: 0.875rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.contact-info {
margin-top: 40px;
transition: all var(--transition-fast);
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 200px;
width: 100%;
}
</style>
.submit-button:hover:not(:disabled) {
background: var(--color-primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.submit-button:active:not(:disabled) {
transform: translateY(0);
}
.submit-button:disabled {
background: var(--color-gray-500);
cursor: not-allowed;
opacity: 0.8;
}
/* Form Success/Error Messages */
.w-form-done,
.w-form-fail {
padding: 1rem 1.5rem;
border-radius: var(--radius);
margin-top: 1rem;
text-align: center;
font-weight: 500;
animation: slideUp 0.3s ease-out;
}
.w-form-done {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.w-form-fail {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Image Side */
.contact-image {
display: flex;
flex-direction: column;
gap: 2rem;
justify-content: center;
align-items: center;
}
.image-wrapper {
width: 100%;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-md);
background: var(--color-gray-100);
aspect-ratio: 3/2;
}
.image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Contact Info Card */
.contact-info-card {
width: 100%;
padding: 1.5rem;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.contact-info-card h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 1rem;
}
.info-item {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
font-size: 0.95rem;
color: var(--color-text-secondary);
}
.info-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
color: var(--color-primary);
}
.info-item a {
color: var(--color-primary);
text-decoration: none;
}
.info-item a:hover {
text-decoration: underline;
}
/* Responsive Adjustments */
@media (max-width: 991px) {
.contactus_wrapper {
grid-template-columns: 1fr;
gap: 2rem;
}
.contact_head {
font-size: 2rem;
}
}
@media (max-width: 767px) {
.contact-section {
padding: 2rem 0;
}
.contact_form {
padding: 1.5rem;
}
.contact-form-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.submit-button {
min-width: 160px;
}
}
@media (max-width: 479px) {
.contact_form {
padding: 1rem;
}
.contact_head {
font-size: 1.5rem;
}
}
</style>
<script>
// 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>