mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 04:02:33 +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]>
234 lines
9.1 KiB
TypeScript
234 lines
9.1 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
import { motion } from "@/components/motion";
|
|
import { ExternalLink, Users, BookOpen } from "lucide-react"
|
|
import { Github } from "@/components/brand"
|
|
import { Band, Head } from '@/components/marketing/band'
|
|
import { Action } from '@hanzo/ui/marketing'
|
|
import { Body, Disc, Lattice, Tile, ink } from './kit'
|
|
import { Box, Text, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { DOCS } from '@/components/home/nav-data'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
|
|
const GROUND = 'linear-gradient(to bottom, var(--background), color-mix(in oklab, var(--muted) 30%, transparent))'
|
|
|
|
const statsItems = [
|
|
{ icon: <Github size={24} />, value: 17, suffix: "M+", label: phrase("Downloads"), countUpDuration: 2 },
|
|
{ icon: <Github size={24} />, value: 25, suffix: "K+", label: phrase("GitHub Stars"), countUpDuration: 2.2 },
|
|
{ icon: <Users size={24} />, value: 280, suffix: "+", label: phrase("Contributors"), countUpDuration: 1.8 },
|
|
];
|
|
|
|
const RESOURCES = [
|
|
{ title: phrase("Documentation"), description: phrase("Comprehensive guides and API references"), link: `${DOCS}/docs/analytics` },
|
|
{ title: phrase("Community Forum"), description: phrase("Join discussions with developers and AI experts"), link: "https://discord.gg/XthHQQj" },
|
|
{ title: phrase("Examples"), description: phrase("Sample projects showcasing Hanzo Analytics"), link: "https://github.com/hanzoai/insights" },
|
|
{ title: phrase("Support"), description: phrase("Get help from our team and community"), link: "/support" },
|
|
]
|
|
|
|
/** A card heading in this section, the 2xl rung. */
|
|
function Heading({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<Text render="h3" display="block" fontSize="$7" lineHeight="1.75rem" fontWeight="700" color="inherit">
|
|
{children}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
/** A placeholder row in the contributor mock. */
|
|
function Bar({ width, mt }: { width: '75%' | '50%'; mt?: '$2' }) {
|
|
return <Box height={8} rounded={4} bg="var(--neutral-700)" width={width} mt={mt} />
|
|
}
|
|
|
|
/**
|
|
* The observer here COUNTS, it does not conceal.
|
|
*
|
|
* It used to do both: one `IntersectionObserver` started the tallies AND drove
|
|
* an `animate={controls}` that held both card grids at `opacity: 0` until it
|
|
* fired — so a reader the observer missed got the stats section as a blank
|
|
* 872px. Starting a count when the number comes into view is a real thing to
|
|
* watch for; deciding whether the copy exists is not, and the two had no
|
|
* business on the same trigger. The reveal is `whileInView` now, like every
|
|
* other on the site, which cannot hide anything (`components/motion.tsx`).
|
|
*/
|
|
const Community = () => {
|
|
const { t } = useI18n();
|
|
const [isInView, setIsInView] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry.isIntersecting) setIsInView(true);
|
|
},
|
|
{ threshold: 0.2 }
|
|
);
|
|
observer.observe(el);
|
|
return () => observer.unobserve(el);
|
|
}, []);
|
|
|
|
const containerVariants = {
|
|
visible: {
|
|
opacity: 1,
|
|
transition: {
|
|
staggerChildren: 0.2
|
|
}
|
|
}
|
|
};
|
|
|
|
const itemVariants = {
|
|
visible: { opacity: 1, y: 0 }
|
|
};
|
|
|
|
return (
|
|
<Band rule={false} pad="$14" measure={1280} ground={GROUND}>
|
|
<Lattice size={20} opacity={0.02} />
|
|
|
|
<Box position="relative" z="var(--z-raised)">
|
|
<Head
|
|
loud
|
|
title={t('Powered by Community, Improved by Collaboration')}
|
|
lede={t('Join thousands of developers building the future of analytics together.')}
|
|
/>
|
|
|
|
<div ref={ref}>
|
|
<motion.div variants={containerVariants} whileInView="visible" viewport={{ once: true }}>
|
|
<Grid columns={{ min: 300, max: 3 }} gap={32} mb={64}>
|
|
{statsItems.map((item) => (
|
|
<motion.div key={item.label} variants={itemVariants}>
|
|
<Tile height="100%" p="$6" bg="$panel" $platform-web={{ textAlign: 'center' }}>
|
|
<XStack justify="center" mb="$4">
|
|
<Disc edge="var(--border)">{item.icon}</Disc>
|
|
</XStack>
|
|
|
|
<Text display="block" mb={8} fontSize="$10" lineHeight="2.2222rem" fontWeight="700" color="inherit">
|
|
{isInView ? <CountUp end={item.value} duration={item.countUpDuration} /> : 0}
|
|
{item.suffix}
|
|
</Text>
|
|
|
|
<Text display="block" fontSize="$3" lineHeight="1.25rem" color="var(--muted-foreground)">
|
|
{t(item.label)}
|
|
</Text>
|
|
</Tile>
|
|
</motion.div>
|
|
))}
|
|
</Grid>
|
|
</motion.div>
|
|
|
|
<motion.div variants={containerVariants} whileInView="visible" viewport={{ once: true }}>
|
|
<Grid columns={{ min: 420, max: 2 }} gap={32}>
|
|
<motion.div variants={itemVariants}>
|
|
<Tile height="100%" p="$6" bg="$panel">
|
|
<XStack items="center" mb="$5" gap="$3">
|
|
<Github size={24} color="var(--foreground)" />
|
|
<Heading>{t('Open Source')}</Heading>
|
|
</XStack>
|
|
|
|
<Body mb={24}>
|
|
{t('Hanzo Analytics is fully open source, with a vibrant community of developers contributing from around the world. Join us on GitHub to collaborate on the future of analytics.')}
|
|
</Body>
|
|
|
|
<YStack gap="$4" mb="$6">
|
|
{[0, 1, 2].map((i) => (
|
|
<XStack key={i} items="center" gap="$3">
|
|
<Box width={32} height={32} rounded="var(--radius-full)" bg="var(--neutral-700)" />
|
|
<Box flex={1}>
|
|
<Bar width="75%" />
|
|
<Bar width="50%" mt="$2" />
|
|
</Box>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
|
|
<Action href="https://github.com/hanzoai/insights">
|
|
<Github size={16} />
|
|
{t('GitHub Repo')}
|
|
<ExternalLink size={12} />
|
|
</Action>
|
|
</Tile>
|
|
</motion.div>
|
|
|
|
<motion.div variants={itemVariants}>
|
|
<Tile height="100%" p="$6" bg="$panel">
|
|
<XStack items="center" mb="$5" gap="$3">
|
|
<BookOpen size={24} color={ink(70)} />
|
|
<Heading>{t('Resources')}</Heading>
|
|
</XStack>
|
|
|
|
<Body mb={24}>
|
|
{t('Access comprehensive documentation, tutorials, and examples to help you make the most of Hanzo Analytics.')}
|
|
</Body>
|
|
|
|
<Grid columns={{ min: 200, max: 2 }} gap={16} mb={32}>
|
|
{RESOURCES.map((resource) => (
|
|
// A card that is a link: its edge brightens on approach.
|
|
<Box
|
|
key={resource.title}
|
|
render={<a href={resource.link} />}
|
|
display="block"
|
|
height="100%"
|
|
p={16}
|
|
rounded="var(--radius-xl)"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-700)"
|
|
hoverStyle={{ borderColor: 'var(--neutral-500)' }}
|
|
$platform-web={{ backgroundColor: 'color-mix(in oklab, var(--neutral-800) 50%, transparent)', textDecoration: 'none' }}
|
|
>
|
|
<Text render="h4" display="block" mb={4} fontSize="$3" lineHeight="1.25rem" fontWeight="500" color="var(--foreground)">
|
|
{t(resource.title)}
|
|
</Text>
|
|
<Text render="p" display="block" fontSize="$2" lineHeight="1.15rem" color="var(--muted-foreground)">
|
|
{t(resource.description)}
|
|
</Text>
|
|
</Box>
|
|
))}
|
|
</Grid>
|
|
|
|
<Action href={`${DOCS}/docs/analytics`}>
|
|
{t('Browse All Resources')}
|
|
<ExternalLink size={12} />
|
|
</Action>
|
|
</Tile>
|
|
</motion.div>
|
|
</Grid>
|
|
</motion.div>
|
|
</div>
|
|
</Box>
|
|
</Band>
|
|
);
|
|
};
|
|
|
|
// Simple CountUp component
|
|
const CountUp = ({ end, duration = 2 }: { end: number; duration?: number }) => {
|
|
const [count, setCount] = useState(0);
|
|
|
|
useEffect(() => {
|
|
let startTime: number;
|
|
let animationFrame: number;
|
|
|
|
const updateCount = (timestamp: number) => {
|
|
if (!startTime) startTime = timestamp;
|
|
const progress = Math.min((timestamp - startTime) / (duration * 1000), 1);
|
|
setCount(Math.floor(progress * end));
|
|
|
|
if (progress < 1) {
|
|
animationFrame = requestAnimationFrame(updateCount);
|
|
}
|
|
};
|
|
|
|
animationFrame = requestAnimationFrame(updateCount);
|
|
|
|
return () => {
|
|
cancelAnimationFrame(animationFrame);
|
|
};
|
|
}, [end, duration]);
|
|
|
|
return <>{count}</>;
|
|
};
|
|
|
|
export default Community;
|