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:
@@ -1,31 +1,38 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
/**
|
||||
* About Page - 關於恩群數位
|
||||
* 展示公司特色、服務優勢和與其他公司的差異
|
||||
*/
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
import AboutHero from '../sections/AboutHero.astro'
|
||||
import FeatureSection from '../sections/FeatureSection.astro'
|
||||
import ComparisonSection from '../sections/ComparisonSection.astro'
|
||||
import CTASection from '../sections/CTASection.astro'
|
||||
|
||||
// Metadata for SEO
|
||||
const title = '關於恩群數位 | 專業數位行銷服務團隊'
|
||||
const description = '恩群數位行銷成立於2018年,提供全方位數位行銷服務。我們在地化優先、數據驅動,是您最可信赖的數位行銷夥伴。'
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<section class="about-section">
|
||||
<div class="container">
|
||||
<h1>關於恩群</h1>
|
||||
<div class="prose prose-custom max-w-none">
|
||||
<p>恩群數位行銷有限公司成立於2018年,專注於數位行銷服務。</p>
|
||||
<p>我們擁有豐富的廣告行銷操作經驗,提供全方位行銷解決方案。</p>
|
||||
<!-- Add more content from HTML -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Layout title={title} description={description}>
|
||||
<!-- Hero Section -->
|
||||
<AboutHero />
|
||||
|
||||
<!-- Service Features Section -->
|
||||
<FeatureSection />
|
||||
|
||||
<!-- Comparison Section -->
|
||||
<ComparisonSection />
|
||||
|
||||
<!-- CTA Section -->
|
||||
<CTASection
|
||||
homeData={{
|
||||
ctaSection: {
|
||||
headline: '準備好開始新的旅程了嗎',
|
||||
description: '讓我們一起為您的品牌打造獨特的數位行銷策略',
|
||||
buttonText: '聯絡我們',
|
||||
buttonLink: '/contact-us',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.about-section {
|
||||
padding: 40px 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
@@ -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 0527,Email: 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>
|
||||
|
||||
@@ -1,66 +1,56 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import VideoHero from "../components/videoHero.astro";
|
||||
/**
|
||||
* Homepage - Main landing page
|
||||
* Fetches data from Payload CMS and renders all sections
|
||||
* Pixel-perfect implementation based on Webflow design
|
||||
*/
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
import HeroSection from '../sections/HeroSection.astro'
|
||||
import PainpointSection from '../sections/PainpointSection.astro'
|
||||
import StatisticsSection from '../sections/StatisticsSection.astro'
|
||||
// ServiceFeatures - REMOVED: Services should only appear in Footer, not as a main section
|
||||
// import ServiceFeatures from '../sections/ServiceFeatures.astro'
|
||||
import ClientCasesSection from '../sections/ClientCasesSection.astro'
|
||||
import PortfolioPreview from '../sections/PortfolioPreview.astro'
|
||||
import CTASection from '../sections/CTASection.astro'
|
||||
import { getHomepageData } from '../lib/api/home'
|
||||
import type { HomeData, PortfolioItem } from '../lib/api/home'
|
||||
|
||||
// Fetch data from Payload CMS
|
||||
let homeData: HomeData | null = null
|
||||
let portfolioItems: PortfolioItem[] = []
|
||||
|
||||
try {
|
||||
const data = await getHomepageData()
|
||||
homeData = data.home
|
||||
portfolioItems = data.portfolioItems || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch homepage data:', error)
|
||||
}
|
||||
|
||||
// Metadata for SEO
|
||||
const title = '恩群數位行銷 | 專業數位行銷服務'
|
||||
const description = '恩群數位行銷提供專業的 Google Ads、社群代操、論壇行銷、網站設計等全方位數位行銷服務,協助您的品牌在數位時代脫穎而出。'
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<!-- Hero Section -->
|
||||
<VideoHero
|
||||
desktopVideo="/video/enchun-hero-background-video.mp4"
|
||||
mobileVideo="/video/enchun-hero-background-video.webm"
|
||||
logo="/enchun-logo-full.svg"
|
||||
header="創造企業更多發展的可能性\n是我們的使命"
|
||||
subheader="Its our destiny to create possibilities for your business."
|
||||
/>
|
||||
<Layout title={title} description={description}>
|
||||
<!-- Hero Section -->
|
||||
<HeroSection homeData={homeData} />
|
||||
|
||||
<!-- Services Section -->
|
||||
<section class="py-16 bg-surface">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<h2 class="text-3xl font-bold text-center text-text mb-12">
|
||||
我們的服務
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold text-primary mb-4">
|
||||
Google Ads
|
||||
</h3>
|
||||
<p class="text-text">
|
||||
專業的Google廣告投放服務,幫助您的品牌觸及目標客戶。
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold text-primary mb-4">
|
||||
社群行銷
|
||||
</h3>
|
||||
<p class="text-text">
|
||||
全方位社群媒體經營,從內容策劃到數據分析,一站式服務。
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-xl font-semibold text-primary mb-4">
|
||||
網站設計
|
||||
</h3>
|
||||
<p class="text-text">
|
||||
現代化響應式網站設計,提升品牌形象和用戶體驗。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Painpoint Section (您的困擾) -->
|
||||
<PainpointSection homeData={homeData} />
|
||||
|
||||
<!-- About Section -->
|
||||
<section class="py-16">
|
||||
<div class="max-w-6xl mx-auto px-4 text-center">
|
||||
<h2 class="text-3xl font-bold text-text mb-8">關於恩群</h2>
|
||||
<p class="text-lg text-text max-w-3xl mx-auto">
|
||||
恩群數位行銷團隊擁有豐富的數位行銷經驗,我們相信在地化優先、高投資轉換率、數據優先、關係優於銷售。
|
||||
每一個客戶都是我們重視的夥伴,我們珍惜與客戶的合作關係。
|
||||
</p>
|
||||
<a
|
||||
href="/about-enchun"
|
||||
class="inline-block mt-6 bg-primary text-white px-6 py-3 rounded-lg font-semibold hover:bg-primary/90 transition-colors"
|
||||
>了解更多</a
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Statistics Section (數據統計) -->
|
||||
<StatisticsSection homeData={homeData} />
|
||||
|
||||
<!-- Service Features - REMOVED: Services should only appear in Footer, not as a main section -->
|
||||
|
||||
<!-- Client Cases Section (客戶案例) -->
|
||||
<ClientCasesSection homeData={homeData} />
|
||||
|
||||
<!-- Portfolio Preview Section -->
|
||||
<PortfolioPreview homeData={homeData} portfolioItems={portfolioItems} />
|
||||
|
||||
<!-- CTA Section -->
|
||||
<CTASection homeData={homeData} />
|
||||
</Layout>
|
||||
|
||||
@@ -1,43 +1,24 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
/**
|
||||
* Marketing Solutions Page - 行銷方案頁面
|
||||
* Pixel-perfect implementation based on Webflow design
|
||||
*/
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
import SolutionsHero from '../sections/SolutionsHero.astro'
|
||||
import ServicesList from '../sections/ServicesList.astro'
|
||||
|
||||
// Metadata for SEO
|
||||
const title = '行銷解決方案 | 恩群數位行銷'
|
||||
const description = '恩群數位行銷提供全方位的數位行銷服務,包括 Google Ads、社群代操、論壇行銷、網紅行銷、網站設計等,協助您的品牌在數位時代脫穎而出。'
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<section class="solutions-section">
|
||||
<div class="container">
|
||||
<h1>行銷方案</h1>
|
||||
<p>我們提供多樣化的行銷方案,幫助您的品牌深入人心。</p>
|
||||
<ul>
|
||||
<li>Google 商家關鍵字</li>
|
||||
<li>Google Ads</li>
|
||||
<li>社群代操</li>
|
||||
<li>論壇行銷</li>
|
||||
<li>網紅行銷</li>
|
||||
<li>形象影片</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<Layout title={title} description={description}>
|
||||
<!-- Hero Section -->
|
||||
<SolutionsHero
|
||||
title="行銷解決方案"
|
||||
subtitle="提供全方位的數位行銷服務,協助您的品牌在數位時代脫穎而出"
|
||||
/>
|
||||
|
||||
<!-- Services List -->
|
||||
<ServicesList />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.solutions-section {
|
||||
padding: 40px 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +1,289 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
/**
|
||||
* News/Blog Listing Page - 行銷放大鏡
|
||||
* URL: /news
|
||||
* Displays all published blog posts from Payload CMS
|
||||
*/
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
import ArticleCard from '../components/blog/ArticleCard.astro'
|
||||
import CategoryFilter from '../components/blog/CategoryFilter.astro'
|
||||
import { fetchPosts, fetchCategories } from '../lib/api/blog'
|
||||
|
||||
// Placeholder for blog posts - would fetch from CMS
|
||||
const posts = [
|
||||
{ slug: 'en-qun-shu-wei-zui-xin-gong-gao', title: '恩群數位最新公告', date: '2023-01-01' },
|
||||
{ slug: 'google-xiao-xue-tang', title: 'Google小學堂', date: '2023-01-02' },
|
||||
// Add more
|
||||
];
|
||||
// Metadata for SEO
|
||||
const title = '行銷放大鏡 | 恩群數位行銷'
|
||||
const description = '閱讀恩群數位的專業行銷文章,掌握最新的數位行銷趨勢、社群經營技巧、Google 廣告策略等實用內容。'
|
||||
|
||||
// Pagination settings
|
||||
const PAGE_SIZE = 12
|
||||
|
||||
// Get query parameters
|
||||
const url = Astro.url
|
||||
const pageParam = url.searchParams.get('page')
|
||||
const page = Math.max(1, parseInt(pageParam || '1'))
|
||||
|
||||
// Fetch posts from Payload CMS
|
||||
const postsData = await fetchPosts(page, PAGE_SIZE)
|
||||
const posts = postsData.docs
|
||||
const totalPages = postsData.totalPages
|
||||
const hasPreviousPage = postsData.hasPreviousPage
|
||||
const hasNextPage = postsData.hasNextPage
|
||||
|
||||
// Fetch categories
|
||||
const categories = await fetchCategories()
|
||||
|
||||
// Filter out categories without slugs and the legacy "文章分類" container
|
||||
const validCategories = categories.filter(c =>
|
||||
c.slug && c.slug !== 'wen-zhang-fen-lei'
|
||||
)
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<section class="news-section">
|
||||
<Layout title={title} description={description}>
|
||||
<!-- Blog Hero Section -->
|
||||
<section class="blog-hero" aria-labelledby="blog-heading">
|
||||
<div class="container">
|
||||
<h1>行銷放大鏡</h1>
|
||||
<div class="posts-grid">
|
||||
{posts.map(post => (
|
||||
<article class="post-card">
|
||||
<h2><a href={`/wen-zhang-fen-lei/${post.slug}`}>{post.title}</a></h2>
|
||||
<p>{post.date}</p>
|
||||
</article>
|
||||
))}
|
||||
<div class="section_header_w_line">
|
||||
<div class="divider_line"></div>
|
||||
<div class="header_subtitle">
|
||||
<h2 id="blog-heading" class="header_subtitle_head">行銷放大鏡</h2>
|
||||
<p class="header_subtitle_paragraph">Marketing Blog</p>
|
||||
</div>
|
||||
<div class="divider_line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<section class="filter-section">
|
||||
<div class="container">
|
||||
<CategoryFilter categories={validCategories} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Blog Posts Grid -->
|
||||
<section class="blog-section" aria-label="文章列表">
|
||||
<div class="container">
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-10 w-full max-w-[1200px] mx-auto">
|
||||
{posts.map((post) => (
|
||||
<ArticleCard post={post} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty-state">
|
||||
<p class="empty-text">暫無文章</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Pagination -->
|
||||
{
|
||||
totalPages > 1 && (
|
||||
<nav class="pagination" aria-label="分頁導航">
|
||||
<div class="pagination-container">
|
||||
{
|
||||
hasPreviousPage && (
|
||||
<a
|
||||
href={`?page=${page - 1}`}
|
||||
class="pagination-link pagination-link-prev"
|
||||
aria-label="上一頁"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
||||
</svg>
|
||||
上一頁
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="pagination-info">
|
||||
<span class="current-page">{page}</span>
|
||||
<span class="page-separator">/</span>
|
||||
<span class="total-pages">{totalPages}</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
hasNextPage && (
|
||||
<a
|
||||
href={`?page=${page + 1}`}
|
||||
class="pagination-link pagination-link-next"
|
||||
aria-label="下一頁"
|
||||
>
|
||||
下一頁
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.news-section {
|
||||
padding: 40px 0;
|
||||
/* Blog Hero Section */
|
||||
.blog-hero {
|
||||
padding: 60px 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
h1 {
|
||||
|
||||
.section_header_w_line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header_subtitle {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.posts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
|
||||
.header_subtitle_head {
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.post-card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
|
||||
.header_subtitle_paragraph {
|
||||
color: var(--color-gray-600, #666);
|
||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.divider_line {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background-color: var(--color-enchunblue, #23608c);
|
||||
}
|
||||
|
||||
/* Filter Section */
|
||||
.filter-section {
|
||||
background-color: #ffffff;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Blog Section */
|
||||
.blog-section {
|
||||
padding: 40px 20px 60px;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-gray-500, #999);
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.pagination-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: #ffffff;
|
||||
border: 2px solid var(--color-gray-300, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.post-card h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.post-card a {
|
||||
color: var(--color-gray-700, #555);
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
.post-card a:hover {
|
||||
color: #007bff;
|
||||
|
||||
.pagination-link:hover {
|
||||
border-color: var(--color-enchunblue, #23608c);
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
background: rgba(35, 96, 140, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
||||
.pagination-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: "Quicksand", sans-serif;
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-600, #666);
|
||||
}
|
||||
|
||||
.current-page {
|
||||
font-weight: 700;
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
}
|
||||
|
||||
.page-separator {
|
||||
color: var(--color-gray-400, #ccc);
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 991px) {
|
||||
.header_subtitle_head {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.blog-hero {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.section_header_w_line {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.divider_line {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.blog-section {
|
||||
padding: 30px 16px 50px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pagination-link {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.header_subtitle_head {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.header_subtitle_paragraph {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,28 +1,181 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
/**
|
||||
* Teams Page - 恩群大本營
|
||||
* 展示工作環境、公司故事和員工福利
|
||||
* Pixel-perfect implementation based on Webflow design
|
||||
*/
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
import TeamsHero from '../sections/TeamsHero.astro'
|
||||
import EnvironmentSlider from '../sections/EnvironmentSlider.astro'
|
||||
import CompanyStory from '../sections/CompanyStory.astro'
|
||||
import BenefitsSection from '../sections/BenefitsSection.astro'
|
||||
|
||||
// Metadata for SEO
|
||||
const title = '恩群大本營 | 恩群數位行銷'
|
||||
const description = '加入恩群數位的團隊,享受優質的工作環境與完善的員工福利。我們重視個人的特質發揮,歡迎樂於學習、善於建立關係的你加入我們。'
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<section class="teams-section">
|
||||
<div class="container">
|
||||
<h1>恩群大本營</h1>
|
||||
<p>認識我們的團隊成員。</p>
|
||||
<!-- Team members would be listed here -->
|
||||
<Layout title={title} description={description}>
|
||||
<!-- Hero Section -->
|
||||
<TeamsHero />
|
||||
|
||||
<!-- Environment Slider Section -->
|
||||
<EnvironmentSlider />
|
||||
|
||||
<!-- Company Story Section -->
|
||||
<CompanyStory />
|
||||
|
||||
<!-- Benefits Section -->
|
||||
<BenefitsSection />
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="section-call4action" aria-labelledby="cta-heading">
|
||||
<div class="container w-container">
|
||||
<div class="c4a-grid">
|
||||
<div class="c4a-content">
|
||||
<h3 id="cta-heading" class="career-c4a-heading">
|
||||
以人的成長為優先<br />
|
||||
創造人的最大價值
|
||||
</h3>
|
||||
<p class="career-c4a-paragraph">
|
||||
在恩群數位裡我們重視個人的特質能夠完全發揮,只要你樂於學習、善於跟人建立關係,並且重要的是你有一個善良的心,恩群數位歡迎你的加入
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://www.104.com.tw/company/1a2x6bkoaj?jobsource=joblist_r_cust"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="c4a-button"
|
||||
>
|
||||
立刻申請面試
|
||||
</a>
|
||||
<div class="c4a-bg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.teams-section {
|
||||
padding: 40px 0;
|
||||
/* CTA Section Styles - Pixel-perfect from Webflow */
|
||||
.section-call4action {
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
background-color: var(--color-gray-100, #f8f9fa);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.w-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
.c4a-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c4a-content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.career-c4a-heading {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1.75rem;
|
||||
color: var(--color-tarawera, #23608c);
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.career-c4a-paragraph {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-600);
|
||||
line-height: 1.6;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.c4a-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-enchunblue);
|
||||
color: white;
|
||||
padding: 16px 32px;
|
||||
border-radius: var(--radius, 8px);
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-base, 0.3s ease);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.c4a-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
|
||||
background-color: var(--color-enchunblue-hover, #1a4d6e);
|
||||
}
|
||||
|
||||
.c4a-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(35, 96, 140, 0.05) 0%, rgba(35, 96, 140, 0.02) 100%);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 991px) {
|
||||
.section-call4action {
|
||||
padding: 60px 16px;
|
||||
}
|
||||
|
||||
.c4a-grid {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.c4a-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.career-c4a-paragraph {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.c4a-button {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.section-call4action {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.career-c4a-heading {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.career-c4a-paragraph {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.c4a-button {
|
||||
padding: 14px 24px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
345
apps/frontend/src/pages/web-portfolios/[slug].astro
Normal file
345
apps/frontend/src/pages/web-portfolios/[slug].astro
Normal file
@@ -0,0 +1,345 @@
|
||||
---
|
||||
/**
|
||||
* Portfolio Detail Page - 作品詳情頁
|
||||
* Displays full project information with live website link
|
||||
* Pixel-perfect implementation based on Webflow design
|
||||
*/
|
||||
import Layout from '../../layouts/Layout.astro'
|
||||
import { fetchPortfolioBySlug, getWebsiteTypeLabel } from '../../lib/api/portfolio'
|
||||
|
||||
// Get slug from params
|
||||
const { slug } = Astro.params
|
||||
|
||||
// Validate slug
|
||||
if (!slug) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
// Fetch portfolio item
|
||||
const portfolio = await fetchPortfolioBySlug(slug)
|
||||
|
||||
// Handle 404 for non-existent portfolio
|
||||
if (!portfolio) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
// Get website type label
|
||||
const typeLabel = getWebsiteTypeLabel(portfolio.websiteType)
|
||||
|
||||
// Get tags
|
||||
const tags = portfolio.tags?.map(t => t.tag) || []
|
||||
|
||||
// SEO metadata
|
||||
const title = `${portfolio.title} | 恩群數位案例分享`
|
||||
const description = portfolio.description || `瀏覽 ${portfolio.title} 專案詳情`
|
||||
---
|
||||
|
||||
<Layout title={title} description={description}>
|
||||
<article class="portfolio-detail">
|
||||
<div class="container">
|
||||
<!-- Back Link -->
|
||||
<a href="/portfolio" class="back-link">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
|
||||
</svg>
|
||||
返回作品列表
|
||||
</a>
|
||||
|
||||
<!-- Project Header -->
|
||||
<header class="project-header">
|
||||
<div class="project-meta">
|
||||
<span class="project-type-badge">{typeLabel}</span>
|
||||
</div>
|
||||
<h1 class="project-title">{portfolio.title}</h1>
|
||||
|
||||
{
|
||||
portfolio.description && (
|
||||
<p class="project-description">{portfolio.description}</p>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Tags -->
|
||||
{
|
||||
tags.length > 0 && (
|
||||
<div class="project-tags">
|
||||
{tags.map((tag) => (
|
||||
<span class="tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
|
||||
<!-- Hero Image -->
|
||||
{
|
||||
portfolio.image?.url && (
|
||||
<div class="project-hero-image">
|
||||
<img
|
||||
src={portfolio.image.url}
|
||||
alt={portfolio.image.alt || portfolio.title}
|
||||
loading="eager"
|
||||
width="1200"
|
||||
height="675"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Project Content -->
|
||||
<div class="project-content">
|
||||
<div class="content-section">
|
||||
<h2>專案介紹</h2>
|
||||
<p>
|
||||
此專案展示了我們在{typeLabel}領域的專業能力。
|
||||
我們致力於為客戶打造符合品牌定位、使用體驗優良的數位產品。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="content-section">
|
||||
<h2>專案連結</h2>
|
||||
{
|
||||
portfolio.url ? (
|
||||
<a
|
||||
href={portfolio.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-primary"
|
||||
>
|
||||
前往網站
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||||
<path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
|
||||
</svg>
|
||||
</a>
|
||||
) : (
|
||||
<p class="text-muted">此專案暫無公開連結</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<div class="detail-cta">
|
||||
<h3>喜歡這個作品嗎?</h3>
|
||||
<p>讓我們一起為您的品牌打造獨特的數位體驗</p>
|
||||
<a href="/contact-us" class="cta-button">
|
||||
聯絡我們
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* Portfolio Detail */
|
||||
.portfolio-detail {
|
||||
padding: 60px 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Back Link */
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: var(--color-enchunblue-hover, #1a4d6e);
|
||||
}
|
||||
|
||||
/* Project Header */
|
||||
.project-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.project-type-badge {
|
||||
display: inline-block;
|
||||
background: var(--color-enchunblue, #23608c);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-dark-blue, #1a1a1a);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-gray-600, #666);
|
||||
max-width: 700px;
|
||||
margin: 0 auto 1.5rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.project-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--color-gray-100, #f5f5f5);
|
||||
color: var(--color-gray-700, #4a5568);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Hero Image */
|
||||
.project-hero-image {
|
||||
margin-bottom: 3rem;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.project-hero-image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Project Content */
|
||||
.project-content {
|
||||
display: grid;
|
||||
gap: 2.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-dark-blue, #1a1a1a);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content-section p {
|
||||
color: var(--color-gray-600, #666);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--color-enchunblue, #23608c);
|
||||
color: white;
|
||||
padding: 0.875rem 1.75rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-enchunblue-hover, #1a4d6e);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-gray-500, #999);
|
||||
}
|
||||
|
||||
/* Detail CTA */
|
||||
.detail-cta {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
background: var(--color-gray-50, #f9fafb);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.detail-cta h3 {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-cta p {
|
||||
color: var(--color-gray-600, #666);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background: var(--color-enchunblue, #23608c);
|
||||
color: white;
|
||||
padding: 14px 28px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background: var(--color-enchunblue-hover, #1a4d6e);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 991px) {
|
||||
.portfolio-detail {
|
||||
padding: 50px 20px;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.portfolio-detail {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.detail-cta {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.detail-cta h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
244
apps/frontend/src/pages/web-portfolios/index.astro
Normal file
244
apps/frontend/src/pages/web-portfolios/index.astro
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
/**
|
||||
* Portfolio Listing Page - 案例分享列表頁
|
||||
* Displays all portfolio items in a 2-column grid
|
||||
* Pixel-perfect implementation based on Webflow design
|
||||
*/
|
||||
import Layout from '../../layouts/Layout.astro'
|
||||
import PortfolioCard from '../../components/PortfolioCard.astro'
|
||||
import { fetchPortfolios } from '../../lib/api/portfolio'
|
||||
|
||||
// Metadata for SEO
|
||||
const title = '案例分享 | 恩群數位行銷'
|
||||
const description = '瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。'
|
||||
|
||||
// Fetch portfolios from Payload CMS
|
||||
const portfoliosData = await fetchPortfolios(1, 100)
|
||||
const portfolios = portfoliosData.docs
|
||||
---
|
||||
|
||||
<Layout title={title} description={description}>
|
||||
<!-- Portfolio Header -->
|
||||
<section class="portfolio-header" aria-labelledby="portfolio-heading">
|
||||
<div class="container">
|
||||
<div class="section_header_w_line">
|
||||
<div class="divider_line"></div>
|
||||
<div class="header_subtitle">
|
||||
<h2 id="portfolio-heading" class="header_subtitle_head">案例分享</h2>
|
||||
<p class="header_subtitle_paragraph">Our Works</p>
|
||||
</div>
|
||||
<div class="divider_line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Portfolio Grid -->
|
||||
<section class="portfolio-grid-section" aria-label="作品列表">
|
||||
<div class="container">
|
||||
{
|
||||
portfolios.length > 0 ? (
|
||||
<ul class="portfolio-grid">
|
||||
{
|
||||
portfolios.map((item) => (
|
||||
<PortfolioCard
|
||||
item={{
|
||||
slug: item.slug,
|
||||
title: item.title,
|
||||
description: item.description || '',
|
||||
image: item.image?.url,
|
||||
tags: item.tags?.map(t => t.tag) || [],
|
||||
externalUrl: item.url || undefined,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
) : (
|
||||
<div class="empty-state">
|
||||
<p class="empty-text">暫無作品資料</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="portfolio-cta" aria-labelledby="cta-heading">
|
||||
<div class="container">
|
||||
<h2 id="cta-heading" class="cta-title">
|
||||
有興趣與我們合作嗎?
|
||||
</h2>
|
||||
<p class="cta-description">
|
||||
讓我們一起為您的品牌打造獨特的數位體驗
|
||||
</p>
|
||||
<a href="/contact-us" class="cta-button">
|
||||
聯絡我們
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* Portfolio Header */
|
||||
.portfolio-header {
|
||||
padding: 60px 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section_header_w_line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header_subtitle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header_subtitle_head {
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header_subtitle_paragraph {
|
||||
color: var(--color-gray-600, #666);
|
||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.divider_line {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background-color: var(--color-enchunblue, #23608c);
|
||||
}
|
||||
|
||||
/* Grid Section */
|
||||
.portfolio-grid-section {
|
||||
padding: 0 20px 60px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-gray-500, #999);
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.portfolio-cta {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cta-description {
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-600, #666);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background-color: var(--color-enchunblue, #23608c);
|
||||
color: white;
|
||||
padding: 16px 32px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(35, 96, 140, 0.3);
|
||||
background-color: var(--color-enchunblue-hover, #1a4d6e);
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 991px) {
|
||||
.portfolio-header {
|
||||
padding: 50px 20px;
|
||||
}
|
||||
|
||||
.header_subtitle_head {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.portfolio-grid {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.portfolio-header {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.header_subtitle_head {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.header_subtitle_paragraph {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.portfolio-grid-section {
|
||||
padding: 0 16px 40px;
|
||||
}
|
||||
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.portfolio-cta {
|
||||
padding: 60px 16px;
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.cta-description {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
padding: 14px 24px;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +1,442 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
/**
|
||||
* Portfolio Detail Page - 案例詳情頁
|
||||
* Pixel-perfect implementation based on Webflow design
|
||||
*/
|
||||
import Layout from '../../layouts/Layout.astro'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
// Portfolio slugs from sitemap
|
||||
const slugs = [
|
||||
'web-design-project-2',
|
||||
'web-design-project-3',
|
||||
'web-design-project-4',
|
||||
'web-design-project-5'
|
||||
];
|
||||
// Get portfolio items
|
||||
const portfolioItems: Record<string, {
|
||||
title: string
|
||||
client?: string
|
||||
date?: string
|
||||
category?: string
|
||||
description: string
|
||||
images?: string[]
|
||||
externalUrl?: string
|
||||
}> = {
|
||||
'corporate-website-1': {
|
||||
title: '企業官網設計案例',
|
||||
client: '知名製造業公司',
|
||||
date: '2024年1月',
|
||||
category: '企業官網',
|
||||
description: `
|
||||
<p>這是一個為知名製造業公司打造的現代化企業官網專案。客戶希望建立一個能夠有效展示產品系列、公司形象以及最新消息的專業網站。</p>
|
||||
|
||||
return slugs.map(slug => ({
|
||||
params: { slug },
|
||||
props: { slug }
|
||||
}));
|
||||
<h3>專案背景</h3>
|
||||
<p>客戶原有的網站設計過時,難以在手機設備上正常瀏覽,且內容管理不夠靈活。他們需要一個響應式設計的現代化網站,能夠自動適應各種設備,並方便內容團隊更新。</p>
|
||||
|
||||
<h3>解決方案</h3>
|
||||
<p>我們採用了最新的網頁技術,打造了一個完全響應式的企業官網。網站架構清晰,導航直觀,產品展示頁面設計精美,同時整合了新聞發佈功能,讓客戶能夠快速分享公司動態。</p>
|
||||
|
||||
<h3>專案成果</h3>
|
||||
<p>網站上線後,客戶反映使用者停留時間增加了 40%,詢問量也顯著提升。響應式設計讓行動用戶也能獲得良好的瀏覽體驗,整體滿意度非常高。</p>
|
||||
`,
|
||||
images: ['/placeholder-portfolio-1.jpg'],
|
||||
},
|
||||
'ecommerce-site-1': {
|
||||
title: '電商平台建置',
|
||||
client: '精品品牌',
|
||||
date: '2024年2月',
|
||||
category: '電商網站',
|
||||
description: `
|
||||
<p>這是一個為精品品牌打造的 B2C 電商網站專案。客戶需要一個功能完整、設計精緻的線上購物平台。</p>
|
||||
|
||||
<h3>專案背景</h3>
|
||||
<p>客戶希望拓展線上銷售渠道,建立一個能夠完整呈現品牌調性的電商網站。需求包含會員系統、購物車、金流整合、庫存管理等完整功能。</p>
|
||||
|
||||
<h3>解決方案</h3>
|
||||
<p>我們設計了一個簡潔優雅的電商平台,整合了完整的購物流程。從商品瀏覽、加入購物車、結帳到訂單追蹤,整個流程流暢無縫。同時整合了主流金流與物流服務。</p>
|
||||
|
||||
<h3>專案成果</h3>
|
||||
<p>網站上線後首月即達到預期銷售目標,客戶反應購物體驗流暢,設計風格也獲得用戶高度評價。</p>
|
||||
`,
|
||||
images: ['/placeholder-portfolio-2.jpg'],
|
||||
},
|
||||
'brand-website-1': {
|
||||
title: '品牌形象網站',
|
||||
client: '新創品牌',
|
||||
date: '2024年3月',
|
||||
category: '品牌網站',
|
||||
description: `
|
||||
<p>這是一個為新創品牌打造的以視覺故事為核心的品牌網站專案。</p>
|
||||
|
||||
<h3>專案背景</h3>
|
||||
<p>客戶是一個新創品牌,需要一個能夠有效傳達品牌故事、價值理念的網站。他們希望網站不只是資訊展示,更能成為品牌與用戶情感連結的橋樑。</p>
|
||||
|
||||
<h3>解決方案</h3>
|
||||
<p>我們以視覺故事為設計核心,運用大型視覺元素、動畫效果和互動設計,打造了一個充滿故事性的品牌網站。每一個區塊都精心設計,引導用戶深入了解品牌。</p>
|
||||
|
||||
<h3>專案成果</h3>
|
||||
<p>網站成功建立了強烈的品牌印象,訪客平均停留時間超過 3 分鐘,品牌認知度顯著提升。</p>
|
||||
`,
|
||||
images: ['/placeholder-portfolio-3.jpg'],
|
||||
},
|
||||
'landing-page-1': {
|
||||
title: '活動行銷頁面',
|
||||
client: '活動主辦單位',
|
||||
date: '2024年4月',
|
||||
category: '活動頁面',
|
||||
description: `
|
||||
<p>這是一個為大型活動設計的高轉換率行銷頁面專案。</p>
|
||||
|
||||
<h3>專案背景</h3>
|
||||
<p>客戶需要一個能夠有效吸引報名、轉換率高的活動頁面。頁面需要能夠清楚傳達活動資訊、吸引目光,並引導用戶完成報名流程。</p>
|
||||
|
||||
<h3>解決方案</h3>
|
||||
<p>我們設計了一個視覺衝擊力強的活動頁面,CTA 按鈕配置經過精心規劃,報名流程簡化到最少步驟。同時加入倒數計時、名額顯示等促進轉換的元素。</p>
|
||||
|
||||
<h3>專案成果</h3>
|
||||
<p>頁面上線後,報名轉換率達到 15%,遠超過預期目標,活動順利達到滿檔狀態。</p>
|
||||
`,
|
||||
images: ['/placeholder-portfolio-4.jpg'],
|
||||
},
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
export async function getStaticPaths() {
|
||||
const slugs = Object.keys(portfolioItems)
|
||||
|
||||
// Placeholder content
|
||||
const project = {
|
||||
title: 'Web Design Project',
|
||||
description: 'Project description...',
|
||||
images: []
|
||||
};
|
||||
return slugs.map((slug) => ({
|
||||
params: { slug },
|
||||
props: { slug }
|
||||
}))
|
||||
}
|
||||
|
||||
const { slug } = Astro.props
|
||||
const project = portfolioItems[slug] || {
|
||||
title: '作品詳情',
|
||||
description: '<p>暫無內容</p>',
|
||||
images: ['/placeholder-portfolio.jpg'],
|
||||
}
|
||||
|
||||
// Metadata for SEO
|
||||
const title = `${project.title} | 恩群數位案例`
|
||||
const description = project.description.replace(/<[^>]*>/g, '').slice(0, 160)
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<section class="project-section">
|
||||
<Layout title={title} description={description}>
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="breadcrumb" aria-label="麵包屑導航">
|
||||
<div class="container">
|
||||
<h1>{project.title}</h1>
|
||||
<p>{project.description}</p>
|
||||
<!-- Images and details -->
|
||||
<ol class="breadcrumb-list">
|
||||
<li><a href="/">首頁</a></li>
|
||||
<li><a href="/website-portfolio">案例分享</a></li>
|
||||
<li aria-current="page">{project.title}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</nav>
|
||||
|
||||
<!-- Portfolio Detail -->
|
||||
<article class="portfolio-detail">
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<header class="portfolio-detail-header">
|
||||
<h1 class="portfolio-detail-title">{project.title}</h1>
|
||||
|
||||
<div class="portfolio-detail-meta">
|
||||
{
|
||||
project.client && (
|
||||
<span class="meta-item">
|
||||
<strong>客戶:</strong>{project.client}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
project.date && (
|
||||
<span class="meta-item">
|
||||
<strong>日期:</strong>{project.date}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
project.category && (
|
||||
<span class="meta-item">
|
||||
<strong>類別:</strong>{project.category}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Featured Image -->
|
||||
{
|
||||
project.images && project.images.length > 0 && (
|
||||
<div class="portfolio-detail-image-wrapper">
|
||||
<img
|
||||
src={project.images[0]}
|
||||
alt={project.title}
|
||||
class="portfolio-detail-image"
|
||||
loading="eager"
|
||||
width="1000"
|
||||
height="562"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Description -->
|
||||
<div class="portfolio-detail-content">
|
||||
<div class="description-wrapper" set:html={project.description} />
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="portfolio-detail-cta">
|
||||
<h3 class="cta-heading">有專案想要討論嗎?</h3>
|
||||
<p class="cta-subheading">我們很樂意聽聽您的需求</p>
|
||||
<a href="/contact-us" class="cta-button">
|
||||
聯絡我們
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Related Projects -->
|
||||
<div class="related-projects">
|
||||
<h3 class="related-title">更多案例</h3>
|
||||
<a href="/website-portfolio" class="view-all-link">
|
||||
查看全部案例 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.project-section {
|
||||
padding: 40px 0;
|
||||
/* Portfolio Detail Styles - Pixel-perfect from Webflow */
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
padding: 16px 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.breadcrumb-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-600);
|
||||
}
|
||||
|
||||
.breadcrumb-list a {
|
||||
color: var(--color-enchunblue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-list a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-list li:not(:last-child)::after {
|
||||
content: '/';
|
||||
margin-left: 8px;
|
||||
color: var(--color-gray-400);
|
||||
}
|
||||
|
||||
/* Detail Section */
|
||||
.portfolio-detail {
|
||||
padding: 60px 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
h1 {
|
||||
|
||||
/* Header */
|
||||
.portfolio-detail-header {
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.portfolio-detail-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-tarawera, #2d3748);
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.portfolio-detail-meta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-600);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* Featured Image */
|
||||
.portfolio-detail-image-wrapper {
|
||||
margin-bottom: 40px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-detail-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.portfolio-detail-content {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.description-wrapper {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.description-wrapper :global(p) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.description-wrapper :global(h3) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-enchunblue);
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.portfolio-detail-cta {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
background: linear-gradient(135deg, rgba(35, 96, 140, 0.05) 0%, rgba(35, 96, 140, 0.02) 100%);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.cta-heading {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-enchunblue);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cta-subheading {
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-600);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background-color: var(--color-enchunblue);
|
||||
color: white;
|
||||
padding: 16px 32px;
|
||||
border-radius: var(--radius, 8px);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-base, 0.3s ease);
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
|
||||
background-color: var(--color-enchunblue-hover, #1a4d6e);
|
||||
}
|
||||
|
||||
/* Related Projects */
|
||||
.related-projects {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 32px;
|
||||
border-top: 1px solid var(--color-border, #e5e7eb);
|
||||
}
|
||||
|
||||
.related-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.view-all-link {
|
||||
color: var(--color-enchunblue);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color var(--transition-fast, 0.2s ease);
|
||||
}
|
||||
|
||||
.view-all-link:hover {
|
||||
color: var(--color-enchunblue-hover, #1a4d6e);
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 991px) {
|
||||
.portfolio-detail {
|
||||
padding: 50px 16px;
|
||||
}
|
||||
|
||||
.portfolio-detail-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-detail-cta {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.cta-heading {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.breadcrumb {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.breadcrumb-list {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.portfolio-detail {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.portfolio-detail-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.portfolio-detail-meta {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.portfolio-detail-cta {
|
||||
padding: 32px 20px;
|
||||
}
|
||||
|
||||
.related-projects {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.description-wrapper {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.description-wrapper :global(h3) {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,57 +1,225 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
/**
|
||||
* Portfolio Listing Page - 案例分享列表頁
|
||||
* Pixel-perfect implementation based on Webflow design
|
||||
*/
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
import PortfolioCard from '../components/PortfolioCard.astro'
|
||||
|
||||
// Placeholder portfolios
|
||||
const portfolios = [
|
||||
{ slug: 'web-design-project-2', title: 'Project 2', description: 'Description...' },
|
||||
// Add more
|
||||
];
|
||||
// Metadata for SEO
|
||||
const title = '案例分享 | 恩群數位行銷'
|
||||
const description = '瀏覽恩群數位的成功案例,包括企業官網、電商網站、品牌網站等設計作品。'
|
||||
|
||||
// Portfolio items - can be fetched from Payload CMS
|
||||
const portfolioItems = [
|
||||
{
|
||||
slug: 'corporate-website-1',
|
||||
title: '企業官網設計案例',
|
||||
description: '為知名製造業打造的現代化企業官網,整合產品展示與新聞發佈功能。',
|
||||
image: '/placeholder-portfolio-1.jpg',
|
||||
tags: ['企業官網', '響應式設計'],
|
||||
},
|
||||
{
|
||||
slug: 'ecommerce-site-1',
|
||||
title: '電商平台建置',
|
||||
description: 'B2C 電商網站,包含會員系統、購物車、金流整合等完整功能。',
|
||||
image: '/placeholder-portfolio-2.jpg',
|
||||
tags: ['電商網站', '金流整合'],
|
||||
},
|
||||
{
|
||||
slug: 'brand-website-1',
|
||||
title: '品牌形象網站',
|
||||
description: '以視覺故事為核心的品牌網站,展現品牌獨特價值與理念。',
|
||||
image: '/placeholder-portfolio-3.jpg',
|
||||
tags: ['品牌網站', '視覺設計'],
|
||||
},
|
||||
{
|
||||
slug: 'landing-page-1',
|
||||
title: '活動行銷頁面',
|
||||
description: '高轉換率的活動頁面設計,有效的 CTA 配置與使用者體驗規劃。',
|
||||
image: '/placeholder-portfolio-4.jpg',
|
||||
tags: ['活動頁面', '行銷'],
|
||||
},
|
||||
]
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<section class="portfolio-section">
|
||||
<Layout title={title} description={description}>
|
||||
<!-- Portfolio Header -->
|
||||
<section class="portfolio-header" aria-labelledby="portfolio-heading">
|
||||
<div class="container">
|
||||
<h1>網站設計作品</h1>
|
||||
<div class="portfolio-grid">
|
||||
{portfolios.map(item => (
|
||||
<div class="portfolio-item">
|
||||
<h2><a href={`/webdesign-profolio/${item.slug}`}>{item.title}</a></h2>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<h1 id="portfolio-heading" class="portfolio-title">案例分享</h1>
|
||||
<p class="portfolio-subtitle">Selected Works</p>
|
||||
<div class="divider-line"></div>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Portfolio Grid -->
|
||||
<section class="portfolio-grid-section" aria-label="作品列表">
|
||||
<ul class="portfolio-grid">
|
||||
{
|
||||
portfolioItems.map((item) => (
|
||||
<PortfolioCard item={item} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="portfolio-cta" aria-labelledby="cta-heading">
|
||||
<div class="container">
|
||||
<h2 id="cta-heading" class="cta-title">
|
||||
有興趣與我們合作嗎?
|
||||
</h2>
|
||||
<p class="cta-description">
|
||||
讓我們一起為您的品牌打造獨特的數位體驗
|
||||
</p>
|
||||
<a href="/contact-us" class="cta-button">
|
||||
聯絡我們
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.portfolio-section {
|
||||
padding: 40px 0;
|
||||
/* Portfolio Page Styles - Pixel-perfect from Webflow */
|
||||
|
||||
/* Header Section */
|
||||
.portfolio-header {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.portfolio-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-enchunblue);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.portfolio-subtitle {
|
||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-gray-700);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
width: 100px;
|
||||
height: 2px;
|
||||
background-color: var(--color-enchunblue);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.divider-line:last-child {
|
||||
width: 60px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Grid Section */
|
||||
.portfolio-grid-section {
|
||||
padding: 0 20px 60px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.portfolio-item {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
|
||||
/* CTA Section */
|
||||
.portfolio-cta {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.portfolio-item a {
|
||||
|
||||
.cta-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-enchunblue);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cta-description {
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-600);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background-color: var(--color-enchunblue);
|
||||
color: white;
|
||||
padding: 16px 32px;
|
||||
border-radius: var(--radius, 8px);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
transition: all var(--transition-base, 0.3s ease);
|
||||
}
|
||||
.portfolio-item a:hover {
|
||||
color: #007bff;
|
||||
|
||||
.cta-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
|
||||
background-color: var(--color-enchunblue-hover, #1a4d6e);
|
||||
}
|
||||
</style>
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 991px) {
|
||||
.portfolio-header {
|
||||
padding: 50px 20px;
|
||||
}
|
||||
|
||||
.portfolio-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-grid {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.portfolio-header {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.portfolio-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.portfolio-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.portfolio-grid-section {
|
||||
padding: 0 16px 40px;
|
||||
}
|
||||
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.portfolio-cta {
|
||||
padding: 60px 16px;
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,79 +1,233 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
/**
|
||||
* Category Page - 文章分類
|
||||
* URL: /wen-zhang-fen-lei/[slug]
|
||||
* Displays posts filtered by category from Payload CMS
|
||||
*/
|
||||
import Layout from '../../layouts/Layout.astro'
|
||||
import ArticleCard from '../../components/blog/ArticleCard.astro'
|
||||
import { fetchCategoryBySlug, fetchPosts, formatDate } from '../../lib/api/blog'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
// Category slugs
|
||||
const slugs = [
|
||||
'en-qun-shu-wei-zui-xin-gong-gao',
|
||||
'xing-xiao-shi-shi-zui-qian-xian',
|
||||
'meta-xiao-xue-tang',
|
||||
'google-xiao-xue-tang'
|
||||
];
|
||||
const { slug } = Astro.params
|
||||
|
||||
return slugs.map(slug => ({
|
||||
params: { slug },
|
||||
props: { slug }
|
||||
}));
|
||||
// Fetch category by slug
|
||||
const category = await fetchCategoryBySlug(slug)
|
||||
|
||||
// Handle 404 for non-existent category
|
||||
if (!category) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
// Fetch posts for this category (limit 100 for category pages)
|
||||
const PAGE_SIZE = 100
|
||||
const postsData = await fetchPosts(1, PAGE_SIZE, slug)
|
||||
const posts = postsData.docs
|
||||
|
||||
// Placeholder - would fetch category and posts from CMS
|
||||
const category = {
|
||||
name: 'Category Name',
|
||||
posts: [
|
||||
{ slug: 'post1', title: 'Post 1', date: '2023-01-01' }
|
||||
]
|
||||
};
|
||||
// Format date function
|
||||
const formatDateTW = (dateStr: string | null | undefined): string => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-TW', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<section class="category-section">
|
||||
<Layout title={category.title}>
|
||||
<!-- Category Header -->
|
||||
<section class="category-hero" aria-labelledby="category-heading">
|
||||
<div class="container">
|
||||
<h1>{category.name}</h1>
|
||||
<div class="posts-list">
|
||||
{category.posts.map(post => (
|
||||
<article class="post-item">
|
||||
<h2><a href={`/xing-xiao-fang-da-jing/${post.slug}`}>{post.title}</a></h2>
|
||||
<p>{post.date}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<a href="/news" class="back-link">← 回到文章列表</a>
|
||||
<h1 id="category-heading" class="category-title">{category.title}</h1>
|
||||
{
|
||||
category.nameEn && (
|
||||
<p class="category-subtitle">{category.nameEn}</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Category Posts -->
|
||||
<section class="category-posts">
|
||||
<div class="container">
|
||||
{
|
||||
posts.length > 0 ? (
|
||||
<div class="posts-grid">
|
||||
{posts.map((post) => (
|
||||
<article class="post-item">
|
||||
<a href={`/xing-xiao-fang-da-jing/${post.slug}`} class="post-link">
|
||||
{
|
||||
post.heroImage?.url && (
|
||||
<div class="post-image">
|
||||
<img
|
||||
src={post.heroImage.url}
|
||||
alt={post.heroImage.alt || post.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div class="post-content">
|
||||
<h3 class="post-title">{post.title}</h3>
|
||||
<time class="post-date">{formatDateTW(post.publishedAt)}</time>
|
||||
{
|
||||
post.excerpt && (
|
||||
<p class="post-excerpt">{post.excerpt.slice(0, 120)}...</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty-state">
|
||||
<p>此分類暫無文章</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.category-section {
|
||||
padding: 40px 0;
|
||||
.category-hero {
|
||||
padding: 60px 20px 40px;
|
||||
text-align: center;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.posts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.post-item {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.post-item h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.post-item a {
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.post-item a:hover {
|
||||
color: #007bff;
|
||||
|
||||
.category-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-dark-blue, #1a1a1a);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.category-subtitle {
|
||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-600, #666);
|
||||
}
|
||||
|
||||
.category-posts {
|
||||
padding: 40px 20px 60px;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.posts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.post-item:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.post-image {
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
background: var(--color-gray-100, #f5f5f5);
|
||||
}
|
||||
|
||||
.post-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-dark-blue, #1a1a1a);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post-date {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-500, #999);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.post-excerpt {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-gray-600, #666);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
color: var(--color-gray-500, #999);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 991px) {
|
||||
.posts-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.category-hero {
|
||||
padding: 40px 16px 30px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.posts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.category-posts {
|
||||
padding: 30px 16px 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,66 +1,356 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
/**
|
||||
* Blog Article Detail Page - 行銷放大鏡文章詳情
|
||||
* URL: /xing-xiao-fang-da-jing/[slug]
|
||||
* Fetches from Payload CMS and renders with Lexical content
|
||||
*/
|
||||
import Layout from '../../layouts/Layout.astro'
|
||||
import RelatedPosts from '../../components/blog/RelatedPosts.astro'
|
||||
import { fetchPostBySlug, fetchRelatedPosts, formatDate } from '../../lib/api/blog'
|
||||
import { serializeLexical } from '../../lib/serializeLexical'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
// Placeholder slugs - would fetch from CMS
|
||||
const slugs = [
|
||||
'2-zhao-yao-kong-xiao-fei-zhe-de-xin',
|
||||
'2022-jie-qing-xing-xiao-quan-gong-lue',
|
||||
// Add all from sitemap
|
||||
];
|
||||
// Get slug from params
|
||||
const { slug } = Astro.params
|
||||
|
||||
return slugs.map(slug => ({
|
||||
params: { slug },
|
||||
props: { slug }
|
||||
}));
|
||||
// Validate slug
|
||||
if (!slug) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
// Fetch post by slug
|
||||
const post = await fetchPostBySlug(slug)
|
||||
|
||||
// Placeholder content - would fetch from CMS
|
||||
const post = {
|
||||
title: 'Sample Post Title',
|
||||
date: 'January 20, 2022',
|
||||
content: 'Sample content...'
|
||||
};
|
||||
// Handle 404 for non-existent or unpublished posts
|
||||
if (!post) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
// Fetch related posts
|
||||
const categorySlugs = post.categories?.map(c => c.slug) || []
|
||||
const relatedPosts = await fetchRelatedPosts(post.id, categorySlugs, 4)
|
||||
|
||||
// Format date
|
||||
const displayDate = formatDate(post.publishedAt)
|
||||
|
||||
// Get primary category
|
||||
const category = post.categories?.[0]
|
||||
|
||||
// Serialize Lexical content to HTML (must await the async function)
|
||||
const contentHtml = await serializeLexical(post.content)
|
||||
|
||||
// SEO metadata
|
||||
const metaTitle = post.meta?.title || post.title
|
||||
const metaDescription = post.meta?.description || post.excerpt || ''
|
||||
const metaImage = post.meta?.image?.url || post.ogImage?.url || post.heroImage?.url || ''
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<section class="post-section">
|
||||
<div class="container">
|
||||
<a href="/news" class="back-link">回到文章列表</a>
|
||||
<article>
|
||||
<h1>{post.title}</h1>
|
||||
<p class="post-date">文章發布日期:{post.date}</p>
|
||||
<div class="post-content prose prose-custom max-w-none">
|
||||
<p>{post.content}</p>
|
||||
<!-- More content would be rendered here with markdown -->
|
||||
<Layout title={metaTitle} description={metaDescription}>
|
||||
<article class="article-detail">
|
||||
<!-- Article Header -->
|
||||
<header class="article-header">
|
||||
<div class="container">
|
||||
<!-- Category Badge -->
|
||||
{
|
||||
category && (
|
||||
<span
|
||||
class="article-category"
|
||||
style={`background-color: ${category.backgroundColor || 'var(--color-enchunblue)'}; color: ${category.textColor || '#fff'}`}
|
||||
>
|
||||
{category.title}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Article Title -->
|
||||
<h1 class="article-title">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<!-- Published Date -->
|
||||
{
|
||||
displayDate && (
|
||||
<time class="article-date" datetime={post.publishedAt || post.createdAt}>
|
||||
{displayDate}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Image -->
|
||||
{
|
||||
post.heroImage?.url && (
|
||||
<div class="article-hero-image">
|
||||
<img
|
||||
src={post.heroImage.url}
|
||||
alt={post.heroImage.alt || post.title}
|
||||
loading="eager"
|
||||
width="1200"
|
||||
height="675"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Article Content -->
|
||||
<div class="article-content">
|
||||
<div class="container">
|
||||
<div class="content-wrapper">
|
||||
<!-- Render rich text content with Lexical to HTML conversion -->
|
||||
<div class="prose" set:html={contentHtml} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Back to List Button -->
|
||||
<div class="article-actions">
|
||||
<div class="container">
|
||||
<a href="/news" class="back-button">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
|
||||
</svg>
|
||||
回到文章列表
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Related Posts Section -->
|
||||
<RelatedPosts posts={relatedPosts} />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.post-section {
|
||||
padding: 40px 0;
|
||||
<!-- Open Graph Tags -->
|
||||
<script define:vars={{ metaImage, metaTitle, metaDescription }}>
|
||||
if (typeof document !== 'undefined') {
|
||||
// Update OG image
|
||||
const ogImage = document.querySelector('meta[property="og:image"]')
|
||||
if (ogImage && metaImage) {
|
||||
ogImage.setAttribute('content', metaImage)
|
||||
}
|
||||
|
||||
// Update OG title
|
||||
const ogTitle = document.querySelector('meta[property="og:title"]')
|
||||
if (ogTitle) {
|
||||
ogTitle.setAttribute('content', metaTitle)
|
||||
}
|
||||
|
||||
// Update OG description
|
||||
const ogDesc = document.querySelector('meta[property="og:description"]')
|
||||
if (ogDesc) {
|
||||
ogDesc.setAttribute('content', metaDescription)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Article Header */
|
||||
.article-header {
|
||||
padding: 60px 20px 40px;
|
||||
text-align: center;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.back-link {
|
||||
|
||||
.article-category {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #007bff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-dark-blue, #1a1a1a);
|
||||
line-height: 1.3;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.article-date {
|
||||
font-family: "Quicksand", "Noto Sans TC", sans-serif;
|
||||
font-size: 1rem;
|
||||
color: var(--color-gray-500, #999);
|
||||
}
|
||||
|
||||
/* Hero Image */
|
||||
.article-hero-image {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
background: var(--color-gray-100, #f5f5f5);
|
||||
}
|
||||
|
||||
.article-hero-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Article Content */
|
||||
.article-content {
|
||||
padding: 60px 20px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Prose styles for rich text content */
|
||||
.prose {
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
color: var(--color-gray-700, #333);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.prose :global(h1),
|
||||
.prose :global(h2),
|
||||
.prose :global(h3),
|
||||
.prose :global(h4) {
|
||||
font-weight: 700;
|
||||
color: var(--color-dark-blue, #1a1a1a);
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose :global(h1) { font-size: 2rem; }
|
||||
.prose :global(h2) { font-size: 1.75rem; }
|
||||
.prose :global(h3) { font-size: 1.5rem; }
|
||||
.prose :global(h4) { font-size: 1.25rem; }
|
||||
|
||||
.prose :global(p) {
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 1.0625rem;
|
||||
}
|
||||
|
||||
.prose :global(a) {
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.prose :global(a:hover) {
|
||||
color: var(--color-enchunblue-hover, #1a4d6e);
|
||||
}
|
||||
|
||||
.prose :global(img) {
|
||||
border-radius: 8px;
|
||||
margin: 2rem 0;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.prose :global(ul),
|
||||
.prose :global(ol) {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.prose :global(li) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose :global(blockquote) {
|
||||
border-left: 4px solid var(--color-enchunblue, #23608c);
|
||||
padding-left: 1rem;
|
||||
margin: 2rem 0;
|
||||
font-style: italic;
|
||||
color: var(--color-gray-600, #666);
|
||||
}
|
||||
|
||||
.prose :global(code) {
|
||||
background: var(--color-gray-100, #f5f5f5);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.prose :global(pre) {
|
||||
background: var(--color-dark-blue, #1a1a1a);
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.prose :global(pre code) {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Article Actions */
|
||||
.article-actions {
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: #ffffff;
|
||||
border: 2px solid var(--color-gray-300, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
color: var(--color-gray-700, #555);
|
||||
font-family: "Noto Sans TC", sans-serif;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
.post-date {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.back-button:hover {
|
||||
border-color: var(--color-enchunblue, #23608c);
|
||||
color: var(--color-enchunblue, #23608c);
|
||||
background: rgba(35, 96, 140, 0.05);
|
||||
}
|
||||
.post-content {
|
||||
/* Prose styles handle typography */
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 991px) {
|
||||
.article-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.article-header {
|
||||
padding: 40px 16px 30px;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.article-hero-image {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.prose :global(h1) { font-size: 1.75rem; }
|
||||
.prose :global(h2) { font-size: 1.5rem; }
|
||||
.prose :global(h3) { font-size: 1.25rem; }
|
||||
.prose :global(p) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user