fix: proper light/dark theme support for all public pages
Docker Build / Build and Push Docker Image (push) Failing after 1m31s

This commit is contained in:
2026-06-14 17:42:31 +08:00
parent 1ddffd236c
commit da8cf3eef0
11 changed files with 298 additions and 426 deletions
+14 -19
View File
@@ -58,7 +58,7 @@ function FooterLinkItem(props: { link: FooterLink }) {
href={props.link.href}
target='_blank'
rel='noopener noreferrer'
className='text-[#8B8D97] hover:text-[#00D2FF] text-sm transition-colors duration-200'
className='text-muted-foreground hover:text-primary text-sm transition-colors duration-200'
>
{label}
</a>
@@ -68,16 +68,13 @@ function FooterLinkItem(props: { link: FooterLink }) {
return (
<Link
to={props.link.href}
className='text-[#8B8D97] hover:text-[#00D2FF] text-sm transition-colors duration-200'
className='text-muted-foreground hover:text-primary text-sm transition-colors duration-200'
>
{label}
</Link>
)
}
// Renders User Agreement / Privacy Policy links inline with the parent's
// copyright row when either is configured in System Settings → Site. Emits
// fragmented siblings so the parent flex container's gap controls spacing.
function LegalLinks(props: { leadingSeparator?: boolean }) {
const { t } = useTranslation()
const { status } = useStatus()
@@ -104,13 +101,13 @@ function LegalLinks(props: { leadingSeparator?: boolean }) {
{items.map((item, index) => (
<Fragment key={item.key}>
{(props.leadingSeparator || index > 0) && (
<span aria-hidden='true' className='text-[#5C5D66]'>
<span aria-hidden='true' className='text-muted-foreground/50'>
·
</span>
)}
<Link
to={item.href}
className='text-[#8B8D97] hover:text-[#00D2FF] transition-colors duration-200'
className='text-muted-foreground hover:text-primary transition-colors duration-200'
>
{item.label}
</Link>
@@ -120,18 +117,16 @@ function LegalLinks(props: { leadingSeparator?: boolean }) {
)
}
// inline=true returns just the inner span for composition in a parent flex
// row. inline=false wraps in a centered/right-aligned div (default).
function ProjectAttribution(props: { currentYear: number; inline?: boolean }) {
const { t } = useTranslation()
const content = (
<span className='text-[#5C5D66]'>
<span className='text-muted-foreground/60'>
&copy; {props.currentYear}{' '}
<a
href='https://git.viaeon.com/admin/new-api'
target='_blank'
rel='noopener noreferrer'
className='text-[#8B8D97] hover:text-[#00D2FF] font-medium transition-colors'
className='text-muted-foreground hover:text-primary font-medium transition-colors'
>
{t('ModelsToken')}
</a>
@@ -142,7 +137,7 @@ function ProjectAttribution(props: { currentYear: number; inline?: boolean }) {
return content
}
return (
<div className='text-[#5C5D66] text-center text-xs sm:text-right'>
<div className='text-muted-foreground/60 text-center text-xs sm:text-right'>
{content}
</div>
)
@@ -164,17 +159,17 @@ export function Footer(props: FooterProps) {
return (
<footer
className={cn(
'border-[#1A1F29] bg-[#0A0E14] border-t',
'border-border bg-card border-t',
props.className
)}
>
<div className='mx-auto w-full max-w-6xl px-6 py-5'>
<div className='border-[#1A1F29] bg-[#0F1419] flex flex-col items-center justify-between gap-4 rounded-lg border px-4 py-4 sm:flex-row sm:px-5'>
<div className='border-border bg-muted/50 flex flex-col items-center justify-between gap-4 rounded-lg border px-4 py-4 sm:flex-row sm:px-5'>
<div
className='text-[#8B8D97] min-w-0 text-center text-sm sm:text-left'
className='text-muted-foreground min-w-0 text-center text-sm sm:text-left'
dangerouslySetInnerHTML={{ __html: footerHtml }}
/>
<div className='border-[#1A1F29] text-[#5C6370] flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-1 border-t pt-4 text-xs sm:w-auto sm:justify-end sm:border-t-0 sm:border-l sm:pt-0 sm:pl-5'>
<div className='border-border text-muted-foreground/60 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-1 border-t pt-4 text-xs sm:w-auto sm:justify-end sm:border-t-0 sm:border-l sm:pt-0 sm:pl-5'>
<LegalLinks />
<ProjectAttribution currentYear={currentYear} inline />
</div>
@@ -186,7 +181,7 @@ export function Footer(props: FooterProps) {
return (
<footer
className={cn('border-[#1A1F29] bg-[#0A0E14] border-t', props.className)}
className={cn('border-border bg-background border-t', props.className)}
>
<div className='mx-auto max-w-5xl px-6 py-8'>
<div className='flex flex-col items-center justify-between gap-4 sm:flex-row'>
@@ -197,13 +192,13 @@ export function Footer(props: FooterProps) {
alt={displayName}
className='size-5 rounded object-contain'
/>
<span className='text-xs font-semibold tracking-tight text-[#C5C6D0]'>
<span className='text-xs font-semibold tracking-tight text-foreground'>
{displayName}
</span>
</Link>
{/* Links + Copyright */}
<div className='text-[#5C6370] flex flex-wrap items-center justify-center gap-x-2 gap-y-1 text-xs'>
<div className='text-muted-foreground/60 flex flex-wrap items-center justify-center gap-x-2 gap-y-1 text-xs'>
<span>&copy; {currentYear} {displayName}</span>
<LegalLinks leadingSeparator />
<ProjectAttribution currentYear={currentYear} inline />
+225 -306
View File
@@ -16,333 +16,281 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact admin@modelstoken.com
*/
import { useCallback, useEffect, useState } from 'react'
import { Link, useNavigate, useRouterState } from '@tanstack/react-router'
import { useEffect, useRef, useState } from 'react'
import { Link, useRouterState } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/stores/auth-store'
import { cn } from '@/lib/utils'
import { useNotifications } from '@/hooks/use-notifications'
import { useStatus } from '@/hooks/use-status'
import { useSystemConfig } from '@/hooks/use-system-config'
import { useTopNavLinks } from '@/hooks/use-top-nav-links'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { Dialog } from '@/components/dialog'
import { LanguageSwitcher } from '@/components/language-switcher'
import { NotificationPopover } from '@/components/notification-popover'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { ThemeSwitch } from '@/components/theme-switch'
import { defaultTopNavLinks } from '../config/top-nav.config'
import type { TopNavLink } from '../types'
import { HeaderLogo } from './header-logo'
import { NotificationPopover } from '@/components/notification-popover'
import { useAuthStore } from '@/stores/auth-store'
import { useNotifications } from '@/hooks/use-notifications'
import { ProfileDropdown } from './profile-dropdown'
const AUTH_PROMPT_SECONDS = 5
type AuthPromptTarget = {
title: string
href: string
}
export interface PublicHeaderProps {
navLinks?: TopNavLink[]
mobileLinks?: TopNavLink[]
navContent?: React.ReactNode
showThemeSwitch?: boolean
showLanguageSwitcher?: boolean
logo?: React.ReactNode
siteName?: string
homeUrl?: string
leftContent?: React.ReactNode
rightContent?: React.ReactNode
showNavigation?: boolean
showAuthButtons?: boolean
showNotifications?: boolean
function HeaderLogo({
src,
loading,
logoLoaded,
className,
}: {
src: string
loading: boolean
logoLoaded: boolean
className?: string
}) {
const [imgError, setImgError] = useState(false)
if (loading || !logoLoaded || imgError) {
return (
<svg
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={cn('text-primary', className)}
>
<path d='M12 2L2 7l10 5 10-5-10-5z' />
<path d='M2 17l10 5 10-5' />
<path d='M2 12l10 5 10-5' />
</svg>
)
}
return (
<img
src={src}
alt='Logo'
className={className}
onError={() => setImgError(true)}
/>
)
}
export function PublicHeader(props: PublicHeaderProps) {
const {
navLinks = defaultTopNavLinks,
showThemeSwitch = true,
showLanguageSwitcher = true,
logo: customLogo,
siteName: customSiteName,
homeUrl = '/',
showAuthButtons = true,
showNotifications = true,
} = props
export function PublicHeader() {
const { t } = useTranslation()
const navigate = useNavigate()
const [scrolled, setScrolled] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
const [authPromptTarget, setAuthPromptTarget] =
useState<AuthPromptTarget | null>(null)
const [authPromptSecondsLeft, setAuthPromptSecondsLeft] =
useState(AUTH_PROMPT_SECONDS)
const router = useRouterState()
const pathname = router.location.pathname
const { status } = useStatus()
const { systemName, logo: systemLogo, loading, customLogo } = useSystemConfig()
const { auth } = useAuthStore()
const {
systemName,
logo: systemLogo,
loading,
logoLoaded,
} = useSystemConfig()
const dynamicLinks = useTopNavLinks()
const isAuthenticated = !!auth.user
const notifications = useNotifications()
const routerState = useRouterState()
const pathname = routerState.location.pathname
const [mobileOpen, setMobileOpen] = useState(false)
const [logoLoaded, setLogoLoaded] = useState(false)
const [scrolled, setScrolled] = useState(false)
const headerRef = useRef<HTMLElement>(null)
const user = auth.user
const isAuthenticated = !!user
const displaySiteName = customSiteName || systemName
const links = dynamicLinks.length > 0 ? dynamicLinks : navLinks
const homeUrl = isAuthenticated ? '/dashboard' : '/'
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 20)
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
const handleScroll = () => setScrolled(window.scrollY > 16)
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
return () => window.removeEventListener('scroll', handleScroll)
}, [])
useEffect(() => {
const img = new Image()
img.onload = () => setLogoLoaded(true)
img.src = systemLogo || '/logo.png'
}, [systemLogo])
useEffect(() => {
document.body.style.overflow = mobileOpen ? 'hidden' : ''
return () => {
document.body.style.overflow = ''
}
return () => { document.body.style.overflow = '' }
}, [mobileOpen])
useEffect(() => {
if (!authPromptTarget) return
const displaySiteName = systemName || 'ModelsToken'
const intervalId = window.setInterval(() => {
setAuthPromptSecondsLeft((seconds) => Math.max(seconds - 1, 0))
}, 1000)
const showLanguageSwitcher = status?.language_option_enabled !== false
const showThemeSwitch = true
const showNotifications = status?.notice_enabled || status?.announcement_enabled
const showAuthButtons = !status?.self_use_mode_enabled
const timeoutId = window.setTimeout(() => {
const redirect = authPromptTarget.href
setAuthPromptTarget(null)
navigate({ to: '/sign-in', search: { redirect } })
}, AUTH_PROMPT_SECONDS * 1000)
const links = [
{ title: 'Home', href: '/' },
...(status?.pricing_enabled !== false ? [{ title: 'Pricing', href: '/pricing' }] : []),
{ title: 'About', href: '/about' },
].filter(Boolean) as { title: string; href: string; external?: boolean; disabled?: boolean }[]
return () => {
window.clearInterval(intervalId)
window.clearTimeout(timeoutId)
const handleNavLinkClick = (
event: React.MouseEvent,
link: { href: string; disabled?: boolean },
isMobile = false
) => {
if (link.disabled) {
event.preventDefault()
return
}
}, [authPromptTarget, navigate])
const closeAuthPrompt = useCallback(() => {
setAuthPromptTarget(null)
setAuthPromptSecondsLeft(AUTH_PROMPT_SECONDS)
}, [])
const navigateToSignIn = useCallback(() => {
const redirect = authPromptTarget?.href || '/'
setAuthPromptTarget(null)
navigate({ to: '/sign-in', search: { redirect } })
}, [authPromptTarget?.href, navigate])
const handleNavLinkClick = useCallback(
(
event: React.MouseEvent<HTMLAnchorElement>,
link: TopNavLink,
closeMobile = false
) => {
if (link.disabled) {
event.preventDefault()
return
}
if (link.requiresAuth) {
event.preventDefault()
if (closeMobile) {
setMobileOpen(false)
}
setAuthPromptSecondsLeft(AUTH_PROMPT_SECONDS)
setAuthPromptTarget({
title: t(link.title),
href: link.href,
})
return
}
if (closeMobile) {
setMobileOpen(false)
}
},
[t]
)
if (isMobile) {
setMobileOpen(false)
}
}
return (
<>
<header className='pointer-events-none fixed inset-x-0 top-0 z-50'>
<div
<header ref={headerRef} className='fixed top-0 right-0 left-0 z-50 px-3 pt-3 sm:px-4 sm:pt-4'>
<div className='mx-auto max-w-5xl'>
<nav
className={cn(
'pointer-events-auto mx-auto transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]',
scrolled ? 'max-w-[52rem] px-3 pt-3' : 'max-w-7xl px-4 pt-0 md:px-6'
)}
>
<nav
className={cn(
'flex items-center justify-between transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)]',
scrolled
? 'bg-[#0A0E14]/80 ring-[#1A1F29]/80 h-12 rounded-2xl pr-1.5 pl-4 shadow-[0_2px_16px_-6px_rgba(0,0,0,0.5)] ring-[0.5px] backdrop-blur-2xl'
? 'bg-background/80 ring-border/80 h-12 rounded-2xl pr-1.5 pl-4 shadow-sm ring-[0.5px] backdrop-blur-2xl'
: 'h-16 px-2'
)}
>
{/* Logo */}
<Link
to={homeUrl}
className='group flex shrink-0 items-center gap-2.5'
>
{/* Logo */}
<Link
to={homeUrl}
className='group flex shrink-0 items-center gap-2.5'
>
<div className='flex size-7 shrink-0 items-center justify-center transition-all duration-300 group-hover:scale-105'>
{loading ? (
<Skeleton className='size-full rounded-lg' />
) : customLogo ? (
customLogo
) : (
<HeaderLogo
src={systemLogo}
loading={loading}
logoLoaded={logoLoaded}
className='size-full rounded-lg object-contain'
/>
)}
</div>
<span className='text-sm font-semibold tracking-tight text-white'>
{loading ? <Skeleton className='h-4 w-16' /> : displaySiteName}
</span>
</Link>
<div className='flex size-7 shrink-0 items-center justify-center transition-all duration-300 group-hover:scale-105'>
{loading ? (
<div className='size-full animate-pulse rounded-lg bg-muted' />
) : customLogo ? (
customLogo
) : (
<HeaderLogo
src={systemLogo}
loading={loading}
logoLoaded={logoLoaded}
className='size-full rounded-lg object-contain'
/>
)}
</div>
<span className='text-sm font-semibold tracking-tight text-foreground'>
{loading ? <div className='h-4 w-16 animate-pulse rounded bg-muted' /> : displaySiteName}
</span>
</Link>
{/* Desktop nav */}
<div className='hidden items-center gap-0.5 sm:flex'>
{links.map((link, i) => {
const isActive = pathname === link.href
if (link.external) {
return (
<a
key={i}
href={link.href}
target='_blank'
rel='noopener noreferrer'
aria-disabled={link.disabled}
tabIndex={link.disabled ? -1 : undefined}
onClick={(event) => handleNavLinkClick(event, link)}
className={cn(
'text-[#8B8D97] hover:text-[#C5C6D0] rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors duration-200',
link.disabled && 'pointer-events-none opacity-50'
)}
>
{t(link.title)}
</a>
)
}
{/* Desktop nav */}
<div className='hidden items-center gap-0.5 sm:flex'>
{links.map((link, i) => {
const isActive = pathname === link.href
if (link.external) {
return (
<Link
<a
key={i}
to={link.href}
disabled={link.disabled}
href={link.href}
target='_blank'
rel='noopener noreferrer'
aria-disabled={link.disabled}
tabIndex={link.disabled ? -1 : undefined}
onClick={(event) => handleNavLinkClick(event, link)}
className={cn(
'relative rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors duration-200',
isActive
? 'text-[#00D2FF]'
: 'text-[#8B8D97] hover:text-[#C5C6D0]',
'text-muted-foreground hover:text-foreground rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors duration-200',
link.disabled && 'pointer-events-none opacity-50'
)}
>
{t(link.title)}
{isActive && (
<span className='absolute bottom-0 left-3 right-3 h-[2px] rounded-full bg-[#00D2FF]' />
)}
</Link>
</a>
)
})}
{(showLanguageSwitcher ||
showThemeSwitch ||
showNotifications) && (
<div className='mx-2 h-4 w-px bg-white/10' />
)}
{showLanguageSwitcher && <LanguageSwitcher className='text-white/70 hover:bg-white/10 hover:text-white dark:hover:bg-white/10' />}
{showThemeSwitch && <ThemeSwitch className='text-white/70 hover:bg-white/10 hover:text-white dark:hover:bg-white/10' />}
{showNotifications && (
<NotificationPopover
className='text-white/70 hover:bg-white/10 hover:text-white dark:hover:bg-white/10'
open={notifications.popoverOpen}
onOpenChange={notifications.setPopoverOpen}
unreadCount={notifications.unreadCount}
activeTab={notifications.activeTab}
onTabChange={notifications.setActiveTab}
notice={notifications.notice}
announcements={notifications.announcements}
loading={notifications.loading}
/>
)}
{showAuthButtons && (
<>
<div className='mx-1 h-4 w-px bg-white/10' />
{loading ? (
<Skeleton className='h-8 w-20 rounded-lg' />
) : isAuthenticated ? (
<ProfileDropdown />
) : (
<Button
size='sm'
className='h-8 rounded-lg bg-[#00D2FF] px-3.5 text-xs font-semibold text-[#0A0E14] hover:bg-[#00B8E6]'
render={<Link to='/sign-in' />}
>
{t('Sign in')}
</Button>
}
return (
<Link
key={i}
to={link.href}
disabled={link.disabled}
onClick={(event) => handleNavLinkClick(event, link)}
className={cn(
'relative rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors duration-200',
isActive
? 'text-primary'
: 'text-muted-foreground hover:text-foreground',
link.disabled && 'pointer-events-none opacity-50'
)}
>
{t(link.title)}
{isActive && (
<span className='absolute bottom-0 left-3 right-3 h-[2px] rounded-full bg-primary' />
)}
</>
)}
</div>
</Link>
)
})}
{/* Mobile: compact actions + hamburger */}
<div className='flex items-center gap-2 sm:hidden'>
{showThemeSwitch && <ThemeSwitch className='text-white/70 hover:bg-white/10 hover:text-white dark:hover:bg-white/10' />}
{showAuthButtons && !loading && isAuthenticated && (
<ProfileDropdown />
)}
<Button
type='button'
variant='ghost'
size='icon'
className='size-9'
onClick={() => setMobileOpen((v) => !v)}
aria-label={t('Toggle navigation menu')}
>
<div className='relative size-4'>
<span
className={cn(
'absolute inset-x-0 block h-[1.5px] origin-center rounded-full bg-current transition-all duration-300',
mobileOpen ? 'top-[7px] rotate-45' : 'top-[3px]'
)}
/>
<span
className={cn(
'absolute inset-x-0 top-[7px] block h-[1.5px] rounded-full bg-current transition-all duration-300',
mobileOpen ? 'scale-x-0 opacity-0' : 'opacity-100'
)}
/>
<span
className={cn(
'absolute inset-x-0 block h-[1.5px] origin-center rounded-full bg-current transition-all duration-300',
mobileOpen ? 'top-[7px] -rotate-45' : 'top-[11px]'
)}
/>
</div>
</Button>
</div>
</nav>
</div>
</header>
{(showLanguageSwitcher ||
showThemeSwitch ||
showNotifications) && (
<div className='mx-2 h-4 w-px bg-border' />
)}
{showLanguageSwitcher && <LanguageSwitcher className='text-muted-foreground hover:bg-accent hover:text-foreground' />}
{showThemeSwitch && <ThemeSwitch className='text-muted-foreground hover:bg-accent hover:text-foreground' />}
{showNotifications && (
<NotificationPopover
className='text-muted-foreground hover:bg-accent hover:text-foreground'
open={notifications.popoverOpen}
onOpenChange={notifications.setPopoverOpen}
unreadCount={notifications.unreadCount}
activeTab={notifications.activeTab}
onTabChange={notifications.setActiveTab}
notice={notifications.notice}
announcements={notifications.announcements}
loading={notifications.loading}
/>
)}
{showAuthButtons && (
<>
<div className='mx-1 h-4 w-px bg-border' />
{loading ? (
<div className='h-8 w-20 animate-pulse rounded-lg bg-muted' />
) : isAuthenticated ? (
<ProfileDropdown />
) : (
<Button
size='sm'
className='h-8 rounded-lg px-3.5 text-xs font-semibold'
render={<Link to='/sign-in' />}
>
{t('Sign in')}
</Button>
)}
</>
)}
</div>
{/* Mobile: compact actions + hamburger */}
<div className='flex items-center gap-2 sm:hidden'>
{showThemeSwitch && <ThemeSwitch className='text-muted-foreground hover:bg-accent hover:text-foreground' />}
{showAuthButtons && !loading && isAuthenticated && (
<ProfileDropdown />
)}
<Button
type='button'
variant='ghost'
size='icon'
className='size-9 text-foreground'
onClick={() => setMobileOpen((v) => !v)}
aria-label={t('Toggle navigation menu')}
>
<div className='relative size-4'>
<span
className={cn(
'absolute inset-x-0 block h-[1.5px] origin-center rounded-full bg-current transition-all duration-300',
mobileOpen ? 'top-[7px] rotate-45' : 'top-[3px]'
)}
/>
<span
className={cn(
'absolute inset-x-0 block h-[1.5px] origin-center rounded-full bg-current transition-all duration-300',
mobileOpen ? 'top-[7px] -rotate-45' : 'top-[11px]'
)}
/>
</div>
</Button>
</div>
</nav>
</div>
</header>
{/* Mobile full-screen overlay */}
<div
className={cn(
'bg-[#0A0E14]/98 fixed inset-0 z-40 backdrop-blur-2xl transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] sm:pointer-events-none sm:hidden',
'bg-background/98 fixed inset-0 z-40 backdrop-blur-2xl transition-all duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] sm:pointer-events-none sm:hidden',
mobileOpen
? 'pointer-events-auto opacity-100'
: 'pointer-events-none opacity-0'
@@ -408,7 +356,7 @@ export function PublicHeader(props: PublicHeaderProps) {
<Link
to={isAuthenticated ? '/dashboard' : '/sign-in'}
onClick={() => setMobileOpen(false)}
className='bg-[#00D2FF] text-[#0A0E14] inline-flex h-10 items-center justify-center rounded-lg text-sm font-semibold transition-opacity hover:opacity-90 active:opacity-80'
className='bg-primary text-primary-foreground inline-flex h-10 items-center justify-center rounded-lg text-sm font-semibold transition-opacity hover:opacity-90 active:opacity-80'
>
{isAuthenticated ? t('Go to Dashboard') : t('Sign in')}
</Link>
@@ -416,35 +364,6 @@ export function PublicHeader(props: PublicHeaderProps) {
</div>
</div>
</div>
<Dialog
open={!!authPromptTarget}
onOpenChange={(open) => {
if (!open) {
closeAuthPrompt()
}
}}
title={t('Sign in required')}
description={t('Please sign in to view {{module}}.', {
module: authPromptTarget?.title || '',
})}
contentClassName='sm:max-w-md'
contentHeight='auto'
footer={
<>
<Button variant='outline' onClick={closeAuthPrompt}>
{t('Cancel')}
</Button>
<Button onClick={navigateToSignIn}>{t('Sign in now')}</Button>
</>
}
>
<div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'>
{t('Redirecting to sign in in {{seconds}} seconds.', {
seconds: authPromptSecondsLeft,
})}
</div>
</Dialog>
</>
)
}
+2 -2
View File
@@ -30,7 +30,7 @@ export function AuthLayout({ children }: AuthLayoutProps) {
const { systemName, logo, loading } = useSystemConfig()
return (
<div className='dark relative grid h-svh max-w-none bg-[#0A0E14]'>
<div className='relative grid h-svh max-w-none bg-background'>
<Link
to='/'
className='absolute top-4 left-4 z-10 flex items-center gap-2 transition-opacity hover:opacity-80 sm:top-8 sm:left-8'
@@ -49,7 +49,7 @@ export function AuthLayout({ children }: AuthLayoutProps) {
{loading ? (
<Skeleton className='h-5 w-20' />
) : (
<h1 className='text-base font-semibold text-white'>{systemName}</h1>
<h1 className='text-base font-semibold text-foreground'>{systemName}</h1>
)}
</Link>
<div className='container flex items-center pt-16 sm:pt-0'>
@@ -112,7 +112,7 @@ export function ForgotPasswordForm({
<Button
type='submit'
className='mt-2 !bg-[#00D2FF] !text-[#0A0E14] hover:!bg-[#00B8E6]'
className='mt-2'
disabled={isLoading || isActive || !turnstileReady}
>
{isActive
+1 -1
View File
@@ -203,7 +203,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
<Button
type='submit'
className='mt-2 w-full !bg-[#00D2FF] !text-[#0A0E14] hover:!bg-[#00B8E6]'
className='mt-2 w-full'
disabled={!isFormValid || isLoading}
>
{isLoading ? <Loader2 className='h-4 w-4 animate-spin' /> : null}
@@ -374,7 +374,7 @@ export function UserAuthForm({
{/* Submit Button */}
<Button
type='submit'
className='mt-2 w-full justify-center gap-2 !bg-[#00D2FF] !text-[#0A0E14] hover:!bg-[#00B8E6]'
className='mt-2 w-full justify-center gap-2'
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
>
{isLoading ? <Loader2 className='animate-spin' /> : <LogIn />}
+1 -1
View File
@@ -32,7 +32,7 @@ export function SignIn() {
<AuthLayout>
<div className='w-full space-y-8'>
<div className='space-y-2'>
<h2 className='text-center text-2xl font-semibold tracking-tight text-white sm:text-left'>
<h2 className='text-center text-2xl font-semibold tracking-tight sm:text-left'>
{t('Sign in')}
</h2>
{!status?.self_use_mode_enabled &&
@@ -354,7 +354,7 @@ export function SignUpForm({
{/* Submit Button */}
<Button
type='submit'
className='mt-2 w-full justify-center gap-2 !bg-[#00D2FF] !text-[#0A0E14] hover:!bg-[#00B8E6]'
className='mt-2 w-full justify-center gap-2'
disabled={
isLoading ||
(requiresLegalConsent && !agreedToLegal) ||
+1 -1
View File
@@ -31,7 +31,7 @@ export function SignUp() {
<AuthLayout>
<div className='w-full space-y-8'>
<div className='space-y-2'>
<h2 className='text-center text-2xl font-semibold tracking-tight text-white sm:text-left'>
<h2 className='text-center text-2xl font-semibold tracking-tight sm:text-left'>
{t('Create an account')}
</h2>
<p className='text-muted-foreground text-left text-sm sm:text-base'>
+49 -89
View File
@@ -27,20 +27,6 @@ interface HeroProps {
isAuthenticated?: boolean
}
const T = {
bg: '#0A0E14',
surface: '#0F1419',
border: '#1A1F29',
accent: '#00D2FF',
accentDim: '#0099BB',
green: '#28C840',
yellow: '#FEBC2E',
gray: '#5C6370',
grayLight: '#8B8D97',
white: '#C5C6D0',
red: '#FF5F57',
}
const features = [
{ icon: Zap, title: 'Multi-model Routing', desc: 'Intelligent routing with automatic failover and load balancing' },
{ icon: Key, title: 'Key Management', desc: 'Centralized API key lifecycle management with usage tracking' },
@@ -58,22 +44,22 @@ export function Hero(props: HeroProps) {
`${window.location.origin}`
return (
<section className='relative overflow-hidden' style={{ backgroundColor: T.bg }}>
<section className='relative overflow-hidden bg-background'>
{/* Grid background texture */}
<div
aria-hidden
className='pointer-events-none absolute inset-0 opacity-[0.03]'
className='pointer-events-none absolute inset-0 opacity-[0.03] dark:opacity-[0.03]'
style={{
backgroundImage:
'linear-gradient(rgba(0,210,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(0,210,255,0.5) 1px, transparent 1px)',
'linear-gradient(var(--primary) 1px, transparent 1px), linear-gradient(90deg, var(--primary) 1px, transparent 1px)',
backgroundSize: '64px 64px',
}}
/>
{/* Top glow */}
{/* Top glow - dark mode only */}
<div
aria-hidden
className='pointer-events-none absolute inset-0'
className='pointer-events-none absolute inset-0 hidden dark:block'
style={{
background:
'radial-gradient(ellipse 50% 40% at 50% 0%, rgba(0,210,255,0.08) 0%, transparent 70%)',
@@ -84,16 +70,13 @@ export function Hero(props: HeroProps) {
{/* ── Hero ── */}
<div className='flex min-h-[calc(100svh-3rem)] flex-col items-center justify-center py-20 text-center'>
{/* Headline */}
<h1
className='max-w-2xl text-[clamp(2.2rem,6vw,4.2rem)] leading-[1.05] font-bold tracking-[-0.03em]'
style={{ color: T.white }}
>
<h1 className='max-w-2xl text-[clamp(2.2rem,6vw,4.2rem)] leading-[1.05] font-bold tracking-[-0.03em] text-foreground'>
{t('Unified LLM')}{' '}
<span style={{ color: T.accent }}>{t('Gateway')}</span>
<span className='text-primary'>{t('Gateway')}</span>
</h1>
{/* Subtitle */}
<p className='mt-5 max-w-md text-[15px] leading-relaxed' style={{ color: T.grayLight }}>
<p className='mt-5 max-w-md text-[15px] leading-relaxed text-muted-foreground'>
{t('One endpoint for all models. OpenAI-compatible, switch and go.')}
</p>
@@ -101,8 +84,7 @@ export function Hero(props: HeroProps) {
<div className='mt-9 flex items-center gap-4'>
{props.isAuthenticated ? (
<Button
className='group h-11 rounded-lg px-7 text-sm font-semibold hover:opacity-90'
style={{ backgroundColor: T.accent, color: T.bg }}
className='group h-11 rounded-lg px-7 text-sm font-semibold'
render={<Link to='/dashboard' />}
>
{t('Go to Dashboard')}
@@ -111,8 +93,7 @@ export function Hero(props: HeroProps) {
) : (
<>
<Button
className='group h-11 rounded-lg px-7 text-sm font-semibold hover:opacity-90'
style={{ backgroundColor: T.accent, color: T.bg }}
className='group h-11 rounded-lg px-7 text-sm font-semibold'
render={<Link to='/sign-up' />}
>
{t('Get Started')}
@@ -120,10 +101,7 @@ export function Hero(props: HeroProps) {
</Button>
<Link
to='/pricing'
className='text-[13px] font-medium transition-colors duration-200'
style={{ color: T.grayLight }}
onMouseEnter={(e) => { e.currentTarget.style.color = T.accent }}
onMouseLeave={(e) => { e.currentTarget.style.color = T.grayLight }}
className='text-[13px] font-medium text-muted-foreground transition-colors duration-200 hover:text-primary'
>
{t('View Pricing')}
</Link>
@@ -134,58 +112,55 @@ export function Hero(props: HeroProps) {
{/* ── Terminal demo ── */}
<div className='-mt-6 pb-24'>
<div
className='mx-auto w-full max-w-2xl overflow-hidden rounded-xl border'
style={{ borderColor: T.border, backgroundColor: T.surface, boxShadow: '0 0 80px -20px rgba(0,210,255,0.1)' }}
>
<div className='mx-auto w-full max-w-2xl overflow-hidden rounded-xl border border-border bg-card shadow-lg dark:shadow-[0_0_80px_-20px_rgba(0,210,255,0.1)]'>
{/* Terminal header */}
<div className='flex items-center gap-2 border-b px-4 py-2.5' style={{ borderColor: T.border }}>
<div className='flex items-center gap-2 border-b border-border px-4 py-2.5'>
<div className='flex gap-1.5'>
<div className='size-2.5 rounded-full' style={{ backgroundColor: T.red }} />
<div className='size-2.5 rounded-full' style={{ backgroundColor: T.yellow }} />
<div className='size-2.5 rounded-full' style={{ backgroundColor: T.green }} />
<div className='size-2.5 rounded-full bg-red-500' />
<div className='size-2.5 rounded-full bg-yellow-500' />
<div className='size-2.5 rounded-full bg-green-500' />
</div>
<span className='ml-2 text-[11px]' style={{ color: T.gray }}>bash api</span>
<span className='ml-2 text-[11px] text-muted-foreground'>bash api</span>
</div>
{/* Terminal content */}
<div className='p-5 font-mono text-[13px] leading-[1.85]'>
<div>
<span style={{ color: T.accent }}>$</span>{' '}
<span style={{ color: T.white }}>curl</span>{' '}
<span style={{ color: T.accent }}>{serverAddress}/v1/chat/completions</span>{' '}
<span style={{ color: T.gray }}>\</span>
<span className='text-primary'>$</span>{' '}
<span className='text-foreground'>curl</span>{' '}
<span className='text-primary'>{serverAddress}/v1/chat/completions</span>{' '}
<span className='text-muted-foreground'>\</span>
</div>
<div className='pl-5'>
<span style={{ color: T.gray }}>-H</span>{' '}
<span style={{ color: T.yellow }}>"Authorization: Bearer sk-..."</span>{' '}
<span style={{ color: T.gray }}>\</span>
<span className='text-muted-foreground'>-H</span>{' '}
<span className='text-yellow-600 dark:text-yellow-400">"Authorization: Bearer sk-..."</span>{' '}
<span className='text-muted-foreground'>\</span>
</div>
<div className='pl-5'>
<span style={{ color: T.gray }}>-d</span>{' '}
<span style={{ color: T.yellow }}>{"'{ \"model\": \"gpt-4o\", \"messages\": [...] }'"}</span>
<span className='text-muted-foreground'>-d</span>{' '}
<span className='text-yellow-600 dark:text-yellow-400">{"'{ \"model\": \"gpt-4o\", \"messages\": [...] }'"}</span>
</div>
<div className='mt-5 border-t pt-4' style={{ borderColor: T.border }}>
<div className='flex items-center gap-2 mb-3'>
<span className='inline-block size-1.5 rounded-full' style={{ backgroundColor: T.green }} />
<span className='text-[11px] font-medium' style={{ color: T.gray }}>200 OK</span>
<span className='text-[11px]' style={{ color: T.gray }}>312ms</span>
<div className='mt-5 border-t border-border pt-4'>
<div className='mb-3 flex items-center gap-2'>
<span className='inline-block size-1.5 rounded-full bg-green-500' />
<span className='text-[11px] font-medium text-muted-foreground'>200 OK</span>
<span className='text-[11px] text-muted-foreground'>312ms</span>
</div>
<div>
<span style={{ color: T.gray }}>{"{"}</span>
<span style={{ color: T.accent }}> "model"</span>
<span style={{ color: T.gray }}>:</span>
<span style={{ color: T.yellow }}> "gpt-4o"</span>
<span style={{ color: T.gray }}>,</span>
<span style={{ color: T.accent }}> "usage"</span>
<span style={{ color: T.gray }}>:</span>
<span style={{ color: T.gray }}> {"{ \"prompt_tokens\": 12, \"completion_tokens\": 47 }"}</span>
<span style={{ color: T.gray }}>{"}"}</span>
<span className='text-muted-foreground'>{"{"}</span>
<span className='text-primary'> "model"</span>
<span className='text-muted-foreground'>:</span>
<span className='text-yellow-600 dark:text-yellow-400'> "gpt-4o"</span>
<span className='text-muted-foreground'>,</span>
<span className='text-primary'> "usage"</span>
<span className='text-muted-foreground'>:</span>
<span className='text-muted-foreground'> {"{ \"prompt_tokens\": 12, \"completion_tokens\": 47 }"}</span>
<span className='text-muted-foreground'>{"}"}</span>
</div>
</div>
<div className='mt-3'>
<span style={{ color: T.accent }}>$</span>
<span className='ml-1 inline-block h-3.5 w-[7px] animate-pulse align-middle' style={{ backgroundColor: T.accent }} />
<span className='text-primary'>$</span>
<span className='ml-1 inline-block h-3.5 w-[7px] animate-pulse bg-primary align-middle' />
</div>
</div>
</div>
@@ -194,10 +169,10 @@ export function Hero(props: HeroProps) {
{/* ── Features ── */}
<div className='pb-28'>
<div className='mb-14 text-center'>
<h2 className='text-xl font-semibold tracking-tight' style={{ color: T.white }}>
<h2 className='text-xl font-semibold tracking-tight text-foreground'>
{t('Full-featured gateway')}
</h2>
<p className='mt-2 text-sm' style={{ color: T.grayLight }}>
<p className='mt-2 text-sm text-muted-foreground'>
{t('Everything you need to manage, route, and monitor LLM API traffic')}
</p>
</div>
@@ -206,30 +181,15 @@ export function Hero(props: HeroProps) {
{features.map((feature) => (
<div
key={feature.title}
className='group rounded-lg border p-5 transition-all duration-200'
style={{
borderColor: T.border,
backgroundColor: T.surface,
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = T.accentDim
e.currentTarget.style.boxShadow = '0 0 24px -8px rgba(0,210,255,0.15)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = T.border
e.currentTarget.style.boxShadow = 'none'
}}
className='group rounded-lg border border-border bg-card p-5 transition-all duration-200 hover:border-primary/50 hover:shadow-md dark:hover:shadow-[0_0_24px_-8px_rgba(0,210,255,0.15)]'
>
<div
className='mb-3 flex size-9 items-center justify-center rounded-md'
style={{ backgroundColor: `${T.accent}10` }}
>
<feature.icon className='size-4' style={{ color: T.accent }} />
<div className='mb-3 flex size-9 items-center justify-center rounded-md bg-primary/10'>
<feature.icon className='size-4 text-primary' />
</div>
<h3 className='text-sm font-semibold' style={{ color: T.white }}>
<h3 className='text-sm font-semibold text-foreground'>
{t(feature.title)}
</h3>
<p className='mt-1.5 text-[13px] leading-relaxed' style={{ color: T.gray }}>
<p className='mt-1.5 text-[13px] leading-relaxed text-muted-foreground'>
{t(feature.desc)}
</p>
</div>
+2 -4
View File
@@ -62,10 +62,8 @@ export function Home() {
return (
<PublicLayout showMainContainer={false}>
<div className='dark'>
<Hero isAuthenticated={isAuthenticated} />
<Footer />
</div>
<Hero isAuthenticated={isAuthenticated} />
<Footer />
</PublicLayout>
)
}