From 3ecf1e169c4cc09713a2b2dc5df16b9038425c48 Mon Sep 17 00:00:00 2001 From: yoge Date: Wed, 25 Mar 2026 13:16:59 +0800 Subject: [PATCH] feat: add layout components (MarketingNavbar, AppNavbar, Footer, layouts) Co-Authored-By: Claude Opus 4.6 --- src/components/layout/AppLayout.tsx | 13 ++ src/components/layout/AppNavbar.tsx | 194 ++++++++++++++++++++++ src/components/layout/Footer.tsx | 47 ++++++ src/components/layout/MarketingLayout.tsx | 15 ++ src/components/layout/MarketingNavbar.tsx | 124 ++++++++++++++ 5 files changed, 393 insertions(+) create mode 100644 src/components/layout/AppLayout.tsx create mode 100644 src/components/layout/AppNavbar.tsx create mode 100644 src/components/layout/Footer.tsx create mode 100644 src/components/layout/MarketingLayout.tsx create mode 100644 src/components/layout/MarketingNavbar.tsx diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..46d6b47 --- /dev/null +++ b/src/components/layout/AppLayout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from 'react-router-dom'; +import AppNavbar from './AppNavbar'; + +export default function AppLayout() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/src/components/layout/AppNavbar.tsx b/src/components/layout/AppNavbar.tsx new file mode 100644 index 0000000..156a0de --- /dev/null +++ b/src/components/layout/AppNavbar.tsx @@ -0,0 +1,194 @@ +import { useState, useRef, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Mail, Users, MessageCircle, ChevronDown, Check, Heart, X, Languages, HelpCircle, Home } from 'lucide-react'; +import { useLanguage } from '../../contexts/LanguageContext'; + +export default function AppNavbar() { + const { language, setLanguage, t } = useLanguage(); + const [showContact, setShowContact] = useState(false); + const [showReward, setShowReward] = useState(false); + const [showLangMenu, setShowLangMenu] = useState(false); + const [copied, setCopied] = useState(false); + const dropdownRef = useRef(null); + const langMenuRef = useRef(null); + + const handleCopyQQ = async () => { + await navigator.clipboard.writeText('1018282100'); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setShowContact(false); + } + if (langMenuRef.current && !langMenuRef.current.contains(event.target as Node)) { + setShowLangMenu(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ {/* Left: Logo + Home link */} +
+ + TexPixel + TexPixel + + + + {t.marketing.nav.home} + +
+ + {/* Right: Actions */} +
+ {/* Language Switcher */} +
+ + + {showLangMenu && ( +
+ + +
+ )} +
+ + {/* User Guide Button */} + + + {/* Reward Button */} +
+ + + {showReward && ( +
setShowReward(false)} + > +
e.stopPropagation()} + > +
+ {t.navbar.rewardTitle} + +
+
+ {t.navbar.rewardTitle} +

+ {t.navbar.rewardThanks}
+ {t.navbar.rewardSubtitle} +

+
+
+
+ )} +
+ + {/* Contact Button with Dropdown */} +
+ + + {showContact && ( +
+ +
+ +
+
+
{t.common.email}
+
yogecoder@gmail.com
+
+
+
+
+ {copied ? ( + + ) : ( + + )} +
+
+
+ {copied ? t.common.copied : t.common.qqGroup} +
+
1018282100
+
+
+
+ )} +
+
+
+ ); +} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx new file mode 100644 index 0000000..42f8ab2 --- /dev/null +++ b/src/components/layout/Footer.tsx @@ -0,0 +1,47 @@ +import { Link } from 'react-router-dom'; +import { useLanguage } from '../../contexts/LanguageContext'; + +export default function Footer() { + const { t } = useLanguage(); + + return ( +
+
+
+
+ TexPixel + TexPixel +
+

{t.marketing.footer.tagline}

+
+ +
+

{t.marketing.footer.product}

+
+ {t.marketing.nav.launchApp} + {t.marketing.nav.pricing} +
+
+ +
+

{t.marketing.footer.resources}

+
+ {t.marketing.nav.docs} + {t.marketing.nav.blog} +
+
+ +
+

{t.marketing.footer.contactTitle}

+ +
+
+ +
+ © {new Date().getFullYear()} TexPixel. All rights reserved. +
+
+ ); +} diff --git a/src/components/layout/MarketingLayout.tsx b/src/components/layout/MarketingLayout.tsx new file mode 100644 index 0000000..a081e3c --- /dev/null +++ b/src/components/layout/MarketingLayout.tsx @@ -0,0 +1,15 @@ +import { Outlet } from 'react-router-dom'; +import MarketingNavbar from './MarketingNavbar'; +import Footer from './Footer'; + +export default function MarketingLayout() { + return ( +
+ +
+ +
+
+
+ ); +} diff --git a/src/components/layout/MarketingNavbar.tsx b/src/components/layout/MarketingNavbar.tsx new file mode 100644 index 0000000..57b8b84 --- /dev/null +++ b/src/components/layout/MarketingNavbar.tsx @@ -0,0 +1,124 @@ +import { useState, useRef, useEffect } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { Languages, ChevronDown, Check, Menu, X } from 'lucide-react'; +import { useLanguage } from '../../contexts/LanguageContext'; + +export default function MarketingNavbar() { + const { language, setLanguage, t } = useLanguage(); + const location = useLocation(); + const [showLangMenu, setShowLangMenu] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const langMenuRef = useRef(null); + const isHome = location.pathname === '/'; + + const navLinks = [ + { to: '/', label: t.marketing.nav.home }, + { to: '/docs', label: t.marketing.nav.docs }, + { to: '/blog', label: t.marketing.nav.blog }, + ]; + + const anchorLinks = isHome + ? [ + { href: '#pricing', label: t.marketing.nav.pricing }, + { href: '#contact', label: t.marketing.nav.contact }, + ] + : []; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (langMenuRef.current && !langMenuRef.current.contains(event.target as Node)) { + setShowLangMenu(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( + + ); +}