mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 04:20:39 +00:00
The /analytics landing page hardcoded theme-independent colors (var(--pure-black), var(--pure-white), --neutral-*) into the hero ground, dashboard mock, code windows and section washes. In light mode that meant near-black ink on a pure-black hero and white chart lines on light surfaces — unreadable and harsh. Swap them for @hanzo/design's theme-aware tokens (--background, --muted, --secondary, --primary, --muted-foreground). Dark rendering is unchanged; light mode now resolves correctly. Verified with headless Chromium (hero bg/ink before vs after) and clean tsc --noEmit / design lint. Co-authored-by: Hanzo Dev <[email protected]>
163 lines
6.6 KiB
TypeScript
163 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
import React from "react";
|
|
import { motion } from "@/components/motion";
|
|
import { ArrowRight, LineChart } from "lucide-react";
|
|
import { Action, Chip, Display, Lede } from '@hanzo/ui/marketing'
|
|
import { Delta, Dots, Figure, Label, Panel, ink, tint } from './kit'
|
|
import { Box, Text, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
|
|
/** Each figure's change is an argument of one sentence, `{change} vs last week`. */
|
|
const STATS = [
|
|
{ label: phrase('Users'), value: '127,543', change: '+12.4%', at: 1 },
|
|
{ label: phrase('Conversion Rate'), value: '4.87%', change: '+0.8%', at: 1.2 },
|
|
{ label: phrase('Revenue'), value: '$45,892', change: '+15.2%', at: 1.4 },
|
|
]
|
|
|
|
/**
|
|
* The points that breathe behind the fold. Spread by the golden angle rather
|
|
* than drawn at random, so the page the server writes is the page the browser
|
|
* hydrates — a random place differs between the two and React says so.
|
|
*/
|
|
const POINTS = Array.from({ length: 20 }, (_, i) => ({
|
|
x: (i * 61.803) % 100,
|
|
y: (i * 38.197 + 17) % 100,
|
|
duration: 3 + ((i * 7) % 10) / 5,
|
|
delay: (i * 13) % 50 / 10,
|
|
}))
|
|
|
|
const HeroSection = () => {
|
|
const { t } = useI18n()
|
|
return (
|
|
<Box
|
|
render="section"
|
|
position="relative"
|
|
display="flex"
|
|
flexDirection="row"
|
|
minH="100vh"
|
|
items="center"
|
|
justify="center"
|
|
overflow="hidden"
|
|
px="$4"
|
|
py="$14"
|
|
$sm={{ px: '$5' }}
|
|
$lg={{ px: '$6' }}
|
|
>
|
|
<Box position="absolute" t={0} r={0} b={0} l={0} z="var(--z-base)" bg="var(--background)" />
|
|
|
|
{/* Animated dots/points background */}
|
|
<Box position="absolute" t={0} r={0} b={0} l={0} overflow="hidden" opacity={0.3}>
|
|
{POINTS.map((p, i) => (
|
|
<View key={i} position="absolute" l={`${p.x}%`} t={`${p.y}%`}>
|
|
<motion.div animate={{ opacity: [0.2, 0.8, 0.2], scale: [1, 1.5, 1] }} transition={{ duration: p.duration, repeat: Infinity, delay: p.delay }}>
|
|
<View width={4} height={4} rounded={9999} bg="var(--primary)" />
|
|
</motion.div>
|
|
</View>
|
|
))}
|
|
</Box>
|
|
|
|
<Box position="relative" z="var(--z-raised)" width="100%" maxW={1280} mx="auto">
|
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6 }}>
|
|
<Box mb={32} $platform-web={{ textAlign: 'center' }}>
|
|
<Chip
|
|
mb={24}
|
|
py={4}
|
|
px={16}
|
|
fontWeight="500" borderColor="var(--white-30)" bg={tint(10)} color={ink(70)}
|
|
>
|
|
{t('Web analytics you host yourself')}
|
|
</Chip>
|
|
<Display>Hanzo Analytics</Display>
|
|
<Lede mt={24} mx="auto" maxW={768}>
|
|
{t('Traffic, sessions and revenue for the sites you run, in a dashboard you can read without training. It is one script on the page and one database you own — no sampling, no data-processing agreement, and no export limits on your own numbers.')}
|
|
</Lede>
|
|
</Box>
|
|
</motion.div>
|
|
|
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, delay: 0.3 }}>
|
|
<XStack mt={40} flexWrap="wrap" gap={16} justify="center">
|
|
<Action href="/login" fill>
|
|
{t('Try Hanzo')}
|
|
</Action>
|
|
<Action href="/contact-sales">
|
|
{t('Request Demo')} <ArrowRight size={20} data-i18n-mirror />
|
|
</Action>
|
|
</XStack>
|
|
</motion.div>
|
|
|
|
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.6, delay: 0.6 }}>
|
|
<Box
|
|
position="relative"
|
|
mt={64}
|
|
mx="auto"
|
|
maxW={1024}
|
|
p="$4"
|
|
rounded="var(--radius-xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-800)"
|
|
overflow="hidden"
|
|
boxShadow="var(--shadow-2xl)"
|
|
$platform-web={{ background: 'linear-gradient(to bottom right, var(--muted), var(--background))' }}
|
|
>
|
|
<XStack mb="$4" items="center" justify="space-between" borderBottomWidth={1} borderColor="var(--neutral-800)" pb="$2">
|
|
<XStack items="center" gap="$2">
|
|
<LineChart size={20} color="var(--foreground)" />
|
|
<Text fontWeight="500" color="var(--foreground)">
|
|
{t('Hanzo Analytics Dashboard')}
|
|
</Text>
|
|
</XStack>
|
|
<Dots />
|
|
</XStack>
|
|
<Grid columns={3} gap={16}>
|
|
<Panel gridColumn="span 2" height={256}>
|
|
{/* Animated chart lines */}
|
|
<Box position="relative" height="100%" width="100%">
|
|
<Box position="absolute" b={0} l={0} width="100%" height={1} bg="var(--neutral-700)" />
|
|
<Box position="absolute" l={0} t={0} height="100%" width={1} bg="var(--neutral-700)" />
|
|
|
|
<svg width="100%" height="100%" viewBox="0 0 100 50">
|
|
<motion.path
|
|
d="M0,50 L10,40 L20,45 L30,35 L40,38 L50,25 L60,30 L70,20 L80,15 L90,10 L100,5"
|
|
fill="none"
|
|
stroke="var(--primary)"
|
|
strokeWidth="1"
|
|
initial={{ pathLength: 0 }}
|
|
animate={{ pathLength: 1 }}
|
|
transition={{ duration: 2, delay: 0.5 }}
|
|
/>
|
|
<motion.path
|
|
d="M0,50 L10,42 L20,40 L30,38 L40,30 L50,32 L60,25 L70,28 L80,20 L90,18 L100,15"
|
|
fill="none"
|
|
stroke="var(--muted-foreground)"
|
|
strokeWidth="1"
|
|
initial={{ pathLength: 0 }}
|
|
animate={{ pathLength: 1 }}
|
|
transition={{ duration: 2, delay: 0.8 }}
|
|
/>
|
|
</svg>
|
|
</Box>
|
|
</Panel>
|
|
<YStack gap="$4">
|
|
{STATS.map((s) => (
|
|
<Panel key={s.label} height="30%">
|
|
<Label>{t(s.label)}</Label>
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: s.at }}>
|
|
<Figure>{s.value}</Figure>
|
|
</motion.div>
|
|
<Delta>{t('{change} vs last week', { change: s.change })}</Delta>
|
|
</Panel>
|
|
))}
|
|
</YStack>
|
|
</Grid>
|
|
</Box>
|
|
</motion.div>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default HeroSection;
|