From a570ecbbcd4de5aa3e33e5368db0bbba0b24d551 Mon Sep 17 00:00:00 2001 From: abingyyds Date: Sat, 21 Mar 2026 17:04:01 +0900 Subject: [PATCH] feat: add clean and corporate white-style theme templates Add two new white-background themes with distinct design languages: - Clean: soft blue accents, rounded cards, gradient CTA section - Corporate: slate/professional palette, sharp borders, structured layout Both themes include full nav (with topup), LanguageSwitch, FadeContent animations, model grid, package cards, and responsive mobile menu. Co-Authored-By: Claude Sonnet 4.6 --- src/context/ThemeContext.jsx | 8 + src/themes/clean/Home.jsx | 231 +++++++++++++++++++++++++++++ src/themes/clean/Layout.jsx | 148 +++++++++++++++++++ src/themes/corporate/Home.jsx | 251 ++++++++++++++++++++++++++++++++ src/themes/corporate/Layout.jsx | 155 ++++++++++++++++++++ 5 files changed, 793 insertions(+) create mode 100644 src/themes/clean/Home.jsx create mode 100644 src/themes/clean/Layout.jsx create mode 100644 src/themes/corporate/Home.jsx create mode 100644 src/themes/corporate/Layout.jsx diff --git a/src/context/ThemeContext.jsx b/src/context/ThemeContext.jsx index ec6d2fc..d1e4511 100644 --- a/src/context/ThemeContext.jsx +++ b/src/context/ThemeContext.jsx @@ -21,6 +21,14 @@ const themeRegistry = { Home: React.lazy(() => import('../themes/minimal/Home')), Layout: React.lazy(() => import('../themes/minimal/Layout')), }, + clean: { + Home: React.lazy(() => import('../themes/clean/Home')), + Layout: React.lazy(() => import('../themes/clean/Layout')), + }, + corporate: { + Home: React.lazy(() => import('../themes/corporate/Home')), + Layout: React.lazy(() => import('../themes/corporate/Layout')), + }, }; // Full-screen loading spinner shown while site info is being fetched diff --git a/src/themes/clean/Home.jsx b/src/themes/clean/Home.jsx new file mode 100644 index 0000000..52c6933 --- /dev/null +++ b/src/themes/clean/Home.jsx @@ -0,0 +1,231 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '../../context/AuthContext'; +import { useSite } from '../../context/SiteContext'; +import { getSiteModels, getSitePackages } from '../../api'; +import CountUp from '../../components/bits/CountUp'; +import FadeContent from '../../components/bits/FadeContent'; + +export default function CleanHome() { + const { t } = useTranslation(); + const { user } = useAuth(); + const { site } = useSite(); + const [models, setModels] = useState([]); + const [packages, setPackages] = useState([]); + + useEffect(() => { + getSiteModels().then(r => { if (r.data.success) setModels(r.data.data || []); }).catch(() => {}); + getSitePackages().then(r => { if (r.data.success) setPackages(r.data.data || []); }).catch(() => {}); + }, []); + + const enabledModels = models.filter(m => m.enabled !== false); + + return ( +
+ {/* Hero */} +
+ +
+
+ + {t('home.lightningFast')} +
+ +

+ {site?.name || t('home.defaultHeroTitle')} +

+ +

+ {t('home.heroSubtitle')} +

+ +
+ {user ? ( + + {t('home.goToDashboard')} → + + ) : ( + <> + + {t('home.getStarted')} → + + + {t('home.viewPricing')} + + + )} +
+
+
+ + {/* Stats */} + +
+
+
+ + +
+

{t('home.aiModels')}

+
+
+
+ % +
+

{t('home.uptime')}

+
+
+
+ <ms +
+

{t('home.latency')}

+
+
+
+
+ + {/* Features */} +
+
+ +
+

{t('home.whyChooseUs')}

+

{t('home.whyChooseUsDesc')}

+
+ +
+
+
+ + + +
+

{t('home.lightningFast')}

+

{t('home.lightningFastDesc')}

+
+ +
+
+ + + +
+

{t('home.securePrivate')}

+

{t('home.securePrivateDesc')}

+
+ +
+
+ + + +
+

{t('home.payAsYouGo')}

+

{t('home.payAsYouGoDesc')}

