chore(workflow): add AI-assisted workflow commands and configurations

Add comprehensive workflow commands for AI-assisted development:
- Claude commands: analyze, clarify, plan
- Kilocode workflows: full feature development lifecycle
- Opencode commands: specification and implementation workflows
- Roo MCP configuration for tool integration

Update .gitignore to exclude .astro build cache directories.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-07 01:06:10 +08:00
parent cf0f779ad4
commit c2d4c8d0a0
122 changed files with 5376 additions and 107 deletions

View File

@@ -0,0 +1,90 @@
---
import { Image } from 'astro:assets';
// Footer component with client-side data fetching
---
<footer class="bg-[var(--color-tropical-blue)] py-10 mt-auto">
<div class="max-w-5xl mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8 mb-8">
<div class="col-span-2">
<Image src="/enchun-logo.svg"
alt="Enchun Digital Logo" class="h-auto w-32 mb-4"
width={919}
height={201}
loading="eager"
decoding="async"
/>
<p class="text-[var(--color-st-tropaz)] text-sm font-light leading-relaxed">恩群數位累積多年廣告行銷操作經驗,擁有全方位行銷人才,讓我們可以為客戶精準的規劃每一分廣告預算,讓你的品牌深入人心。更重要的是恩群的存在,為了成為每家公司最佳數位夥伴,作為彼此最堅強的後盾,你會知道有我們的陪伴 你並不孤單。</p>
</div>
<div>
<h3 class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4">聯絡我們</h3>
<a href="https://www.facebook.com/EnChun-Taiwan-100979265112420" target="_blank" class="flex items-center mb-2">
<Image src="/fb-icon.svg"
alt="Phone Icon" class="h-auto w-6 mb-2"
width={16}
height={16}
loading="eager"
decoding="async"
/>
</a>
<p class="text-[var(--color-st-tropaz)] mb-2">諮詢電話:<br> 02 5570 0527</p>
<a href="mailto:enchuntaiwan@gmail.com" class="text-primary hover:text-secondary transition-colors">enchuntaiwan@gmail.com</a>
</div>
<div>
<h3 class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4">行銷方案</h3>
<ul class="space-y-2" id="marketing-solutions">
<li><span class="text-gray-500">載入中...</span></li>
</ul>
</div>
<div>
<h3 class="text-lg font-bold text-[var(--color-st-tropaz)] mb-4">行銷放大鏡</h3>
<ul class="space-y-2" id="marketing-articles">
<li><span class="text-gray-500">載入中...</span></li>
</ul>
</div>
</div>
<div class="absolute inset-x-0 w-screen bg-[var(--color-amber)] py-3 text-center">
<p class="text-[var(--color-tarawera)]">copyright ©  Enchun digital  2018 - {new Date().getFullYear()}</p>
</div>
</div>
</footer>
<script>
// Client-side data fetching for footer
async function loadFooterData() {
try {
console.log('Fetching footer data...');
const response = await fetch('/api/globals/footer?depth=2&draft=false&locale=undefined&trash=false');
const data = await response.json();
console.log('Footer data loaded:', data);
// Update marketing solutions
const marketingUl = document.getElementById('marketing-solutions');
if (marketingUl && data.navItems?.[0]?.childNavItems) {
const links = data.navItems[0].childNavItems.map(item =>
`<li><a href="${item.link?.url || '#'}" class="font-normal text-[var(--color-st-tropaz)] hover:text-[var(--color-dove-gray)] transition-colors">${item.link?.label}</a></li>`
).join('');
marketingUl.innerHTML = links;
}
// Update marketing articles (行銷放大鏡)
const articlesUl = document.getElementById('marketing-articles');
if (articlesUl && data.navItems?.[1]?.childNavItems) {
const links = data.navItems[1].childNavItems.map(item =>
`<li><a href="${item.link?.url || '#'}" class="font-normal text-[var(--color-st-tropaz)] hover:text-[var(--color-dove-gray)] transition-colors">${item.link?.label}</a></li>`
).join('');
articlesUl.innerHTML = links;
}
} catch (error) {
console.error('Failed to load footer data:', error);
}
}
// Load footer data when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadFooterData);
} else {
loadFooterData();
}
</script>

