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:
68
frontend/src/app/not-found.tsx
Normal file
68
frontend/src/app/not-found.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)}
|
||||
|
||||
46
frontend/src/components/Shimmer.tsx
Normal file
46
frontend/src/components/Shimmer.tsx
Normal 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" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user