+
+
+
+
+
+ + {/* Models */} + {enabledModels.length > 0 && ( +
+ +
+

{t('home.availableModels')}

+

{t('home.availableModelsDesc', { count: enabledModels.length })}

+
+ +
+ {enabledModels.slice(0, 12).map((m, i) => ( +
+
+ + {m.display_name || m.model_name} + +
+ ))} +
+ + {enabledModels.length > 12 && ( +
+ + {t('home.viewAllModels', { count: enabledModels.length })} → + +
+ )} + +
+ )} + + {/* Packages */} + {packages.length > 0 && ( +
+
+ +
+

{t('home.plansPackages')}

+

{t('home.choosePlan')}

+
+ +
+ {packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => ( +
+ {i === 1 && Popular} +

{pkg.name}

+ {pkg.description &&

{pkg.description}

} +
+
+ ${pkg.price} + {pkg.original_price > pkg.price && ( + ${pkg.original_price} + )} +
+ {pkg.duration > 0 &&

{t('home.days', { count: pkg.duration })}

} +
+ + {user ? t('home.subscribe') : t('home.getStarted')} + +
+ ))} +
+
+
+
+ )} + + {/* CTA */} +
+ +
+

{t('home.readyToStart')}

+

{t('home.readyToStartDesc')}

