169 lines
5.3 KiB
TypeScript
169 lines
5.3 KiB
TypeScript
"use client"
|
|
|
|
import { animate, motion } from "framer-motion"
|
|
import React, { useEffect } from "react"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export interface CardProps {
|
|
className?: string
|
|
children?: React.ReactNode
|
|
withSparkles?: boolean
|
|
withIcons?: Array<{
|
|
icon: React.ReactNode
|
|
size?: "sm" | "md" | "lg"
|
|
className?: string
|
|
}>
|
|
}
|
|
|
|
const sizeMap = {
|
|
sm: "h-8 w-8",
|
|
md: "h-12 w-12",
|
|
lg: "h-16 w-16",
|
|
}
|
|
|
|
export function Card({ className, children, withSparkles = false, withIcons = [] }: CardProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"max-w-sm w-full mx-auto p-8 rounded-xl border border-white/20 backdrop-blur-md bg-white/20 dark:bg-black/20 shadow-lg group",
|
|
className
|
|
)}
|
|
>
|
|
{(withSparkles || withIcons.length > 0) && (
|
|
<div
|
|
className={cn(
|
|
"h-[15rem] md:h-[20rem] rounded-xl z-40 relative",
|
|
"bg-neutral-300 dark:bg-[rgba(40,40,40,0.70)] [mask-image:radial-gradient(50%_50%_at_50%_50%,white_0%,transparent_100%)]"
|
|
)}
|
|
>
|
|
{withIcons.length > 0 && <AnimatedIcons icons={withIcons} />}
|
|
{withSparkles && <AnimatedSparkles />}
|
|
</div>
|
|
)}
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 保持原有的 AnimatedCard 组件以向后兼容
|
|
export function AnimatedCard({ className, title, description, icons = [] }: {
|
|
className?: string
|
|
title?: React.ReactNode
|
|
description?: React.ReactNode
|
|
icons?: Array<{
|
|
icon: React.ReactNode
|
|
size?: "sm" | "md" | "lg"
|
|
className?: string
|
|
}>
|
|
}) {
|
|
return (
|
|
<Card className={className} withIcons={icons}>
|
|
{title && (
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-white py-2">
|
|
{title}
|
|
</h3>
|
|
)}
|
|
{description && (
|
|
<p className="text-sm font-normal text-neutral-600 dark:text-neutral-400 max-w-sm">
|
|
{description}
|
|
</p>
|
|
)}
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
|
|
const AnimatedIcons = ({ icons }: {
|
|
icons: Array<{
|
|
icon: React.ReactNode
|
|
size?: "sm" | "md" | "lg"
|
|
className?: string
|
|
}>
|
|
}) => {
|
|
return (
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
{icons.map((iconData, index) => (
|
|
<motion.div
|
|
key={index}
|
|
className={cn(
|
|
"rounded-full flex items-center justify-center bg-[rgba(248,248,248,0.01)] shadow-[0px_0px_8px_0px_rgba(248,248,248,0.25)_inset,0px_32px_24px_-16px_rgba(0,0,0,0.40)]",
|
|
sizeMap[iconData.size || "md"],
|
|
iconData.className
|
|
)}
|
|
animate={{
|
|
scale: [1, 1.1, 1],
|
|
rotate: [0, 5, -5, 0],
|
|
}}
|
|
transition={{
|
|
duration: 3,
|
|
repeat: Infinity,
|
|
delay: index * 0.2,
|
|
}}
|
|
>
|
|
{iconData.icon}
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const Container = React.forwardRef<
|
|
HTMLDivElement,
|
|
React.HTMLAttributes<HTMLDivElement>
|
|
>(({ className, ...props }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
`rounded-full flex items-center justify-center bg-[rgba(248,248,248,0.01)]
|
|
shadow-[0px_0px_8px_0px_rgba(248,248,248,0.25)_inset,0px_32px_24px_-16px_rgba(0,0,0,0.40)]`,
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
Container.displayName = "Container"
|
|
|
|
const AnimatedSparkles = () => (
|
|
<div className="h-40 w-px absolute top-20 m-auto z-40 bg-gradient-to-b from-transparent via-cyan-500 to-transparent animate-move">
|
|
<div className="w-10 h-32 top-1/2 -translate-y-1/2 absolute -left-10">
|
|
<Sparkles />
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
const Sparkles = () => {
|
|
const randomMove = () => Math.random() * 2 - 1
|
|
const randomOpacity = () => Math.random()
|
|
const random = () => Math.random()
|
|
|
|
return (
|
|
<div className="absolute inset-0">
|
|
{[...Array(12)].map((_, i) => (
|
|
<motion.span
|
|
key={`star-${i}`}
|
|
animate={{
|
|
top: `calc(${random() * 100}% + ${randomMove()}px)`,
|
|
left: `calc(${random() * 100}% + ${randomMove()}px)`,
|
|
opacity: randomOpacity(),
|
|
scale: [1, 1.2, 0],
|
|
}}
|
|
transition={{
|
|
duration: random() * 2 + 4,
|
|
repeat: Infinity,
|
|
ease: "linear",
|
|
}}
|
|
style={{
|
|
position: "absolute",
|
|
top: `${random() * 100}%`,
|
|
left: `${random() * 100}%`,
|
|
width: `2px`,
|
|
height: `2px`,
|
|
borderRadius: "50%",
|
|
zIndex: 1,
|
|
}}
|
|
className="inline-block bg-black dark:bg-white"
|
|
/>
|
|
))}
|
|
</div>
|
|
)
|
|
} |