fix(public): make all around improvements
This commit is contained in:
@@ -9,12 +9,16 @@ export default function Layout({
|
||||
children: ReactNode;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<main className="overflow-hidden">
|
||||
<HeroContainer className="h-screen pointer-events-none" />
|
||||
<div className="absolute h-screen inset-0 radial-gradient-dot-pages select-none pointer-events-none" />
|
||||
<div className="-mt-[calc(100vh-100px)] relative min-h-[500px] pb-12">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<>
|
||||
<Navbar />
|
||||
<main className="overflow-hidden">
|
||||
<HeroContainer className="h-screen pointer-events-none" />
|
||||
<div className="absolute h-screen inset-0 radial-gradient-dot-pages select-none pointer-events-none" />
|
||||
<div className="-mt-[calc(100vh-100px)] relative min-h-[500px] pb-12">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,11 +60,7 @@ export default async function Layout({ children }: { children: ReactNode }) {
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={cn(GeistSans.variable, GeistMono.variable)}>
|
||||
<RootProvider>
|
||||
<TooltipProvider>
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Footer } from '@/components/footer';
|
||||
import { Hero } from '@/components/hero';
|
||||
import Navbar from '@/components/navbar';
|
||||
import { Faq } from '@/components/sections/faq';
|
||||
import { Features } from '@/components/sections/features';
|
||||
import { Pricing } from '@/components/sections/pricing';
|
||||
@@ -16,24 +18,28 @@ export const experimental_ppr = true;
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main>
|
||||
<Hero />
|
||||
<Features />
|
||||
<Testimonials />
|
||||
<Suspense
|
||||
fallback={
|
||||
<StatsPure
|
||||
projectCount={882}
|
||||
eventCount={634_000_000}
|
||||
last24hCount={7_000_000}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Stats />
|
||||
</Suspense>
|
||||
<Faq />
|
||||
<Pricing />
|
||||
<Sdks />
|
||||
</main>
|
||||
<>
|
||||
<Navbar />
|
||||
<main>
|
||||
<Hero />
|
||||
<Features />
|
||||
<Testimonials />
|
||||
<Suspense
|
||||
fallback={
|
||||
<StatsPure
|
||||
projectCount={882}
|
||||
eventCount={634_000_000}
|
||||
last24hCount={7_000_000}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Stats />
|
||||
</Suspense>
|
||||
<Faq />
|
||||
<Pricing />
|
||||
<Sdks />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useIsDarkMode } from '@/lib/dark-mode';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
type Frame = {
|
||||
@@ -14,22 +13,11 @@ type Frame = {
|
||||
};
|
||||
|
||||
function LivePreview() {
|
||||
const isDark = useIsDarkMode();
|
||||
const colorScheme = isDark ? 'dark' : 'light';
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLoaded(true);
|
||||
}, []);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<iframe
|
||||
// src={`http://localhost:3000/share/overview/zef2XC?header=0&range=30d&colorScheme=${colorScheme}`}
|
||||
src={`https://dashboard.openpanel.dev/share/overview/zef2XC?header=0&range=30d&colorScheme=${colorScheme}`}
|
||||
src={
|
||||
'https://dashboard.openpanel.dev/share/overview/zef2XC?header=0&range=30d'
|
||||
}
|
||||
className="w-full h-full"
|
||||
title="Live preview"
|
||||
scrolling="no"
|
||||
@@ -38,15 +26,17 @@ function LivePreview() {
|
||||
}
|
||||
|
||||
function Image({ src }: { src: string }) {
|
||||
const isDark = useIsDarkMode();
|
||||
const colorScheme = isDark ? 'dark' : 'light';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
className="w-full h-full"
|
||||
src={`/${src}-${colorScheme}.png`}
|
||||
alt={src}
|
||||
className="w-full h-full block dark:hidden"
|
||||
src={`/${src}-light.png`}
|
||||
alt={`${src} light`}
|
||||
/>
|
||||
<img
|
||||
className="w-full h-full hidden dark:block"
|
||||
src={`/${src}-dark.png`}
|
||||
alt={`${src} dark`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -78,12 +68,12 @@ export function HeroCarousel() {
|
||||
label: 'Retention',
|
||||
Component: () => <Image src="retention" />,
|
||||
},
|
||||
// {
|
||||
// id: 'profile',
|
||||
// key: 'profile',
|
||||
// label: 'Inspect profile',
|
||||
// Component: () => <Image src="profile" />,
|
||||
// },
|
||||
{
|
||||
id: 'profile',
|
||||
key: 'profile',
|
||||
label: 'Inspect profile',
|
||||
Component: () => <Image src="profile" />,
|
||||
},
|
||||
];
|
||||
|
||||
const [activeFrames, setActiveFrames] = useState<Frame[]>([frames[0]]);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DollarSignIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { HeroCarousel } from './hero-carousel';
|
||||
import { HeroMap } from './hero-map';
|
||||
import { Tag } from './tag';
|
||||
import { Button } from './ui/button';
|
||||
import { WorldMap } from './world-map';
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
@@ -13,7 +14,11 @@ export function Hero() {
|
||||
|
||||
{/* Content */}
|
||||
<div className="container relative z-10">
|
||||
<div className="max-w-2xl col gap-4 pt-28 text-center mx-auto ">
|
||||
<div className="max-w-2xl col gap-4 pt-44 text-center mx-auto ">
|
||||
<Tag className="self-center">
|
||||
<DollarSignIcon className="size-4" />
|
||||
Free during beta
|
||||
</Tag>
|
||||
<h1 className="text-4xl md:text-6xl font-bold leading-[1.1]">
|
||||
An open-source alternative to <span>Mixpanel</span>
|
||||
</h1>
|
||||
@@ -31,7 +36,7 @@ export function Hero() {
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Free for 30 days, no credit card required
|
||||
Free trail for 30 days, no credit card required
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Logo } from './logo';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
const Navbar = () => {
|
||||
const pathname = usePathname();
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const navbarRef = useRef<HTMLDivElement>(null);
|
||||
@@ -36,10 +35,6 @@ const Navbar = () => {
|
||||
return () => window.removeEventListener('click', handleClick);
|
||||
}, [isMobileMenuOpen]);
|
||||
|
||||
if (pathname.startsWith('/docs')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="fixed top-4 z-50 w-full" ref={navbarRef}>
|
||||
<div className="container">
|
||||
|
||||
@@ -194,7 +194,7 @@ export function EventsFeature() {
|
||||
return (
|
||||
<div className="overflow-hidden p-8 max-h-[700px]">
|
||||
<div
|
||||
className="min-w-[1000px] gap-4 flex flex-col overflow-hidden relative isolate"
|
||||
className="min-w-[500px] gap-4 flex flex-col overflow-hidden relative isolate"
|
||||
// style={{ height: 60 * TOTAL_EVENTS + 16 * (TOTAL_EVENTS - 1) }}
|
||||
>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
@@ -228,7 +228,9 @@ export function EventsFeature() {
|
||||
{event.location}
|
||||
</div>
|
||||
<div className="w-[150px] py-2 px-4 truncate">
|
||||
<img src={getPlatformIcon(event.platform)} alt="" />
|
||||
<span className="mr-2 text-xl relative top-px">
|
||||
{getPlatformIcon(event.platform)}
|
||||
</span>
|
||||
{event.platform}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -38,25 +38,18 @@ const COHORT_DATA = [
|
||||
{
|
||||
week: 'Week 7',
|
||||
users: '2,108',
|
||||
retention: [100, 82, 71, 65, 61, 57],
|
||||
retention: [100, 82, 71, 65, 61],
|
||||
},
|
||||
{
|
||||
week: 'Week 8',
|
||||
users: '1,896',
|
||||
retention: [100, 83, 72, 66, 62, 58],
|
||||
retention: [100, 83, 72, 66],
|
||||
},
|
||||
{
|
||||
week: 'Week 9',
|
||||
users: '2,156',
|
||||
retention: [100, 81, 70, 64, 60, 56],
|
||||
retention: [100, 81, 70],
|
||||
},
|
||||
{ week: 'Week 10', users: '2,089', retention: [100, 84, 73, 67, 63] },
|
||||
{ week: 'Week 11', users: '1,967', retention: [100, 82, 71, 65] },
|
||||
{ week: 'Week 12', users: '2,198', retention: [100, 83, 72] },
|
||||
{ week: 'Week 13', users: '2,045', retention: [100, 81] },
|
||||
// { week: 'Week 14', users: '1,978', retention: [100, 84, 73] },
|
||||
// { week: 'Week 15', users: '2,134', retention: [100, 82] },
|
||||
// { week: 'Week 16', users: '1,923', retention: [100] },
|
||||
];
|
||||
const COHORT_DATA_ALT = [
|
||||
{
|
||||
@@ -92,25 +85,18 @@ const COHORT_DATA_ALT = [
|
||||
{
|
||||
week: 'Week 7',
|
||||
users: '2,432',
|
||||
retention: [100, 93, 88, 72, 64, 60],
|
||||
retention: [100, 93, 88, 72, 64],
|
||||
},
|
||||
{
|
||||
week: 'Week 8',
|
||||
users: '2,123',
|
||||
retention: [100, 78, 76, 69, 65, 61],
|
||||
retention: [100, 78, 76, 69],
|
||||
},
|
||||
{
|
||||
week: 'Week 9',
|
||||
users: '2,567',
|
||||
retention: [100, 70, 64, 61, 59, 58],
|
||||
retention: [100, 70, 64],
|
||||
},
|
||||
{ week: 'Week 10', users: '2,345', retention: [100, 88, 77, 71, 67] },
|
||||
{ week: 'Week 11', users: '2,234', retention: [100, 86, 75, 69] },
|
||||
{ week: 'Week 12', users: '2,543', retention: [100, 79, 76] },
|
||||
{ week: 'Week 13', users: '2,321', retention: [100, 77] },
|
||||
// { week: 'Week 14', users: '1,978', retention: [100, 84, 73] },
|
||||
// { week: 'Week 15', users: '2,134', retention: [100, 82] },
|
||||
// { week: 'Week 16', users: '1,923', retention: [100] },
|
||||
];
|
||||
|
||||
function RetentionCell({ percentage }: { percentage: number }) {
|
||||
|
||||
@@ -6,6 +6,15 @@ import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
|
||||
|
||||
function useMediaQuery(query: string) {
|
||||
const [matches, setMatches] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
setMatches(media.matches);
|
||||
}, [query]);
|
||||
return matches;
|
||||
}
|
||||
|
||||
const Slider = ({
|
||||
ref,
|
||||
className,
|
||||
@@ -19,36 +28,44 @@ const Slider = ({
|
||||
max: number;
|
||||
step: number;
|
||||
onValueChange: (value: number[]) => void;
|
||||
}) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full touch-none select-none items-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-white/10">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-white/90" />
|
||||
</SliderPrimitive.Track>
|
||||
{tooltip ? (
|
||||
<Tooltip open disableHoverableContent>
|
||||
<TooltipTrigger asChild>
|
||||
}) => {
|
||||
const isDesktop = useMediaQuery('(min-width: 768px)');
|
||||
return (
|
||||
<>
|
||||
{!isDesktop && (
|
||||
<div className="text-sm text-muted-foreground mb-4">{tooltip}</div>
|
||||
)}
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex w-full touch-none select-none items-center',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-white/10">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-white/90" />
|
||||
</SliderPrimitive.Track>
|
||||
{tooltip && isDesktop ? (
|
||||
<Tooltip open disableHoverableContent>
|
||||
<TooltipTrigger asChild>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-white bg-black ring-offset-black transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
className="rounded-full bg-black text-white/70 py-1 text-xs border-white/30"
|
||||
>
|
||||
{tooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-white bg-black ring-offset-black transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
className="rounded-full bg-black text-white/70 py-1 text-xs border-white/30"
|
||||
>
|
||||
{tooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-white bg-black ring-offset-black transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
)}
|
||||
</SliderPrimitive.Root>
|
||||
);
|
||||
)}
|
||||
</SliderPrimitive.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,18 +58,38 @@ const COORDINATES = [
|
||||
{ lat: -43.532, lng: 172.6306 }, // Christchurch, New Zealand
|
||||
];
|
||||
|
||||
const getRandomCoordinates = (count: number) => {
|
||||
const shuffled = [...COORDINATES].sort(() => 0.5 - Math.random());
|
||||
return shuffled.slice(0, count);
|
||||
};
|
||||
|
||||
export function WorldMap() {
|
||||
const [visiblePins, setVisiblePins] = useState<typeof COORDINATES>([]);
|
||||
const [visiblePins, setVisiblePins] = useState<typeof COORDINATES>([
|
||||
{ lat: 61.2181, lng: -149.9003 },
|
||||
{ lat: 31.2304, lng: 121.4737 },
|
||||
{ lat: 59.5613, lng: 150.8086 },
|
||||
{ lat: 64.8378, lng: -147.7164 },
|
||||
{ lat: -33.8688, lng: 151.2093 },
|
||||
{ lat: 43.0621, lng: 141.3544 },
|
||||
{ lat: 58.3019, lng: -134.4197 },
|
||||
{ lat: 37.5665, lng: 126.978 },
|
||||
{ lat: -41.2865, lng: 174.7762 },
|
||||
{ lat: -36.8485, lng: 174.7633 },
|
||||
{ lat: -31.9505, lng: 115.8605 },
|
||||
{ lat: 35.6762, lng: 139.6503 },
|
||||
{ lat: 49.2827, lng: -123.1207 },
|
||||
{ lat: -12.4634, lng: 130.8456 },
|
||||
{ lat: 56.1304, lng: 101.614 },
|
||||
{ lat: 22.3193, lng: 114.1694 },
|
||||
{ lat: 55.3422, lng: -131.6461 },
|
||||
{ lat: 32.7157, lng: -117.1611 },
|
||||
{ lat: 61.5815, lng: -149.444 },
|
||||
{ lat: 60.5544, lng: -151.2583 },
|
||||
]);
|
||||
const activePinColor = '#2265EC';
|
||||
const inactivePinColor = '#818181';
|
||||
const visiblePinsCount = 20;
|
||||
|
||||
// Helper function to get random coordinates
|
||||
const getRandomCoordinates = (count: number) => {
|
||||
const shuffled = [...COORDINATES].sort(() => 0.5 - Math.random());
|
||||
return shuffled.slice(0, count);
|
||||
};
|
||||
|
||||
// Helper function to update pins
|
||||
const updatePins = () => {
|
||||
setVisiblePins((current) => {
|
||||
@@ -97,16 +117,13 @@ export function WorldMap() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Initial pins
|
||||
setVisiblePins(getRandomCoordinates(10));
|
||||
|
||||
// Update pins every 4 seconds
|
||||
const interval = setInterval(updatePins, 2000);
|
||||
const interval = setInterval(updatePins, 4000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const map = useMemo(() => {
|
||||
const map = new DottedMap({ map: JSON.parse(mapJsonString) });
|
||||
const map = new DottedMap({ map: mapJsonString as any });
|
||||
|
||||
visiblePins.forEach((coord) => {
|
||||
map.addPin({
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 714 KiB After Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 262 KiB |
Reference in New Issue
Block a user