refactor: align landing page and 404 with Calculate blueprint
- Hero: remove framer-motion, CSS stagger animations, glass pill CTA button, refined typography and scroll indicator - Stats: remove framer-motion, Lucide icons, tighter glass cards with mono labels - ToolsGrid: remove framer-motion, editorial section heading, 4-col xl grid - ToolCard: replace framer-motion motion.Link with plain Link + CSS hover, compact layout (icon→title→desc→badges+arrow), ElementType icon prop - Footer: remove framer-motion, matches sidebar footer style - BackToTop: remove framer-motion, JS scroll progress bar (1px primary line), compact glass button - not-found: remove framer-motion and shadcn Button, glass pill CTA, 120px mono 404, CSS stagger - page.tsx: remove unnecessary 'use client' directive Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,73 +1,53 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import AnimatedBackground from '@/components/AnimatedBackground';
|
import AnimatedBackground from '@/components/AnimatedBackground';
|
||||||
import Logo from '@/components/Logo';
|
import Logo from '@/components/Logo';
|
||||||
import { Button } from '@/components/ui/button';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
import { Home } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative min-h-screen dark text-foreground flex flex-col">
|
<main className="relative min-h-screen dark text-foreground flex flex-col">
|
||||||
<AnimatedBackground />
|
<AnimatedBackground />
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center px-4 py-20 relative z-10">
|
|
||||||
<div className="max-w-6xl mx-auto text-center">
|
|
||||||
{/* Logo */}
|
|
||||||
<motion.div
|
|
||||||
className="mb-8 flex justify-center"
|
|
||||||
initial={{ opacity: 0, y: -50 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8 }}
|
|
||||||
>
|
|
||||||
<Logo size={100} />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* 404 heading */}
|
<div className="flex-1 flex flex-col items-center justify-center px-6 py-20 relative z-10 text-center">
|
||||||
<motion.h1
|
|
||||||
className="text-7xl md:text-9xl font-bold mb-6 text-primary"
|
{/* Logo */}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<div style={{ animation: 'fadeIn 0.5s ease-out both' }}>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<Logo size={52} />
|
||||||
transition={{ duration: 0.8, delay: 0.2 }}
|
</div>
|
||||||
>
|
|
||||||
|
{/* 404 */}
|
||||||
|
<div
|
||||||
|
className="mt-8"
|
||||||
|
style={{ animation: 'slideUp 0.5s ease-out 0.15s both' }}
|
||||||
|
>
|
||||||
|
<span className="text-[80px] md:text-[120px] font-bold font-mono text-primary leading-none tabular-nums block">
|
||||||
404
|
404
|
||||||
</motion.h1>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Message */}
|
||||||
<motion.p
|
<div
|
||||||
className="text-xl md:text-3xl font-medium mb-4"
|
className="mt-4 space-y-1"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
style={{ animation: 'slideUp 0.5s ease-out 0.3s both' }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
>
|
||||||
transition={{ duration: 0.8, delay: 0.4 }}
|
<p className="text-sm font-medium text-foreground/70">Page not found</p>
|
||||||
>
|
<p className="text-[11px] text-muted-foreground/50 font-mono max-w-xs mx-auto leading-relaxed">
|
||||||
Page Not Found
|
The tool or page you're looking for doesn't exist or has been moved.
|
||||||
</motion.p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* CTA */}
|
||||||
<motion.p
|
<div
|
||||||
className="text-base md:text-lg text-muted-foreground/80 mb-12 max-w-md mx-auto"
|
className="mt-8"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
style={{ animation: 'slideUp 0.5s ease-out 0.45s both' }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
>
|
||||||
transition={{ duration: 0.8, delay: 0.6 }}
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center gap-2 px-5 py-2.5 glass rounded-lg border border-primary/30 hover:border-primary/60 hover:bg-primary/10 text-sm font-medium text-foreground/70 hover:text-foreground transition-all duration-200"
|
||||||
>
|
>
|
||||||
The tool or page you are looking for doesn't exist or has been moved.
|
<ArrowLeft className="w-3.5 h-3.5 text-primary" />
|
||||||
</motion.p>
|
Back to Home
|
||||||
|
</Link>
|
||||||
{/* CTA Button */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.8 }}
|
|
||||||
>
|
|
||||||
<Link href="/">
|
|
||||||
<Button size="lg" className="rounded-full px-8 h-14 text-lg font-semibold bg-gradient-to-r from-purple-500 to-cyan-500 hover:from-purple-600 hover:to-cyan-600 border-none transition-all duration-300">
|
|
||||||
<Home className="mr-2 h-5 w-5" />
|
|
||||||
Back to Home
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import AnimatedBackground from '@/components/AnimatedBackground';
|
import AnimatedBackground from '@/components/AnimatedBackground';
|
||||||
import Hero from '@/components/Hero';
|
import Hero from '@/components/Hero';
|
||||||
import Stats from '@/components/Stats';
|
import Stats from '@/components/Stats';
|
||||||
|
|||||||
@@ -1,76 +1,44 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion, useScroll, useSpring } from 'framer-motion';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { ChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
export default function BackToTop() {
|
export default function BackToTop() {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const { scrollYProgress } = useScroll();
|
const barRef = useRef<HTMLDivElement>(null);
|
||||||
const scaleX = useSpring(scrollYProgress, {
|
|
||||||
stiffness: 100,
|
|
||||||
damping: 30,
|
|
||||||
restDelta: 0.001,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const toggleVisibility = () => {
|
const onScroll = () => {
|
||||||
if (window.pageYOffset > 300) {
|
setIsVisible(window.scrollY > 300);
|
||||||
setIsVisible(true);
|
if (barRef.current) {
|
||||||
} else {
|
const el = document.documentElement;
|
||||||
setIsVisible(false);
|
const scrolled = el.scrollTop / (el.scrollHeight - el.clientHeight);
|
||||||
|
barRef.current.style.transform = `scaleX(${scrolled})`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
window.addEventListener('scroll', toggleVisibility);
|
return () => window.removeEventListener('scroll', onScroll);
|
||||||
return () => window.removeEventListener('scroll', toggleVisibility);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scrollToTop = () => {
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Progress bar */}
|
{/* Scroll progress bar */}
|
||||||
<motion.div
|
<div
|
||||||
className="fixed top-0 left-0 right-0 h-1 bg-gradient-to-r from-purple-500 to-cyan-500 transform origin-left z-50"
|
ref={barRef}
|
||||||
style={{ scaleX }}
|
className="fixed top-0 left-0 right-0 h-px bg-primary z-50 origin-left"
|
||||||
|
style={{ transform: 'scaleX(0)', transition: 'transform 0.1s linear' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Back to top button */}
|
{/* Back to top button */}
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<motion.button
|
<button
|
||||||
onClick={scrollToTop}
|
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||||
className="fixed bottom-8 right-8 p-4 rounded-full glass hover:bg-accent/50 text-purple-400 hover:text-purple-300 transition-colors shadow-lg z-40 group"
|
className="fixed bottom-6 right-6 w-8 h-8 glass rounded-lg flex items-center justify-center text-muted-foreground/40 hover:text-primary hover:border-primary/40 transition-all duration-200 z-40"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, y: 20 }}
|
|
||||||
whileHover={{ scale: 1.1 }}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
aria-label="Back to top"
|
aria-label="Back to top"
|
||||||
|
style={{ animation: 'fadeIn 0.2s ease-out both' }}
|
||||||
>
|
>
|
||||||
<svg
|
<ChevronUp className="w-3.5 h-3.5" />
|
||||||
className="w-6 h-6"
|
</button>
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{/* Tooltip */}
|
|
||||||
<span className="absolute bottom-full right-0 mb-2 px-3 py-1 text-xs text-white bg-gray-900 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
|
|
||||||
Back to top
|
|
||||||
</span>
|
|
||||||
</motion.button>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,47 +1,34 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { GitFork, Heart } from 'lucide-react';
|
import { GitFork, Heart } from 'lucide-react';
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="relative py-12 px-4">
|
<footer className="relative py-10 px-6">
|
||||||
<div className="max-w-6xl mx-auto border-t border-border pt-12">
|
<div className="max-w-5xl mx-auto border-t border-border/20 pt-8">
|
||||||
<motion.div
|
<div className="flex items-center justify-between">
|
||||||
className="flex flex-col md:flex-row items-center justify-between gap-6"
|
<p className="flex items-center gap-1 text-[9px] text-muted-foreground/40 font-mono">
|
||||||
initial={{ opacity: 0 }}
|
© {currentYear} Kit
|
||||||
whileInView={{ opacity: 1 }}
|
<Heart className="w-2 h-2 text-primary/70 shrink-0 animate-pulse" fill="currentColor" />
|
||||||
viewport={{ once: true }}
|
<a
|
||||||
transition={{ duration: 0.6 }}
|
href="https://pivoine.art"
|
||||||
>
|
target="_blank"
|
||||||
{/* Copyright */}
|
rel="noopener noreferrer"
|
||||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
className="hover:text-foreground/70 transition-colors"
|
||||||
© {currentYear} Kit.
|
>
|
||||||
<Heart className="h-4 w-4 text-primary shrink-0 animate-pulse" fill="currentColor" />
|
Valknar
|
||||||
<a
|
</a>
|
||||||
href="https://pivoine.art"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
title="Pivoine.Art"
|
|
||||||
className="font-medium underline underline-offset-4 decoration-primary/0 hover:decoration-primary transition-all duration-300"
|
|
||||||
>
|
|
||||||
Valknar
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Source link */}
|
|
||||||
<a
|
<a
|
||||||
href="https://dev.pivoine.art/valknar/kit-ui"
|
href="https://dev.pivoine.art/valknar/kit-ui"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
title="View source"
|
title="View source"
|
||||||
className="text-muted-foreground hover:text-primary transition-colors duration-300"
|
className="text-muted-foreground/30 hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
<GitFork className="h-5 w-5" />
|
<GitFork className="w-3.5 h-3.5" />
|
||||||
</a>
|
</a>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,108 +1,80 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { Toolbox } from 'lucide-react';
|
import { Toolbox } from 'lucide-react';
|
||||||
import Logo from './Logo';
|
import Logo from './Logo';
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
/**
|
|
||||||
* Smoothly scrolls the window to the tools section without modifying the URL hash.
|
|
||||||
*/
|
|
||||||
const scrollToTools = () => {
|
const scrollToTools = () => {
|
||||||
const toolsSection = document.getElementById('tools');
|
document.getElementById('tools')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
if (toolsSection) {
|
|
||||||
toolsSection.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-screen flex flex-col items-center justify-center px-4 py-20">
|
<section className="relative min-h-screen flex flex-col items-center justify-center px-6 py-24">
|
||||||
<div className="max-w-6xl mx-auto text-center">
|
<div className="flex flex-col items-center text-center max-w-2xl mx-auto">
|
||||||
{/* Logo */}
|
|
||||||
<motion.div
|
|
||||||
className="mb-8 flex justify-center"
|
|
||||||
initial={{ opacity: 0, y: -50 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8 }}
|
|
||||||
>
|
|
||||||
<Logo size={130} />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Main heading */}
|
{/* Logo */}
|
||||||
<motion.h1
|
<div style={{ animation: 'fadeIn 0.6s ease-out both' }}>
|
||||||
className="text-6xl md:text-8xl font-bold mb-6 text-primary"
|
<Logo size={64} />
|
||||||
initial={{ opacity: 0, y: 20 }}
|
</div>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.2 }}
|
{/* Badge */}
|
||||||
|
<div
|
||||||
|
className="mt-8 flex items-center gap-2 px-3 py-1 glass rounded-full"
|
||||||
|
style={{ animation: 'slideUp 0.5s ease-out 0.2s both' }}
|
||||||
|
>
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
||||||
|
<span className="text-[10px] font-mono text-muted-foreground/60 tracking-widest uppercase">
|
||||||
|
Browser-first toolkit
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h1
|
||||||
|
className="mt-5 text-6xl md:text-8xl font-bold text-foreground tracking-tight leading-none"
|
||||||
|
style={{ animation: 'slideUp 0.5s ease-out 0.3s both' }}
|
||||||
>
|
>
|
||||||
Kit
|
Kit
|
||||||
</motion.h1>
|
</h1>
|
||||||
|
|
||||||
{/* Subtitle */}
|
|
||||||
<motion.p
|
|
||||||
className="text-xl md:text-2xl text-muted-foreground mb-4 max-w-2xl mx-auto"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.4 }}
|
|
||||||
>
|
|
||||||
Your Creative Toolkit
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<motion.p
|
<p
|
||||||
className="text-base md:text-lg text-muted-foreground/80 mb-12 max-w-xl mx-auto"
|
className="mt-5 text-sm text-muted-foreground/60 max-w-sm leading-relaxed"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
style={{ animation: 'slideUp 0.5s ease-out 0.4s both' }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.6 }}
|
|
||||||
>
|
>
|
||||||
A curated collection of creative and utility tools for developers and creators.
|
A curated collection of browser-based tools for developers and creators.
|
||||||
Simple, powerful, and always at your fingertips.
|
Everything runs locally — no uploads, no tracking.
|
||||||
</motion.p>
|
</p>
|
||||||
|
|
||||||
{/* CTA Buttons */}
|
{/* CTA */}
|
||||||
<motion.div
|
<div
|
||||||
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-16"
|
className="mt-8"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
style={{ animation: 'slideUp 0.5s ease-out 0.5s both' }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.8 }}
|
|
||||||
>
|
>
|
||||||
<motion.button
|
<button
|
||||||
onClick={scrollToTools}
|
onClick={scrollToTools}
|
||||||
className="group relative px-8 py-4 rounded-full bg-gradient-to-r from-purple-500 to-cyan-500 text-white font-semibold shadow-lg overflow-hidden"
|
className="flex items-center gap-2 px-5 py-2.5 glass rounded-lg border border-primary/30 hover:border-primary/60 hover:bg-primary/10 text-sm font-medium text-foreground/70 hover:text-foreground transition-all duration-200"
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
>
|
||||||
<span className="relative z-10 inline-flex items-center gap-2">
|
<Toolbox className="w-3.5 h-3.5 text-primary" />
|
||||||
<Toolbox className="h-5 w-5" />
|
Explore Tools
|
||||||
Explore Tools
|
</button>
|
||||||
</span>
|
</div>
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 bg-gradient-to-r from-purple-600 to-cyan-600"
|
|
||||||
initial={{ x: '100%' }}
|
|
||||||
whileHover={{ x: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
/>
|
|
||||||
</motion.button>
|
|
||||||
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Scroll indicator */}
|
{/* Scroll indicator */}
|
||||||
<motion.button
|
<button
|
||||||
onClick={scrollToTools}
|
onClick={scrollToTools}
|
||||||
className="mx-auto flex flex-col items-center gap-2 cursor-pointer group"
|
className="mt-20 flex flex-col items-center gap-2 group"
|
||||||
initial={{ opacity: 0 }}
|
style={{ animation: 'fadeIn 0.5s ease-out 0.9s both' }}
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.8, delay: 1 }}
|
|
||||||
>
|
>
|
||||||
<span className="text-base text-gray-500 group-hover:text-gray-400 transition-colors">Scroll to explore</span>
|
<span className="text-[9px] font-mono text-muted-foreground/25 uppercase tracking-widest group-hover:text-muted-foreground/50 transition-colors">
|
||||||
<motion.div
|
Scroll
|
||||||
className="w-6 h-10 border-2 border-gray-600 group-hover:border-purple-400 rounded-full p-1 transition-colors"
|
</span>
|
||||||
animate={{ y: [0, 10, 0] }}
|
<div className="w-4 h-7 border border-muted-foreground/15 rounded-full flex items-start justify-center pt-1.5 group-hover:border-primary/30 transition-colors">
|
||||||
transition={{ duration: 1.5, repeat: Infinity }}
|
<div
|
||||||
>
|
className="w-0.5 h-1.5 bg-primary/50 rounded-full"
|
||||||
<div className="w-1 h-2 bg-gradient-to-b from-purple-400 to-cyan-400 rounded-full mx-auto" />
|
style={{ animation: 'float 1.5s ease-in-out infinite' }}
|
||||||
</motion.div>
|
/>
|
||||||
</motion.button>
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,68 +1,35 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { tools } from '@/lib/tools';
|
import { tools } from '@/lib/tools';
|
||||||
import { motion } from 'framer-motion';
|
import { Box, Code2, Shield } from 'lucide-react';
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{
|
{ value: tools.length, label: 'Tools', icon: Box },
|
||||||
number: tools.length,
|
{ value: '100%', label: 'Open Source', icon: Code2 },
|
||||||
label: 'Tools',
|
{ value: '∞', label: 'Privacy First', icon: Shield },
|
||||||
icon: (
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: '100%',
|
|
||||||
label: 'Open Source',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
number: '∞',
|
|
||||||
label: 'Privacy First',
|
|
||||||
icon: (
|
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Stats() {
|
export default function Stats() {
|
||||||
return (
|
return (
|
||||||
<section className="relative py-16 px-4">
|
<section className="relative py-8 px-6">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-xl mx-auto">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, i) => {
|
||||||
<motion.div
|
const Icon = stat.icon;
|
||||||
key={stat.label}
|
return (
|
||||||
className="glass rounded-2xl p-8 text-center"
|
<div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
key={stat.label}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
className="glass rounded-xl p-5 flex flex-col items-center text-center"
|
||||||
viewport={{ once: true }}
|
style={{ animation: `slideUp 0.5s ease-out ${0.1 + i * 0.1}s both` }}
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
||||||
whileHover={{ y: -5 }}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
className="inline-flex items-center justify-center w-12 h-12 mb-4 rounded-xl bg-primary/10 text-primary"
|
|
||||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
|
||||||
transition={{ type: 'spring', stiffness: 300 }}
|
|
||||||
>
|
>
|
||||||
{stat.icon}
|
<div className="w-7 h-7 rounded-md bg-primary/10 flex items-center justify-center mb-3">
|
||||||
</motion.div>
|
<Icon className="w-3.5 h-3.5 text-primary" />
|
||||||
<div className="text-4xl font-bold mb-2 bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-cyan-400">
|
</div>
|
||||||
{stat.number}
|
<span className="text-2xl font-bold tabular-nums text-foreground">{stat.value}</span>
|
||||||
|
<span className="text-[10px] font-mono text-muted-foreground/40 uppercase tracking-widest mt-1">
|
||||||
|
{stat.label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground text-base font-medium">
|
);
|
||||||
{stat.label}
|
})}
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,91 +1,56 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
const MotionLink = motion.create(Link);
|
import { ElementType } from 'react';
|
||||||
|
|
||||||
interface ToolCardProps {
|
interface ToolCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: ReactNode;
|
icon: ElementType;
|
||||||
url: string;
|
url: string;
|
||||||
index: number;
|
index: number;
|
||||||
badges?: string[];
|
badges?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ToolCard({ title, description, icon, url, index, badges }: ToolCardProps) {
|
export default function ToolCard({ title, description, icon: Icon, url, index, badges }: ToolCardProps) {
|
||||||
return (
|
return (
|
||||||
<MotionLink
|
<Link
|
||||||
href={url}
|
href={url}
|
||||||
className="group relative block h-full"
|
className="group glass rounded-xl p-4 flex flex-col h-full transition-all duration-200 hover:border-primary/30 hover:bg-primary/3"
|
||||||
initial={{ opacity: 0, y: 50 }}
|
style={{ animation: `slideUp 0.5s ease-out ${0.05 * index}s both` }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
||||||
whileHover={{ y: -10 }}
|
|
||||||
>
|
>
|
||||||
<div className="glass relative overflow-hidden rounded-2xl p-8 h-full transition-all duration-300 group-hover:shadow-2xl group-hover:bg-card/80">
|
{/* Icon */}
|
||||||
{/* Subtle hover overlay */}
|
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center mb-3 shrink-0 group-hover:bg-primary/15 transition-colors">
|
||||||
<div className="absolute inset-0 opacity-0 group-hover:opacity-10 transition-opacity duration-300 bg-primary" />
|
<Icon className="w-4 h-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Icon */}
|
{/* Title */}
|
||||||
<motion.div
|
<h3 className="text-sm font-semibold text-foreground/80 group-hover:text-foreground transition-colors mb-1.5">
|
||||||
className="mb-6 flex justify-center"
|
{title}
|
||||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
</h3>
|
||||||
transition={{ type: 'spring', stiffness: 300 }}
|
|
||||||
>
|
|
||||||
<div className="p-4 rounded-xl bg-primary/10 text-primary shadow-lg shadow-black/5">
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Title */}
|
{/* Description */}
|
||||||
<h3 className="text-2xl font-bold mb-3 text-foreground transition-all duration-300 group-hover:text-primary">
|
<p className="text-[11px] text-muted-foreground/50 leading-relaxed flex-1 mb-3">
|
||||||
{title}
|
{description}
|
||||||
</h3>
|
</p>
|
||||||
|
|
||||||
{/* Badges */}
|
{/* Footer: badges + arrow */}
|
||||||
{badges && badges.length > 0 && (
|
<div className="flex items-end justify-between gap-2">
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
{badges && badges.length > 0 ? (
|
||||||
{badges.map((badge) => (
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{badges.slice(0, 2).map((badge) => (
|
||||||
<span
|
<span
|
||||||
key={badge}
|
key={badge}
|
||||||
className="text-xs px-2 py-1 rounded-full bg-primary/5 border border-primary/10 text-muted-foreground font-medium"
|
className="text-[9px] font-mono px-1.5 py-0.5 rounded border border-border/30 text-muted-foreground/35"
|
||||||
>
|
>
|
||||||
{badge}
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
)}
|
)}
|
||||||
|
<ArrowRight className="w-3 h-3 text-muted-foreground/25 group-hover:text-primary group-hover:translate-x-0.5 transition-all duration-200 shrink-0" />
|
||||||
{/* Description */}
|
|
||||||
<p className="text-muted-foreground group-hover:text-foreground/80 transition-colors duration-300">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Arrow icon */}
|
|
||||||
<motion.div
|
|
||||||
className="absolute bottom-8 right-8 text-muted-foreground group-hover:text-primary transition-colors duration-300"
|
|
||||||
initial={{ x: 0 }}
|
|
||||||
whileHover={{ x: 5 }}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-6 h-6"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M13 7l5 5m0 0l-5 5m5-5H6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</MotionLink>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,37 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import ToolCard from './ToolCard';
|
import ToolCard from './ToolCard';
|
||||||
import { tools } from '@/lib/tools';
|
import { tools } from '@/lib/tools';
|
||||||
|
|
||||||
export default function ToolsGrid() {
|
export default function ToolsGrid() {
|
||||||
return (
|
return (
|
||||||
<section id="tools" className="relative py-20 px-4">
|
<section id="tools" className="relative py-16 px-6">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
|
|
||||||
{/* Section heading */}
|
{/* Section heading */}
|
||||||
<motion.div
|
<div
|
||||||
className="text-center mb-16"
|
className="mb-8"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
style={{ animation: 'fadeIn 0.5s ease-out both' }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
>
|
>
|
||||||
<h2 className="text-4xl md:text-5xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-cyan-400">
|
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest block mb-1">
|
||||||
Available Tools
|
Available Tools
|
||||||
</h2>
|
</span>
|
||||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
<p className="text-xs text-muted-foreground/40">
|
||||||
Explore our collection of carefully crafted tools designed to boost your productivity and creativity
|
Carefully crafted tools for your workflow
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Tools grid */}
|
{/* Tools grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3">
|
||||||
{tools.map((tool, index) => {
|
{tools.map((tool, index) => (
|
||||||
const Icon = tool.icon;
|
<ToolCard
|
||||||
return (
|
key={tool.href}
|
||||||
<ToolCard
|
title={tool.shortTitle}
|
||||||
key={tool.href}
|
description={tool.summary}
|
||||||
title={tool.title}
|
icon={tool.icon}
|
||||||
description={tool.summary}
|
url={tool.href}
|
||||||
icon={<Icon className="w-12 h-12" />}
|
badges={tool.badges}
|
||||||
url={tool.href}
|
index={index}
|
||||||
badges={tool.badges}
|
/>
|
||||||
index={index}
|
))}
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user