refactor(portfolio): update skills data and remove awards/education sections

- Update skills data with additional frameworks and tools
- Remove AwardsSection and EducationSection components
- Update project documentation to reflect changes
This commit is contained in:
joyzhao
2025-06-15 15:43:27 +08:00
parent 720686751a
commit 22799c9d8a
6 changed files with 38 additions and 175 deletions

View File

@@ -4,7 +4,7 @@
## 1. 项目概述
`zhaoguiyang.site` 是一个现代化的个人作品集网站模板,具有动画和玻璃拟态效果。该项目旨在展示个人信息、工作经历、技能项目、奖项和教育背景
`zhaoguiyang.site` 是一个现代化的个人作品集网站模板,具有动画和玻璃拟态效果。该项目旨在展示个人信息、工作经历、技能项目。
## 2. 技术栈和框架
@@ -23,10 +23,14 @@
- `ExperienceSection.tsx`: 工作经历区域。
- `SkillsSection.tsx`: 技能展示区域。
- `ProjectsSection.tsx`: 项目展示区域。
- `AwardsSection.tsx`: 奖项展示区域。
- `EducationSection.tsx`: 教育背景区域。
- `GlassHeader.tsx`: 玻璃拟态效果的导航栏。
- `Footer.tsx`: 页脚。
- `LanguageSwitcher.tsx`: 语言切换组件。
- `ThemeToggle.tsx`: 主题切换组件 (位于 `src/components/ui/`)
- **UI 组件 (src/components/ui/)**: 基于 Shadcn UI 风格,提供可复用的基础 UI 元素。
- `button.tsx`: 按钮组件。
- `card.tsx`: 卡片组件 (包含 `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`)。
- `glass-card.tsx`: 自定义玻璃拟态效果的卡片组件,使用 Framer Motion。
- **UI 辅助库**:
- `class-variance-authority`: 用于创建可变样式的组件。
- `clsx`: 用于有条件地组合类名。
@@ -55,8 +59,6 @@
│ └── profile.jpg
├── src/
│ ├── components/ # React 组件目录
│ │ ├── AwardsSection.tsx
│ │ ├── EducationSection.tsx
│ │ ├── ExperienceSection.tsx
│ │ ├── Footer.tsx
│ │ ├── GlassHeader.tsx
@@ -65,14 +67,24 @@
│ │ ├── ProjectsSection.tsx
│ │ ├── SkillsSection.tsx
│ │ ├── TimelineItem.tsx # 时间轴条目组件
│ │ ── ui/ # 可能包含更通用的 UI 组件 (当前为空)
│ │ ── LanguageSwitcher.tsx # 语言切换组件
│ │ └── ui/ # 通用 UI 组件 (基于 Shadcn UI 风格)
│ │ ├── button.tsx # 按钮组件
│ │ ├── card.tsx # 卡片组件
│ │ ├── glass-card.tsx # 玻璃拟态卡片组件
│ │ └── theme-toggle.tsx # 主题切换组件
│ ├── i18n/ # 国际化相关文件
│ │ ├── ui.ts # 存储翻译文本
│ │ └── utils.ts # i18n 辅助函数
│ ├── layouts/ # Astro 布局组件
│ │ └── Layout.astro # 全局页面布局
│ ├── lib/ # 辅助函数和数据
│ │ ├── data.ts # 存储个人信息、经历、技能等静态数据
│ │ └── utils.ts # 通用工具函数 (当前可能为空或包含少量工具)
│ ├── pages/ # Astro 页面组件
│ │ ── index.astro # 网站主页
│ │ ── index.astro # 网站主页 (英文)
│ │ └── zh/ # 中文语言目录
│ │ └── index.astro # 网站主页 (中文)
│ └── styles/ # 全局样式
│ └── global.css
└── tsconfig.json # TypeScript 配置文件
@@ -84,15 +96,19 @@
- **`package.json`**: 定义项目元数据、依赖项和 npm 脚本(如 `dev`, `build`, `preview`)。
- **`tsconfig.json`**: 配置 TypeScript 编译器选项,包括路径别名(`@/*` 指向 `./src/*`)和 JSX 设置。
- **`src/layouts/Layout.astro`**: 定义网站的全局 HTML 结构、头部信息(`<head>`)、全局样式和一些客户端脚本(如主题切换逻辑)。所有页面都将使用此布局。
- **`src/pages/index.astro`**: 网站的入口页面它导入并使用 `Layout.astro` 作为布局,并按顺序引入各个 React 内容区域组件。注意 `client:only="react"`指令,表示这些 React 组件仅在客户端渲染。
- **`src/lib/data.ts`**: 存储网站展示的静态数据,如个人信息、工作经历、教育背景、技能列表、项目信息和奖项。这使得内容易于更新和管理,而无需直接修改组件代码
- **`src/pages/index.astro`**: 网站的英文版入口页面它导入并使用 `Layout.astro` 作为布局,并按顺序引入各个 React 内容区域组件(如个人简介、工作经历、技能、项目)。通过 `getLangFromUrl` 从 URL 判断当前语言并传递给布局和组件。注意 `client:only="react"`指令,表示这些 React 组件仅在客户端渲染。
- **`src/pages/zh/index.astro`**: 网站的中文版入口页面,结构与 `index.astro` 类似,但为中文内容服务,展示个人简介、工作经历、技能和项目
- **`src/lib/data.ts`**: 存储网站展示的静态数据,如个人信息、工作经历、技能列表和项目信息。文件中的许多键值对应于 `src/i18n/ui.ts` 中的翻译键,以支持多语言。这使得内容易于更新和管理,而无需直接修改组件代码。
- **`src/i18n/ui.ts`**: 定义了支持的语言 (英语和简体中文) 以及所有需要翻译的文本内容的键值对。是实现网站国际化的核心文件。
- **`src/i18n/utils.ts`**: 包含国际化相关的辅助函数,如 `getLangFromUrl` (从 URL 获取当前语言)、`useTranslations` (提供一个 `t` 函数用于在组件中获取翻译文本) 和 `getLocalizedPath` (生成本地化路径)。
- **`src/components/*.tsx`**: 构成页面主要内容的 React 组件。它们从 `src/lib/data.ts` 获取数据并使用 Tailwind CSS 进行样式设置,使用 Framer Motion 实现动画效果。
- **`public/`**: 存放可以直接通过 URL 访问的静态文件,如 `favicon.svg``profile.jpg`
## 6. 数据流和状态管理
- **静态数据**: 大部分内容数据(如个人信息、工作经历等)都存储在 `src/lib/data.ts`,并直接导入到相应的 React 组件中进行渲染。这是一个静态网站,因此没有复杂的客户端状态管理(如 Redux 或 Zustand
- **主题切换**: `src/layouts/Layout.astro` 中包含一个内联脚本,用于处理暗黑/明亮模式的主题切换,并将用户的偏好存储在 `localStorage` 中。
- **静态数据与国际化**: 大部分内容数据(如个人信息、工作经历、技能、项目等)的键名存储在 `src/lib/data.ts`。这些键名随后被用于从 `src/i18n/ui.ts` 中获取对应语言的实际文本。React 组件接收 `lang` prop并使用 `useTranslations` 工具函数来获取翻译后的文本进行渲染
- **主题切换**: `src/layouts/Layout.astro` 中包含一个内联脚本,用于处理暗黑/明亮模式的主题切换,并将用户的偏好存储在 `localStorage` 中。此外,`src/components/ui/ThemeToggle.tsx` 组件提供了用户手动切换主题的交互界面。
- **语言切换**: 语言切换通过 `src/components/LanguageSwitcher.tsx` 组件实现,它可能通过改变 URL 路径 (例如 `/en/``/zh/`) 来加载不同语言版本的页面。`src/i18n/utils.ts` 中的 `getLangFromUrl``getLocalizedPath` 辅助这一过程。
## 7. 构建和部署
@@ -114,6 +130,6 @@
## 9. 潜在的改进和扩展方向
- **CMS 集成**: 对于更动态的内容管理,可以考虑集成一个 Headless CMS (如 Sanity, Contentful)。`package.json` 中提到了 `Sanity (CMS)`,这可能是一个未来的方向或之前的尝试。
- **国际化 (i18n)**: 如果需要支持多语言,可以引入 Astro React 的 i18n 库
- **国际化 (i18n)**: 项目已实现基本的国际化功能,支持英语和简体中文。主要通过 `src/i18n/ui.ts` 管理翻译文本,`src/i18n/utils.ts` 提供辅助函数,并在 Astro 页面和 React 组件中应用。URL 结构 (如 `/zh/`) 用于区分不同语言版本。可以进一步完善,例如支持更多的语言或更复杂的复数、日期格式化等
- **更复杂的交互**: 对于需要更复杂客户端逻辑的功能,可能需要引入更全面的状态管理方案。
- **API 路由**: Astro 支持 API 路由,可以用于处理表单提交、与后端服务交互等。

View File

@@ -1,67 +0,0 @@
import React from "react";
import { awards } from "@/lib/data";
import { useTranslations } from "@/i18n/utils";
import { Trophy } from "lucide-react";
import MotionWrapper from "./MotionWrapper";
import { GlassCard } from "./ui/glass-card";
import { motion } from "framer-motion";
export default function AwardsSection({ lang }: { lang: "en" | "zh" }) {
const t = useTranslations(lang);
return (
<section
id="awards"
className="py-12 bg-gradient-to-b from-background to-muted/10"
>
<div className="container max-w-4xl mx-auto px-6 md:px-4">
<MotionWrapper>
<h2 className="text-2xl font-bold mb-8 text-center md:text-left">
🏆 {t('awards.title')}
</h2>
</MotionWrapper>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{awards.map((award, index) => (
<MotionWrapper key={award.name + award.date} delay={index * 0.1}>
<GlassCard className="p-4 dark:border-purple-500/10 hover:border-purple-500/30 transition-all duration-300 flex flex-col h-full">
<div className="flex items-center mb-2">
<motion.div
whileHover={{ rotate: 20 }}
transition={{ type: "spring", stiffness: 500 }}
className="flex items-center justify-center bg-gradient-to-r from-amber-500 to-yellow-500 rounded-full p-1.5 mr-2"
>
<Trophy className="h-4 w-4 text-white" />
</motion.div>
<h3 className="font-medium">{t(award.name as any)}</h3>
</div>
<p className="text-xs text-muted-foreground mb-1 pl-8">
🏢 {t(award.issuer as any)}
</p>
<div className="flex flex-col space-y-2 mt-auto">
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground bg-background/50 px-2 py-1 rounded-md">
📅 {t(award.date as any)}
</span>
<motion.span
className="text-xs px-2 py-1 bg-purple-500/10 rounded-full"
whileHover={{ scale: 1.05 }}
>
{t(award.position as any)}
</motion.span>
</div>
<motion.span
className="text-xs text-muted-foreground/80 bg-background/50 px-2 py-1 rounded-md w-fit"
whileHover={{ scale: 1.05 }}
>
{award.type === "awards.type.international" ? "🌎 " : "🇮🇳 "}
{t(award.type as any)}
</motion.span>
</div>
</GlassCard>
</MotionWrapper>
))}
</div>
</div>
</section>
);
}

View File

@@ -1,76 +0,0 @@
import { education } from "@/lib/data";
import TimelineItem from "./TimelineItem";
import { useTranslations } from "@/i18n/utils";
import { Award } from "lucide-react";
import MotionWrapper from "./MotionWrapper";
import { motion } from "framer-motion";
export default function EducationSection({ lang }: { lang: "en" | "zh" }) {
const t = useTranslations(lang);
return (
<section
id="education"
className="py-12 bg-gradient-to-b from-muted/10 to-background"
>
<div className="container max-w-4xl mx-auto px-6 md:px-4">
<MotionWrapper>
<h2 className="text-2xl font-bold mb-8 text-center md:text-left">
🎓 {t('education.title')}
</h2>
</MotionWrapper>
<div className="mb-8">
{education.map((edu, index) => {
const institutionKey = edu.institution.toLowerCase().replace(/[^a-z0-9]/g, '');
return (
<TimelineItem
key={edu.institution}
title={`🎓 ${t(`education.${institutionKey}.degree` as any)}`}
subtitle={`🏛️ ${t(`education.${institutionKey}.institution` as any)}`}
date={`📅 ${t(`education.${institutionKey}.period` as any)}`}
isLast={index === education.length - 1}
index={index}
>
<p className="text-sm text-muted-foreground mb-3">
📍 {t(`education.${institutionKey}.location` as any)}
</p>
{edu.achievements && edu.achievements.length > 0 && (
<motion.div
className="mt-3 p-4 bg-background/80 backdrop-blur-sm backdrop-filter rounded-lg border border-purple-500/20 dark:bg-card/10 dark:border-purple-500/10 shadow-sm"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
>
<div className="flex items-center mb-3">
<div className="h-6 w-6 flex items-center justify-center rounded-full bg-purple-500/10 mr-2">
<Award className="h-4 w-4 text-purple-500" />
</div>
<h4 className="text-sm font-medium">
{t('education.achievementsAndActivities')}
</h4>
</div>
<ul className="list-none ml-4 space-y-2 text-sm">
{edu.achievements.map((achievement, i) => (
<motion.li
key={i}
className="text-muted-foreground relative pl-6"
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: 0.1 * i }}
viewport={{ once: true }}
>
{t(`education.${institutionKey}.achievements.${i}` as any)}
</motion.li>
))}
</ul>
</motion.div>
)}
</TimelineItem>
)})}
</div>
</div>
</section>
);
}

