랜딩 페이지: D3RO 브랜드 프로모션 사이트 + GitHub Pages 배포
- Vite + React 19 + Tailwind CSS - 4가지 디자인 레퍼런스 통합 (CRT 스크린, 크로스헤어, 노이즈, 인스트루먼트) - 섹션 9개: Hero, Features, HowItWorks, Privacy, Pricing, FAQ, CTA, Header, Footer - 재사용 컴포넌트 8개 (Badge, Container, InstrumentCard, GlowButton 등) - i18n 10개 국어 (en/ko/ja/zh/es/fr/de/pt/ru/vi) - GitHub Pages 자동 배포 워크플로우
This commit is contained in:
parent
eb83682269
commit
5892125740
45 changed files with 6225 additions and 0 deletions
40
site/src/sections/CTA.tsx
Normal file
40
site/src/sections/CTA.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Container } from '../components/Container'
|
||||
import { Crosshair } from '../components/Crosshair'
|
||||
import { WaveBars } from '../components/WaveBars'
|
||||
import { GlowButton } from '../components/GlowButton'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export function CTA() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<section className="relative py-24 md:py-32">
|
||||
{/* Background glow */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[400px] bg-brand-amber/[0.03] rounded-full blur-[120px] pointer-events-none" />
|
||||
|
||||
<Container className="relative text-center">
|
||||
<Crosshair className="inline-block mb-8">
|
||||
<WaveBars className="h-8 px-2" />
|
||||
</Crosshair>
|
||||
|
||||
<h2 className="font-display text-display font-bold text-neutral-50 mb-6 max-w-3xl mx-auto">
|
||||
{t.cta.title1} <span className="text-brand-amber">{t.cta.title2}</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-base md:text-lg text-neutral-400 max-w-xl mx-auto mb-10 leading-relaxed">
|
||||
{t.cta.subtitle1}
|
||||
<br />
|
||||
{t.cta.subtitle2}
|
||||
</p>
|
||||
|
||||
<GlowButton href="https://github.com/user/D3ROVoice/releases" variant="primary" size="lg">
|
||||
{t.cta.downloadBtn}
|
||||
</GlowButton>
|
||||
|
||||
<p className="font-mono text-nano text-neutral-600 mt-8 uppercase tracking-widest">
|
||||
{t.cta.systemReq}
|
||||
</p>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
68
site/src/sections/FAQ.tsx
Normal file
68
site/src/sections/FAQ.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { useState } from 'react'
|
||||
import { SectionHeader } from '../components/SectionHeader'
|
||||
import { Container } from '../components/Container'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export function FAQ() {
|
||||
const { t } = useI18n()
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null)
|
||||
|
||||
return (
|
||||
<section id="faq" className="relative py-24 md:py-32">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface-800/30 to-transparent pointer-events-none" />
|
||||
|
||||
<Container className="relative max-w-3xl">
|
||||
<SectionHeader
|
||||
index={t.faq.index}
|
||||
title={t.faq.title}
|
||||
/>
|
||||
|
||||
<div className="space-y-px">
|
||||
{t.faq.items.map((faq, i) => {
|
||||
const isOpen = openIndex === i
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="border-b border-white/[0.04]"
|
||||
>
|
||||
<button
|
||||
onClick={() => setOpenIndex(isOpen ? null : i)}
|
||||
className="w-full flex items-start justify-between gap-4 py-5 text-left group"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="font-mono text-nano text-neutral-600 mt-1 flex-shrink-0">
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-neutral-200 group-hover:text-white transition-colors">
|
||||
{faq.q}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-neutral-500 flex-shrink-0 mt-1 transition-transform duration-200 ${
|
||||
isOpen ? 'rotate-45' : ''
|
||||
}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className={`accordion-content ${isOpen ? 'open' : ''}`}>
|
||||
<div>
|
||||
<p className="pl-10 pb-5 text-sm text-neutral-400 leading-relaxed">
|
||||
{faq.a}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
166
site/src/sections/Features.tsx
Normal file
166
site/src/sections/Features.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { SectionHeader } from '../components/SectionHeader'
|
||||
import { InstrumentCard } from '../components/InstrumentCard'
|
||||
import { Container } from '../components/Container'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
interface FeatureMeta {
|
||||
icon: ReactNode
|
||||
tag: 'FREE' | 'PRO' | 'PRO+'
|
||||
}
|
||||
|
||||
const featureMeta: FeatureMeta[] = [
|
||||
{ icon: <MicIcon />, tag: 'FREE' },
|
||||
{ icon: <SparklesIcon />, tag: 'FREE' },
|
||||
{ icon: <LanguageIcon />, tag: 'FREE' },
|
||||
{ icon: <CaptionIcon />, tag: 'PRO' },
|
||||
{ icon: <ConversationIcon />, tag: 'PRO+' },
|
||||
{ icon: <FileIcon />, tag: 'PRO+' },
|
||||
{ icon: <ChainIcon />, tag: 'PRO' },
|
||||
{ icon: <ContextIcon />, tag: 'PRO' },
|
||||
{ icon: <MemoIcon />, tag: 'PRO' },
|
||||
]
|
||||
|
||||
const tagStyles = {
|
||||
FREE: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20',
|
||||
PRO: 'text-brand-amber bg-brand-amber/10 border-brand-amber/20',
|
||||
'PRO+': 'text-violet-400 bg-violet-500/10 border-violet-500/20',
|
||||
} as const
|
||||
|
||||
export function Features() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<section id="features" className="relative py-24 md:py-32">
|
||||
<Container>
|
||||
<SectionHeader
|
||||
index={t.features.index}
|
||||
title={t.features.title}
|
||||
subtitle={t.features.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{t.features.items.map((item, i) => {
|
||||
const meta = featureMeta[i]
|
||||
return (
|
||||
<InstrumentCard key={i}>
|
||||
{/* Top row: icon + tag */}
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="w-10 h-10 rounded-card bg-surface-400 border border-white/[0.06] flex items-center justify-center text-brand-amber">
|
||||
{meta.icon}
|
||||
</div>
|
||||
<span className={`text-nano font-mono font-semibold tracking-widest uppercase px-2 py-0.5 border rounded-sm ${tagStyles[meta.tag]}`}>
|
||||
{meta.tag}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="font-display text-lg font-semibold text-neutral-100 mb-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-400 leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
|
||||
{/* Bottom arrow */}
|
||||
<div className="mt-5 flex justify-end">
|
||||
<div className="w-7 h-7 rounded-full border border-white/[0.06] flex items-center justify-center text-neutral-500 group-hover:text-brand-amber group-hover:border-brand-amber/20 transition-colors">
|
||||
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="7" y1="17" x2="17" y2="7" />
|
||||
<polyline points="7 7 17 7 17 17" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</InstrumentCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ── Inline SVG Icons ─────────────────────────────────── */
|
||||
|
||||
function MicIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="9" y="2" width="6" height="11" rx="3" />
|
||||
<path d="M5 10a7 7 0 0 0 14 0" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SparklesIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function LanguageIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 8l6 6" /><path d="M4 14l6-6 2-3" /><path d="M2 5h12" /><path d="M7 2h1" />
|
||||
<path d="M22 22l-5-10-5 10" /><path d="M14 18h6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CaptionIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
<path d="M7 15h4" /><path d="M13 15h4" /><path d="M7 11h10" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ConversationIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function FileIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ChainIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MemoIcon() {
|
||||
return (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
80
site/src/sections/Footer.tsx
Normal file
80
site/src/sections/Footer.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Container } from '../components/Container'
|
||||
import { Led } from '../components/Led'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
const techStack = [
|
||||
'Electron', 'React 19', 'faster-whisper', 'Ollama',
|
||||
'better-sqlite3', 'TypeScript', 'uiohook-napi',
|
||||
]
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<footer className="border-t border-white/[0.04] py-12 md:py-16">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
|
||||
{/* Left: brand */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Led color="amber" size="sm" />
|
||||
<span className="font-mono text-sm font-semibold tracking-tight text-neutral-300">
|
||||
D3RO·VOICE
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 leading-relaxed max-w-sm">
|
||||
{t.footer.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right: links */}
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-3 md:justify-end">
|
||||
<a href="https://github.com/user/D3ROVoice" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-1.5">
|
||||
<GithubIcon />
|
||||
GitHub
|
||||
</a>
|
||||
<a href="#features" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
|
||||
{t.nav.features}
|
||||
</a>
|
||||
<a href="#pricing" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
|
||||
{t.nav.pricing}
|
||||
</a>
|
||||
<a href="#faq" className="font-mono text-nano uppercase tracking-widest text-neutral-500 hover:text-neutral-300 transition-colors">
|
||||
{t.nav.faq}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tech stack bar */}
|
||||
<div className="flex flex-wrap items-center gap-2 mb-8">
|
||||
{techStack.map((tech) => (
|
||||
<span
|
||||
key={tech}
|
||||
className="font-mono text-nano uppercase tracking-widest text-neutral-600 bg-surface-700/50 px-2.5 py-1 rounded-sm border border-white/[0.03]"
|
||||
>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 pt-6 border-t border-white/[0.04]">
|
||||
<span className="font-mono text-nano text-neutral-600 uppercase tracking-widest">
|
||||
{t.footer.copyright.replace('{year}', String(new Date().getFullYear()))}
|
||||
</span>
|
||||
<span className="font-mono text-nano text-neutral-600 uppercase tracking-widest">
|
||||
{t.footer.builtWith}
|
||||
</span>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
function GithubIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
96
site/src/sections/Header.tsx
Normal file
96
site/src/sections/Header.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Led } from '../components/Led'
|
||||
import { LanguageSwitcher } from '../components/LanguageSwitcher'
|
||||
import { navItems } from '../tokens'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export function Header() {
|
||||
const { t } = useI18n()
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 40)
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
return () => window.removeEventListener('scroll', onScroll)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 mix-blend-difference ${
|
||||
scrolled ? 'backdrop-blur-xl' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-5 md:px-8 lg:px-12 h-16 flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<a href="#" className="flex items-center gap-2 group">
|
||||
<Led color="amber" pulse size="md" />
|
||||
<span className="font-mono text-sm font-semibold tracking-tight text-white">
|
||||
D3RO<span className="text-neutral-400">·</span>VOICE
|
||||
</span>
|
||||
</a>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
{navItems.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="font-mono text-nano uppercase tracking-widest text-neutral-400 hover:text-white transition-colors"
|
||||
>
|
||||
{t.nav[link.key]}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Right side: language + CTA */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<LanguageSwitcher />
|
||||
<a
|
||||
href="https://github.com/user/D3ROVoice/releases"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber/90 rounded-panel hover:bg-brand-amber transition-colors"
|
||||
>
|
||||
{t.nav.download}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
className="md:hidden w-9 h-9 flex items-center justify-center"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<span className={`block w-5 h-px bg-white transition-all ${mobileOpen ? 'rotate-45 translate-y-[3.5px]' : ''}`} />
|
||||
<span className={`block w-5 h-px bg-white transition-all ${mobileOpen ? '-rotate-45 -translate-y-[3.5px]' : ''}`} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Nav */}
|
||||
{mobileOpen && (
|
||||
<div className="md:hidden bg-surface-900/95 backdrop-blur-xl border-t border-white/[0.06] px-5 py-6 space-y-1">
|
||||
{navItems.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="block font-mono text-xs uppercase tracking-widest text-neutral-400 hover:text-brand-amber py-3 border-b border-white/[0.03]"
|
||||
>
|
||||
{t.nav[link.key]}
|
||||
</a>
|
||||
))}
|
||||
<div className="flex items-center justify-between mt-4 gap-3">
|
||||
<LanguageSwitcher />
|
||||
<a
|
||||
href="https://github.com/user/D3ROVoice/releases"
|
||||
className="flex-1 text-center px-5 py-2.5 font-mono text-xs uppercase tracking-wider text-white bg-brand-amber rounded-panel"
|
||||
>
|
||||
{t.nav.download}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
100
site/src/sections/Hero.tsx
Normal file
100
site/src/sections/Hero.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { Badge } from '../components/Badge'
|
||||
import { Crosshair } from '../components/Crosshair'
|
||||
import { DataPoint } from '../components/DataPoint'
|
||||
import { Container } from '../components/Container'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
const CJK_LOCALES = new Set(['ko', 'ja', 'zh'])
|
||||
|
||||
export function Hero() {
|
||||
const { t, locale } = useI18n()
|
||||
const isCJK = CJK_LOCALES.has(locale)
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden bg-grid pt-16 pb-12">
|
||||
{/* Background effects */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/3 left-1/2 -translate-x-1/2 w-[700px] h-[700px] rounded-full bg-brand-amber/[0.025] blur-[140px]" />
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-brand-amber/15 to-transparent" />
|
||||
</div>
|
||||
|
||||
<Container className="relative flex-1 flex flex-col justify-center">
|
||||
{/* Meta corners */}
|
||||
<div className="hidden lg:block absolute top-8 left-5 md:left-8 lg:left-12">
|
||||
<Badge led ledColor="green">{t.hero.systemStatus}</Badge>
|
||||
</div>
|
||||
<div className="hidden lg:block absolute top-8 right-5 md:right-8 lg:right-12 font-mono text-nano uppercase tracking-widest text-neutral-500">
|
||||
{t.hero.protocol}
|
||||
</div>
|
||||
|
||||
{/* Main hero content */}
|
||||
<div className="max-w-5xl pt-12 md:pt-0">
|
||||
<Crosshair className="inline-block mb-8">
|
||||
<Badge led>{t.hero.badge}</Badge>
|
||||
</Crosshair>
|
||||
|
||||
<h1
|
||||
className="font-display text-hero font-bold text-neutral-50 mb-6 max-w-4xl"
|
||||
style={isCJK ? { lineHeight: 1.15 } : undefined}
|
||||
>
|
||||
<span className="block">{t.hero.title1}</span>
|
||||
<span className="block">{t.hero.title2}</span>
|
||||
<span className="block text-brand-amber">{t.hero.title3}</span>
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl text-base md:text-lg text-neutral-400 leading-relaxed mb-10">
|
||||
{t.hero.subtitle}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-start gap-4 mb-16">
|
||||
<a
|
||||
href="https://github.com/user/D3ROVoice/releases"
|
||||
className="glow-btn inline-flex items-center gap-2 px-8 py-4 font-mono text-sm font-medium uppercase tracking-wider text-white bg-brand-amber rounded-panel"
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<DownloadIcon />
|
||||
{t.hero.downloadBtn}
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
href="#features"
|
||||
className="inline-flex items-center gap-2 px-8 py-4 font-mono text-sm uppercase tracking-wider text-neutral-400 border border-white/[0.08] rounded-panel hover:border-white/[0.15] hover:text-neutral-200 transition-all"
|
||||
>
|
||||
{t.hero.viewFeatures}
|
||||
<ArrowIcon />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data strip */}
|
||||
<div className="border-t border-white/[0.06] pt-8 mt-auto">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
<DataPoint label={t.hero.dataProcessing} value="100%" unit="local" />
|
||||
<DataPoint label={t.hero.dataCloudTraffic} value="0" unit="bytes" />
|
||||
<DataPoint label={t.hero.dataLatency} value="<2s" />
|
||||
<DataPoint label={t.hero.dataPrivacy} value="10/10" />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function DownloadIcon() {
|
||||
return (
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ArrowIcon() {
|
||||
return (
|
||||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="7" y1="17" x2="17" y2="7" />
|
||||
<polyline points="7 7 17 7 17 17" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
112
site/src/sections/HowItWorks.tsx
Normal file
112
site/src/sections/HowItWorks.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { SectionHeader } from '../components/SectionHeader'
|
||||
import { Crosshair } from '../components/Crosshair'
|
||||
import { Container } from '../components/Container'
|
||||
import { Led } from '../components/Led'
|
||||
import { WaveBars } from '../components/WaveBars'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export function HowItWorks() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<section id="pipeline" className="relative py-24 md:py-32">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-surface-800/40 to-transparent pointer-events-none" />
|
||||
|
||||
<Container className="relative">
|
||||
<SectionHeader
|
||||
index={t.pipeline.index}
|
||||
title={t.pipeline.title}
|
||||
subtitle={t.pipeline.subtitle}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-start">
|
||||
{/* Left: visual panel */}
|
||||
<Crosshair className="order-2 lg:order-1">
|
||||
<div className="crt-screen rounded-card p-6 md:p-8 aspect-[4/3] flex flex-col justify-between">
|
||||
<div className="relative z-10 flex items-center justify-between mb-6">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60">
|
||||
{t.pipeline.panelTitle}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-500">{t.pipeline.panelActive}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex-1 flex flex-col justify-center space-y-4">
|
||||
{/* Simulated terminal output */}
|
||||
<div className="font-mono text-xs text-neutral-500 leading-relaxed space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" size="sm" />
|
||||
<span className="text-emerald-400/70">{t.pipeline.sttReady}</span>
|
||||
<span className="text-neutral-600">faster-whisper base</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" size="sm" />
|
||||
<span className="text-emerald-400/70">{t.pipeline.llmConnected}</span>
|
||||
<span className="text-neutral-600">qwen3:4b @ localhost:11434</span>
|
||||
</div>
|
||||
<div className="h-px bg-white/[0.04] my-3" />
|
||||
<div className="flex items-start gap-2">
|
||||
<Led color="amber" size="sm" pulse className="mt-1" />
|
||||
<div>
|
||||
<span className="text-brand-amber/50 block mb-1">{t.pipeline.transcription}</span>
|
||||
<span className="text-neutral-300">"{t.pipeline.sampleInput}"</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Led color="green" size="sm" className="mt-1" />
|
||||
<div>
|
||||
<span className="text-emerald-400/50 block mb-1">{t.pipeline.aiPolish}</span>
|
||||
<span className="text-neutral-300">"{t.pipeline.sampleOutput}"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex items-center justify-between pt-4 border-t border-white/[0.04]">
|
||||
<WaveBars className="h-6" />
|
||||
<span className="font-mono text-nano text-neutral-600">{t.pipeline.panelLatency}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Crosshair>
|
||||
|
||||
{/* Right: numbered steps */}
|
||||
<div className="order-1 lg:order-2 space-y-1">
|
||||
{t.pipeline.steps.map((step, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="group relative flex gap-5 p-5 rounded-card border border-transparent hover:border-white/[0.04] hover:bg-surface-700/30 transition-all"
|
||||
>
|
||||
{/* Number */}
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-card bg-surface-600 border border-white/[0.06] flex items-center justify-center">
|
||||
<span className="font-mono text-lg font-bold text-brand-amber">{String(i + 1).padStart(2, '0')}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60 block mb-1">
|
||||
{step.label}
|
||||
</span>
|
||||
<h3 className="font-display text-lg font-semibold text-neutral-100 mb-1.5">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-400 leading-relaxed mb-2">
|
||||
{step.description}
|
||||
</p>
|
||||
<span className="inline-block font-mono text-nano text-neutral-600 bg-surface-600/60 px-2.5 py-1 rounded-sm border border-white/[0.03]">
|
||||
{step.detail}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Connector */}
|
||||
{i < t.pipeline.steps.length - 1 && (
|
||||
<div className="absolute left-[42px] top-[72px] w-px h-5 bg-gradient-to-b from-surface-300 to-transparent hidden lg:block" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
120
site/src/sections/Pricing.tsx
Normal file
120
site/src/sections/Pricing.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { SectionHeader } from '../components/SectionHeader'
|
||||
import { Container } from '../components/Container'
|
||||
import { GlowButton } from '../components/GlowButton'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
function CellValue({ value }: { value: string | boolean }) {
|
||||
if (value === true) {
|
||||
return (
|
||||
<span className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_4px_rgba(52,211,153,0.6)]" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (value === false) {
|
||||
return (
|
||||
<span className="flex items-center justify-center">
|
||||
<span className="w-2 h-2 rounded-full bg-neutral-600" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <span className="font-mono text-xs text-neutral-300">{value}</span>
|
||||
}
|
||||
|
||||
export function Pricing() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<section id="pricing" className="relative py-24 md:py-32">
|
||||
<Container>
|
||||
<SectionHeader
|
||||
index={t.pricing.index}
|
||||
title={t.pricing.title}
|
||||
subtitle={t.pricing.subtitle}
|
||||
/>
|
||||
|
||||
{/* Pricing table */}
|
||||
<div className="overflow-x-auto -mx-5 md:mx-0">
|
||||
<table className="w-full min-w-[640px] border-collapse">
|
||||
{/* Header */}
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.08]">
|
||||
<th className="text-left py-4 px-4 font-mono text-nano uppercase tracking-widest text-neutral-500 w-[40%]">
|
||||
{t.pricing.featureLabel}
|
||||
</th>
|
||||
<th className="text-center py-4 px-4 w-[20%]">
|
||||
<div className="font-mono text-nano uppercase tracking-widest text-neutral-500 mb-1">{t.pricing.free}</div>
|
||||
<div className="font-display text-xl font-bold text-neutral-300">$0</div>
|
||||
<div className="font-mono text-nano text-neutral-600">{t.pricing.forever}</div>
|
||||
</th>
|
||||
<th className="text-center py-4 px-4 w-[20%] relative">
|
||||
<div className="absolute inset-x-0 -top-3 flex justify-center">
|
||||
<span className="px-3 py-0.5 rounded-full bg-brand-amber text-white text-nano font-mono font-semibold uppercase tracking-widest">
|
||||
{t.pricing.popular}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-nano uppercase tracking-widest text-brand-amber mt-4 mb-1">{t.pricing.pro}</div>
|
||||
<div className="font-display text-xl font-bold text-neutral-100">$29</div>
|
||||
<div className="font-mono text-nano text-neutral-600">{t.pricing.oneTime}</div>
|
||||
</th>
|
||||
<th className="text-center py-4 px-4 w-[20%]">
|
||||
<div className="font-mono text-nano uppercase tracking-widest text-neutral-500 mb-1">{t.pricing.proPlus}</div>
|
||||
<div className="font-display text-xl font-bold text-neutral-300">$49</div>
|
||||
<div className="font-mono text-nano text-neutral-600">{t.pricing.oneTime}</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* Body */}
|
||||
<tbody>
|
||||
{t.pricing.rows.map((row, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={`border-b border-white/[0.03] hover:bg-surface-700/30 transition-colors ${
|
||||
i % 2 === 0 ? 'bg-transparent' : 'bg-surface-800/20'
|
||||
}`}
|
||||
>
|
||||
<td className="py-3 px-4 font-mono text-xs text-neutral-400">
|
||||
{row.feature}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<CellValue value={row.free} />
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center bg-brand-amber-muted/30">
|
||||
<CellValue value={row.pro} />
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<CellValue value={row.proPlus} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* CTA row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-8">
|
||||
<div className="flex justify-center">
|
||||
<GlowButton href="https://github.com/user/D3ROVoice/releases" variant="secondary" size="md">
|
||||
{t.pricing.downloadFree}
|
||||
</GlowButton>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<GlowButton href="#" variant="primary" size="md">
|
||||
{t.pricing.getPro}
|
||||
</GlowButton>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<GlowButton href="#" variant="secondary" size="md">
|
||||
{t.pricing.getProPlus}
|
||||
</GlowButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-center font-mono text-nano text-neutral-600 mt-6 uppercase tracking-widest">
|
||||
{t.pricing.taxNote}
|
||||
</p>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
76
site/src/sections/Privacy.tsx
Normal file
76
site/src/sections/Privacy.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { SectionHeader } from '../components/SectionHeader'
|
||||
import { Container } from '../components/Container'
|
||||
import { Led } from '../components/Led'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
const ledColors: Array<'amber' | 'green' | 'red'> = [
|
||||
'green', 'green', 'green', 'green', 'green', 'green',
|
||||
]
|
||||
|
||||
export function Privacy() {
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<section id="privacy" className="relative py-24 md:py-32 overflow-hidden">
|
||||
<Container>
|
||||
<SectionHeader
|
||||
index={t.privacy.index}
|
||||
title={t.privacy.title}
|
||||
subtitle={t.privacy.subtitle}
|
||||
/>
|
||||
|
||||
{/* CRT instrument panel */}
|
||||
<div className="crt-screen rounded-card p-6 md:p-10 noise-texture mb-12">
|
||||
<div className="relative z-10">
|
||||
{/* Panel header */}
|
||||
<div className="flex items-center justify-between mb-8 pb-4 border-b border-white/[0.04]">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-brand-amber/60">
|
||||
{t.privacy.monitorTitle}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color="green" pulse />
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-emerald-400/60">
|
||||
{t.privacy.allClear}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metric grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6 md:gap-8">
|
||||
{t.privacy.metrics.map((metric, i) => (
|
||||
<div key={i} className="flex flex-col gap-2">
|
||||
<span className="font-mono text-nano uppercase tracking-widest text-neutral-500">
|
||||
{metric.label}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Led color={ledColors[i]} size="sm" />
|
||||
<span className="font-mono text-sm font-semibold text-neutral-200 tracking-wide">
|
||||
{metric.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Guarantees list */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{t.privacy.guarantees.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-start gap-3 p-4 rounded-card bg-surface-700/30 border border-white/[0.03]"
|
||||
>
|
||||
<div className="w-5 h-5 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-3 h-3 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-300 leading-relaxed">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue