mirror of
https://github.com/JOYCEQL/magic-resume.git
synced 2026-07-03 14:07:11 +02:00
feat: home page ssr
This commit is contained in:
@@ -56,7 +56,7 @@
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #c1c1c1 #f1f1f1;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.16) transparent;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
+65
-155
@@ -1,176 +1,86 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Logo from "@/components/shared/Logo";
|
||||
import ThemeToggle from "@/components/shared/ThemeToggle";
|
||||
import Header from "@/components/home/Header";
|
||||
import { HeroAnimation } from "@/components/home/HeroAnimation";
|
||||
import { FeaturesAnimation } from "@/components/home/FeaturesAnimation";
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "简约设计",
|
||||
description: "专注于内容创作,简洁的界面让您的思路更加清晰",
|
||||
icon: "✨",
|
||||
},
|
||||
{
|
||||
title: "本地存储",
|
||||
description: "所有数据存储在本地,无需担心隐私泄露",
|
||||
icon: "🔒",
|
||||
},
|
||||
{
|
||||
title: "智能助手",
|
||||
description: "AI助手帮您优化简历内容,提供专业建议",
|
||||
icon: "🤖",
|
||||
},
|
||||
];
|
||||
|
||||
export default function LandingPage() {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [lastScrollY, setLastScrollY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
if (currentScrollY < 10) {
|
||||
setIsVisible(true);
|
||||
setLastScrollY(currentScrollY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentScrollY > lastScrollY && currentScrollY > 200) {
|
||||
setIsVisible(false);
|
||||
} else if (currentScrollY < lastScrollY) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
|
||||
setLastScrollY(currentScrollY);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [lastScrollY]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="fixed w-full z-50 flex justify-center">
|
||||
<motion.div
|
||||
className="w-[90%] max-w-5xl"
|
||||
initial={{ y: 0, opacity: 1 }}
|
||||
animate={{
|
||||
y: isVisible ? 0 : -100,
|
||||
opacity: isVisible ? 1 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
}}
|
||||
>
|
||||
<div className="mt-4 rounded-full bg-background/70 backdrop-blur-[8px] border border-border/50">
|
||||
<div className="relative flex items-center justify-between h-12 px-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Logo size={32} />
|
||||
<span className="font-bold text-base">Magic Resume</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ThemeToggle>
|
||||
<div className="w-8 h-8 relative cursor-pointer rounded-md hover:bg-accent/50 flex items-center justify-center">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] absolute inset-0 m-auto rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="h-[1.2rem] w-[1.2rem] absolute inset-0 m-auto rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
</div>
|
||||
</ThemeToggle>
|
||||
<Link href="/dashboard">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-primary hover:opacity-90 text-white h-8 text-sm rounded-full px-4"
|
||||
>
|
||||
开始使用
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<Header />
|
||||
|
||||
<section className="relative pt-32 pb-16">
|
||||
<div className="absolute inset-0 -z-10 h-full w-full bg-white dark:bg-black bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:14px_24px]" />
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<motion.div
|
||||
className="text-center space-y-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.h1
|
||||
className="text-4xl md:text-6xl font-bold text-primary"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
专注内容,隐私优先
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className="text-lg text-muted-foreground max-w-2xl mx-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
>
|
||||
数据完全存储在本地,确保您的隐私安全。
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
>
|
||||
<Link href="/dashboard">
|
||||
<Button size="lg" className="rounded-[10px] px-8">
|
||||
立即创建简历
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<div className="text-center space-y-6">
|
||||
<HeroAnimation>
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-primary">
|
||||
专注内容,隐私优先
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
数据完全存储在本地,确保您的隐私安全。
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<Link href="/dashboard">
|
||||
<Button size="lg" className="rounded-[10px] px-8">
|
||||
立即创建简历
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</HeroAnimation>
|
||||
|
||||
<motion.div
|
||||
className="mt-16 relative rounded-2xl overflow-hidden"
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1, delay: 0.4 }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/10 via-violet-500/10 to-blue-500/10 rounded-2xl" />
|
||||
<Image
|
||||
width={1280}
|
||||
height={720}
|
||||
src="/web-shot.png"
|
||||
alt="resume-shot"
|
||||
className="w-full h-auto relative z-10 rounded-2xl shadow-2xl"
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
<div className="mt-16 relative rounded-2xl overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/10 via-violet-500/10 to-blue-500/10 rounded-2xl" />
|
||||
<Image
|
||||
width={1280}
|
||||
height={720}
|
||||
src="/web-shot.png"
|
||||
alt="resume-shot"
|
||||
className="w-full h-auto relative z-10 rounded-2xl shadow-2xl"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 relative overflow-hidden">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
title: "简约设计",
|
||||
description: "专注于内容创作,简洁的界面让您的思路更加清晰",
|
||||
icon: "✨",
|
||||
},
|
||||
{
|
||||
title: "本地存储",
|
||||
description: "所有数据存储在本地,无需担心隐私泄露",
|
||||
icon: "🔒",
|
||||
},
|
||||
{
|
||||
title: "智能助手",
|
||||
description: "AI助手帮您优化简历内容,提供专业建议",
|
||||
icon: "🤖",
|
||||
},
|
||||
].map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
className="relative p-6 rounded-2xl bg-card border group hover:shadow-lg transition-shadow duration-500"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/0 via-violet-500/5 to-blue-500/0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl" />
|
||||
<div className="relative">
|
||||
<div className="text-3xl mb-4">{feature.icon}</div>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">{feature.description}</p>
|
||||
{features.map((feature, index) => (
|
||||
<FeaturesAnimation key={feature.title} index={index}>
|
||||
<div className="relative p-6 rounded-2xl bg-card border group hover:shadow-lg transition-shadow duration-500">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/0 via-violet-500/5 to-blue-500/0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl" />
|
||||
<div className="relative">
|
||||
<div className="text-3xl mb-4">{feature.icon}</div>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</FeaturesAnimation>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,7 +89,7 @@ export default function LandingPage() {
|
||||
<footer className="border-t py-8">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<p> 2024 Magic Resume. All rights reserved.</p>
|
||||
<p>© 2024 Magic Resume. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Layout, Layers, Cloud, Sparkles, Zap, Shield } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "智能排版",
|
||||
description: "AI自动优化布局,告别繁琐调整",
|
||||
icon: Layout,
|
||||
gradient: "from-blue-500 to-cyan-500",
|
||||
delay: 0,
|
||||
},
|
||||
{
|
||||
title: "一键生成",
|
||||
description: "输入基本信息,AI助手帮你完成剩余内容",
|
||||
icon: Sparkles,
|
||||
gradient: "from-violet-500 to-purple-500",
|
||||
delay: 0.1,
|
||||
},
|
||||
{
|
||||
title: "多端同步",
|
||||
description: "随时随地编辑,数据实时云端同步",
|
||||
icon: Cloud,
|
||||
gradient: "from-pink-500 to-rose-500",
|
||||
delay: 0.2,
|
||||
},
|
||||
{
|
||||
title: "极速体验",
|
||||
description: "毫秒级响应,流畅的编辑体验",
|
||||
icon: Zap,
|
||||
gradient: "from-amber-500 to-orange-500",
|
||||
delay: 0.3,
|
||||
},
|
||||
{
|
||||
title: "安全可靠",
|
||||
description: "数据安全加密,保护你的隐私",
|
||||
icon: Shield,
|
||||
gradient: "from-emerald-500 to-green-500",
|
||||
delay: 0.4,
|
||||
},
|
||||
{
|
||||
title: "专业模板",
|
||||
description: "海量精选模板,一键套用",
|
||||
icon: Layers,
|
||||
gradient: "from-blue-500 to-indigo-500",
|
||||
delay: 0.5,
|
||||
},
|
||||
];
|
||||
|
||||
export default function AnimatedFeatures() {
|
||||
return (
|
||||
<section className="py-24 px-6 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-gray-50/50 to-white dark:from-gray-900/50 dark:to-black" />
|
||||
|
||||
<div className="max-w-7xl mx-auto relative">
|
||||
<div className="text-center mb-16">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-3xl md:text-4xl font-bold mb-4 bg-gradient-to-r from-blue-600 to-violet-600 text-transparent bg-clip-text inline-block"
|
||||
>
|
||||
强大的功能特性
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto"
|
||||
>
|
||||
集成多项创新技术,为你提供极致的简历制作体验
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: feature.delay }}
|
||||
className="group relative"
|
||||
>
|
||||
<div className="absolute -inset-0.5 bg-gradient-to-r from-blue-500 to-violet-500 rounded-2xl blur opacity-0 group-hover:opacity-30 transition duration-500" />
|
||||
<div className="relative p-8 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 hover:border-transparent dark:hover:border-transparent transition-colors duration-300">
|
||||
<div className={cn(
|
||||
"w-12 h-12 rounded-lg mb-6 p-2.5",
|
||||
"bg-gradient-to-br",
|
||||
feature.gradient
|
||||
)}>
|
||||
<feature.icon className="w-full h-full text-white" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold mb-3 text-gray-900 dark:text-white">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import EditButton from "@/components/shared/EditButton";
|
||||
import TypewriterText from "./TypewriterText";
|
||||
|
||||
export default function AnimatedHero() {
|
||||
return (
|
||||
<section className="pt-32 pb-16 px-6 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-violet-50 dark:from-blue-950/20 dark:to-violet-950/20 opacity-50" />
|
||||
<div className="absolute inset-0">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-400/30 rounded-full mix-blend-multiply filter blur-xl animate-blob" />
|
||||
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-violet-400/30 rounded-full mix-blend-multiply filter blur-xl animate-blob animation-delay-2000" />
|
||||
<div className="absolute bottom-1/4 left-1/3 w-96 h-96 bg-pink-400/30 rounded-full mix-blend-multiply filter blur-xl animate-blob animation-delay-4000" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto text-center space-y-8 relative">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<h1 className=" mb-6 text-5x sm:text-6xl font-bold">
|
||||
<span className="bg-gradient-to-r from-blue-600 to-violet-600 text-transparent bg-clip-text leading-tight tracking-tight">
|
||||
AI智能
|
||||
</span>
|
||||
<span className="">打造</span>
|
||||
完美简历
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<EditButton className="bg-gradient-to-r from-blue-600 to-violet-600 text-white px-8 py-3 rounded-full hover:opacity-90 transition-opacity text-lg">
|
||||
免费开始使用
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</EditButton>
|
||||
<button className="px-8 py-3 rounded-full border border-gray-200 dark:border-white/10 hover:border-gray-300 dark:hover:border-white/20 transition-colors text-lg">
|
||||
查看演示
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
import { openAIRequest } from "@/utils";
|
||||
|
||||
const CreateResume = () => {
|
||||
const createResumeByAI = () => {
|
||||
openAIRequest("你是谁");
|
||||
};
|
||||
return (
|
||||
<button
|
||||
onClick={createResumeByAI}
|
||||
className="px-6 py-4 bg-gradient-to-r from-blue-600 to-violet-600 text-white rounded-r-lg hover:opacity-90 transition-opacity font-medium"
|
||||
>
|
||||
生成简历
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateResume;
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
interface FeaturesAnimationProps extends PropsWithChildren {
|
||||
index: number;
|
||||
}
|
||||
|
||||
export function FeaturesAnimation({ children, index }: FeaturesAnimationProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Logo from "@/components/shared/Logo";
|
||||
import ThemeToggle from "@/components/shared/ThemeToggle";
|
||||
|
||||
export default function Header() {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [lastScrollY, setLastScrollY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
if (currentScrollY < 10) {
|
||||
setIsVisible(true);
|
||||
setLastScrollY(currentScrollY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentScrollY > lastScrollY && currentScrollY > 200) {
|
||||
setIsVisible(false);
|
||||
} else if (currentScrollY < lastScrollY) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
|
||||
setLastScrollY(currentScrollY);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [lastScrollY]);
|
||||
|
||||
return (
|
||||
<div className="fixed w-full z-50 flex justify-center">
|
||||
<motion.div
|
||||
className="w-[90%] max-w-5xl"
|
||||
initial={{ y: 0, opacity: 1 }}
|
||||
animate={{
|
||||
y: isVisible ? 0 : -100,
|
||||
opacity: isVisible ? 1 : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
}}
|
||||
>
|
||||
<div className="mt-4 rounded-full bg-background/70 backdrop-blur-[8px] border border-border/50">
|
||||
<div className="relative flex items-center justify-between h-12 px-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Logo size={32} />
|
||||
<span className="font-bold text-base">Magic Resume</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ThemeToggle>
|
||||
<div className="w-8 h-8 relative cursor-pointer rounded-md hover:bg-accent/50 flex items-center justify-center">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] absolute inset-0 m-auto rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="h-[1.2rem] w-[1.2rem] absolute inset-0 m-auto rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
</div>
|
||||
</ThemeToggle>
|
||||
<Link href="/dashboard">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-primary hover:opacity-90 text-white h-8 text-sm rounded-full px-4"
|
||||
>
|
||||
开始使用
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export function HeroAnimation({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
interface TypewriterTextProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export default function TypewriterText({ text }: TypewriterTextProps) {
|
||||
const [displayText, setDisplayText] = useState("");
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentIndex < text.length) {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
setDisplayText((prev) => prev + text[currentIndex]);
|
||||
setCurrentIndex((c) => c + 1);
|
||||
},
|
||||
50 + Math.random() * 50
|
||||
);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [currentIndex, text]);
|
||||
|
||||
return (
|
||||
<span className="relative">
|
||||
{displayText}
|
||||
<span className="absolute -right-1 top-1/4 h-1/2 w-0.5 bg-blue-500 animate-pulse" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user