View File

@@ -0,0 +1,149 @@
---
import { Image } from 'astro:assets';
// Header component
---
<header class="sticky top-0 z-50 bg-transparent">
<nav class="max-w-5xl mx-auto px-4 py-4">
<ul class="flex items-center justify-between list-none">
<li class="flex-shrink-0">
<a href="/" class="block">
<!-- Uses Astro's optimized Image component for the site logo -->
<Image src="/enchun-logo.svg" alt="Enchun Digital Marketing"
class="w-32 h-auto"
width={919}
height={201}
loading="eager"
decoding="async"
/>
</a>
</li>
<li class="hidden md:flex items-center space-x-6" id="desktop-nav">
<!-- Navigation items will be populated by JavaScript -->
</li>
<!-- Mobile menu button -->
<li class="md:hidden">
<button class="text-[var(--color-enchunblue)] hover:text-[var(--color-enchunblue)]/80" id="mobile-menu-button">
<svg width="24" height="24" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6">
<path d="M6 27H22.5C23.325 27 24 26.325 24 25.5C24 24.675 23.325 24 22.5 24H6C5.175 24 4.5 24.675 4.5 25.5C4.5 26.325 5.175 27 6 27ZM6 19.5H18C18.825 19.5 19.5 18.825 19.5 18C19.5 17.175 18.825 16.5 18 16.5H6C5.175 16.5 4.5 17.175 4.5 18C4.5 18.825 5.175 19.5 6 19.5ZM4.5 10.5C4.5 11.325 5.175 12 6 12H22.5C23.325 12 24 11.325 24 10.5C24 9.675 23.325 9 22.5 9H6C5.175 9 4.5 9.675 4.5 10.5ZM30.45 22.32L26.13 18L30.45 13.68C30.5889 13.5411 30.699 13.3763 30.7742 13.1948C30.8493 13.0134 30.888 12.8189 30.888 12.6225C30.888 12.4261 30.8493 12.2316 30.7742 12.0502C30.699 11.8687 30.5889 11.7039 30.45 11.565C30.3111 11.4261 30.1463 11.316 29.9648 11.2408C29.7834 11.1657 29.5889 11.127 29.3925 11.127C29.1961 11.127 29.0016 11.1657 28.8202 11.2408C28.6387 11.316 28.4739 11.4261 28.335 11.565L22.95 16.95C22.8109 17.0888 22.7006 17.2536 22.6254 17.4351C22.5501 17.6165 22.5113 17.811 22.5113 18.0075C22.5113 18.204 22.5501 18.3985 22.6254 18.5799C22.7006 18.7614 22.8109 18.9262 22.95 19.065L28.335 24.45C28.92 25.035 29.865 25.035 30.45 24.45C31.02 23.865 31.035 22.905 30.45 22.32V22.32Z" fill="currentColor"/>
</svg>
</button>
</li>
</ul>
<!-- Mobile menu -->
<div class="md:hidden hidden" id="mobile-menu">
<ul class="pt-4 pb-2 space-y-2" id="mobile-nav">
<!-- Mobile navigation items will be populated by JavaScript -->
</ul>
</div>
</nav>
</header>
<script>
interface NavItem {
link: {
type: 'reference' | 'custom';
label: string;
url?: string;
reference?: {
slug: string;
};
newTab?: boolean;
};
}
// Fetch navigation data from Payload CMS
async function fetchNavigation() {
try {
// Use local proxy in development to avoid CORS issues
const apiUrl = `/api/globals/header?depth=2&draft=false&locale=undefined&trash=false`;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.navItems || [];
} catch (error) {
console.error('Error fetching navigation:', error);
throw error;
}
}
// Generate navigation link HTML
function createNavLink(item: NavItem) {
const { link } = item;
let href = '';
if (link.type === 'custom' && link.url) {
href = link.url;
} else if (link.type === 'reference' && link.reference?.slug) {
href = `/${link.reference.slug}`;
}
const target = link.newTab ? ' target="_blank" rel="noopener noreferrer"' : '';
const label = link.label;
// Check if current page matches this link
const currentPath = window.location.pathname;
const isActive = currentPath === href || (href === '/' && currentPath === '/');
// Add badges for specific items (positioned in top right corner)
let badge = '';
if (label.includes('行銷方案')) {
badge = '<span class="absolute -top-1 -right-1 bg-red-500 text-white text-[0.5rem] px-1 py-0.5 rounded-full">hot</span>';
} else if (label.includes('行銷放大鏡')) {
badge = '<span class="absolute -top-1 -right-1 bg-red-500 text-white text-[0.5rem] px-1 py-0.5 rounded-full">new</span>';
}
const containerClass = badge ? 'relative inline-block' : '';
const activeClass = isActive ? ' nav-active' : '';
return `<a href="${href}" class="${containerClass} text-lg font-normal text-shadow-md hover:text-primary transition-colors px-3 py-2${activeClass}"${target}>${label}${badge}</a>`;
}
// Populate navigation
async function populateNavigation() {
const navItems = await fetchNavigation();
const desktopNav = document.getElementById('desktop-nav');
const mobileNav = document.getElementById('mobile-nav');
if (desktopNav && mobileNav) {
// Clear existing content
desktopNav.innerHTML = '';
mobileNav.innerHTML = '';
// Populate desktop navigation
navItems.forEach((item) => {
const linkHtml = createNavLink(item);
const li = document.createElement('li');
li.innerHTML = linkHtml;
desktopNav.appendChild(li);
});
// Populate mobile navigation
navItems.forEach((item) => {
const linkHtml = createNavLink(item).replace('px-3 py-2', 'block px-3 py-2').replace('relative inline-block', 'relative inline-block block');
const li = document.createElement('li');
li.innerHTML = linkHtml;
mobileNav.appendChild(li);
});
}
}
// Initialize navigation
populateNavigation();
// Simple mobile menu toggle
const button = document.getElementById('mobile-menu-button');
const menu = document.getElementById('mobile-menu');
if (button && menu) {
button.addEventListener('click', () => {
menu.classList.toggle('hidden');
});
}
</script>