+
+ {user ? ( + + {t('home.goToDashboard')} + + ) : ( + <> + + {t('home.createFreeAccount')} + + + {t('home.signIn')} + + + )} +
+
+
+
+
+ ); +} diff --git a/src/themes/clean/Layout.jsx b/src/themes/clean/Layout.jsx new file mode 100644 index 0000000..a5496eb --- /dev/null +++ b/src/themes/clean/Layout.jsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react'; +import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '../../context/AuthContext'; +import { useSite } from '../../context/SiteContext'; +import LanguageSwitch from '../../components/LanguageSwitch'; + +export default function CleanLayout() { + const { t } = useTranslation(); + const { user, logout } = useAuth(); + const { site } = useSite(); + const navigate = useNavigate(); + const location = useLocation(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + const siteName = site?.name || 'AI Platform'; + + const navItems = [ + { to: '/', label: t('nav.home'), auth: false }, + { to: '/pricing', label: t('nav.pricing'), auth: false }, + { to: '/packages', label: t('nav.packages'), auth: false }, + { to: '/dashboard', label: t('nav.dashboard'), auth: true }, + { to: '/tokens', label: t('nav.apiKeys'), auth: true }, + ...(site?.enable_topup ? [{ to: '/topup', label: t('nav.topup'), auth: true }] : []), + ]; + + const visibleNavItems = navItems.filter((n) => !n.auth || user); + + return ( +
+ {/* Announcement */} + {site?.announcement && ( +
+

{site.announcement}

+
+ )} + + {/* Header */} +
+
+ + {site?.logo ? ( + {siteName} + ) : ( +
+ {siteName.charAt(0)} +
+ )} + + {siteName} + + + + + +
+ + + {user ? ( +
+ {user.display_name || user.username} + +
+ ) : ( + <> + + {t('nav.login')} + + + {t('nav.signUp')} + + + )} + + +
+
+ + {mobileMenuOpen && ( +
+ +
+ )} +
+ +
+ + +
+ ); +} diff --git a/src/themes/corporate/Home.jsx b/src/themes/corporate/Home.jsx new file mode 100644 index 0000000..75afe86 --- /dev/null +++ b/src/themes/corporate/Home.jsx @@ -0,0 +1,251 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '../../context/AuthContext'; +import { useSite } from '../../context/SiteContext'; +import { getSiteModels, getSitePackages } from '../../api'; +import CountUp from '../../components/bits/CountUp'; +import FadeContent from '../../components/bits/FadeContent'; + +export default function CorporateHome() { + const { t } = useTranslation(); + const { user } = useAuth(); + const { site } = useSite(); + const [models, setModels] = useState([]); + const [packages, setPackages] = useState([]); + + useEffect(() => { + getSiteModels().then(r => { if (r.data.success) setModels(r.data.data || []); }).catch(() => {}); + getSitePackages().then(r => { if (r.data.success) setPackages(r.data.data || []); }).catch(() => {}); + }, []); + + const enabledModels = models.filter(m => m.enabled !== false); + + return ( +
+ {/* Hero */} +
+
+
+
+ +
+
+ + {t('home.lightningFast')} +
+ +

+ {site?.name || t('home.defaultHeroTitle')} +

+ +

+ {t('home.heroSubtitle')} +

+ +
+ {user ? ( + + {t('home.goToDashboard')} + + + ) : ( + <> + + {t('home.getStarted')} + + + + {t('home.viewPricing')} + + + )} +
+
+
+ + {/* Stats bar */} + +
+
+
+ + +
+

{t('home.aiModels')}

+
+
+
+ % +
+

{t('home.uptime')}

+
+
+
+ <ms +
+

{t('home.latency')}

+
+
+
+
+
+ + {/* Features */} +
+
+ +
+

{t('home.whyChooseUs')}

+

{t('home.whyChooseUsDesc')}

+
+ +
+
+
+ + + +
+

{t('home.lightningFast')}

+

{t('home.lightningFastDesc')}

+
+ +
+
+ + + +
+

{t('home.securePrivate')}

+

{t('home.securePrivateDesc')}

+
+ +
+
+ + + +
+

{t('home.payAsYouGo')}

+

{t('home.payAsYouGoDesc')}

+
+
+
+
+
+ + {/* Models */} + {enabledModels.length > 0 && ( +
+ +
+

{t('home.availableModels')}

+

{t('home.availableModelsDesc', { count: enabledModels.length })}

+
+ +
+ {enabledModels.slice(0, 12).map((m, i) => ( +
+
+ + {m.display_name || m.model_name} + +
+ ))} +
+ + {enabledModels.length > 12 && ( +
+ + {t('home.viewAllModels', { count: enabledModels.length })} + + +
+ )} + +
+ )} + + {/* Packages */} + {packages.length > 0 && ( +
+
+ +
+

{t('home.plansPackages')}

+

{t('home.choosePlan')}

+
+ +
+ {packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => ( +
+ {i === 1 && ( +
+ Popular +
+ )} +
+

{pkg.name}

+ {pkg.description &&

{pkg.description}

} +
+
+ ${pkg.price} + {pkg.original_price > pkg.price && ( + ${pkg.original_price} + )} +
+ {pkg.duration > 0 &&

{t('home.days', { count: pkg.duration })}

} +
+
+
+ + {user ? t('home.subscribe') : t('home.getStarted')} + +
+
+ ))} +
+
+
+
+ )} + + {/* CTA */} +
+ +
+
+
+

{t('home.readyToStart')}

+

{t('home.readyToStartDesc')}

+
+ {user ? ( + + {t('home.goToDashboard')} + + + ) : ( + <> + + {t('home.createFreeAccount')} + + + + {t('home.signIn')} + + + )} +
+
+
+ +
+
+ ); +} diff --git a/src/themes/corporate/Layout.jsx b/src/themes/corporate/Layout.jsx new file mode 100644 index 0000000..524f498 --- /dev/null +++ b/src/themes/corporate/Layout.jsx @@ -0,0 +1,155 @@ +import React, { useState } from 'react'; +import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '../../context/AuthContext'; +import { useSite } from '../../context/SiteContext'; +import LanguageSwitch from '../../components/LanguageSwitch'; + +export default function CorporateLayout() { + const { t } = useTranslation(); + const { user, logout } = useAuth(); + const { site } = useSite(); + const navigate = useNavigate(); + const location = useLocation(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + const siteName = site?.name || 'AI Platform'; + + const navItems = [ + { to: '/', label: t('nav.home'), auth: false }, + { to: '/pricing', label: t('nav.pricing'), auth: false }, + { to: '/packages', label: t('nav.packages'), auth: false }, + { to: '/dashboard', label: t('nav.dashboard'), auth: true }, + { to: '/tokens', label: t('nav.apiKeys'), auth: true }, + ...(site?.enable_topup ? [{ to: '/topup', label: t('nav.topup'), auth: true }] : []), + ]; + + const visibleNavItems = navItems.filter((n) => !n.auth || user); + + return ( +
+ {/* Announcement */} + {site?.announcement && ( +
+

{site.announcement}

+
+ )} + + {/* Header */} +
+
+ + {site?.logo ? ( + {siteName} + ) : ( +
+ {siteName.charAt(0)} +
+ )} + + {siteName} + + + + + +
+ + + {user ? ( +
+ + {user.display_name || user.username} + + +
+ ) : ( + <> + + {t('nav.login')} + + + {t('nav.signUp')} + + + )} + + +
+
+ + {mobileMenuOpen && ( +
+ +
+ )} +
+ +
+ + +
+ ); +}