mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-28 07:32:52 +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]>
222 lines
8.6 KiB
TypeScript
222 lines
8.6 KiB
TypeScript
'use client'
|
|
|
|
import React, { useRef } from "react";
|
|
import { motion } from "@/components/motion";
|
|
import { useScroll, useTransform } from "framer-motion";
|
|
import { ShoppingCart, DollarSign, CreditCard, Zap } from "lucide-react";
|
|
import { Band, Head, INK } from '@/components/marketing/band'
|
|
import { Body, Figure, Label, Panel, Sub, Title, ink } 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'
|
|
|
|
const FEATURES = [
|
|
{
|
|
icon: Zap,
|
|
title: phrase("Revenue beside the traffic"),
|
|
description: phrase("Send an amount and a currency with the event and it lands in the same reports as everything else. Totals, order values and the sessions behind them.")
|
|
},
|
|
{
|
|
icon: DollarSign,
|
|
title: phrase("Which campaign paid for itself"),
|
|
description: phrase("Attribution splits revenue across the channels and campaigns that touched the session, so spend can be argued with numbers rather than instinct.")
|
|
},
|
|
{
|
|
icon: CreditCard,
|
|
title: phrase("The checkout, step by step"),
|
|
description: phrase("Cart to payment as a funnel, with the abandonment rate per step. Add a goal for the one step you would defend in a meeting.")
|
|
},
|
|
]
|
|
|
|
const PRODUCTS = [
|
|
{ name: "Premium Headphones", score: 98 },
|
|
{ name: "Wireless Keyboard", score: 85 },
|
|
{ name: "Smart Watch", score: 79 },
|
|
]
|
|
|
|
/** The arrow beside a trend. */
|
|
const UP = (
|
|
<svg width={12} height={12} fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
</svg>
|
|
)
|
|
|
|
/** A figure's change, with its arrow. */
|
|
function Trend({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<XStack items="center" mt={4} gap={4} $platform-web={{ color: ink(70) }}>
|
|
{UP}
|
|
<Text fontSize="$2" lineHeight="1.15rem" color="inherit">
|
|
{children}
|
|
</Text>
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
const CommerceAnalytics = () => {
|
|
const { t } = useI18n();
|
|
const sectionRef = useRef<HTMLElement>(null);
|
|
const { scrollYProgress } = useScroll({
|
|
target: sectionRef,
|
|
offset: ["start end", "end start"]
|
|
});
|
|
|
|
const scale = useTransform(scrollYProgress, [0, 0.5], [0.95, 1]);
|
|
const opacity = useTransform(scrollYProgress, [0, 0.3], [0.5, 1]);
|
|
|
|
return (
|
|
<Band rule={false} pad="$14" measure={1280} ground={INK}>
|
|
{/* Background wash */}
|
|
<View
|
|
aria-hidden
|
|
position="absolute"
|
|
t={0}
|
|
r={0}
|
|
b={0}
|
|
l={0}
|
|
opacity={0.5}
|
|
$platform-web={{ background: 'linear-gradient(to bottom right, var(--white-10), var(--white-10))' }}
|
|
/>
|
|
|
|
<Box position="relative" z="var(--z-raised)" ref={sectionRef as never}>
|
|
<Head
|
|
loud
|
|
title={t('If you sell something')}
|
|
lede={t('Revenue is a report here, not an integration you buy on top.')}
|
|
/>
|
|
|
|
<Grid columns={{ min: 420, max: 2 }} gap={64}>
|
|
<YStack gap={48}>
|
|
{FEATURES.map((feature, index) => (
|
|
<motion.div
|
|
key={feature.title}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
>
|
|
<XStack>
|
|
<XStack
|
|
render="span"
|
|
mr={16}
|
|
p={12}
|
|
self="flex-start"
|
|
rounded={16}
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
$platform-web={{ background: 'linear-gradient(to bottom right, var(--white-20), var(--white-10))', color: 'var(--foreground)' }}
|
|
>
|
|
<feature.icon size={24} />
|
|
</XStack>
|
|
<Box>
|
|
<Title mb={8}>{t(feature.title)}</Title>
|
|
<Body>{t(feature.description)}</Body>
|
|
</Box>
|
|
</XStack>
|
|
</motion.div>
|
|
))}
|
|
</YStack>
|
|
|
|
{/* The dashboard grows into place as the section scrolls in. */}
|
|
{/* inline-style: `scale` and `opacity` are scroll-driven MotionValues framer writes per frame; no gui prop takes one. */}
|
|
<motion.div style={{ scale, opacity }}>
|
|
<Box
|
|
rounded={16}
|
|
overflow="hidden"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-800)"
|
|
boxShadow="var(--shadow-xl)"
|
|
$platform-web={{ background: 'linear-gradient(to bottom right, var(--muted), var(--background))' }}
|
|
>
|
|
<Box p="$4" borderBottomWidth={1} borderColor="var(--neutral-800)">
|
|
<XStack items="center" gap="$2">
|
|
<ShoppingCart size={20} color="var(--foreground)" />
|
|
<Text fontWeight="500" color="var(--foreground)">
|
|
{t('Commerce Dashboard')}
|
|
</Text>
|
|
</XStack>
|
|
</Box>
|
|
|
|
<Box p="$5">
|
|
<Box mb="$5">
|
|
<Sub mb={16}>{t('Sales Performance')}</Sub>
|
|
<Box position="relative" height={160} overflow="hidden" rounded="var(--radius-lg)" bg="$hover">
|
|
{/* Bar chart */}
|
|
<XStack position="absolute" t={0} r={0} b={0} l={0} items="flex-end" justify="space-around" px="$4" pb="$4">
|
|
{[65, 80, 55, 90, 70, 85, 60].map((height, i) => (
|
|
<motion.div
|
|
key={i}
|
|
initial={{ height: 0 }}
|
|
whileInView={{ height: `${height}%` }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: i * 0.1 }}
|
|
>
|
|
<View
|
|
width={24}
|
|
height="100%"
|
|
borderTopLeftRadius={4}
|
|
borderTopRightRadius={4}
|
|
$platform-web={{ background: 'linear-gradient(to top, var(--primary), var(--white-10))' }}
|
|
/>
|
|
</motion.div>
|
|
))}
|
|
</XStack>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Grid columns={2} gap={16} mb={24}>
|
|
<Panel bg="$panel">
|
|
<Label>{t('Conversion Rate')}</Label>
|
|
<Figure mt={8} color="inherit">
|
|
4.78%
|
|
</Figure>
|
|
<Trend>+0.6%</Trend>
|
|
</Panel>
|
|
<Panel bg="$panel">
|
|
<Label>{t('Avg. Order Value')}</Label>
|
|
<Figure mt={8} color="inherit">
|
|
$87.32
|
|
</Figure>
|
|
<Trend>+2.4%</Trend>
|
|
</Panel>
|
|
</Grid>
|
|
|
|
<Box borderTopWidth={1} borderColor="var(--neutral-800)" pt="$4">
|
|
<Sub mb={16}>{t('Product Recommendations')}</Sub>
|
|
<YStack gap="$3">
|
|
{PRODUCTS.map((product, i) => (
|
|
<XStack key={product.name} items="center" justify="space-between">
|
|
<Text fontSize="$3" lineHeight="1.25rem" $platform-web={{ color: ink(80) }}>
|
|
{product.name}
|
|
</Text>
|
|
<XStack items="center" gap="$2">
|
|
<Box width={96} height={8} overflow="hidden" rounded="var(--radius-full)" bg="var(--neutral-800)">
|
|
<motion.div
|
|
initial={{ width: 0 }}
|
|
whileInView={{ width: `${product.score}%` }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.8, delay: 0.3 + i * 0.1 }}
|
|
>
|
|
<View height={8} bg="var(--primary)" />
|
|
</motion.div>
|
|
</Box>
|
|
<Text fontSize="$2" lineHeight="1.15rem" color="var(--foreground)">
|
|
{product.score}%
|
|
</Text>
|
|
</XStack>
|
|
</XStack>
|
|
))}
|
|
</YStack>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</motion.div>
|
|
</Grid>
|
|
</Box>
|
|
</Band>
|
|
);
|
|
};
|
|
|
|
export default CommerceAnalytics;
|