View File

@@ -0,0 +1,80 @@
---
// Admin layout for protected pages
---
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin - 恩群數位行銷</title>
<meta name="description" content="Admin panel for Enchun Digital Marketing" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head>
<body>
<header class="admin-header">
<nav class="admin-nav">
<div class="admin-nav-brand">
<a href="/admin/dashboard">Admin Panel</a>
</div>
<ul class="admin-nav-links">
<li><a href="/admin/dashboard">Dashboard</a></li>
<li><a href="/admin/cms">CMS</a></li>
<li><button id="logout-btn">Logout</button></li>
</ul>
</nav>
</header>
<main class="admin-main">
<slot />
</main>
</body>
</html>
<script>
// Simple logout handler
document.getElementById('logout-btn')?.addEventListener('click', async () => {
// Call logout API or redirect
window.location.href = 'https://cms.enchun.tw/admin/logout';
});
</script>
<style>
.admin-header {
background: #333;
color: white;
padding: 10px 0;
}
.admin-nav {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.admin-nav-brand a {
color: white;
text-decoration: none;
font-weight: bold;
}
.admin-nav-links {
display: flex;
list-style: none;
gap: 20px;
}
.admin-nav-links a {
color: white;
text-decoration: none;
}
.admin-nav-links button {
background: #555;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
.admin-main {
padding: 20px;
min-height: calc(100vh - 60px);
}
</style>

View File

@@ -0,0 +1,41 @@
---
import '../styles/tailwind.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
// Main layout for the site
---
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>恩群數位行銷</title>
<meta name="description" content="恩群數位累積多年廣告行銷操作經驗,擁有全方位行銷人才,讓我們可以為客戶精準的規劃每一分廣告預算,讓你的品牌深入人心。" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="bg-background text-text font-sans min-h-screen flex flex-col">
<Header />
<main class="flex-1">
<slot />
</main>
<Footer />
</body>
</html>
<style>
body {
font-family: var(--font-family-sans);
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
</style>

View File

@@ -0,0 +1,32 @@
import { defineMiddleware } from 'astro:middleware';
import { authService } from './services/auth';
export const onRequest = defineMiddleware(async (context, next) => {
const { url, cookies, redirect } = context;
// Check if the route is under /admin/
if (url.pathname.startsWith('/admin/')) {
// Get token from cookie
const token = cookies.get('payload-token')?.value;
if (!token) {
// Redirect to CMS login subdomain
return redirect('https://cms.enchun.tw/admin/login');
}
// Validate token
authService.token = token;
const user = await authService.getCurrentUser();
if (!user) {
// Token invalid, redirect to login
cookies.delete('payload-token');
return redirect('https://cms.enchun.tw/admin/login');
}
// User authenticated, proceed
// Could add role checks here if needed
}
return next();
});

View File

@@ -0,0 +1,31 @@
---
import Layout from '../layouts/Layout.astro';
---
<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>
<style>
.about-section {
padding: 40px 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
</style>

View File

@@ -0,0 +1,30 @@
---
import AdminLayout from '../../layouts/AdminLayout.astro';
---
<AdminLayout>
<div class="cms-section">
<h1>Payload CMS</h1>
<p>The CMS is hosted on a separate subdomain for security.</p>
<a href="https://cms.enchun.tw/admin" class="btn" target="_blank">Open CMS</a>
</div>
</AdminLayout>
<style>
.cms-section {
text-align: center;
padding: 40px 0;
}
.btn {
display: inline-block;
background: #007bff;
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 4px;
font-size: 18px;
}
.btn:hover {
background: #0056b3;
}
</style>

View File

@@ -0,0 +1,56 @@
---
import AdminLayout from '../../layouts/AdminLayout.astro';
---
<AdminLayout>
<div class="dashboard">
<h1>Admin Dashboard</h1>
<div class="dashboard-grid">
<div class="dashboard-card">
<h2>Content Management</h2>
<p>Manage blog posts, portfolios, and categories.</p>
<a href="/admin/cms" class="btn">Go to CMS</a>
</div>
<div class="dashboard-card">
<h2>Analytics</h2>
<p>View site analytics and performance metrics.</p>
<a href="#" class="btn">View Analytics</a>
</div>
</div>
</div>
</AdminLayout>
<style>
.dashboard {
max-width: 1200px;
margin: 0 auto;
}
h1 {
margin-bottom: 30px;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.dashboard-card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
background: #f9f9f9;
}
.dashboard-card h2 {
margin-top: 0;
}
.btn {
display: inline-block;
background: #007bff;
color: white;
padding: 10px 15px;
text-decoration: none;
border-radius: 4px;
}
.btn:hover {
background: #0056b3;
}
</style>

View File

@@ -0,0 +1,6 @@
---
// Admin login page - redirects to CMS subdomain
---
<meta http-equiv="refresh" content="0; url=https://cms.enchun.tw/admin/login" />
<p>Redirecting to login...</p>

View File

@@ -0,0 +1,85 @@
---
import Layout from '../layouts/Layout.astro';
---
<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 />
</div>
<div class="form-group">
<label for="email">電子郵件</label>
<input type="email" id="email" name="email" required />
</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 {
padding: 40px 0;
}
.container {
max-width: 800px;
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 {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
textarea {
height: 150px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.contact-info {
margin-top: 40px;
text-align: center;
}
</style>

View File

@@ -1,16 +1,47 @@
---
import '../styles/tailwind.css';
import Layout from '../layouts/Layout.astro';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>enchun.tw</title>
</head>
<body class="bg-surface text-text">
<main class="mx-auto flex min-h-screen max-w-4xl flex-col items-center justify-center gap-6 px-4 text-center">
<h1 class="text-4xl font-semibold text-primary">enchun.tw migration scaffold</h1>
<p class="text-lg text-secondary">Astro SSR frontend is ready for development.</p>
</main>
</body>
</html>
<Layout>
<!-- Hero Section -->
<section class="bg-gradient-to-r from-primary to-secondary text-white py-20">
<div class="max-w-6xl mx-auto px-4 text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-6">恩群數位行銷</h1>
<p class="text-xl md:text-2xl mb-8">累積多年廣告行銷操作經驗,全方位行銷人才,為您精準規劃每一分廣告預算</p>
<a href="/contact-us" class="bg-white text-primary px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors">聯絡我們</a>
</div>
</section>
<!-- 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>
<!-- 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>
</Layout>

View File

@@ -0,0 +1,43 @@
---
import Layout from '../layouts/Layout.astro';
---
<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>
<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>

View File

@@ -0,0 +1,61 @@
---
import Layout from '../layouts/Layout.astro';
// 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
];
---
<Layout>
<section class="news-section">
<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>
</div>
</section>
</Layout>
<style>
.news-section {
padding: 40px 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.post-card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
}
.post-card h2 {
margin-top: 0;
}
.post-card a {
text-decoration: none;
color: #333;
}
.post-card a:hover {
color: #007bff;
}
</style>

View File

@@ -0,0 +1,28 @@
---
import Layout from '../layouts/Layout.astro';
---
<Layout>
<section class="teams-section">
<div class="container">
<h1>恩群大本營</h1>
<p>認識我們的團隊成員。</p>
<!-- Team members would be listed here -->
</div>
</section>
</Layout>
<style>
.teams-section {
padding: 40px 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
</style>

View File

@@ -0,0 +1,52 @@
---
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'
];
return slugs.map(slug => ({
params: { slug },
props: { slug }
}));
}
const { slug } = Astro.props;
// Placeholder content
const project = {
title: 'Web Design Project',
description: 'Project description...',
images: []
};
---
<Layout>
<section class="project-section">
<div class="container">
<h1>{project.title}</h1>
<p>{project.description}</p>
<!-- Images and details -->
</div>
</section>
</Layout>
<style>
.project-section {
padding: 40px 0;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
</style>

View File

@@ -0,0 +1,57 @@
---
import Layout from '../layouts/Layout.astro';
// Placeholder portfolios
const portfolios = [
{ slug: 'web-design-project-2', title: 'Project 2', description: 'Description...' },
// Add more
];
---
<Layout>
<section class="portfolio-section">
<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>
</div>
</section>
</Layout>
<style>
.portfolio-section {
padding: 40px 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
.portfolio-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.portfolio-item {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
}
.portfolio-item a {
text-decoration: none;
color: #333;
}
.portfolio-item a:hover {
color: #007bff;
}
</style>

View File

@@ -0,0 +1,79 @@
---
import Layout from '../../layouts/Layout.astro';
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'
];
return slugs.map(slug => ({
params: { slug },
props: { slug }
}));
}
const { slug } = Astro.props;
// Placeholder - would fetch category and posts from CMS
const category = {
name: 'Category Name',
posts: [
{ slug: 'post1', title: 'Post 1', date: '2023-01-01' }
]
};
---
<Layout>
<section class="category-section">
<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>
</div>
</section>
</Layout>
<style>
.category-section {
padding: 40px 0;
}
.container {
max-width: 1000px;
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 {
text-decoration: none;
color: #333;
}
.post-item a:hover {
color: #007bff;
}
</style>

View File

@@ -0,0 +1,66 @@
---
import Layout from '../../layouts/Layout.astro';
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
];
return slugs.map(slug => ({
params: { slug },
props: { slug }
}));
}
const { slug } = Astro.props;
// Placeholder content - would fetch from CMS
const post = {
title: 'Sample Post Title',
date: 'January 20, 2022',
content: 'Sample content...'
};
---
<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 -->
</div>
</article>
</div>
</section>
</Layout>
<style>
.post-section {
padding: 40px 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.post-date {
color: #666;
margin-bottom: 20px;
}
.post-content {
/* Prose styles handle typography */
}
</style>

View File

@@ -0,0 +1,114 @@
// Payload CMS Authentication Service
// Handles authentication with Payload CMS backend
// Get API base URL from wrangler.toml configuration
function getApiBaseUrl() {
// Check for environment-specific URLs from wrangler.toml
if (typeof process !== 'undefined' && process.env) {
return process.env.PAYLOAD_CMS_URL;
}
// Fallback for client-side or when process.env is not available
return import.meta.env.PUBLIC_PAYLOAD_CMS_URL || 'https://enchun-admin.anlstudio.cc';
}
const PAYLOAD_URL = getApiBaseUrl();
const PAYLOAD_API_KEY = import.meta.env.PAYLOAD_CMS_API_KEY;
export interface User {
id: string;
email: string;
role: 'admin' | 'editor';
}
export interface AuthResponse {
user: User;
token: string;
}
export class AuthService {
private token: string | null = null;
constructor() {
// Load token from localStorage or cookie on client
if (typeof window !== 'undefined') {
this.token = localStorage.getItem('payload-token');
}
}
async login(email: string, password: string): Promise<AuthResponse> {
const response = await fetch(`${PAYLOAD_URL}/api/users/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(PAYLOAD_API_KEY && { 'Authorization': `Bearer ${PAYLOAD_API_KEY}` }),
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
const data: AuthResponse = await response.json();
this.token = data.token;
// Store token
if (typeof window !== 'undefined') {
localStorage.setItem('payload-token', data.token);
}
return data;
}
async logout(): Promise<void> {
this.token = null;
if (typeof window !== 'undefined') {
localStorage.removeItem('payload-token');
}
// Optional: Call logout endpoint
try {
await fetch(`${PAYLOAD_URL}/api/users/logout`, {
method: 'POST',
headers: {
...(this.token && { 'Authorization': `Bearer ${this.token}` }),
},
});
} catch (error) {
// Ignore logout errors
}
}
async getCurrentUser(): Promise<User | null> {
if (!this.token) return null;
try {
const response = await fetch(`${PAYLOAD_URL}/api/users/me`, {
headers: {
'Authorization': `Bearer ${this.token}`,
},
});
if (!response.ok) {
this.token = null;
return null;
}
const data = await response.json();
return data.user;
} catch (error) {
this.token = null;
return null;
}
}
getToken(): string | null {
return this.token;
}
isAuthenticated(): boolean {
return !!this.token;
}
}
export const authService = new AuthService();

View File

@@ -1,3 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "./theme.css";
@import "tailwindcss";
@config "../../tailwind.config.mjs";

View File

@@ -0,0 +1,369 @@
/* Theme CSS Variables and Custom Styles */
/* CSS Custom Properties for Theme */
:root {
/* Color Palette */
--color-primary: #1F3A93;
--color-secondary: #F39C12;
--color-accent: #16A085;
--color-enchunblue: #3083BF;
--color-background: #FFFFFF;
--color-surface: #F7FAFC;
--color-text: #1A202C;
--color-text-muted: #718096;
--color-border: #E2E8F0;
/*
Purpose:
Define Enchun brand color palette as CSS custom properties for easy, semantic access in components.
Each color is named by its original key (minus 'www.enchun.tw/') in kebab-case for clarity and maintainability.
*/
/*
Purpose:
Define extended Enchun brand color palette as CSS custom properties, all prefixed with --color- for consistency and semantic clarity.
This ensures all color variables are easily discoverable and maintainable across the codebase.
*/
--color-alabaster: #fafafa;
--color-alto: #d1d1d1;
--color-amber: #ffc107;
--color-black: #000000;
--color-boston-blue: #3083bf;
--color-concrete: #f2f2f2;
--color-cream-can: #f6c456;
--color-dove-gray: #6b6b6b;
--color-dusty-gray: #999999;
--color-emperor: #4f4f4f;
--color-gray: #878787;
--color-killarney: #3c6f50;
--color-lucky-point: #171c61;
--color-manatee: #939494;
--color-mercury: #e3e3e3;
--color-mine-shaft: #333333;
--color-mine-shaft-60: #222222;
--color-nobel: #b6b6b6;
--color-oslo-gray: #939494;
--color-pomegranate: #f44336;
--color-silver: #bdbdbd;
--color-silver-chalice: #acacac;
--color-st-tropaz: #2b618f;
--color-tarawera: #062841;
--color-tropical-blue: #c7e4fa;
--color-tundora: #4d4d4d;
--color-turbo: #ffef00;
--color-valencia: #d84038;
--color-viking: #67aee1;
--color-white: #ffffff;
--color-wild-sand: #f6f6f6;
/* Typography */
--font-family-sans: 'Noto Sans CJK TC', 'Inter', system-ui, -apple-system, sans-serif;
--font-family-heading: 'Noto Sans CJK TC', 'Inter', system-ui, -apple-system, sans-serif;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border Radius */
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
}
/* Dark Theme (if needed in future) */
@media (prefers-color-scheme: dark) {
:root {
--color-background: #1A202C;
--color-surface: #2D3748;
--color-text: #F7FAFC;
--color-text-muted: #A0AEC0;
--color-border: #4A5568;
}
}
/* Base Styles */
* {
box-sizing: border-box;
}
html {
font-family: var(--font-family-sans);
line-height: 1.6;
color: var(--color-text);
background-color: var(--color-background);
}
body {
margin: 0;
padding: 0;
font-family: inherit;
line-height: inherit;
color: inherit;
background-color: inherit;
}
/* Typography Classes */
.text-gradient {
background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Animation Classes */
.fade-in {
animation: fadeIn var(--transition-normal) ease-in-out;
}
.slide-up {
animation: slideUp var(--transition-normal) ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Utility Classes */
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.shadow-custom {
box-shadow: var(--shadow-lg);
}
/* Component Specific Styles */
.nav-link {
position: relative;
transition: color var(--transition-fast);
}
.nav-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--color-primary);
transition: width var(--transition-fast);
}
.nav-link:hover::after {
width: 100%;
}
/* Active Navigation Link Indicator */
.nav-active {
position: relative;
}
.nav-active::after {
content: '';
position: absolute;
bottom: -2px;
left: 50%;
transform: translateX(-50%);
width: 70%;
height: 2px;
background: var(--color-secondary);
}
/* Prose/Markdown Styles */
.prose-custom {
color: var(--color-text);
font-family: var(--font-family-sans);
}
.prose-custom h1,
.prose-custom h2,
.prose-custom h3,
.prose-custom h4,
.prose-custom h5,
.prose-custom h6 {
color: var(--color-text);
font-weight: 700;
line-height: 1.2;
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-md);
}
.prose-custom h1 {
font-size: 2.25rem;
}
.prose-custom h2 {
font-size: 1.875rem;
}
.prose-custom h3 {
font-size: 1.5rem;
}
.prose-custom p {
margin-bottom: var(--spacing-md);
line-height: 1.7;
}
.prose-custom a {
color: var(--color-primary);
text-decoration: none;
transition: color var(--transition-fast);
}
.prose-custom a:hover {
color: #1a2f7a;
text-decoration: underline;
}
.prose-custom strong {
color: var(--color-text);
font-weight: 600;
}
.prose-custom em {
color: var(--color-text-muted);
}
.prose-custom ul,
.prose-custom ol {
margin-bottom: var(--spacing-md);
padding-left: var(--spacing-lg);
}
.prose-custom li {
margin-bottom: var(--spacing-xs);
line-height: 1.6;
}
.prose-custom blockquote {
border-left: 4px solid var(--color-primary);
padding-left: var(--spacing-md);
margin: var(--spacing-lg) 0;
color: var(--color-text-muted);
font-style: italic;
background: var(--color-surface);
padding: var(--spacing-md);
border-radius: var(--radius-md);
}
.prose-custom code {
background: var(--color-surface);
color: var(--color-text);
padding: 0.125rem 0.25rem;
border-radius: var(--radius-sm);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875em;
}
.prose-custom pre {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--spacing-md);
overflow-x: auto;
margin: var(--spacing-lg) 0;
}
.prose-custom pre code {
background: transparent;
padding: 0;
border-radius: 0;
}
.prose-custom hr {
border: 0;
border-top: 1px solid var(--color-border);
margin: var(--spacing-2xl) 0;
}
.prose-custom table {
width: 100%;
border-collapse: collapse;
margin: var(--spacing-lg) 0;
}
.prose-custom th,
.prose-custom td {
border: 1px solid var(--color-border);
padding: var(--spacing-sm) var(--spacing-md);
text-align: left;
}
.prose-custom th {
background: var(--color-surface);
font-weight: 600;
}
.prose-custom img {
max-width: 100%;
height: auto;
border-radius: var(--radius-md);
margin: var(--spacing-md) 0;
}
/* Button Styles */
.btn-primary {
background: var(--color-primary);
color: white;
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius-md);
border: none;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-primary:hover {
background: #1a2f7a;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-secondary {
background: var(--color-secondary);
color: white;
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius-md);
border: none;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-secondary:hover {
background: #e08e0b;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}