Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions .agent/skills/git-pr-description/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,21 @@ git diff master..HEAD

## ⚠️ 修改的內容

依模組 / 元件分組列出改動:

### [元件或模組名稱]
- 具體改了什麼、為什麼這樣改

### [另一個元件或模組名稱]
- 具體改了什麼
依功能與需求分組:
- **功能名稱 / 需求項目**:說明此組變更的業務目標
- **修改方向**:簡述(效能、修復、樣式等)
- **內容**:列出具體修改點,**禁止**出現任何檔案路徑(包含相對路徑),一律改用功能描述,例如「新增手風琴展開動畫」而非「修改 `src/components/FAQ.jsx`」

### [功能名稱 / 需求項目]
- **修改方向**:...
- **內容**:
- 具體修改點 1(純功能描述)
- 具體修改點 2(純功能描述)

### [另一個功能名稱]
- **修改方向**:...
- **內容**:
- ...

## 🧪 測試步驟

Expand Down Expand Up @@ -150,9 +158,16 @@ git diff master..HEAD
- 不要在 code block 外面加額外的 `📝 PR Title:` 等前綴,直接輸出可複製的 markdown
- code block 內的第一行為 PR Title,空一行後接 Description
- 使用者可要求調整任何部分後再複製使用
- **重要**:Description 中**禁止**出現任何檔案路徑(包含相對路徑),一律改用純功能描述。

---

## 🛑 格式嚴格規範

- **禁止任何 Markdown 連結格式**:`[文字](...)`
- **禁止任何 URI / scheme**:比如 `file://`、`cci:`
- **禁止出現任何檔案路徑**:不論相對或絕對路徑,一律不出現在 Description 中,改以純功能描述取代

## 邊界情況處理

- **存在未提交的變更**:提醒使用者先提交或 stash,避免遺漏
Expand Down
22 changes: 16 additions & 6 deletions .agent/skills/git-pr-description/references/pr-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@

## ⚠️ 修改的內容

<!-- 依模組或元件分組,每組列出:改了什麼、為什麼這樣改 -->

### [元件 / 模組名稱]
<!--
依功能或需求分組,每組列出:修改方向、具體內容
規則:
- 禁止任何 Markdown 連結格式:[文字](...)
- 禁止任何 URI / scheme:比如 file://、cci:
- 禁止出現任何檔案路徑(包含相對路徑),一律改用純功能描述
-->

- 變更說明
### [功能名稱 / 需求項目]
- **修改方向**:簡述調整目的 (例如:優化效能、修復邏輯錯誤、調整樣式)
- **內容**:
- 具體修改點 1(純功能描述)
- 具體修改點 2(純功能描述)

### [元件 / 模組名稱]
### [另一個功能名稱]
- **修改方向**:...
- **內容**:
- ...

- 變更說明

## 🧪 測試步驟

Expand Down
4 changes: 4 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import SocialProof from './components/SocialProof';
import Features from './components/Features';
import UseCases from './components/UseCases';
import Pricing from './components/Pricing';
import FAQ from './components/FAQ';
import CallToAction from './components/CallToAction';
import Footer from './components/Footer';
import CookieConsent from './components/CookieConsent';

function App() {
return (
Expand All @@ -17,9 +19,11 @@ function App() {
<Features />
<UseCases />
<Pricing />
<FAQ />
<CallToAction />
</main>
<Footer />
<CookieConsent />
</div>
);
}
Expand Down
60 changes: 60 additions & 0 deletions src/components/CookieConsent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState, useEffect } from 'react';

const CookieConsent = () => {
const [showBanner, setShowBanner] = useState(false);
const [animateIn, setAnimateIn] = useState(false);

useEffect(() => {
const consent = localStorage.getItem('cookie-consent');
if (!consent) {
setShowBanner(true);
// Small delay to allow browser to paint before adding class for transition
setTimeout(() => setAnimateIn(true), 50);
}
}, []);

const handleAccept = (type) => {
localStorage.setItem('cookie-consent', type);
setAnimateIn(false); // Triggers exit animation

// Wait for animation to finish before removing from DOM
setTimeout(() => {
setShowBanner(false);
}, 400);
};

if (!showBanner) return null;

return (
<div className={`cookie-consent ${animateIn ? 'cookie-consent--visible' : ''}`}>
<div className="container">
<div className="cookie-consent__inner">
<div className="cookie-consent__content">
<h3 className="cookie-consent__title">我們重視您的隱私</h3>
<p className="cookie-consent__text">
我們使用 Cookie 來改善您的瀏覽體驗、提供個人化內容並分析網站流量。
點擊「接受所有」即表示您同意我們使用 Cookie。
<a href="#" className="cookie-consent__link">了解更多</a>
</p>
</div>
<div className="cookie-consent__actions">
<button
className="btn btn--outline-white btn--sm"
onClick={() => handleAccept('necessary')}
>
僅必要
</button>
<button
className="btn btn--primary btn--sm"
onClick={() => handleAccept('accepted')}
>
接受所有
</button>
</div>
</div>
</div>
</div>
);
};

