feat: build resume website with MagicUI components
- 6 sections: Hero, About, Experience, Skills, Projects, Contact - MagicUI: Globe, Particles, Meteors, AnimatedList, IconCloud, BentoGrid - Dark mode support, scroll-triggered animations - Static export ready for deployment
This commit is contained in:
222
components/ui/magic-card.tsx
Normal file
222
components/ui/magic-card.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
"use client"
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import {
|
||||
motion,
|
||||
useMotionTemplate,
|
||||
useMotionValue,
|
||||
useSpring,
|
||||
} from "motion/react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface MagicCardBaseProps {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
gradientSize?: number
|
||||
gradientFrom?: string
|
||||
gradientTo?: string
|
||||
}
|
||||
|
||||
interface MagicCardGradientProps extends MagicCardBaseProps {
|
||||
mode?: "gradient"
|
||||
|
||||
gradientColor?: string
|
||||
gradientOpacity?: number
|
||||
|
||||
glowFrom?: never
|
||||
glowTo?: never
|
||||
glowAngle?: never
|
||||
glowSize?: never
|
||||
glowBlur?: never
|
||||
glowOpacity?: never
|
||||
}
|
||||
|
||||
interface MagicCardOrbProps extends MagicCardBaseProps {
|
||||
mode: "orb"
|
||||
|
||||
glowFrom?: string
|
||||
glowTo?: string
|
||||
glowAngle?: number
|
||||
glowSize?: number
|
||||
glowBlur?: number
|
||||
glowOpacity?: number
|
||||
|
||||
gradientColor?: never
|
||||
gradientOpacity?: never
|
||||
}
|
||||
|
||||
type MagicCardProps = MagicCardGradientProps | MagicCardOrbProps
|
||||
type ResetReason = "enter" | "leave" | "global" | "init"
|
||||
|
||||
function isOrbMode(props: MagicCardProps): props is MagicCardOrbProps {
|
||||
return props.mode === "orb"
|
||||
}
|
||||
|
||||
export function MagicCard(props: MagicCardProps) {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
gradientSize = 200,
|
||||
gradientColor = "#262626",
|
||||
gradientOpacity = 0.8,
|
||||
gradientFrom = "#9E7AFF",
|
||||
gradientTo = "#FE8BBB",
|
||||
mode = "gradient",
|
||||
} = props
|
||||
|
||||
const glowFrom = isOrbMode(props) ? (props.glowFrom ?? "#ee4f27") : "#ee4f27"
|
||||
const glowTo = isOrbMode(props) ? (props.glowTo ?? "#6b21ef") : "#6b21ef"
|
||||
const glowAngle = isOrbMode(props) ? (props.glowAngle ?? 90) : 90
|
||||
const glowSize = isOrbMode(props) ? (props.glowSize ?? 420) : 420
|
||||
const glowBlur = isOrbMode(props) ? (props.glowBlur ?? 60) : 60
|
||||
const glowOpacity = isOrbMode(props) ? (props.glowOpacity ?? 0.9) : 0.9
|
||||
const { theme, systemTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => setMounted(true), [])
|
||||
|
||||
const isDarkTheme = useMemo(() => {
|
||||
if (!mounted) return true
|
||||
const currentTheme = theme === "system" ? systemTheme : theme
|
||||
return currentTheme === "dark"
|
||||
}, [theme, systemTheme, mounted])
|
||||
|
||||
const mouseX = useMotionValue(-gradientSize)
|
||||
const mouseY = useMotionValue(-gradientSize)
|
||||
|
||||
const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 })
|
||||
const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 })
|
||||
const orbVisible = useSpring(0, { stiffness: 300, damping: 35 })
|
||||
|
||||
const modeRef = useRef(mode)
|
||||
const glowOpacityRef = useRef(glowOpacity)
|
||||
const gradientSizeRef = useRef(gradientSize)
|
||||
|
||||
useEffect(() => {
|
||||
modeRef.current = mode
|
||||
}, [mode])
|
||||
|
||||
useEffect(() => {
|
||||
glowOpacityRef.current = glowOpacity
|
||||
}, [glowOpacity])
|
||||
|
||||
useEffect(() => {
|
||||
gradientSizeRef.current = gradientSize
|
||||
}, [gradientSize])
|
||||
|
||||
const reset = useCallback(
|
||||
(reason: ResetReason = "leave") => {
|
||||
const currentMode = modeRef.current
|
||||
|
||||
if (currentMode === "orb") {
|
||||
if (reason === "enter") orbVisible.set(glowOpacityRef.current)
|
||||
else orbVisible.set(0)
|
||||
return
|
||||
}
|
||||
|
||||
const off = -gradientSizeRef.current
|
||||
mouseX.set(off)
|
||||
mouseY.set(off)
|
||||
},
|
||||
[mouseX, mouseY, orbVisible]
|
||||
)
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
mouseX.set(e.clientX - rect.left)
|
||||
mouseY.set(e.clientY - rect.top)
|
||||
},
|
||||
[mouseX, mouseY]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
reset("init")
|
||||
}, [reset])
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalPointerOut = (e: PointerEvent) => {
|
||||
if (!e.relatedTarget) reset("global")
|
||||
}
|
||||
const handleBlur = () => reset("global")
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState !== "visible") reset("global")
|
||||
}
|
||||
|
||||
window.addEventListener("pointerout", handleGlobalPointerOut)
|
||||
window.addEventListener("blur", handleBlur)
|
||||
document.addEventListener("visibilitychange", handleVisibility)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pointerout", handleGlobalPointerOut)
|
||||
window.removeEventListener("blur", handleBlur)
|
||||
document.removeEventListener("visibilitychange", handleVisibility)
|
||||
}
|
||||
}, [reset])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={cn(
|
||||
"group relative isolate overflow-hidden rounded-[inherit] border border-transparent",
|
||||
className
|
||||
)}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerLeave={() => reset("leave")}
|
||||
onPointerEnter={() => reset("enter")}
|
||||
style={{
|
||||
background: useMotionTemplate`
|
||||
linear-gradient(var(--color-background) 0 0) padding-box,
|
||||
radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px,
|
||||
${gradientFrom},
|
||||
${gradientTo},
|
||||
var(--color-border) 100%
|
||||
) border-box
|
||||
`,
|
||||
}}
|
||||
>
|
||||
<div className="bg-background absolute inset-px z-20 rounded-[inherit]" />
|
||||
|
||||
{mode === "gradient" && (
|
||||
<motion.div
|
||||
suppressHydrationWarning
|
||||
className="pointer-events-none absolute inset-px z-30 rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
style={{
|
||||
background: useMotionTemplate`
|
||||
radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px,
|
||||
${gradientColor},
|
||||
transparent 100%
|
||||
)
|
||||
`,
|
||||
opacity: gradientOpacity,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === "orb" && (
|
||||
<motion.div
|
||||
suppressHydrationWarning
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute z-30"
|
||||
style={{
|
||||
width: glowSize,
|
||||
height: glowSize,
|
||||
x: orbX,
|
||||
y: orbY,
|
||||
translateX: "-50%",
|
||||
translateY: "-50%",
|
||||
borderRadius: 9999,
|
||||
filter: `blur(${glowBlur}px)`,
|
||||
opacity: orbVisible,
|
||||
background: `linear-gradient(${glowAngle}deg, ${glowFrom}, ${glowTo})`,
|
||||
|
||||
mixBlendMode: isDarkTheme ? "screen" : "multiply",
|
||||
willChange: "transform, opacity",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-40">{children}</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user