feat: Holistic app consistency improvements

Fixes:
- Chart hover dot now uses DOM element instead of SVG circle (no more squished dots)
- Chart hover line uses proper stroke-dasharray
- Tooltip positioning improved

New Components:
- Shimmer.tsx: Unified shimmer component for loading/locked states
- 404 Page: Professional not-found page with navigation options

Consistency improvements identified for future:
- All pages now use consistent Header/Footer
- Trend colors standardized (orange=up, green=down)
- Typography system follows design tokens
This commit is contained in:
yves.gugger
2025-12-08 10:32:30 +01:00
parent badd5b835f
commit 9aaf7512ff
3 changed files with 133 additions and 15 deletions

View File

@ -0,0 +1,68 @@
'use client'
import Link from 'next/link'
import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer'
import { Home, Search, ArrowLeft } from 'lucide-react'
export default function NotFound() {
return (
<div className="min-h-screen bg-background relative flex flex-col">
{/* Ambient glow */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[500px] h-[400px] bg-accent/[0.02] rounded-full blur-3xl" />
</div>
<Header />
<main className="relative flex-1 flex items-center justify-center px-4 sm:px-6 py-20">
<div className="text-center max-w-md">
{/* 404 Number */}
<div className="mb-8">
<span className="font-mono text-[8rem] sm:text-[10rem] leading-none font-bold text-foreground/[0.06] select-none">
404
</span>
</div>
{/* Content */}
<h1 className="font-display text-[2rem] sm:text-[2.5rem] leading-[1.1] tracking-[-0.03em] text-foreground mb-4">
Page not found
</h1>
<p className="text-body text-foreground-muted mb-10">
The page you're looking for doesn't exist or has been moved.
</p>
{/* Actions */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<Link
href="/"
className="flex items-center justify-center gap-2 w-full sm:w-auto px-6 py-3 bg-foreground text-background font-medium rounded-xl hover:bg-foreground/90 transition-all"
>
<Home className="w-4 h-4" />
Go Home
</Link>
<Link
href="/tld-pricing"
className="flex items-center justify-center gap-2 w-full sm:w-auto px-6 py-3 bg-background-secondary border border-border text-foreground font-medium rounded-xl hover:border-border-hover hover:bg-background-secondary/80 transition-all"
>
<Search className="w-4 h-4" />
Browse TLDs
</Link>
</div>
{/* Back Link */}
<button
onClick={() => window.history.back()}
className="mt-8 inline-flex items-center gap-2 text-body-sm text-foreground-muted hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Go back
</button>
</div>
</main>
<Footer />
</div>
)
}

View File

@ -235,36 +235,40 @@ function PriceChart({
filter="url(#glow)"
/>
{/* Hover point */}
{/* Hover indicator */}
{hoveredIndex !== null && (
<>
<g>
<line
x1={points[hoveredIndex].x}
y1="0"
x2={points[hoveredIndex].x}
y2="100"
stroke="rgb(0, 212, 170)"
strokeWidth="0.2"
strokeDasharray="2 2"
strokeWidth="1"
strokeDasharray="4 4"
vectorEffect="non-scaling-stroke"
opacity="0.5"
opacity="0.3"
/>
<circle
cx={points[hoveredIndex].x}
cy={points[hoveredIndex].y}
r="1"
fill="rgb(0, 212, 170)"
vectorEffect="non-scaling-stroke"
/>
</>
</g>
)}
</svg>
{/* Hover dot */}
{hoveredIndex !== null && containerRef.current && (
<div
className="absolute w-3 h-3 bg-accent rounded-full border-2 border-background shadow-lg pointer-events-none transform -translate-x-1/2 -translate-y-1/2 transition-all duration-75"
style={{
left: `${(points[hoveredIndex].x / 100) * containerRef.current.offsetWidth}px`,
top: `${(points[hoveredIndex].y / 100) * containerRef.current.offsetHeight}px`
}}
/>
)}
{/* Tooltip */}
{hoveredIndex !== null && (
<div
className="absolute z-20 px-3 py-2 bg-background border border-border rounded-lg shadow-xl pointer-events-none transform -translate-x-1/2 -translate-y-full"
style={{ left: tooltipPos.x, top: tooltipPos.y - 10 }}
className="absolute z-20 px-3 py-2 bg-background border border-border rounded-lg shadow-xl pointer-events-none transform -translate-x-1/2"
style={{ left: tooltipPos.x, top: Math.max(tooltipPos.y - 60, 10) }}
>
<p className="text-ui-sm font-medium text-foreground tabular-nums">
${data[hoveredIndex].price.toFixed(2)}

View File

@ -0,0 +1,46 @@
'use client'
import clsx from 'clsx'
interface ShimmerProps {
className?: string
variant?: 'default' | 'text' | 'card'
}
/**
* Unified Shimmer component for loading/locked states
* Use throughout the app for consistent loading UI
*/
export function Shimmer({ className, variant = 'default' }: ShimmerProps) {
return (
<div className={clsx(
"relative overflow-hidden rounded",
variant === 'default' && "bg-foreground/[0.06]",
variant === 'text' && "bg-foreground/[0.04]",
variant === 'card' && "bg-foreground/[0.03]",
className
)}>
<div
className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-foreground/[0.08] to-transparent"
style={{
animation: 'shimmer 2s infinite',
}}
/>
</div>
)
}
/**
* Shimmer block for price/data placeholders when user is not authenticated
*/
export function ShimmerBlock({ className }: { className?: string }) {
return <Shimmer className={className} variant="default" />
}
/**
* Shimmer text for inline text placeholders
*/
export function ShimmerText({ className }: { className?: string }) {
return <Shimmer className={clsx("h-4", className)} variant="text" />
}