feat: home page ssr

This commit is contained in:
JOYCEQL
2024-12-16 20:25:48 +08:00
committed by qingchen
parent 44429d2e28
commit 024ccbede3
9 changed files with 182 additions and 364 deletions
+1 -1
View File
@@ -56,7 +56,7 @@
* {
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
scrollbar-color: rgba(255, 255, 255, 0.16) transparent;
}
.dark {
+65 -155
View File
@@ -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>&#169; 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>
);
}