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)"
|
filter="url(#glow)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hover point */}
|
{/* Hover indicator */}
|
||||||
{hoveredIndex !== null && (
|
{hoveredIndex !== null && (
|
||||||
<>
|
<g>
|
||||||
<line
|
<line
|
||||||
x1={points[hoveredIndex].x}
|
x1={points[hoveredIndex].x}
|
||||||
y1="0"
|
y1="0"
|
||||||
x2={points[hoveredIndex].x}
|
x2={points[hoveredIndex].x}
|
||||||
y2="100"
|
y2="100"
|
||||||
stroke="rgb(0, 212, 170)"
|
stroke="rgb(0, 212, 170)"
|
||||||
strokeWidth="0.2"
|
strokeWidth="1"
|
||||||
strokeDasharray="2 2"
|
strokeDasharray="4 4"
|
||||||
vectorEffect="non-scaling-stroke"
|
vectorEffect="non-scaling-stroke"
|
||||||
opacity="0.5"
|
opacity="0.3"
|
||||||
/>
|
/>
|
||||||
<circle
|
</g>
|
||||||
cx={points[hoveredIndex].x}
|
|
||||||
cy={points[hoveredIndex].y}
|
|
||||||
r="1"
|
|
||||||
fill="rgb(0, 212, 170)"
|
|
||||||
vectorEffect="non-scaling-stroke"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</svg>
|
</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 */}
|
{/* Tooltip */}
|
||||||
{hoveredIndex !== null && (
|
{hoveredIndex !== null && (
|
||||||
<div
|
<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"
|
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: tooltipPos.y - 10 }}
|
style={{ left: tooltipPos.x, top: Math.max(tooltipPos.y - 60, 10) }}
|
||||||
>
|
>
|
||||||
<p className="text-ui-sm font-medium text-foreground tabular-nums">
|
<p className="text-ui-sm font-medium text-foreground tabular-nums">
|
||||||
${data[hoveredIndex].price.toFixed(2)}
|
${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