feat: integrate Sanity client and enhance project showcase with dynamic data fetching

This commit is contained in:
grtsinry43 2025-05-18 17:32:48 +08:00
parent 31dbaf0261
commit 1254192b13
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
10 changed files with 8317 additions and 201 deletions

View File

@ -39,6 +39,7 @@
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"@sanity/image-url": "^1.1.0",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -51,6 +52,7 @@
"lucide-react": "^0.454.0",
"next": "15.2.4",
"next-intl": "^4.1.0",
"next-sanity": "^9.11.1",
"next-themes": "latest",
"react": "^19",
"react-day-picker": "8.10.1",

8086
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,3 @@
onlyBuiltDependencies:
- esbuild
- sharp

View File

@ -149,7 +149,9 @@ export default function HomePage() {
<GsapSkillsTree/>
</motion.div>
<GsapProjectsShowcase/>
<motion.div>
<GsapProjectsShowcase/>
</motion.div>
{/* Projects Section with enhanced visuals */}
{/*<ProjectsSection/>*/}

View File

@ -74,12 +74,12 @@ export default function AboutSection() {
<div className="space-y-4 text-lg text-muted-foreground">
<p> Java/JavaScript Kotlin/TypeScript
</p>
Web </p>
<p>
Web Android
</p>
<p></p>
<p></p>
</div>
<div className="flex gap-3">
@ -128,10 +128,16 @@ export default function AboutSection() {
<SkillBadge name="React"/>
<SkillBadge name="Next.js"/>
<SkillBadge name="Vue.js"/>
<SkillBadge name="Spring Boot"/>
<SkillBadge name="Nuxt.js"/>
<SkillBadge name="TypeScript"/>
<SkillBadge name="JavaScript"/>
<SkillBadge name="Kotlin"/>
<SkillBadge name="Python"/>
<SkillBadge name="Jupyter Notebook"/>
<SkillBadge name="FastAPI"/>
<SkillBadge name="PyTorch"/>
<SkillBadge name="Spring Boot"/>
<SkillBadge name="Ktor"/>
<SkillBadge name="Java"/>
<SkillBadge name="Jetpack Compose"/>
</div>
@ -140,9 +146,13 @@ export default function AboutSection() {
<div>
<p className="font-mono text-sm text-primary mb-2">{"// 开发工具"}</p>
<div className="flex flex-wrap gap-2">
<SkillBadge name="WebStorm"/>
<SkillBadge name="VS Code"/>
<SkillBadge name="IntelliJ IDEA"/>
<SkillBadge name="Android Studio"/>
<SkillBadge name="RustRover"/>
<SkillBadge name="PyCharm"/>
<SkillBadge name="CLion"/>
<SkillBadge name="Git"/>
<SkillBadge name="Figma"/>
<SkillBadge name="Docker"/>

View File

@ -2,7 +2,7 @@ import React from 'react';
import {motion} from "framer-motion";
import {InfiniteMovingCards} from "@/components/ui/infinite-moving-cards";
interface CardItem{
interface CardItem {
quote: string;
name: string;
title: string;
@ -10,48 +10,88 @@ interface CardItem{
const cardItems:CardItem[] = [
{
quote: "Java",
name: "Java",
title: "Java"
quote: "湖南长沙,中南学子,逐梦全栈的 Archlinux 爱好者。",
name: "grtsinry43",
title: "坐标与身份:湖南长沙 | 中南大学 | 大二学生"
},
{
quote: "JavaScript",
name: "JavaScript",
title: "JavaScript"
quote: "主攻前端 (Vue/React),心系 Kotlin/TypeScript 的未来。",
name: "技术栈 (当前)JavaScript, Vue.js, React.js",
title: "技术栈 (未来)Kotlin, TypeScript"
},
{
quote: "TypeScript",
name: "TypeScript",
title: "TypeScript"
quote: "JetBrains 全家桶用户,曾是 Vim 的忠实粉丝。",
name: "开发工具JetBrains IDE",
title: "曾用工具Vim"
},
{
quote: "Kotlin",
name: "Kotlin",
title: "Kotlin"
quote: "对 ML 怀揣兴趣,也曾涉猎 AE/C4D 视频渲染。",
name: "兴趣领域:机器学习",
title: "略懂技能:视频渲染 (AE, C4D)"
},
]
{
quote: "讨厌 NV 驱动的 Kernel PanicArch 用户的小执念。",
name: "系统偏好Arch Linux",
title: "痛点NVIDIA 驱动问题"
},
{
quote: "音游达人,活跃于 Arcaea, Phigros 等多个平台。",
name: "兴趣爱好:音游",
title: "涉猎音游Arcaea, Phigros, Muse Dash, osu!, Malody, pjsk, Rizline"
},
{
quote: "INFJ-T 型格,在内耗与渴望中寻找平衡。",
name: "性格特点INFJ-T",
title: "内心状态:内省,共情,社恐与社交渴望并存"
},
{
quote: "自诩“屎山”制造者,却怀揣用代码改变世界的梦想。",
name: "开发理念:实用至上",
title: "开发目标:用技术创造价值"
},
{
quote: "重写博客与团委网站,在开源社区留下足迹。",
name: "开源项目Grtblog, 中南大学团委网站",
title: "开源态度:积极参与,乐于分享"
},
{
quote: "年度关键词:迷茫求索,热爱坚守,于血泪中成长。",
name: "2024 年度总结",
title: "感悟:在迷茫中成长,因热爱而坚持"
},
{
quote: "新年 Flag减肥至 60kg 以下,深耕前端框架。",
name: "2025 年度目标 (部分)",
title: "个人与技术双重提升"
},
{
quote: "渴望成为有影响力的开源作者,在技术道路上不断探索。",
name: "未来愿景",
title: "成为独当一面的前端工程师与开源贡献者"
},
];
const CardSection = () => {
return (
<div className="w-full pb-6 bg-slate-950">
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true, amount: 0.3 }}
initial={{opacity: 0, y: 40}}
whileInView={{opacity: 1, y: 0}}
transition={{duration: 0.8}}
viewport={{once: true, amount: 0.3}}
className="flex items-center justify-center space-x-2 mb-12"
>
<div className="h-px w-12 bg-primary/60" />
<div className="h-px w-12 bg-primary/60"/>
<h2 className="text-lg font-medium text-primary"></h2>
<div className="h-px w-12 bg-primary/60" />
<div className="h-px w-12 bg-primary/60"/>
</motion.div>
<motion.h3
className="text-3xl md:text-4xl font-bold tracking-tight text-center mb-16 text-white"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true, amount: 0.3 }}
initial={{opacity: 0}}
whileInView={{opacity: 1}}
transition={{duration: 0.8, delay: 0.2}}
viewport={{once: true, amount: 0.3}}
>
<span className="text-primary">"成分复杂"</span>
</motion.h3>

View File

@ -242,7 +242,7 @@ export default function GsapPersonalIntro() {
/>
<AnimatedGradientText
text="grtsinry43"
className="text-7xl font-bold tracking-tight"
className="text-7xl font-bold tracking-tight pb-2"
gradient="from-blue-600 via-purple-600 to-blue-600"
delay={0.5}
/>
@ -265,10 +265,11 @@ export default function GsapPersonalIntro() {
</h4>
</div>
<p className="text-lg text-muted-foreground">
Web Android
<br/>
</p>
<div className="flex flex-wrap gap-4 items-center">
<div className="flex flex-wrap gap-4 items-center pb-4">
<motion.div
className="flex items-center gap-2 icon-item"
whileHover={{scale: 1.05, x: 5}}

View File

@ -66,19 +66,19 @@ export default function PersonalitySection() {
}
return (
<section id="personality" ref={containerRef} className="relative min-h-screen py-24 md:py-32 overflow-hidden">
<section id="personality" ref={containerRef} className="relative min-h-screen py-24 md:py-32 overflow-hidden bg-neutral-950">
{/* Background elements */}
<div className="absolute inset-0 z-0 overflow-hidden">
<motion.div
className="absolute top-1/3 right-1/4 w-64 h-64 rounded-full bg-purple-500/5 blur-3xl"
className="absolute top-1/3 right-1/4 w-64 h-64 rounded-full bg-purple-900/30 blur-3xl"
style={{
y: useTransform(scrollYProgress, [0, 1], [0, -100]),
x: useTransform(scrollYProgress, [0, 1], [0, 50]),
}}
/>
<motion.div
className="absolute bottom-1/3 left-1/4 w-96 h-96 rounded-full bg-emerald-500/5 blur-3xl"
className="absolute bottom-1/3 left-1/4 w-96 h-96 rounded-full bg-emerald-900/30 blur-3xl"
style={{
y: useTransform(scrollYProgress, [0, 1], [0, -150]),
x: useTransform(scrollYProgress, [0, 1], [0, -50]),
@ -95,19 +95,19 @@ export default function PersonalitySection() {
viewport={{ once: true, amount: 0.3 }}
className="flex items-center justify-center space-x-2 mb-12"
>
<div className="h-px w-12 bg-primary/60" />
<h2 className="text-lg font-medium text-primary"></h2>
<div className="h-px w-12 bg-primary/60" />
<div className="h-px w-12 bg-blue-700/60" />
<h2 className="text-lg font-medium text-blue-400"></h2>
<div className="h-px w-12 bg-blue-700/60" />
</motion.div>
<motion.h3
className="text-3xl md:text-4xl font-bold tracking-tight text-center mb-16"
className="text-3xl md:text-4xl font-bold tracking-tight text-center mb-16 text-neutral-100"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true, amount: 0.3 }}
>
<span className="text-primary"></span>
<span className="text-blue-400"></span>
</motion.h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
@ -116,7 +116,7 @@ export default function PersonalitySection() {
variants={containerVariants}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
className="space-y-4 text-lg text-neutral-700 dark:text-neutral-300"
className="space-y-4 text-lg text-neutral-200"
>
<motion.p variants={itemVariants} custom={1}>
16
@ -124,10 +124,10 @@ export default function PersonalitySection() {
INFJ
</motion.p>
<motion.p variants={itemVariants} custom={2}>
INFJ的特行者在迷雾中寻找光
</motion.p>
<motion.p variants={itemVariants} custom={3}>
infj
infj
</motion.p>
</motion.div>
@ -153,11 +153,11 @@ export default function PersonalitySection() {
className="relative"
>
<blockquote
className="relative z-10 rounded-2xl bg-gradient-to-r from-emerald-50 to-blue-50 dark:from-emerald-900/30 dark:to-blue-900/30 p-8 italic text-neutral-700 dark:text-neutral-200 border-l-4 border-blue-500 shadow-lg">
className="relative z-10 rounded-2xl bg-gradient-to-r from-emerald-950 to-blue-950 p-8 italic text-neutral-200 border-l-4 border-blue-700 shadow-lg">
"明明拿了反派的成长剧本,却依旧想成为正道的光。"
</blockquote>
<motion.div
className="absolute top-0 right-0 -mt-4 -mr-4 h-24 w-24 rounded-full bg-gradient-to-r from-emerald-500/10 to-blue-500/10 blur-xl"
className="absolute top-0 right-0 -mt-4 -mr-4 h-24 w-24 rounded-full bg-gradient-to-r from-emerald-800/30 to-blue-800/30 blur-xl"
animate={{
scale: [1, 1.2, 1],
opacity: [0.5, 0.8, 0.5],
@ -169,7 +169,7 @@ export default function PersonalitySection() {
}}
/>
<motion.div
className="absolute bottom-0 left-0 -mb-4 -ml-4 h-24 w-24 rounded-full bg-gradient-to-r from-emerald-500/10 to-blue-500/10 blur-xl"
className="absolute bottom-0 left-0 -mb-4 -ml-4 h-24 w-24 rounded-full bg-gradient-to-r from-emerald-800/30 to-blue-800/30 blur-xl"
animate={{
scale: [1, 1.2, 1],
opacity: [0.5, 0.8, 0.5],
@ -188,7 +188,7 @@ export default function PersonalitySection() {
initial={{opacity: 0, y: 20}}
animate={isInView ? {opacity: 1, y: 0} : {}}
transition={{duration: 0.6, delay: 0.7}}
className="text-xl font-bold mb-6"
className="text-xl font-bold mb-6 text-neutral-100"
>
INFJ
</motion.h4>
@ -215,11 +215,11 @@ export default function PersonalitySection() {
x: 10,
transition: {duration: 0.2},
}}
className="flex items-center p-4 rounded-xl bg-white dark:bg-neutral-900 shadow-sm border border-neutral-200 dark:border-neutral-800 transform-gpu"
className="flex items-center p-4 rounded-xl bg-neutral-900 shadow-sm border border-neutral-800 transform-gpu"
>
<div
className="h-3 w-3 rounded-full bg-gradient-to-r from-emerald-500 to-blue-500 mr-4"/>
<span className="text-neutral-700 dark:text-neutral-300">{trait}</span>
<span className="text-neutral-200">{trait}</span>
</motion.div>
))}
</div>
@ -238,9 +238,9 @@ export default function PersonalitySection() {
rotate: 0,
transition: {duration: 0.3},
}}
className="bg-white dark:bg-neutral-900 p-8 rounded-2xl border border-neutral-200 dark:border-neutral-800 shadow-lg transform-gpu"
className="bg-neutral-900 p-8 rounded-2xl border border-neutral-800 shadow-lg transform-gpu"
>
<h4 className="text-xl font-bold mb-6 text-center">INFJ </h4>
<h4 className="text-xl font-bold mb-6 text-center text-neutral-100">INFJ </h4>
<div className="aspect-square relative z-10">
<svg viewBox="0 0 200 200" className="w-full h-full">
<defs>
@ -256,22 +256,22 @@ export default function PersonalitySection() {
</filter>
</defs>
<circle cx="100" cy="100" r="80" fill="none" stroke="rgba(100,116,139,0.2)"
<circle cx="100" cy="100" r="80" fill="none" stroke="rgba(51,65,85,0.4)"
strokeWidth="1"/>
<circle cx="100" cy="100" r="60" fill="none" stroke="rgba(100,116,139,0.2)"
<circle cx="100" cy="100" r="60" fill="none" stroke="rgba(51,65,85,0.4)"
strokeWidth="1"/>
<circle cx="100" cy="100" r="40" fill="none" stroke="rgba(100,116,139,0.2)"
<circle cx="100" cy="100" r="40" fill="none" stroke="rgba(51,65,85,0.4)"
strokeWidth="1"/>
<circle cx="100" cy="100" r="20" fill="none" stroke="rgba(100,116,139,0.2)"
<circle cx="100" cy="100" r="20" fill="none" stroke="rgba(51,65,85,0.4)"
strokeWidth="1"/>
<line x1="100" y1="20" x2="100" y2="180" stroke="rgba(100,116,139,0.2)"
<line x1="100" y1="20" x2="100" y2="180" stroke="rgba(51,65,85,0.4)"
strokeWidth="1"/>
<line x1="20" y1="100" x2="180" y2="100" stroke="rgba(100,116,139,0.2)"
<line x1="20" y1="100" x2="180" y2="100" stroke="rgba(51,65,85,0.4)"
strokeWidth="1"/>
<line x1="37" y1="37" x2="163" y2="163" stroke="rgba(100,116,139,0.2)"
<line x1="37" y1="37" x2="163" y2="163" stroke="rgba(51,65,85,0.4)"
strokeWidth="1"/>
<line x1="37" y1="163" x2="163" y2="37" stroke="rgba(100,116,139,0.2)"
<line x1="37" y1="163" x2="163" y2="37" stroke="rgba(51,65,85,0.4)"
strokeWidth="1"/>
<motion.polygon
@ -295,32 +295,31 @@ export default function PersonalitySection() {
filter="url(#personalityGlow)"
/>
<text x="100" y="15" textAnchor="middle" fill="currentColor" fontSize="8">
<text x="100" y="15" textAnchor="middle" fill="#e0e7ef" fontSize="8">
</text>
<text x="185" y="100" textAnchor="start" fill="currentColor" fontSize="8">
<text x="185" y="100" textAnchor="start" fill="#e0e7ef" fontSize="8">
</text>
<text x="100" y="190" textAnchor="middle" fill="currentColor" fontSize="8">
<text x="100" y="190" textAnchor="middle" fill="#e0e7ef" fontSize="8">
</text>
<text x="15" y="100" textAnchor="end" fill="currentColor" fontSize="8">
<text x="15" y="100" textAnchor="end" fill="#e0e7ef" fontSize="8">
</text>
<text x="37" y="37" textAnchor="middle" fill="currentColor" fontSize="8">
<text x="37" y="37" textAnchor="middle" fill="#e0e7ef" fontSize="8">
</text>
<text x="37" y="163" textAnchor="middle" fill="currentColor" fontSize="8">
<text x="37" y="163" textAnchor="middle" fill="#e0e7ef" fontSize="8">
</text>
<text x="163" y="37" textAnchor="middle" fill="currentColor" fontSize="8">
<text x="163" y="37" textAnchor="middle" fill="#e0e7ef" fontSize="8">
</text>
<text x="163" y="163" textAnchor="middle" fill="currentColor" fontSize="8">
<text x="163" y="163" textAnchor="middle" fill="#e0e7ef" fontSize="8">
</text>
{/* Animated points */}
{[
{cx: 100, cy: 30},
{cx: 150, cy: 45},
@ -374,24 +373,24 @@ export default function PersonalitySection() {
scale: 1.05,
transition: {duration: 0.3},
}}
className="bg-white dark:bg-neutral-900 p-8 rounded-2xl border border-neutral-200 dark:border-neutral-800 shadow-lg"
className="bg-neutral-900 p-8 rounded-2xl border border-neutral-800 shadow-lg"
>
<h4 className="text-xl font-bold mb-6">INFJ </h4>
<h4 className="text-xl font-bold mb-6 text-neutral-100">INFJ </h4>
<div className="relative pt-8">
<div
className="absolute top-0 left-0 w-full flex justify-between text-xs text-neutral-500">
className="absolute top-0 left-0 w-full flex justify-between text-xs text-neutral-400">
<span>0%</span>
<span>1%</span>
<span>2%</span>
<span>3%</span>
</div>
<div
className="h-16 bg-gradient-to-r from-emerald-50 to-blue-50 dark:from-emerald-900/30 dark:to-blue-900/30 rounded-lg relative">
className="h-16 bg-gradient-to-r from-emerald-950 to-blue-950 rounded-lg relative">
<motion.div
initial={{width: 0}}
animate={isInView ? {width: "16%"} : {}}
transition={{duration: 1.5, delay: 1.2, ease: "easeOut"}}
className="absolute top-0 left-0 h-full bg-gradient-to-r from-emerald-500 to-blue-500 rounded-lg opacity-70"
className="absolute top-0 left-0 h-full bg-gradient-to-r from-emerald-600 to-blue-700 rounded-lg opacity-80"
/>
<motion.div
initial={{opacity: 0, scale: 0}}
@ -408,7 +407,7 @@ export default function PersonalitySection() {
}
: {}
}
className="absolute top-1/2 left-[16%] -translate-x-1/2 -translate-y-1/2 h-8 w-8 rounded-full bg-white dark:bg-neutral-800 border-2 border-emerald-500 flex items-center justify-center text-xs font-bold text-emerald-600 dark:text-emerald-400"
className="absolute top-1/2 left-[16%] -translate-x-1/2 -translate-y-1/2 h-8 w-8 rounded-full bg-neutral-900 border-2 border-emerald-500 flex items-center justify-center text-xs font-bold text-emerald-400"
>
1.5%
</motion.div>
@ -430,8 +429,8 @@ export default function PersonalitySection() {
className="absolute top-full left-[16%] -translate-x-1/2 mt-2 text-center"
>
<span
className="text-sm font-medium text-neutral-700 dark:text-neutral-300">INFJ</span>
<p className="text-xs text-neutral-500"></p>
className="text-sm font-medium text-neutral-200">INFJ</span>
<p className="text-xs text-neutral-400"></p>
</motion.div>
</div>
</div>
@ -444,4 +443,3 @@ export default function PersonalitySection() {
</section>
)
}

View File

@ -6,62 +6,47 @@ import {Github, ExternalLink, ArrowRight} from "lucide-react"
import {Button} from "@/components/ui/button"
import {Badge} from "@/components/ui/badge"
import {useTheme} from "next-themes"
import {client} from "@/sanity/client";
import {SanityDocument} from "next-sanity"
import imageUrlBuilder from "@sanity/image-url"
import type {SanityImageSource} from "@sanity/image-url/lib/types/types"
interface Project {
id: number
title: string
_id: number
name: string
description: string
tags: string[]
image: string
techStack: string
cover: any // Sanity image object
githubUrl?: string
liveUrl?: string
deployUrl?: string
}
const PROJECTS_QUERY = `*[_type == "project"]|order(publishedAt desc)[0...12]
{_id,name,description,githubUrl,deployUrl,progress,techStack,publishedAt,cover}`;
const options = {next: {revalidate: 30}};
const {projectId, dataset} = client.config()
const urlFor = (source: SanityImageSource) =>
projectId && dataset
? imageUrlBuilder({projectId, dataset}).image(source)
: null
export default function GsapProjectsShowcase() {
const containerRef = useRef<HTMLDivElement>(null)
const horizontalRef = useRef<HTMLDivElement>(null)
const [containerWidth, setContainerWidth] = useState(0)
const [isInView, setIsInView] = useState(false)
const {theme} = useTheme()
const [projects, setProjects] = useState<Project[]>([])
// Project data
const projects: Project[] = [
{
id: 1,
title: "grtblog",
description:
"个人博客站点,一站式快速解决方案—一套源,扩展性强,快速部署的博客框架,使用 Nextjs + SpringBoot 构建,致力于轻松搭建个性化你的博客站点",
tags: ["React", "Next.js", "Spring Boot", "Umi.js", "Socket.io", "Radix UI"],
image: "/placeholder.svg?height=600&width=800",
githubUrl: "https://github.com/grtsinry43/grtblog",
liveUrl: "https://grtblog.js.org",
},
{
id: 2,
title: "csu-dynamic-youth",
description: "途有青,去追山!学校官微毕业节的微信网页程序,统计参与次数与时长,生成排行榜,完成完成打卡",
tags: ["微信小程序", "Vue.js", "微信API"],
image: "/placeholder.svg?height=600&width=800",
githubUrl: "#",
},
{
id: 3,
title: "Portfolio Website",
description: "个人作品集网站,展示我的项目和技能,使用 Next.js 和 Framer Motion 构建",
tags: ["Next.js", "Framer Motion", "Tailwind CSS", "TypeScript"],
image: "/placeholder.svg?height=600&width=800",
githubUrl: "#",
liveUrl: "#",
},
{
id: 4,
title: "AI Chat Application",
description: "基于人工智能的聊天应用,支持多种语言和自然语言处理",
tags: ["React", "Node.js", "OpenAI API", "WebSockets"],
image: "/placeholder.svg?height=600&width=800",
githubUrl: "#",
},
]
useEffect(() => {
const fetchData = async () => {
const data = await client.fetch<Project[]>(PROJECTS_QUERY, {}, options);
setProjects(data);
};
fetchData();
}, []);
// 检测元素是否在视口中
useEffect(() => {
@ -88,8 +73,7 @@ export default function GsapProjectsShowcase() {
// 计算水平滚动容器的宽度
useEffect(() => {
if (horizontalRef.current) {
// 计算所有项目卡片的总宽度 + 间距
const totalWidth = projects.length * 600 + (projects.length - 1) * 40
const totalWidth = projects.length * 600 + (projects.length - 1) * 40 + 300 + 40 + 800
setContainerWidth(totalWidth)
}
}, [projects.length])
@ -193,7 +177,6 @@ export default function GsapProjectsShowcase() {
},
)
}
if (tags) {
gsap.fromTo(
tags,
@ -212,7 +195,6 @@ export default function GsapProjectsShowcase() {
},
)
}
if (links) {
gsap.fromTo(
links,
@ -234,7 +216,6 @@ export default function GsapProjectsShowcase() {
})
return () => {
// 清理动画
scrollTween.kill()
ScrollTrigger.getAll().forEach((trigger) => trigger.kill())
}
@ -269,9 +250,9 @@ export default function GsapProjectsShowcase() {
transition={{duration: 0.8, delay: 0.2}}
viewport={{once: true, amount: 0.3}}
>
<span className="bg-gradient-to-r from-emerald-600 to-blue-600 bg-clip-text text-transparent">
</span>
<span className="bg-gradient-to-r from-emerald-600 to-blue-600 bg-clip-text text-transparent">
</span>
</motion.h3>
<motion.p
@ -281,7 +262,7 @@ export default function GsapProjectsShowcase() {
transition={{duration: 0.8, delay: 0.3}}
viewport={{once: true, amount: 0.3}}
>
-
~
</motion.p>
</div>
</div>
@ -291,71 +272,76 @@ export default function GsapProjectsShowcase() {
ref={horizontalRef}
className="absolute top-0 left-0 h-screen w-fit flex items-center gap-10 pl-[calc(50vw-300px)] pr-[100px] pt-[250px]"
>
{projects.map((project, index) => (
<div
key={project.id}
className="project-card flex-shrink-0 w-[600px] h-[500px] bg-card/90 backdrop-blur-sm rounded-2xl overflow-hidden shadow-xl border border-border/50 relative"
>
<div className="project-image absolute inset-0 z-0">
<div
className="absolute inset-0 bg-gradient-to-t from-card via-card/80 to-transparent z-10"/>
<img
src={project.image || "/placeholder.svg"}
alt={project.title}
className="w-full h-full object-cover"
/>
</div>
<div className="relative z-20 flex flex-col justify-end h-full p-8">
<h4 className="project-title text-3xl font-bold mb-4">{project.title}</h4>
<p className="project-desc text-lg text-muted-foreground mb-6">{project.description}</p>
<div className="project-tags flex flex-wrap gap-2 mb-6">
{project.tags.map((tag) => (
<Badge key={tag} variant="outline"
className="bg-primary/10 text-primary border-primary/20">
{tag}
</Badge>
))}
{projects.map((project, index) => {
const coverUrl = project.cover
? urlFor(project.cover)?.width(600).height(500).url()
: "/placeholder.svg"
return (
<div
key={project._id}
className="project-card flex-shrink-0 w-[600px] h-[500px] bg-card/90 backdrop-blur-sm rounded-2xl overflow-hidden shadow-xl border border-border/50 relative"
>
<div className="project-image absolute inset-0 z-0">
<div
className="absolute inset-0 bg-gradient-to-t from-card via-card/80 to-transparent z-10"/>
<img
src={coverUrl}
alt={project.name}
className="w-full h-full object-cover"
/>
</div>
<div className="project-links flex gap-4">
{project.githubUrl && (
<Button asChild variant="outline" size="sm"
className="bg-primary/10 text-primary border-primary/20">
<a
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<Github className="h-4 w-4"/>
GitHub
</a>
</Button>
)}
{project.liveUrl && (
<Button asChild size="sm" variant="ghost">
<a
href={project.liveUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<ExternalLink className="h-4 w-4"/>
</a>
</Button>
)}
</div>
<div className="relative z-20 flex flex-col justify-end h-full p-8">
<h4 className="project-title text-3xl font-bold mb-4">{project.name}</h4>
<p className="project-desc text-lg text-muted-foreground mb-6">{project.description}</p>
{/* 项目序号 */}
<div className="absolute top-8 right-8 text-8xl font-bold text-primary/10">
{String(index + 1).padStart(2, "0")}
<div className="project-tags flex flex-wrap gap-2 mb-6">
{project.techStack.split(',').map((tag: string) => (
<Badge key={tag} variant="outline"
className="bg-primary/10 text-primary border-primary/20">
{tag}
</Badge>
))}
</div>
<div className="project-links flex gap-4">
{project.githubUrl && (
<Button asChild variant="outline" size="sm"
className="bg-primary/10 text-primary border-primary/20">
<a
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<Github className="h-4 w-4"/>
GitHub
</a>
</Button>
)}
{project.deployUrl && (
<Button asChild size="sm" variant="ghost">
<a
href={project.deployUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<ExternalLink className="h-4 w-4"/>
</a>
</Button>
)}
</div>
{/* 项目序号 */}
<div className="absolute top-8 right-8 text-8xl font-bold text-primary/10">
{String(index + 1).padStart(2, "0")}
</div>
</div>
</div>
</div>
))}
)
})}
{/* 结束提示 */}
<div className="flex-shrink-0 w-[300px] h-[500px] flex items-center justify-center">
@ -363,7 +349,7 @@ export default function GsapProjectsShowcase() {
<p className="text-muted-foreground mb-4"></p>
<Button asChild>
<a href="#contact" className="flex items-center gap-2">
<ArrowRight className="h-4 w-4"/>
<ArrowRight className="h-4 w-4"/>
</a>
</Button>
</div>
@ -382,4 +368,4 @@ export default function GsapProjectsShowcase() {
</div>
</section>
)
}
}

8
src/sanity/client.ts Normal file
View File

@ -0,0 +1,8 @@
import {createClient} from "next-sanity";
export const client = createClient({
projectId: "3p9gcb1i",
dataset: "production",
apiVersion: "2024-01-01",
useCdn: false,
});