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
20 changes: 20 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import React, { useState, useEffect } from 'react';
import Navbar from './components/Navbar';
import Hero from './components/Hero';
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 Loading from './components/Loading';
import CookieConsent from './components/CookieConsent';

function App() {
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
// Simulate loading delay
const timer = setTimeout(() => {
setIsLoading(false);
}, 1500);
return () => clearTimeout(timer);
}, []);

if (isLoading) {
return <Loading />;
}

return (
<div className="app">
<Navbar />
Expand All @@ -17,9 +35,11 @@ function App() {
<Features />
<UseCases />
<Pricing />
<FAQ />
<CallToAction />
</main>
<Footer />
<CookieConsent />
</div>
);
}
Expand Down
12 changes: 8 additions & 4 deletions src/components/CallToAction.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { useI18n } from '../contexts/I18nContext';

function CallToAction() {
const { t } = useI18n();

return (
<section className="cta-banner" id="demo" aria-labelledby="cta-title">
<div className="cta-banner__bg" aria-hidden="true" />
<div className="container cta-banner__inner">
<h2 id="cta-title" className="cta-banner__title">
準備好提升你的業務效率了嗎?
{t('cta.title')}
</h2>
<p className="cta-banner__desc">
加入超過 500 家企業的行列,用 SalesPilot 讓你的銷售團隊如虎添翼。14 天免費試用,不需信用卡。
{t('cta.desc')}
</p>
<div className="cta-banner__actions">
<a href="#demo" className="btn btn--white btn--lg">
立即預約 Demo
{t('cta.btn_primary')}
</a>
<a href="#pricing" className="btn btn--outline-white btn--lg">
查看方案比較
{t('cta.btn_secondary')}
</a>
</div>
</div>
Expand Down
30 changes: 30 additions & 0 deletions src/components/CookieConsent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useState, useEffect } from 'react';
import { useI18n } from '../contexts/I18nContext';

const CookieConsent = () => {
const { t } = useI18n();
const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
const consent = localStorage.getItem('cookieConsent');
if (!consent) {
setIsVisible(true);
}
}, []);

const handleAccept = () => {
localStorage.setItem('cookieConsent', 'true');
setIsVisible(false);
};

if (!isVisible) return null;

return (
<div className="cookie-consent">
<p>{t('cookie.message')}</p>
<button onClick={handleAccept}>{t('cookie.accept')}</button>
</div>
);
};

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

const FAQItem = ({ question, answer }) => {
const [isOpen, setIsOpen] = useState(false);

return (
<div className={`faq-item ${isOpen ? 'open' : ''}`} onClick={() => setIsOpen(!isOpen)}>
<div className="faq-question">
{question}
<span className="faq-toggle">{isOpen ? '-' : '+'}</span>
</div>
{isOpen && <div className="faq-answer">{answer}</div>}
</div>
);
};

const FAQ = () => {
const { t } = useI18n();
const questions = [
{ q: t('faq.q1'), a: t('faq.a1') },
{ q: t('faq.q2'), a: t('faq.a2') },
{ q: t('faq.q3'), a: t('faq.a3') },
];

return (
<section className="faq-section">
<div className="container">
<h2>{t('faq.title')}</h2>
<div className="faq-list">
{questions.map((item, index) => (
<FAQItem key={index} question={item.q} answer={item.a} />
))}
</div>
</div>
</section>
);
};

export default FAQ;
20 changes: 15 additions & 5 deletions src/components/Features.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { FEATURES } from '../data/features';
import { useI18n } from '../contexts/I18nContext';

