├── .eslintrc.json
├── .gitignore
├── README.md
├── app
├── [locale]
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── icon.ico
├── layout.tsx
└── not-found.tsx
├── components.json
├── components
├── About.tsx
├── Contact.tsx
├── Experience.tsx
├── ExperienceLabel.tsx
├── Footer.tsx
├── Header.tsx
├── Intro.tsx
├── LanguageSwitch.tsx
├── Project.tsx
├── Projects.tsx
├── SectionDivider.tsx
├── SectionHeading.tsx
├── Skills.tsx
├── SubmitBtn.tsx
├── ThemeTwich.tsx
└── WidgetWrapper.tsx
├── context
├── action-section-context.tsx
└── theme-context.tsx
├── i18n.ts
├── lib
├── data.ts
├── hooks.ts
└── utils.ts
├── messages
├── en.json
└── zh.json
├── middleware.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── 2048-game.png
├── breaddit.png
├── bubble.wav
├── d3.png
├── game-hub.png
├── joy-fullstack-resume.pdf
├── knowledge-sharing-platform.png
├── light-off.mp3
├── light-on.mp3
├── profile.png
├── typing-speed.png
└── 前端开发-彭郁洁.pdf
├── tailwind.config.js
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Portfolio Project
2 | Welcome to my portfolio project! This repository features my personal portfolio website, inspired by [ByteGrad](https://www.youtube.com/watch?v=sUKptmUVIBM&t=21888s).
3 |
4 |
5 |
6 |
7 |
8 | ## What I Did
9 | - Bug Fixes: Fixed the scrolling bug from the original project.
10 | - Updated Projects Section: Revamped the "Intro" and "Projects" sections.
11 | - UX Enhancements: Added sound effects, animations, and arrow indicators for a more engaging experience.
12 | - Multilingual Support: Added multiple language options using i18nNext.
13 |
14 | ## Contributions
15 | Contributions are welcome! Fork the repository and submit a pull request.
16 |
--------------------------------------------------------------------------------
/app/[locale]/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/app/[locale]/layout.tsx:
--------------------------------------------------------------------------------
1 | import Header from "@/components/Header"
2 | import "./globals.css"
3 | import { Inter } from "next/font/google"
4 | import ThemeContextProvider from "@/context/theme-context"
5 | import { ActionSectionContextProvider } from "@/context/action-section-context"
6 | import Footer from "@/components/Footer"
7 | import ThemeSwitch from "@/components/ThemeTwich"
8 | // import { usePathname } from "next/navigation"
9 | import LanguageSwitch from "@/components/LanguageSwitch"
10 | import { NextIntlClientProvider, useMessages } from "next-intl"
11 | import WidgetWrapper from "@/components/WidgetWrapper"
12 |
13 | const inter = Inter({ subsets: ["latin"] })
14 |
15 | export default function RootLayout({
16 | children,
17 | params: { locale },
18 | }: {
19 | children: React.ReactNode
20 | params: { locale: string }
21 | }) {
22 | const messages = useMessages()
23 | // const pathname = usePathname()
24 | // const isProjectDetail = pathname.includes("projects")
25 | return (
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {children}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/app/[locale]/page.tsx:
--------------------------------------------------------------------------------
1 | import Intro from "@/components/Intro"
2 | import SectionDivider from "@/components/SectionDivider"
3 | import About from "@/components/About"
4 | import Projects from "@/components/Projects"
5 | import Skills from "@/components/Skills"
6 | import Experience from "@/components/Experience"
7 | import { isMobileDevice } from "@/lib/utils"
8 | // import Contact from "@/components/Contact"
9 |
10 | export const metadata = {
11 | title: "Joy | Personal Portfolio",
12 | description: "Joy is a full-stack developer with 2 years of experience.",
13 | }
14 |
15 | export default function Home() {
16 | const isMobile = isMobileDevice()
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {/* */}
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/app/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/app/icon.ico
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function LocaleLayout({
2 | children,
3 | }: {
4 | children: React.ReactNode
5 | }) {
6 | return children
7 | }
8 |
--------------------------------------------------------------------------------
/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
7 |
8 |
Something went wrong!
9 |
10 |
11 | Try to add "en" in the beginning of the path. For example:
12 | localhost:3000/en
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/[locale]/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/About.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React from "react"
4 | import { motion } from "framer-motion"
5 | import SectionHeading from "./SectionHeading"
6 | import { useSectionInView } from "@/lib/hooks"
7 | import { useLocale, useTranslations } from "next-intl"
8 |
9 | export default function About() {
10 | const { ref } = useSectionInView("About")
11 | const t = useTranslations("AboutSection")
12 | const sectionLan = useTranslations("SectionName")
13 | const activeLocale = useLocale()
14 |
15 | return (
16 |
24 | {sectionLan("about")}
25 | {activeLocale == "zh" ? (
26 | // {t("desc")}
//这样一段话没法分段
27 |
28 |
我是一个热爱学习、思考且富有韧性的人。
29 |
30 | 从2019年到2023年,我在美丽的珞珈山武汉大学度过了四个春夏秋冬。在这段时间里,我开启了前端开发的大门,并在学校的大数据研究院和蔚来公司分别实习了半年。作为一名转码选手,我遇到了许多困难,也曾有过自我怀疑的时刻。但回顾过去,我发现是对自我提升的热爱以及与团队共同奋斗的激情支撑着我坚持下来。
31 |
32 |
33 | 本科毕业后,我来到了苏格兰的海边小镇,在圣安德鲁斯大学度过了一段难忘的时光。在这里,我养成了规律运动和健康饮食的好习惯,并结识了许多志同道合的朋友。我甚至在当地的意大利餐馆当厨师,并在一家酒店担任早餐服务员,丰富了我的人生体验。
34 |
35 |
36 | 适应异国他乡的生活并非易事,但这段经历让我学会了从逆境、冲突、失败甚至积极事件中快速恢复的能力。我坚信,坚韧、专注和自信是我最重要的品质之一。
37 |
38 |
39 |
40 | 目前,我在准备2025年秋招。同时在自己的 小红书 、
41 |
46 | 个人博客
47 |
48 | 和 b站 记录技术和生活。
49 |
50 |
51 | ) : (
52 | <>
53 |
54 | My journey into programming kicked off during my undergrad in
55 |
56 | {" "}
57 | Digital Publishing
58 | at {}
59 |
60 |
64 | Wuhan University.
65 |
66 | {" "}
67 | As a freshman, I got my feet wet with the basics of computer
68 | science, databases, and Python. But what truly sparked my passion
69 | was a course on semantic publishing—there, I used HTML and CSS to
70 | create a webpage dedicated to my favorite figure skater,{" "}
71 |
76 | Yuzuru Hanyu.{" "}
77 |
78 | This experience opened my eyes to the charm of web development———
79 |
80 | literally, you can build anything you envision with code.
81 |
82 |
83 |
84 |
85 | I sharpened my front-end development skills through internships at
86 | Internships at Wuhan University's Big Data Institute and later
87 | at{" "}
88 |
89 |
90 | {" "}
91 | NIO Inc.
92 |
93 | {" "}
94 | Working closely with developers, designers, testers, and product
95 | managers, I loved the buzz of a team pulling together to make our
96 | product better. And there’s nothing quite like the thrill of seeing
97 | my own code being used by thousands—it’s what solidified my decision
98 | to pursue a career in front-end development.
99 |
100 |
101 |
102 | I’m now doing my Masters in
103 | Computing and IT at the{" "}
104 |
105 |
106 | University of St Andrews.{" "}
107 |
108 |
109 | I thrive on programming challenges and enjoy working with teams to
110 | solve complex problems. I specialize in technologies such as
111 | React, Next.js and Vue ,
112 | and have a solid understanding of{" "}
113 |
114 | JavaScript, TypeScript and HTML/CSS
115 |
116 | .{" "}
117 |
118 |
119 |
120 | In my spare time, I enjoy exploring new technologies and building
121 | interesting projects. I also run my social media accounts on
122 | platforms like Bilibili and Xiaohongshu, where I share tech tips and
123 | tricks. And when I’m not at the computer, you’ll find me
124 |
125 | {" "}
126 | cooking up a storm, catching a movie, or keeping fit with regular
127 | workouts.
128 |
129 |
130 | >
131 | )}
132 |
133 | )
134 | }
135 |
--------------------------------------------------------------------------------
/components/Contact.tsx:
--------------------------------------------------------------------------------
1 | // "use client"
2 | // import React from "react"
3 | // import SectionHeading from "./SectionHeading"
4 | // import { motion } from "framer-motion"
5 | // import { useSectionInView } from "@/lib/hooks"
6 | // import toast, { Toaster } from "react-hot-toast"
7 | // import SubmitBtn from "./SubmitBtn"
8 |
9 | // export default function Contact() {
10 | // // const { ref } = useSectionInView("Contact")
11 | // return (
12 | //
29 | // Contact Me
30 | //
31 | // Please contact me directly at{" "}
32 | //
33 | // joyyujiepeng@gmail.com
34 | // {" "}
35 | // or through this form.
36 | //
37 | //
38 | //
74 | //
75 | // )
76 | // }
77 |
--------------------------------------------------------------------------------
/components/Experience.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React from "react"
4 | import {
5 | VerticalTimeline,
6 | VerticalTimelineElement,
7 | } from "react-vertical-timeline-component"
8 | import "react-vertical-timeline-component/style.min.css"
9 | import {
10 | experiencesData,
11 | experiencesDataZn,
12 | headerLanguageMap,
13 | } from "@/lib/data"
14 | import SectionHeading from "./SectionHeading"
15 | import { motion } from "framer-motion"
16 | import { useTheme } from "@/context/theme-context"
17 | import { ExperienceLabel } from "./ExperienceLabel"
18 | import { useLocale } from "next-intl"
19 |
20 | export default function Experience({ isMobile }: { isMobile: boolean }) {
21 | const { theme } = useTheme()
22 | const variants = {
23 | left: {
24 | hidden: { x: -200, opacity: 0 },
25 | visible: { x: 0, opacity: 1, transition: { duration: 0.5 } },
26 | },
27 | right: {
28 | hidden: { x: 200, opacity: 0 },
29 | visible: { x: 0, opacity: 1, transition: { duration: 0.5 } },
30 | },
31 | }
32 |
33 | const activeLocale = useLocale()
34 |
35 | const experienceDataShown =
36 | activeLocale == "zh" ? experiencesDataZn : experiencesData
37 |
38 | return (
39 |
40 |
41 |
42 | {" "}
43 | {activeLocale === "zh"
44 | ? headerLanguageMap["Experiences"]
45 | : "My Experiences"}
46 |
47 | {!isMobile ? (
48 |
49 | {experienceDataShown.map((item, index) => (
50 |
58 | {item.icon}>}
77 | iconStyle={{
78 | background:
79 | theme === "light" ? "white" : "rgba(255, 255, 255, 0.15)",
80 | fontSize: "1.5rem",
81 | }}
82 | >
83 | {item.title}
84 | {item.location}
85 |
86 | {item.description}
87 |
88 |
89 |
90 | ))}
91 |
92 | ) : (
93 |
94 | {experienceDataShown.map((item, index) => (
95 |
99 |
100 | {item.icon}
101 |
102 | {item.date}
103 |
104 |
{item.title}
105 |
{item.location}
106 |
107 | {item.description}
108 |
109 |
110 |
111 | ))}
112 |
113 | )}
114 |
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/components/ExperienceLabel.tsx:
--------------------------------------------------------------------------------
1 | import { useSectionInView } from "@/lib/hooks"
2 |
3 | export function ExperienceLabel() {
4 | const { ref } = useSectionInView("Experiences", 0.1)
5 | return (
6 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | export default function Footer() {
4 | return (
5 |
6 |
7 | © 2024 Yujie(Joy). All rights reserved.
8 |
9 |
10 | About this website: built with
11 | React & Next.js (App Router & Server Actions), TypeScript, Tailwind CSS,
12 | Framer Motion, Vercel hosting.
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { motion } from "framer-motion"
4 | import { links } from "@/lib/data"
5 | import Link from "next/link"
6 | import clsx from "clsx"
7 | import { headerLanguageMap } from "@/lib/data"
8 | import { useActiveSectionContext } from "@/context/action-section-context"
9 | import { useLocale } from "next-intl"
10 |
11 | function Header() {
12 | const { activeSection, setActiveSection, setTimeOfLastClick } =
13 | useActiveSectionContext()
14 | const activeLocale = useLocale()
15 | return (
16 |
17 |
23 |
24 |
25 | {links.map((link, index) => (
26 |
32 | {
42 | setActiveSection(link.name)
43 | setTimeOfLastClick(Date.now())
44 | }}
45 | >
46 | {activeLocale === "zh"
47 | ? headerLanguageMap[link.name]
48 | : link.name}
49 | {link.name === activeSection && (
50 |
55 | )}
56 |
57 |
58 | ))}
59 |
60 |
61 |
62 | )
63 | }
64 |
65 | export default Header
66 |
--------------------------------------------------------------------------------
/components/Intro.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Image from "next/image"
4 | import { motion } from "framer-motion"
5 | import { BsArrowRight, BsLinkedin } from "react-icons/bs"
6 | import { HiDownload } from "react-icons/hi"
7 | import { FaGithubSquare } from "react-icons/fa"
8 | import Link from "next/link"
9 | import { Source_Code_Pro } from "next/font/google"
10 | import { useLocale } from "next-intl"
11 | import { useSectionInView } from "@/lib/hooks"
12 | import { TypeAnimation } from "react-type-animation"
13 | import { useActiveSectionContext } from "@/context/action-section-context"
14 | import { useTranslations } from "next-intl"
15 | import useSound from "use-sound"
16 |
17 | const sourceCodePro = Source_Code_Pro({ subsets: ["latin"], weight: "400" })
18 |
19 | export default function Intro() {
20 | const { ref } = useSectionInView("Home")
21 | const activeLocale = useLocale()
22 | const { setActiveSection, setTimeOfLastClick } = useActiveSectionContext()
23 | const t = useTranslations("IntroSection")
24 | const [playHover] = useSound("/bubble.wav", { volume: 0.5 })
25 |
26 | return (
27 |
32 |
33 |
34 |
39 |
48 |
49 | {
51 | console.log("sound")
52 | playHover()
53 | }}
54 | initial={{ scale: 0 }}
55 | animate={{ scale: 1 }}
56 | whileHover={{ scale: 1.25, rotate: 15 }}
57 | className="absolute text-4xl bottom-0 right-0 hover:rotate-2"
58 | transition={{
59 | type: "spring",
60 | duration: 0.7,
61 | delay: 0.1,
62 | stiffness: 125,
63 | }}
64 | >
65 | 👋
66 |
67 |
68 |
69 |
74 |
75 | {t("hello_im")}
76 |
77 |
83 |
84 | {t("name")}
85 |
86 |
87 |
88 |
91 | I'm a{" "}
92 |
93 |
97 |
108 |
109 |
110 |
111 | {t("short_intro")}
112 | {activeLocale === "en" && (
113 |
114 | My focus is{" "}
115 | React (Next.js) .
116 |
117 | )}
118 |
119 |
120 |
128 | {/* {
131 | setActiveSection("Contact")
132 | setTimeOfLastClick(Date.now())
133 | }}
134 | className="group bg-gray-900 px-4 py-2 text-sm sm:text-lg text-white sm:px-7 sm:py-3 flex items-center gap-2 rounded-full outline-none focus:scale-110 hover:scale-110 hover:bg-gray-950 active:scale-105 transition"
135 | >
136 | Contact me here
137 |
138 | */}
139 |
140 |
150 | {t("download_cv")}
151 |
152 |
153 |
158 |
159 |
160 |
161 |
166 |
167 |
168 |
173 | {t("blog")}
174 |
175 | {/* */}
176 |
177 |
178 |
179 | )
180 | }
181 |
--------------------------------------------------------------------------------
/components/LanguageSwitch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { IoLanguageOutline } from "react-icons/io5"
4 | import { useLocale } from "next-intl"
5 | import { usePathname, useRouter } from "next/navigation"
6 |
7 | export default function LanguageSwitch() {
8 | const localActive = useLocale()
9 | const router = useRouter()
10 | const pathname = usePathname()
11 |
12 | const onChangeLanguage = (e: React.MouseEvent) => {
13 | const nextLocale = localActive === "en" ? "zh" : "en"
14 | const newPath = pathname.replace(/^\/(en|zh)/, `/${nextLocale}/`)
15 | router.replace(newPath, {
16 | scroll: false,
17 | })
18 | }
19 |
20 | return (
21 | <>
22 |
26 | Change Language
27 | {/* */}
28 |
29 |
30 | {" "}
31 | {localActive == "en" ? "EN" : "ZH"}
32 |
33 |
34 | >
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/components/Project.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useRef } from "react"
4 | import { projectsData } from "@/lib/data"
5 | import Image from "next/image"
6 | import { motion, useScroll, useTransform } from "framer-motion"
7 | import { FaGithubSquare } from "react-icons/fa"
8 | import Link from "next/link"
9 | import { FiExternalLink } from "react-icons/fi"
10 | import { useLocale } from "next-intl"
11 |
12 | type ProjectProps = (typeof projectsData)[number]
13 |
14 | export default function Project({
15 | title,
16 | description,
17 | desc_zh,
18 | title_zh,
19 | tags,
20 | imageUrl,
21 | projectUrl,
22 | demoUrl,
23 | }: ProjectProps) {
24 | const ref = useRef(null)
25 | const { scrollYProgress } = useScroll({
26 | target: ref,
27 | offset: ["0 1", "1.33 1"],
28 | })
29 | const scaleProgess = useTransform(scrollYProgress, [0, 1], [0.8, 1])
30 | const opacityProgess = useTransform(scrollYProgress, [0, 1], [0.6, 1])
31 | const activeLocale = useLocale()
32 |
33 | return (
34 |
42 |
43 |
44 |
45 |
46 |
47 | {activeLocale === "zh" ? title_zh : title}
48 |
49 |
50 |
51 |
52 | {" "}
53 |
58 | Code
59 |
60 |
61 |
62 | {demoUrl && (
63 |
68 | Live demo
69 |
70 |
71 | )}
72 |
73 |
74 |
75 |
76 | {activeLocale === "zh" ? desc_zh : description}
77 |
78 |
79 | {tags.map((tag, index) => (
80 |
84 | {tag}
85 |
86 | ))}
87 |
88 |
89 |
90 |
107 |
108 |
109 | )
110 | }
111 |
--------------------------------------------------------------------------------
/components/Projects.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React from "react"
4 | import { headerLanguageMap, projectsData } from "@/lib/data"
5 | import { useSectionInView } from "@/lib/hooks"
6 | import SectionHeading from "./SectionHeading"
7 | import Project from "./Project"
8 | import { useLocale } from "next-intl"
9 | import Link from "next/link"
10 | import { FaAngleRight } from "react-icons/fa6"
11 |
12 | export default function Projects() {
13 | const { ref } = useSectionInView("Projects", 0.1)
14 | const activeLocale = useLocale()
15 |
16 | return (
17 |
18 |
19 | {" "}
20 | {activeLocale === "zh"
21 | ? headerLanguageMap["Projects"]
22 | : "Featured Projects"}
23 |
24 |
25 | {projectsData.map((project, index) => (
26 |
27 |
28 |
29 | ))}
30 |
31 |
36 | View All Projects
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/components/SectionDivider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { motion } from "framer-motion"
4 | import Link from "next/link"
5 | import { IoIosArrowDown } from "react-icons/io"
6 |
7 | export default function SectionDivider() {
8 | return (
9 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/components/SectionHeading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type SectionHeadingProps = {
4 | children: React.ReactNode;
5 | };
6 |
7 | export default function SectionHeading({ children }: SectionHeadingProps) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/components/Skills.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React from "react"
4 | import { headerLanguageMap, skillsData } from "@/lib/data"
5 | import { useSectionInView } from "@/lib/hooks"
6 | import { motion } from "framer-motion"
7 | import SectionHeading from "./SectionHeading"
8 | import { useLocale } from "next-intl"
9 |
10 | const fadeInAnimationVariants = {
11 | initial: {
12 | opacity: 0,
13 | y: 100,
14 | },
15 | animate: (index: number) => ({
16 | opacity: 1,
17 | y: 0,
18 | transition: {
19 | delay: 0.05 * index,
20 | },
21 | }),
22 | }
23 |
24 | export default function Skills() {
25 | const { ref } = useSectionInView("Skills")
26 | const activeLocale = useLocale()
27 | return (
28 |
33 |
34 | {" "}
35 | {activeLocale === "zh"
36 | ? headerLanguageMap["Skills"]
37 | : "My Skills"}
38 |
39 |
40 | {skillsData.map((skill, index) => (
41 |
52 | {skill}
53 |
54 | ))}
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/components/SubmitBtn.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { useFormStatus } from "react-dom"
3 | import { FaPaperPlane } from "react-icons/fa"
4 |
5 | export default function SubmitBtn({ className }: { className?: string }) {
6 | const { pending } = useFormStatus()
7 |
8 | return (
9 |
14 | {pending ? (
15 |
16 | ) : (
17 | <>
18 | Submit{" "}
19 | {" "}
20 | >
21 | )}
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/components/ThemeTwich.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "@/context/theme-context"
4 | import React from "react"
5 | import { BsMoon, BsSun } from "react-icons/bs"
6 | export default function ThemeSwitch() {
7 |
8 |
9 | const { theme, toggleTheme } = useTheme()
10 | return (
11 |
15 | change dark mode
16 | {theme === "light" ? : }
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/components/WidgetWrapper.tsx:
--------------------------------------------------------------------------------
1 | export default function WidgetWrapper({
2 | children,
3 | }: {
4 | children: React.ReactNode
5 | }) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/context/action-section-context.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, {
4 | useState,
5 | createContext,
6 | SetStateAction,
7 | Dispatch,
8 | useContext,
9 | } from "react"
10 | import { links } from "@/lib/data"
11 |
12 | export type SectionName = (typeof links)[number]["name"]
13 | type ActionSectionContextProviderProps = {
14 | children: React.ReactNode
15 | }
16 |
17 | type ActionSectionContextType = {
18 | activeSection: SectionName
19 | setActiveSection: Dispatch<
20 | SetStateAction<"Home" | "About" | "Projects" | "Skills" | "Experiences">
21 | >
22 | timeOfLastClick: number
23 | setTimeOfLastClick: React.Dispatch>
24 | }
25 |
26 | const ActionSectionContext = createContext(
27 | null
28 | )
29 |
30 | export function ActionSectionContextProvider({
31 | children,
32 | }: ActionSectionContextProviderProps) {
33 | const [activeSection, setActiveSection] = useState("Home")
34 | const [timeOfLastClick, setTimeOfLastClick] = useState(0) // we need to keep track of this to disable the observer temporarily when user clicks on a link
35 |
36 | return (
37 |
45 | {children}
46 |
47 | )
48 | }
49 |
50 | export function useActiveSectionContext() {
51 | const context = useContext(ActionSectionContext)
52 | if (!context) {
53 | throw new Error(
54 | "useActiveSectionContext must be used within a ActionSectionContextProvider"
55 | )
56 | }
57 | return context
58 | }
59 |
--------------------------------------------------------------------------------
/context/theme-context.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import React, { createContext, useContext, useEffect, useState } from "react"
4 | import useSound from "use-sound"
5 |
6 | type ThemeContextType = {
7 | theme: string
8 | toggleTheme: () => void
9 | }
10 |
11 | type ThemeContextProviderProp = {
12 | children: React.ReactNode
13 | }
14 |
15 | const ThemeContext = createContext(null)
16 |
17 | const ThemeContextProvider = ({ children }: ThemeContextProviderProp) => {
18 | const [theme, setTheme] = useState("light")
19 | const [playLight] = useSound("/light-on.mp3", { volume: 0.5 })
20 | const [playDark] = useSound("/light-off.mp3", { volume: 0.5 })
21 |
22 | const toggleTheme = () => {
23 | if (theme === "light") {
24 | setTheme("dark")
25 | playDark()
26 | window.localStorage.setItem("theme", "dark")
27 | document.documentElement.classList.add("dark")
28 | } else {
29 | setTheme("light")
30 | playLight()
31 | window.localStorage.setItem("theme", "light")
32 | document.documentElement.classList.remove("dark")
33 | }
34 | }
35 |
36 | useEffect(() => {
37 | const localTheme = window.localStorage.getItem("theme")
38 | if (localTheme) {
39 | setTheme(localTheme)
40 | if (localTheme === "dark") {
41 | document.documentElement.classList.add("dark")
42 | } else {
43 | document.documentElement.classList.remove("dark")
44 | }
45 | } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
46 | setTheme("dark")
47 | document.documentElement.classList.add("dark")
48 | }
49 | }, [theme])
50 |
51 | return (
52 |
53 | {children}
54 |
55 | )
56 | }
57 |
58 | export function useTheme() {
59 | const context = useContext(ThemeContext)
60 | if (!context) {
61 | throw new Error("useTheme must be used within a ThemeContextProvider")
62 | }
63 | return context
64 | }
65 |
66 | export default ThemeContextProvider
67 |
--------------------------------------------------------------------------------
/i18n.ts:
--------------------------------------------------------------------------------
1 | import { notFound } from 'next/navigation';
2 | import { getRequestConfig } from 'next-intl/server';
3 |
4 | // Can be imported from a shared config
5 | const locales = ['en', 'zh'];
6 |
7 | export default getRequestConfig(async ({ locale }) => {
8 | // Validate that the incoming `locale` parameter is valid
9 | if (!locales.includes(locale as any)) notFound();
10 |
11 | return {
12 | messages: (await import(`./messages/${locale}.json`)).default
13 | };
14 | });
--------------------------------------------------------------------------------
/lib/data.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FaReact } from "react-icons/fa";
3 | import { FaVuejs } from "react-icons/fa";
4 | import { LuGraduationCap } from "react-icons/lu";
5 | import knowledgeSharingPlatformImage from '@/public/knowledge-sharing-platform.png';
6 | import breadditImage from "@/public/breaddit.png";
7 | import gameHubImage from "@/public/game-hub.png";
8 | import typingSpeedImage from "@/public/typing-speed.png";
9 | import visualizationImage from "@/public/d3.png";
10 |
11 | export const links = [
12 | {
13 | name: "Home",
14 | hash: "#home",
15 | },
16 | {
17 | name: "About",
18 | hash: "#about",
19 | },
20 | {
21 | name: "Projects",
22 | hash: "#projects",
23 | },
24 | {
25 | name: "Skills",
26 | hash: "#skills",
27 | },
28 | {
29 | name: "Experiences",
30 | hash: "#experience",
31 | },
32 | // {
33 | // name: "Contact",
34 | // hash: "#contact",
35 | // },
36 | ] as const;
37 |
38 |
39 | export const headerLanguageMap = {
40 | Home: '首页',
41 | About: '关于我',
42 | Projects: '我的项目',
43 | Skills: '我的技能',
44 | Experiences: '我的经历',
45 | }
46 |
47 | export const experiencesData = [
48 | {
49 | title: "MSc in Computing and IT",
50 | location: "University of St Andrews, UK",
51 | description:
52 | "Achieved a Master's degree in Computing and IT, acquiring in-depth knowledge in areas such as Human Computer Interaction, Computer Communication Systems, and Information Security. Developed strong capabilities in computational thinking, user-centred design, and data visualisation, preparing for effective application in technology-driven environments",
53 | icon: React.createElement(LuGraduationCap),
54 | date: "2023 Sep - 2024 May",
55 | },
56 | {
57 | title: "Frontend Intern",
58 | location: React.createElement("span", {},
59 | React.createElement("a", {
60 | href: "https://www.nio.com/",
61 | style: { textDecoration: 'underline' },
62 | target: "_blank"
63 | }, "NIO Inc."),
64 | " Wuhan, China"
65 | ),
66 | description:
67 | "Developed NIO's third-generation station list and detail pages using Vue3, TypeScript, and Baidu Maps API. Implemented role-based access control for the Task Wizard page, enhancing system security. Collaborated effectively within a Jira-managed environment, utilizing Jenkins for deployment processes.",
68 | icon: React.createElement(FaVuejs),
69 | date: "2022 Aug - 2022 Dec",
70 | },
71 | {
72 | title: "Frontend Assistant",
73 | location: "Wuhan University | Wuhan, China",
74 | description:
75 | "Developed and maintained Finknow, a Financial Knowledge Graph Query and Analysis Platform using umi (React framework) and Ant Design Pro. Utilized graphin, a React toolkit for graph analysis based on G6, to develop an Equity Network Penetration Graph, enhancing data visualization capabilities.",
76 | icon: React.createElement(FaReact),
77 | date: "2022 May - 2022 July",
78 | },
79 | {
80 | title: "BA in Digital Publishing",
81 | location: "Wuhan University, China",
82 | description:
83 | "Graduated with a Bachelor of Arts in Digital Publishing, securing a GPA of 3.81/4.0. Gained foundational knowledge in digital media and publishing technologies.",
84 | icon: React.createElement(LuGraduationCap),
85 | date: "2019 Sep - 2023 Jun",
86 | },
87 |
88 | ]
89 |
90 | export const experiencesDataZn = [
91 | {
92 | "title": "计算机与信息技术硕士",
93 | "location": "英国圣安德鲁斯大学",
94 | "description": "在人机交互、计算机通信系统和信息安全等领域深入学习。培养了计算思维、以用户为中心的设计和数据可视化方面的强大能力,预计以一等学位毕业(GPA17/20)。",
95 | icon: React.createElement(LuGraduationCap),
96 | "date": "2023年9月 - 2024年12月"
97 | },
98 | {
99 | "title": "前端实习生",
100 | "location": "蔚来汽车(中国武汉)",
101 | "description": `参与团队多个平台的迭代开发,独立完成内部告警平台的值班表页面开发。协助开发蔚来第三代场站列表与详情页,集成百度地图 API 实现定位与选点功能。优化任务魔棒页面,新增管理员角色,重构权限管理逻辑,用头像列表展示管理员,并整合飞书 API 实现消息发送。自定义 Element-UI 级联选择组件,解决全选德国 2000+ 城市时浏览器卡死问题,通过懒加载优化显著提升性能。遵循 Agile/Scrum 开发流程,进行两周一迭代,通过 Jira 跟进项目需求,设计技术方案,并与测试、后端、产品及 设计团队密切对接。`,
102 | "icon": React.createElement(FaVuejs),
103 | "date": "2022年8月 - 2022年12月"
104 | },
105 | {
106 | "title": "前端开发",
107 | "location": "武汉大学大数据研究院",
108 | "description": "使用umi(React框架)和Ant Design Pro开发和维护Finknow,一个金融知识图谱查询和分析平台。利用基于G6的React图分析工具包graphin开发了股权网络穿透图,增强了数据可视化功能。",
109 | "icon": React.createElement(FaReact),
110 | "date": "2022年5月 - 2022年8月"
111 | },
112 | {
113 | "title": "数字出版学士",
114 | "location": "武汉大学",
115 | "description": "以3.81/4.0的GPA毕业,获得数字出版学士学位,掌握了数字媒体和出版技术的基础知识。",
116 | "icon": React.createElement(LuGraduationCap),
117 | "date": "2019年9月 - 2023年6月"
118 | }
119 | ]
120 |
121 |
122 | export type ProjectTags = typeof projectsData[number]["tags"];
123 |
124 | export const projectsData = [
125 | {
126 | "title": "Ethical Digital Nation Collaborative Platform",
127 | "title_zh": "数字道德国家协作平台",
128 | "description":
129 | "A collaborative platform enhancing cooperation among Scottish higher education institutions in digital ethics.",
130 | "desc_zh": "旨在促进苏格兰高校在数字道德领域合作的协作平台。该平台集成用户访谈、工作坊、OAuth登录、最新数字博客RSS feed显示、完备的事件管理系统(包含高级评分与评论功能)以及注重可访问性的响应式设计。",
131 | "tags": ["React", "Next.js 14", "TypeScript", "TailwindCSS", "Convex", "Clerk"],
132 | "imageUrl": knowledgeSharingPlatformImage,
133 | "projectUrl": "https://github.com/Codefreyy/Ethical-Digital-Nation",
134 | "demoUrl": "https://yujie-ethical-digital-nation.netlify.app/"
135 | },
136 | {
137 | title: "Typing Speed",
138 | title_zh: '打字测验',
139 | description:
140 | "A comprehensive typing speed test application that tracks your overall typing performance. It provides detailed statistics, including total words typed, errors made, and accuracy rate, allowing users to monitor their progress and improve their typing efficiency.",
141 | desc_zh: "一个打字速度测试应用。敲击即开始打字,计时结束后将显示总敲击单词数、正确率、错误率等。该应用UI简洁现代,支持Dark Mode切换。",
142 | tags: ["React", "TypeScript", "Tailwind", 'Framer Motion'],
143 | imageUrl: typingSpeedImage,
144 | projectUrl: 'https://github.com/Codefreyy/typing-speed-game',
145 | demoUrl: 'https://joy-typing-speed.netlify.app/',
146 | },
147 | {
148 | title: "Breddit",
149 | title_zh: "社交新闻论坛",
150 | description:
151 | `A modern full-stack Reddit clone with infinite scrolling, secure NextAuth Google authentication, and a custom feed for authenticated users. It uses Upstash Redis for caching and React-Query for efficient, responsive data fetching with optimistic updates.
152 | `,
153 | desc_zh: "一个现代且简洁的Reddit克隆项目,使用Next.js、TypeScript和Tailwind CSS构建。项目功能包括无限滚动动态加载帖子、NextAuth与Google认证、为认证用户提供自定义Feed、高级缓存、乐观更新、React-Query数据获取、美观的帖子编辑器、图片上传和链接预览、以及完整的评论功能。",
154 | tags: ["Next.js", "TypeScript", "Upstash", "React-Query", "TailwindCSS"],
155 | imageUrl: breadditImage,
156 | projectUrl: 'https://github.com/Codefreyy/Breddit',
157 | demoUrl: 'https://joy-breddit.vercel.app/',
158 | },
159 | {
160 | title: "Global Wealth Spectrum Visualisation",
161 | title_zh: '世界财富可视化光谱',
162 | description: "This interactive visualization explores how tax policies influence wealth, how industries impact fortunes differently by gender, and how these effects vary across continents. Dive into our data to uncover the complex layers of global wealth.",
163 | desc_zh: "交互式可视化图表探讨了税收政策如何影响财富、不同行业对不同性别的财富的影响以及这些影响在各大洲之间的差异。深入了解我们的数据,揭示全球财富的复杂层次。",
164 | tags: ["d3.js", "HTML", "CSS", "Vanilla JavaScript"],
165 | imageUrl: visualizationImage,
166 | projectUrl: 'https://github.com/Codefreyy/d3-evolution-visualisation',
167 | demoUrl: 'https://global-wealth-spectrum.netlify.app/',
168 | },
169 |
170 |
171 | ]
172 |
173 | export const skillsData = [
174 | "HTML",
175 | "CSS",
176 | "JavaScript",
177 | "TypeScript",
178 | "React",
179 | "Next",
180 | "Vue2",
181 | "Vue3",
182 | "Node",
183 | "Express",
184 | "Git",
185 | "Github",
186 | "Tailwind",
187 | "Chakra UI",
188 | "Boostrap",
189 | "Prisma",
190 | "MongoDB",
191 | "Framer Motion",
192 | "d3",
193 | "UI/UX"
194 | ]
195 |
--------------------------------------------------------------------------------
/lib/hooks.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect } from "react";
4 | import { useInView } from "react-intersection-observer";
5 | import { SectionName } from '@/context/action-section-context'
6 | import { useActiveSectionContext } from '@/context/action-section-context';
7 |
8 | export function useSectionInView(sectionName: SectionName, threshold = 0.75) {
9 | const { ref, inView } = useInView({
10 | threshold,
11 | });
12 |
13 | const { setActiveSection, timeOfLastClick } = useActiveSectionContext();
14 | useEffect(() => {
15 | console.log(sectionName, inView, Date.now() - timeOfLastClick);
16 | if (inView && Date.now() - timeOfLastClick > 1000) {
17 | setActiveSection(sectionName);
18 | }
19 | }, [inView, setActiveSection, timeOfLastClick, sectionName]);
20 |
21 | return {
22 | ref,
23 | };
24 | }
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { headers } from 'next/headers';
2 | import { UAParser } from 'ua-parser-js';
3 |
4 | export const isMobileDevice = () => {
5 | if (typeof process === 'undefined') {
6 | throw new Error('[Server method] you are importing a server-only module outside of server');
7 | }
8 |
9 | const { get } = headers();
10 | const ua = get('user-agent');
11 |
12 | const device = new UAParser(ua || '').getDevice();
13 |
14 | const isMobile = device.type === 'mobile';
15 |
16 | return isMobile;
17 | };
--------------------------------------------------------------------------------
/messages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "IntroSection": {
3 | "hello_im": "Hi, my name is",
4 | "name": "Joy Peng",
5 | "short_intro": "I enjoy building things for the web.",
6 | "download_cv": "Download CV",
7 | "blog": "Blog"
8 | },
9 | "SectionName": {
10 | "about": "About me",
11 | "experience": "Experience",
12 | "projects": "Projects",
13 | "skills": "Skills"
14 | },
15 | "AboutSection": {
16 | "desc": "During my undergraduate studies at the School of Information Management at Wuhan University, I learned computer fundamentals, database principles and practice, and Python courses, while delving into front-end development in my junior year. After interning at the Big Data Research Institute of Wuhan University and NIO, I became even more certain that this was the career path I wanted to pursue. Thus, I decided to pursue a master's degree in Computing and IT at the University of St Andrews in the UK. I thoroughly enjoy programming challenges, and working with my team to develop projects and solve complex problems brings me great satisfaction. I'm proficient in using technologies like React, Vue3, and Next.js, and have a deep understanding of TypeScript and HTML/CSS 💻.In my free time, I like learning new skills and working on fun projects 🛠️. I also run my own accounts on social media platforms like Bilibili and Xiaohongshu to share technical knowledge and learning resources 🌐. When I'm not at my computer, I enjoy cooking, watching movies, and working out 🍳🎥💪. Regular exercise, sleep, and diet are my secrets to staying energized 🌟."
17 | }
18 | }
--------------------------------------------------------------------------------
/messages/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "IntroSection": {
3 | "hello_im": "你好,我是",
4 | "name": "彭郁洁",
5 | "short_intro": "我享受搭建项目的乐趣!",
6 | "download_cv": "下载简历",
7 | "blog": "个人博客"
8 | },
9 | "SectionName": {
10 | "about": "",
11 | "experience": "我的经历",
12 | "projects": "我的项目",
13 | "skills": "我的技能"
14 | },
15 | "AboutSection": {
16 | "desc": "我是一个喜欢学习、思考,有韧性的人。2019到2023年,我在珞珈山上的武汉大学度过了4个春夏秋冬。期间,我打开了前端开发的大门。先后在学校的大数据研究院和蔚来实习了半年。作为转码选手,我遇到了不少困难,也有过自我怀疑的时候。但回头看是什么让我坚持下来?我想是我享受自我提升,享受在团队中和大家打成一片,并肩作战的感觉。本科毕业后,我来到苏格兰的海边小镇🏴,在安静美丽的圣安德鲁斯大学度过了我人生中难忘的一段时光。 在这里,我养成了规律运动和健康饮食的好习惯,也认识了许多志同道合的好朋友。我甚至在这儿的意大利餐馆当厨师,在Hotel做早餐服务员。当然,适应异国他乡的生活并不容易,但我锻炼了从逆境、矛盾、失败甚至是积极事件中恢复常态的能力。坚持、专注和自信,是我认为最重要的品质之一。"
17 | }
18 | }
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import createMiddleware from 'next-intl/middleware';
2 |
3 | export default createMiddleware({
4 | // A list of all locales that are supported
5 | locales: ['en', 'zh'],
6 |
7 | // Used when no locale matches
8 | defaultLocale: 'en'
9 | });
10 |
11 | export const config = {
12 | // Match only internationalized pathnames
13 | matcher: ['/', '/(zh|en)/:path*']
14 | };
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const createNextIntlPlugin = require("next-intl/plugin")
2 |
3 | const withNextIntl = createNextIntlPlugin()
4 |
5 | /** @type {import('next').NextConfig} */
6 | const nextConfig = {
7 | images: {
8 | remotePatterns: [
9 | {
10 | protocol: "https",
11 | hostname: "images.unsplash.com",
12 | port: "",
13 | },
14 | ],
15 | },
16 | }
17 |
18 | module.exports = withNextIntl(nextConfig)
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "portfolio",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@types/node": "20.12.7",
13 | "@types/react": "18.2.79",
14 | "@types/react-dom": "18.2.25",
15 | "@types/ua-parser-js": "^0.7.39",
16 | "autoprefixer": "10.4.19",
17 | "clsx": "^2.1.1",
18 | "eslint": "8.57.0",
19 | "eslint-config-next": "14.2.3",
20 | "framer-motion": "^11.1.7",
21 | "next": "14.2.3",
22 | "next-intl": "^3.12.2",
23 | "postcss": "8.4.38",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "react-hot-toast": "^2.4.1",
27 | "react-icons": "^5.1.0",
28 | "react-intersection-observer": "^9.10.0",
29 | "react-type-animation": "^3.2.0",
30 | "react-vertical-timeline-component": "^3.6.0",
31 | "resend": "^0.16.0",
32 | "tailwindcss": "3.4.3",
33 | "typescript": "5.4.5",
34 | "ua-parser-js": "^1.0.37",
35 | "use-sound": "^4.0.1"
36 | },
37 | "devDependencies": {
38 | "@types/react-vertical-timeline-component": "^3.3.6"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/2048-game.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/2048-game.png
--------------------------------------------------------------------------------
/public/breaddit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/breaddit.png
--------------------------------------------------------------------------------
/public/bubble.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/bubble.wav
--------------------------------------------------------------------------------
/public/d3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/d3.png
--------------------------------------------------------------------------------
/public/game-hub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/game-hub.png
--------------------------------------------------------------------------------
/public/joy-fullstack-resume.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/joy-fullstack-resume.pdf
--------------------------------------------------------------------------------
/public/knowledge-sharing-platform.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/knowledge-sharing-platform.png
--------------------------------------------------------------------------------
/public/light-off.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/light-off.mp3
--------------------------------------------------------------------------------
/public/light-on.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/light-on.mp3
--------------------------------------------------------------------------------
/public/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/profile.png
--------------------------------------------------------------------------------
/public/typing-speed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/typing-speed.png
--------------------------------------------------------------------------------
/public/前端开发-彭郁洁.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Codefreyy/joy-personal-portfolio/ae72269ef0bd85721d2dc7f1973a17098a538fd5/public/前端开发-彭郁洁.pdf
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
5 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
12 | "gradient-conic":
13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
14 | },
15 | colors: {
16 | pink: "#d22d68",
17 | yellow: "#eed062",
18 | },
19 | },
20 | },
21 | darkMode: "class",
22 | plugins: [],
23 | }
24 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | './@/**/*.{ts,tsx}',
11 |
12 | ],
13 | prefix: "",
14 | theme: {
15 | container: {
16 | center: true,
17 | padding: "2rem",
18 | screens: {
19 | "2xl": "1400px",
20 | },
21 | },
22 | extend: {
23 | colors: {
24 | border: "hsl(var(--border))",
25 | input: "hsl(var(--input))",
26 | ring: "hsl(var(--ring))",
27 | background: "hsl(var(--background))",
28 | foreground: "hsl(var(--foreground))",
29 | primary: {
30 | DEFAULT: "hsl(var(--primary))",
31 | foreground: "hsl(var(--primary-foreground))",
32 | },
33 | secondary: {
34 | DEFAULT: "hsl(var(--secondary))",
35 | foreground: "hsl(var(--secondary-foreground))",
36 | },
37 | destructive: {
38 | DEFAULT: "hsl(var(--destructive))",
39 | foreground: "hsl(var(--destructive-foreground))",
40 | },
41 | muted: {
42 | DEFAULT: "hsl(var(--muted))",
43 | foreground: "hsl(var(--muted-foreground))",
44 | },
45 | accent: {
46 | DEFAULT: "hsl(var(--accent))",
47 | foreground: "hsl(var(--accent-foreground))",
48 | },
49 | popover: {
50 | DEFAULT: "hsl(var(--popover))",
51 | foreground: "hsl(var(--popover-foreground))",
52 | },
53 | card: {
54 | DEFAULT: "hsl(var(--card))",
55 | foreground: "hsl(var(--card-foreground))",
56 | },
57 | },
58 | borderRadius: {
59 | lg: "var(--radius)",
60 | md: "calc(var(--radius) - 2px)",
61 | sm: "calc(var(--radius) - 4px)",
62 | },
63 | keyframes: {
64 | "accordion-down": {
65 | from: { height: "0" },
66 | to: { height: "var(--radix-accordion-content-height)" },
67 | },
68 | "accordion-up": {
69 | from: { height: "var(--radix-accordion-content-height)" },
70 | to: { height: "0" },
71 | },
72 | },
73 | animation: {
74 | "accordion-down": "accordion-down 0.2s ease-out",
75 | "accordion-up": "accordion-up 0.2s ease-out",
76 | },
77 | },
78 | },
79 | plugins: [require("tailwindcss-animate")],
80 | } satisfies Config
81 |
82 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------