fix(public): make all around improvements

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-11-14 10:39:05 +01:00
parent 4e8afb4eea
commit ab4efcfd77
13 changed files with 6185 additions and 135 deletions

View File

@@ -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 />
</>
);
}

View File

@@ -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>

View File

@@ -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 />
</>
);
}

View File

@@ -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]]);

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 }) {

View File

@@ -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

View File

@@ -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