function Features() {
const { t } = useI18n();
const featuresList = t('features.list');

// Merge static icons with localized text
const features = Array.isArray(featuresList) ? featuresList.map((f, i) => ({
...f,
icon: FEATURES[i]?.icon // Fallback to icon from data if order matches
})) : [];

return (
<section className="features" id="features" aria-labelledby="features-title">
<div className="container">
<div className="section-header">
<span className="section-header__badge">核心功能</span>
<span className="section-header__badge">{t('nav.features')}</span>
<h2 id="features-title" className="section-header__title">
一個平台,解決所有銷售挑戰
{t('features.title')}
</h2>
<p className="section-header__desc">
從管線管理到數據分析,SalesPilot 涵蓋業務團隊日常所需的每一項功能。
{t('features.desc')}
</p>
</div>

<div className="features__grid" role="list">
{FEATURES.map((feature) => (
{features.map((feature) => (
<article
key={feature.id}
className="features__card"
Expand All @@ -25,7 +35,7 @@ function Features() {
{feature.icon}
</div>
<h3 className="features__title">{feature.title}</h3>
<p className="features__desc">{feature.description}</p>
<p className="features__desc">{feature.desc}</p>
</article>
))}
</div>
Expand Down
22 changes: 13 additions & 9 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FOOTER_DATA } from '../data/footer';
import { useI18n } from '../contexts/I18nContext';

const SOCIAL_ICONS = {
fb: (
Expand All @@ -24,7 +25,10 @@ const SOCIAL_ICONS = {
};

function Footer() {
const { brand, columns, social, copyright } = FOOTER_DATA;
const { t } = useI18n();
const { brand, social } = FOOTER_DATA;
const footerData = t('footer');
const columns = footerData.cols || [];

return (
<footer className="footer" role="contentinfo">
Expand All @@ -35,7 +39,7 @@ function Footer() {
<span className="footer__logo-icon" aria-hidden="true">◆</span>
{brand.name}
</a>
<p className="footer__brand-desc">{brand.description}</p>
<p className="footer__brand-desc">{footerData.brandDesc}</p>
<div className="footer__social" role="list" aria-label="社群媒體連結">
{social.map((s) => (
<a
Expand All @@ -52,14 +56,14 @@ function Footer() {
</div>

<div className="footer__columns">
{columns.map((col) => (
<div key={col.title} className="footer__column">
{columns.map((col, i) => (
<div key={i} className="footer__column">
<h3 className="footer__column-title">{col.title}</h3>
<ul className="footer__column-list">
{col.links.map((link) => (
<li key={link.label}>
<a href={link.href} className="footer__column-link">
{link.label}
{col.links.map((link, j) => (
<li key={j}>
<a href="#" className="footer__column-link">
{link}
</a>
</li>
))}
Expand All @@ -70,7 +74,7 @@ function Footer() {
</div>

<div className="footer__bottom">
<p className="footer__copyright">{copyright}</p>
<p className="footer__copyright">{footerData.copyright}</p>
</div>
</div>
</footer>
Expand Down
25 changes: 15 additions & 10 deletions src/components/Hero.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { HERO_CONTENT } from '../data/hero';
import { useI18n } from '../contexts/I18nContext';

function Hero() {
const { title, subtitle, stats, primaryCta, secondaryCta } = HERO_CONTENT;
const { t, locale } = useI18n();
const { primaryCta, secondaryCta } = HERO_CONTENT; // Keep hrefs from data file if needed, or move them too.

// Get structured data from locales
const stats = t('hero.stats');

return (
<section className="hero" aria-labelledby="hero-title">
Expand All @@ -13,26 +18,26 @@ function Hero() {
<div className="hero__inner container">
<div className="hero__content">
<h1 id="hero-title" className="hero__title">
{title.split('\n').map((line, i) => (
{t('hero.title').split('\n').map((line, i) => (
<span key={i}>
{line}
{i === 0 && <br />}
</span>
))}
</h1>
<p className="hero__subtitle">{subtitle}</p>
<p className="hero__subtitle">{t('hero.subtitle')}</p>

<div className="hero__actions">
<a href={primaryCta.href} className="btn btn--primary btn--lg">
{primaryCta.label}
{t('hero.cta')}
</a>
<a href={secondaryCta.href} className="btn btn--outline btn--lg">
{secondaryCta.label}
{t('hero.secondaryCta')}
</a>
</div>

<div className="hero__stats" role="list" aria-label="關鍵成效數據">
{stats.map((stat) => (
{Array.isArray(stats) && stats.map((stat) => (
<div key={stat.label} className="hero__stat" role="listitem">
<span className="hero__stat-value">{stat.value}</span>
<span className="hero__stat-label">{stat.label}</span>
Expand All @@ -58,22 +63,22 @@ function Hero() {
<div className="hero__mockup-content">
<div className="hero__mockup-pipeline">
<div className="hero__mockup-col">
<div className="hero__mockup-col-header">初步接觸</div>
<div className="hero__mockup-col-header">{t('hero.mockup.col1')}</div>
<div className="hero__mockup-card hero__mockup-card--blue" />
<div className="hero__mockup-card hero__mockup-card--blue" />
</div>
<div className="hero__mockup-col">
<div className="hero__mockup-col-header">需求確認</div>
<div className="hero__mockup-col-header">{t('hero.mockup.col2')}</div>
<div className="hero__mockup-card hero__mockup-card--purple" />
</div>
<div className="hero__mockup-col">
<div className="hero__mockup-col-header">報價中</div>
<div className="hero__mockup-col-header">{t('hero.mockup.col3')}</div>
<div className="hero__mockup-card hero__mockup-card--amber" />
<div className="hero__mockup-card hero__mockup-card--amber" />
<div className="hero__mockup-card hero__mockup-card--amber" />
</div>
<div className="hero__mockup-col">
<div className="hero__mockup-col-header">成交 🎉</div>
<div className="hero__mockup-col-header">{t('hero.mockup.col4')}</div>
<div className="hero__mockup-card hero__mockup-card--green" />
</div>
</div>
Expand Down
15 changes: 15 additions & 0 deletions src/components/Loading.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { useI18n } from '../contexts/I18nContext';

const Loading = () => {
const { t } = useI18n();

return (
<div className="loading-container">
<div className="spinner"></div>
<p>{t('loading.text')}</p>
</div>
);
};

export default Loading;
29 changes: 25 additions & 4 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { useState } from 'react';
import { useTheme } from '../contexts/ThemeContext';
import { useI18n } from '../contexts/I18nContext';
import { NAV_LINKS, BRAND } from '../data/navigation';

function Navbar() {
const [menuOpen, setMenuOpen] = useState(false);
const { theme, toggleTheme } = useTheme();
const { locale, toggleLocale, t } = useI18n();

// Map href to translation key suffix
const getLabel = (href) => {
if (href.includes('features')) return t('nav.features');
if (href.includes('pricing')) return t('nav.pricing');
if (href.includes('use-cases')) return t('nav.useCases');
if (href.includes('social-proof')) return t('nav.socialProof');
return '';
};

return (
<header className="navbar" role="banner">
Expand Down Expand Up @@ -34,14 +47,22 @@ function Navbar() {
{NAV_LINKS.map((link) => (
<li key={link.href} className="navbar__item">
<a href={link.href} className="navbar__link" onClick={() => setMenuOpen(false)}>
{link.label}
{getLabel(link.href)}
</a>
</li>
))}
</ul>
<a href="#demo" className="btn btn--primary btn--sm navbar__cta">
預約 Demo
</a>
<div className="navbar__actions">
<button className="btn-icon" onClick={toggleTheme} aria-label="Toggle Theme">
{theme === 'light' ? '🌙' : '☀️'}
</button>
<button className="btn-icon" onClick={toggleLocale} aria-label="Toggle Language">
{locale === 'zh' ? 'EN' : '中'}
</button>
<a href="#demo" className="btn btn--primary btn--sm navbar__cta">
{t('nav.getStarted')}
</a>
</div>
</nav>
</div>
</header>
Expand Down
Loading