152 lines
6.5 KiB
TypeScript
152 lines
6.5 KiB
TypeScript
"use client";
|
||
import Image from "next/image";
|
||
import { websites } from "../data/urls";
|
||
import Header from "@/components/header";
|
||
import Footer from "@/components/footer";
|
||
import { motion, useMotionTemplate, useMotionValue, useSpring, useTransform } from "framer-motion";
|
||
import { MouseEvent } from "react";
|
||
|
||
function SpotlightCard({ website, variants }: { website: any; variants: any }) {
|
||
const mouseX = useMotionValue(0);
|
||
const mouseY = useMotionValue(0);
|
||
|
||
const x = useMotionValue(0);
|
||
const y = useMotionValue(0);
|
||
const rotateX = useSpring(useTransform(y, [-0.5, 0.5], ["2.5deg", "-2.5deg"]), { stiffness: 200, damping: 20 });
|
||
const rotateY = useSpring(useTransform(x, [-0.5, 0.5], ["-2.5deg", "2.5deg"]), { stiffness: 200, damping: 20 });
|
||
|
||
function handleMouseMove({ currentTarget, clientX, clientY }: MouseEvent) {
|
||
const { left, top, width, height } = currentTarget.getBoundingClientRect();
|
||
mouseX.set(clientX - left);
|
||
mouseY.set(clientY - top);
|
||
|
||
x.set((clientX - left) / width - 0.5);
|
||
y.set((clientY - top) / height - 0.5);
|
||
}
|
||
|
||
function handleMouseLeave() {
|
||
x.set(0);
|
||
y.set(0);
|
||
}
|
||
|
||
return (
|
||
<motion.div variants={variants} className="h-full" style={{ perspective: 1000 }}>
|
||
<motion.a
|
||
href={website.link}
|
||
rel="noopener noreferrer"
|
||
target="_blank"
|
||
className="group relative block h-full"
|
||
onMouseMove={handleMouseMove}
|
||
onMouseLeave={handleMouseLeave}
|
||
style={{
|
||
rotateX,
|
||
rotateY,
|
||
transformStyle: "preserve-3d",
|
||
}}
|
||
whileHover={{ scale: 1.02, y: -5 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
>
|
||
<div className="absolute inset-0 bg-black dark:bg-white rounded-none translate-y-2 translate-x-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||
|
||
<div className="relative h-full bg-white dark:bg-[#0a0a0a] border border-gray-200 dark:border-gray-800 p-6 rounded-none transition-all duration-300 group-hover:border-black dark:group-hover:border-white overflow-hidden">
|
||
<motion.div
|
||
className="pointer-events-none absolute -inset-px opacity-0 transition duration-300 group-hover:opacity-100 z-10"
|
||
style={{
|
||
background: useMotionTemplate`
|
||
radial-gradient(
|
||
400px circle at ${mouseX}px ${mouseY}px,
|
||
rgba(0, 0, 0, 0.08),
|
||
transparent 80%
|
||
)
|
||
`,
|
||
}}
|
||
/>
|
||
<motion.div
|
||
className="pointer-events-none absolute -inset-px opacity-0 transition duration-300 group-hover:opacity-100 z-10 hidden dark:block"
|
||
style={{
|
||
background: useMotionTemplate`
|
||
radial-gradient(
|
||
400px circle at ${mouseX}px ${mouseY}px,
|
||
rgba(255, 255, 255, 0.1),
|
||
transparent 80%
|
||
)
|
||
`,
|
||
}}
|
||
/>
|
||
|
||
<div className="relative z-20">
|
||
<div className="flex items-start justify-between mb-6">
|
||
<div className="p-3 bg-gray-50 dark:bg-white rounded-xl group-hover:bg-gray-100 dark:group-hover:bg-gray-200 transition-all duration-300 shadow-sm border border-gray-100 dark:border-gray-200">
|
||
<Image src={website.icon} alt={website.title} width={40} height={40} className="transition-all duration-300" />
|
||
</div>
|
||
<span className="text-gray-300 dark:text-gray-600 group-hover:text-black dark:group-hover:text-white transition-transform duration-300 group-hover:rotate-45">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<line x1="7" y1="17" x2="17" y2="7"></line>
|
||
<polyline points="7 7 17 7 17 17"></polyline>
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
|
||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white group-hover:underline decoration-2 underline-offset-4">{website.title}</h3>
|
||
<p className="text-gray-500 dark:text-gray-400 text-sm leading-relaxed line-clamp-3">{website.description}</p>
|
||
</div>
|
||
</div>
|
||
</motion.a>
|
||
</motion.div>
|
||
);
|
||
}
|
||
|
||
export default function Home() {
|
||
const containerVariants = {
|
||
hidden: { opacity: 0 },
|
||
visible: {
|
||
opacity: 1,
|
||
transition: {
|
||
staggerChildren: 0.08,
|
||
delayChildren: 0.1,
|
||
},
|
||
},
|
||
};
|
||
|
||
const itemVariants = {
|
||
hidden: { y: 20, opacity: 0, scale: 0.98, filter: "blur(4px)" },
|
||
visible: {
|
||
y: 0,
|
||
opacity: 1,
|
||
scale: 1,
|
||
filter: "blur(0px)",
|
||
transition: { type: "spring" as const, stiffness: 100, damping: 15, mass: 1 },
|
||
},
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col min-h-screen bg-white dark:bg-black text-black dark:text-white">
|
||
<Header />
|
||
|
||
<motion.main className="w-full max-w-7xl mx-auto px-4 md:px-6 py-16 grow flex flex-col items-center" initial="hidden" animate="visible" variants={containerVariants}>
|
||
{/* Hero Section */}
|
||
<motion.section className="max-w-4xl w-full mb-24 text-center relative" variants={itemVariants}>
|
||
<h2 className="text-5xl md:text-6xl font-black tracking-tighter mb-8 bg-clip-text text-transparent bg-linear-to-b from-gray-900 to-gray-600 dark:from-white dark:to-gray-400">
|
||
智能计算实验室
|
||
</h2>
|
||
|
||
<div className="relative inline-block">
|
||
<p className="text-xl text-left md:text-2xl leading-relaxed text-gray-600 dark:text-gray-400 font-light max-w-2xl mx-auto">
|
||
在物理的世界里,粒子有形形色色的活法,有的粒子很轻,终生不为人所注意;有的粒子会一鸣惊人,在湮灭时释放出大量的光;有的粒子狡黠善变,一辈子不停地改变身份,时时刻刻要成为别的粒子;但有的粒子一生稳定,怀揣着出发时得到的巨大能量,一生孤独而平静地飞行,只活在自己的轨道里。
|
||
</p>
|
||
<p className="text-right text-sm text-gray-400 dark:text-gray-500 mt-4 font-mono">—— 李斐然《丁肇中 伟大的是物理》</p>
|
||
</div>
|
||
</motion.section>
|
||
|
||
{/* Grid Section */}
|
||
<motion.section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full" variants={containerVariants}>
|
||
{websites.map((website, index) => (
|
||
<SpotlightCard key={index} website={website} variants={itemVariants} />
|
||
))}
|
||
</motion.section>
|
||
</motion.main>
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|