export default CookieConsent;
90 changes: 90 additions & 0 deletions src/components/FAQ.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useState } from 'react';

const FAQ_DATA = [
{
id: 1,
question: "SalesPilot CRM 與其他工具有什麼不同?",
answer: "SalesPilot 專為高成長團隊設計。不同於傳統 CRM 笨重且難以使用,我們專注於自動化、速度以及美觀的使用者介面,讓您的團隊真正享受使用的過程。"
},
{
id: 2,
question: "有提供免費試用嗎?",
answer: "有的!我們提供 14 天免費試用,您可以完整體驗所有 Pro 功能。無需綁定信用卡即可開始使用,讓您無風險地探索我們的平台。"
},
{
id: 3,
question: "我可以從目前的 CRM 匯入資料嗎?",
answer: "當然可以。我們提供 CSV 和 Excel 檔案的一鍵匯入工具。此外,我們也提供 Salesforce、HubSpot 和 Pipedrive 的直接遷移工具,讓切換過程無縫接軌。"
},
{
id: 4,
question: "我的資料安全嗎?",
answer: "安全性是我們的首要考量。我們使用企業級 AES-256 加密技術保護靜態資料,並使用 TLS 1.3 保護傳輸中的資料。我們符合 SOC 2 Type II 標準,並定期進行安全稽核。"
},
{
id: 5,
question: "你們有提供與其他工具的整合嗎?",
answer: "是的,SalesPilot 可與超過 2,000+ 個應用程式連接。我們與 Slack、Gmail、Outlook、Zoom 和 Stripe 有原生整合,並完全支援 Zapier,以自動化您的整個工作流程。"
},
{
id: 6,
question: "如果我需要升級或降級方案怎麼辦?",
answer: "您可以隨時從後台儀表板更改您的方案。費用將根據使用天數自動按比例調整,您只需支付實際使用的部分。"
}
];

const FAQ = () => {
const [openItems, setOpenItems] = useState([]);

const toggleItem = (id) => {
setOpenItems(prev => {
if (prev.includes(id)) {
return prev.filter(item => item !== id);
} else {
return [...prev, id];
}
});
};

return (
<section className="faq section" id="faq">
<div className="container">
<div className="section-header">
<span className="section-header__badge">支援中心</span>
<h2 className="section-header__title">常見問答</h2>
<p className="section-header__desc">關於 SalesPilot CRM 的所有疑問。找不到您需要的答案嗎?歡迎與我們友善的團隊聯繫。</p>
</div>

<div className="faq__grid">
{FAQ_DATA.map((item) => {
const isOpen = openItems.includes(item.id);
return (
<div
key={item.id}
className={`faq__item ${isOpen ? 'faq__item--open' : ''}`}
>
<button
className="faq__question"
onClick={() => toggleItem(item.id)}
aria-expanded={isOpen}
>
<span className="faq__question-text">{item.question}</span>
<span className="faq__icon">
{isOpen ? '−' : '+'}
</span>
</button>
<div className="faq__answer-wrapper">
<div className="faq__answerWrapperInner">
<p className="faq__answer">{item.answer}</p>
</div>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};

export default FAQ;
19 changes: 15 additions & 4 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useState } from 'react';
import { useTheme } from '../context/ThemeContext';
import { NAV_LINKS, BRAND } from '../data/navigation';

function Navbar() {
const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false);
const { theme, toggleTheme } = useTheme();

return (
<header className="navbar" role="banner">
Expand Down Expand Up @@ -39,9 +41,18 @@ function Navbar() {
</li>
))}
</ul>
<a href="#demo" className="btn btn--primary btn--sm navbar__cta">
預約 Demo
</a>
<div className="navbar__actions">
<button
className="theme-toggle"
onClick={toggleTheme}
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
<a href="#demo" className="btn btn--primary btn--sm navbar__cta">
預約 Demo
</a>
</div>
</nav>
</div>
</header>
Expand Down
44 changes: 44 additions & 0 deletions src/context/ThemeContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { createContext, useContext, useEffect, useState } from 'react';

const ThemeContext = createContext();

export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};

export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(() => {
// Check localStorage first
const storedTheme = localStorage.getItem('theme');
if (storedTheme) {
return storedTheme;
}
// Fallback to system preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
return 'light';
}
return 'dark';
});

useEffect(() => {
const root = document.documentElement;
// Set the data-theme attribute
root.setAttribute('data-theme', theme);
// Persist to localStorage
localStorage.setItem('theme', theme);
}, [theme]);

const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
1 change: 1 addition & 0 deletions src/data/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const NAV_LINKS = [
{ label: '應用場景', href: '#use-cases' },
{ label: '方案價格', href: '#pricing' },
{ label: '客戶見證', href: '#social-proof' },
{ label: '常見問答', href: '#faq' },
];

export const BRAND = {
Expand Down
Loading