View File

@@ -68,25 +68,23 @@ export const skills = {
"Solidity",
],
frontendDevelopment: [
"Vue",
"Nuxtjs",
"React",
"Nextjs",
"Reactjs",
"React Native",
"Shadcn UI",
"PrimeVue",
"Naive-ui",
"Tailwind CSS",
"HTML",
"CSS",
],
backendDevelopment: ["Nodejs", "Expressjs"],
databaseAndStorage: ["PostgreSQL", "Drizzle (ORM)"],
cloudAndDevOps: ["AWS"],
backendDevelopment: ["Nodejs", "Expressjs", "Nestjs", "Fastify"],
databaseAndStorage: ["PostgreSQL", "MongoDB", "Drizzle (ORM)", "Mongoose", "Prisma"],
cloudAndDevOps: ["AWS", "Cloudflare", "Vercel"],
toolsAndServices: [
"Clerk (Auth)",
"Sanity (CMS)",
"Tinybird (analytics)",
"Zod",
"Sentry",
"Mixpanel",
"Trigger.dev",
"betterAuth",
"Clerk (Auth)",
],
};

View File

@@ -6,8 +6,6 @@ import HeroSection from "@/components/HeroSection";
import ExperienceSection from "@/components/ExperienceSection";
import SkillsSection from "@/components/SkillsSection";
import ProjectsSection from "@/components/ProjectsSection";
import AwardsSection from "@/components/AwardsSection";
import EducationSection from "@/components/EducationSection";
import Footer from "@/components/Footer";
const lang = getLangFromUrl(Astro.url);
@@ -20,8 +18,6 @@ const lang = getLangFromUrl(Astro.url);
<ExperienceSection lang={lang} client:only="react" />
<SkillsSection lang={lang} client:only="react" />
<ProjectsSection lang={lang} client:only="react" />
<AwardsSection lang={lang} client:only="react" />
<EducationSection lang={lang} client:only="react" />
</main>
<Footer lang={lang} client:only="react" />
</Layout>

View File

@@ -6,8 +6,6 @@ import HeroSection from "@/components/HeroSection";
import ExperienceSection from "@/components/ExperienceSection";
import SkillsSection from "@/components/SkillsSection";
import ProjectsSection from "@/components/ProjectsSection";
import AwardsSection from "@/components/AwardsSection";
import EducationSection from "@/components/EducationSection";
import Footer from "@/components/Footer";
const lang = getLangFromUrl(Astro.url);
@@ -20,8 +18,6 @@ const lang = getLangFromUrl(Astro.url);
<ExperienceSection lang={lang} client:only="react" />
<SkillsSection lang={lang} client:only="react" />
<ProjectsSection lang={lang} client:only="react" />
<AwardsSection lang={lang} client:only="react" />
<EducationSection lang={lang} client:only="react" />
</main>
<Footer lang={lang} client:only="react" />
</Layout>