feat: implement the first section of landing page

This commit is contained in:
Henry Li
2026-01-23 00:15:21 +08:00
parent 459d9d0287
commit 307972f93e
14 changed files with 757 additions and 7 deletions

View File

@@ -0,0 +1,43 @@
"use client"
import React, { memo } from "react"
interface AuroraTextProps {
children: React.ReactNode
className?: string
colors?: string[]
speed?: number
}
export const AuroraText = memo(
({
children,
className = "",
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
speed = 1,
}: AuroraTextProps) => {
const gradientStyle = {
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
colors[0]
})`,
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
animationDuration: `${10 / speed}s`,
}
return (
<span className={`relative inline-block ${className}`}>
<span className="sr-only">{children}</span>
<span
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
style={gradientStyle}
aria-hidden="true"
>
{children}
</span>
</span>
)
}
)
AuroraText.displayName = "AuroraText"

View File

@@ -0,0 +1,67 @@
"use client"
import { ComponentPropsWithoutRef, useEffect, useRef } from "react"
import { useInView, useMotionValue, useSpring } from "motion/react"
import { cn } from "@/lib/utils"
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
value: number
startValue?: number
direction?: "up" | "down"
delay?: number
decimalPlaces?: number
}
export function NumberTicker({
value,
startValue = 0,
direction = "up",
delay = 0,
className,
decimalPlaces = 0,
...props
}: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null)
const motionValue = useMotionValue(direction === "down" ? value : startValue)
const springValue = useSpring(motionValue, {
damping: 60,
stiffness: 100,
})
const isInView = useInView(ref, { once: true, margin: "0px" })
useEffect(() => {
if (isInView) {
const timer = setTimeout(() => {
motionValue.set(direction === "down" ? startValue : value)
}, delay * 1000)
return () => clearTimeout(timer)
}
}, [motionValue, isInView, delay, value, direction, startValue])
useEffect(
() =>
springValue.on("change", (latest) => {
if (ref.current) {
ref.current.textContent = Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces,
}).format(Number(latest.toFixed(decimalPlaces)))
}
}),
[springValue, decimalPlaces]
)
return (
<span
ref={ref}
className={cn(
"inline-block tracking-wider text-black tabular-nums dark:text-white",
className
)}
{...props}
>
{startValue}
</span>
)
}

View File

@@ -0,0 +1,51 @@
"use client";
import { useEffect, useState } from "react";
import { AnimatePresence, motion, MotionProps } from "motion/react";
import { cn } from "@/lib/utils";
import { AuroraText } from "./aurora-text";
interface WordRotateProps {
words: string[];
duration?: number;
motionProps?: MotionProps;
className?: string;
}
export function WordRotate({
words,
duration = 2500,
motionProps = {
initial: { opacity: 0, y: -50, filter: "blur(16px)" },
animate: { opacity: 1, y: 0, filter: "blur(0px)" },
exit: { opacity: 0, y: 50, filter: "blur(16px)" },
transition: { duration: 0.25, ease: "easeOut" },
},
className,
}: WordRotateProps) {
const [index, setIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setIndex((prevIndex) => (prevIndex + 1) % words.length);
}, duration);
// Clean up interval on unmount
return () => clearInterval(interval);
}, [words, duration]);
return (
<div className="overflow-hidden py-2">
<AnimatePresence mode="wait">
<motion.h1
key={words[index]}
className={cn(className)}
{...motionProps}
>
<AuroraText>{words[index]}</AuroraText>
</motion.h1>
</AnimatePresence>
</div>
);
}