init
npm create vite@latest ./ -- --template react
npm install xxxx --legacy-peer-deps命令与其说是告诉npm要去干什么,不如说是告诉npm不要去干什么。
legacy的意思:遗产/(软件或硬件)已过时但因使用范围广而难以替代的;而npm install xxxx --legacy-peer-deps命令用于绕过peerDependency里依赖的自动安装;它告诉npm忽略项目中引入的各个依赖模块之间依赖相同但版本不同的问题,以npm v3-v6的方式去继续执行安装操作。
所以其实该命令并没有真的解决冲突,而是忽略了冲突,以“过时”(v3-v6)的方式进行下载操作。
npm i --legacy-peer-deps @react-three/fiber @react-three/drei maath react-tilt three react-vertical-timeline-component @emailjs/browser framer-motion react-router-dom
npm i --legacy-peer-deps -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Poppins", sans-serif;
scroll-behavior: smooth;
color-scheme: dark;
}
.hash-span {
margin-top: -100px;
padding-bottom: 100px;
display: block;
}
.black-gradient {
background: #000000; /* fallback for old browsers */
background: -webkit-linear-gradient(
to right,
#434343,
#000000
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to right,
#434343,
#000000
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}
.violet-gradient {
background: #804dee;
background: linear-gradient(-90deg, #804dee 0%, rgba(60, 51, 80, 0) 100%);
background: -webkit-linear-gradient(
-90deg,
#804dee 0%,
rgba(60, 51, 80, 0) 100%
);
}
.green-pink-gradient {
background: "#00cea8";
background: linear-gradient(90.13deg, #00cea8 1.9%, #bf61ff 97.5%);
background: -webkit-linear-gradient(-90.13deg, #00cea8 1.9%, #bf61ff 97.5%);
}
.orange-text-gradient {
background: #f12711; /* fallback for old browsers */
background: -webkit-linear-gradient(
to top,
#f12711,
#f5af19
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to top,
#f12711,
#f5af19
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.green-text-gradient {
background: #11998e; /* fallback for old browsers */
background: -webkit-linear-gradient(
to top,
#11998e,
#38ef7d
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to top,
#11998e,
#38ef7d
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.blue-text-gradient {
/* background: -webkit-linear-gradient(#eee, #333); */
background: #56ccf2; /* fallback for old browsers */
background: -webkit-linear-gradient(
to top,
#2f80ed,
#56ccf2
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to top,
#2f80ed,
#56ccf2
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.pink-text-gradient {
background: #ec008c; /* fallback for old browsers */
background: -webkit-linear-gradient(
to top,
#ec008c,
#fc6767
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to top,
#ec008c,
#fc6767
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* canvas- styles */
.canvas-loader {
font-size: 10px;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
animation: mulShdSpin 1.1s infinite ease;
transform: translateZ(0);
}
@keyframes mulShdSpin {
0%,
100% {
box-shadow: 0em -2.6em 0em 0em #ffffff,
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.5),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7);
}
12.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7),
1.8em -1.8em 0 0em #ffffff, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5);
}
25% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #ffffff,
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
37.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5),
2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em #ffffff,
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
50% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.5),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #ffffff,
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
62.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5),
0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #ffffff,
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
75% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.5),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #ffffff,
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
}
87.5% {
box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5),
-2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #ffffff;
}
}
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx}"],
mode: "jit",
theme: {
extend: {
colors: {
primary: "#050816",
secondary: "#aaa6c3",
tertiary: "#151030",
"black-100": "#100d25",
"black-200": "#090325",
"white-100": "#f3f3f3",
},
boxShadow: {
card: "0px 35px 120px -15px #211e35",
},
screens: {
xs: "450px",
},
backgroundImage: {
"hero-pattern": "url('/src/assets/herobg.png')",
},
},
},
plugins: [],
};
const styles = {
paddingX: "sm:px-16 px-6",
paddingY: "sm:py-16 py-6",
padding: "sm:px-16 px-6 sm:py-16 py-10",
heroHeadText:
"font-black text-white lg:text-[80px] sm:text-[60px] xs:text-[50px] text-[40px] lg:leading-[98px] mt-2",
heroSubText:
"text-[#dfd9ff] font-medium lg:text-[30px] sm:text-[26px] xs:text-[20px] text-[16px] lg:leading-[40px]",
sectionHeadText:
"text-white font-black md:text-[60px] sm:text-[50px] xs:text-[40px] text-[30px]",
sectionSubText:
"sm:text-[18px] text-[14px] text-secondary uppercase tracking-wider",
};
export { styles };
export const textVariant = (delay) => {
return {
hidden: {
y: -50,
opacity: 0,
},
show: {
y: 0,
opacity: 1,
transition: {
type: "spring",
duration: 1.25,
delay: delay,
},
},
};
};
export const fadeIn = (direction, type, delay, duration) => {
return {
hidden: {
x: direction === "left" ? 100 : direction === "right" ? -100 : 0,
y: direction === "up" ? 100 : direction === "down" ? -100 : 0,
opacity: 0,
},
show: {
x: 0,
y: 0,
opacity: 1,
transition: {
type: type,
delay: delay,
duration: duration,
ease: "easeOut",
},
},
};
};
export const zoomIn = (delay, duration) => {
return {
hidden: {
scale: 0,
opacity: 0,
},
show: {
scale: 1,
opacity: 1,
transition: {
type: "tween",
delay: delay,
duration: duration,
ease: "easeOut",
},
},
};
};
export const slideIn = (direction, type, delay, duration) => {
return {
hidden: {
x: direction === "left" ? "-100%" : direction === "right" ? "100%" : 0,
y: direction === "up" ? "100%" : direction === "down" ? "100%" : 0,
},
show: {
x: 0,
y: 0,
transition: {
type: type,
delay: delay,
duration: duration,
ease: "easeOut",
},
},
};
};
export const staggerContainer = (staggerChildren, delayChildren) => {
return {
hidden: {},
show: {
transition: {
staggerChildren: staggerChildren,
delayChildren: delayChildren || 0,
},
},
};
};
import {
mobile,
backend,
creator,
web,
javascript,
typescript,
html,
css,
reactjs,
redux,
tailwind,
nodejs,
mongodb,
git,
figma,
docker,
meta,
starbucks,
tesla,
shopify,
carrent,
jobit,
tripguide,
threejs,
} from "../assets";
export const navLinks = [
{
id: "about",
title: "About",
},
{
id: "work",
title: "Work",
},
{
id: "contact",
title: "Contact",
},
];
const services = [
{
title: "Web Developer",
icon: web,
},
{
title: "React Native Developer",
icon: mobile,
},
{
title: "Backend Developer",
icon: backend,
},
{
title: "Content Creator",
icon: creator,
},
];
const technologies = [
{
name: "HTML 5",
icon: html,
},
{
name: "CSS 3",
icon: css,
},
{
name: "JavaScript",
icon: javascript,
},
{
name: "TypeScript",
icon: typescript,
},
{
name: "React JS",
icon: reactjs,
},
{
name: "Redux Toolkit",
icon: redux,
},
{
name: "Tailwind CSS",
icon: tailwind,
},
{
name: "Node JS",
icon: nodejs,
},
{
name: "MongoDB",
icon: mongodb,
},
{
name: "Three JS",
icon: threejs,
},
{
name: "git",
icon: git,
},
{
name: "figma",
icon: figma,
},
{
name: "docker",
icon: docker,
},
];
const experiences = [
{
title: "React.js Developer",
company_name: "Starbucks",
icon: starbucks,
iconBg: "#383E56",
date: "March 2020 - April 2021",
points: [
"Developing and maintaining web applications using React.js and other related technologies.",
"Collaborating with cross-functional teams including designers, product managers, and other developers to create high-quality products.",
"Implementing responsive design and ensuring cross-browser compatibility.",
"Participating in code reviews and providing constructive feedback to other developers.",
],
},
{
title: "React Native Developer",
company_name: "Tesla",
icon: tesla,
iconBg: "#E6DEDD",
date: "Jan 2021 - Feb 2022",
points: [
"Developing and maintaining web applications using React.js and other related technologies.",
"Collaborating with cross-functional teams including designers, product managers, and other developers to create high-quality products.",
"Implementing responsive design and ensuring cross-browser compatibility.",
"Participating in code reviews and providing constructive feedback to other developers.",
],
},
{
title: "Web Developer",
company_name: "Shopify",
icon: shopify,
iconBg: "#383E56",
date: "Jan 2022 - Jan 2023",
points: [
"Developing and maintaining web applications using React.js and other related technologies.",
"Collaborating with cross-functional teams including designers, product managers, and other developers to create high-quality products.",
"Implementing responsive design and ensuring cross-browser compatibility.",
"Participating in code reviews and providing constructive feedback to other developers.",
],
},
{
title: "Full stack Developer",
company_name: "Meta",
icon: meta,
iconBg: "#E6DEDD",
date: "Jan 2023 - Present",
points: [
"Developing and maintaining web applications using React.js and other related technologies.",
"Collaborating with cross-functional teams including designers, product managers, and other developers to create high-quality products.",
"Implementing responsive design and ensuring cross-browser compatibility.",
"Participating in code reviews and providing constructive feedback to other developers.",
],
},
];
const testimonials = [
{
testimonial:
"I thought it was impossible to make a website as beautiful as our product, but Rick proved me wrong.",
name: "Sara Lee",
designation: "CFO",
company: "Acme Co",
image: "https://randomuser.me/api/portraits/women/4.jpg",
},
{
testimonial:
"I've never met a web developer who truly cares about their clients' success like Rick does.",
name: "Chris Brown",
designation: "COO",
company: "DEF Corp",
image: "https://randomuser.me/api/portraits/men/5.jpg",
},
{
testimonial:
"After Rick optimized our website, our traffic increased by 50%. We can't thank them enough!",
name: "Lisa Wang",
designation: "CTO",
company: "456 Enterprises",
image: "https://randomuser.me/api/portraits/women/6.jpg",
},
];
const projects = [
{
name: "Car Rent",
description:
"Web-based platform that allows users to search, book, and manage car rentals from various providers, providing a convenient and efficient solution for transportation needs.",
tags: [
{
name: "react",
color: "blue-text-gradient",
},
{
name: "mongodb",
color: "green-text-gradient",
},
{
name: "tailwind",
color: "pink-text-gradient",
},
],
image: carrent,
source_code_link: "https://github.com/",
},
{
name: "Job IT",
description:
"Web application that enables users to search for job openings, view estimated salary ranges for positions, and locate available jobs based on their current location.",
tags: [
{
name: "react",
color: "blue-text-gradient",
},
{
name: "restapi",
color: "green-text-gradient",
},
{
name: "scss",
color: "pink-text-gradient",
},
],
image: jobit,
source_code_link: "https://github.com/",
},
{
name: "Trip Guide",
description:
"A comprehensive travel booking platform that allows users to book flights, hotels, and rental cars, and offers curated recommendations for popular destinations.",
tags: [
{
name: "nextjs",
color: "blue-text-gradient",
},
{
name: "supabase",
color: "green-text-gradient",
},
{
name: "css",
color: "pink-text-gradient",
},
],
image: tripguide,
source_code_link: "https://github.com/",
},
];
export { services, technologies, experiences, testimonials, projects };
import { BrowserRouter } from "react-router-dom";
import {
About,
Experience,
Feedbacks,
Hero,
Navbar,
Tech,
Works,
StarsCanvas,
Contact,
} from "./components";
const App = () => {
return (
<BrowserRouter>
<div className="relative z-0 bg-primary">
<div className="bg-hero-pattern bg-cover bg-no-repeat bg-center">
<Navbar />
<Hero />
</div>
<About />
<Experience />
<Tech />
<Works />
<Feedbacks />
<div className="relative z-0">
<Contact />
<StarsCanvas />
</div>
</div>
</BrowserRouter>
);
};
export default App;
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Malred | Portfolio</title>
</head>
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { styles } from "../styles";
import { navLinks } from "../constants";
import { logo, menu, close } from "../assets";
const Navbar = () => {
// 当前是哪个页面: about/work/contact?
const [active, setActive] = useState("");
// 小屏幕: 右上角menu是否被点击,展开导航条,切换图片
const [toggle, setToggle] = useState(false);
return (
<nav
className={`
${styles.paddingX} w-full flex items-center py-5 fixed top-0 z-20 bg-primary
`}
>
<div className="w-full flex justify-between items-center max-w-7xl mx-auto">
<Link
to={"/"}
className="flex items-center gap-2"
onClick={() => {
// 设置当前在哪个页面
setActive("");
// 滚动
window.scrollTo(0, 0);
}}
>
{/* app.logo.com 生成logo */}
<img src={logo} alt="logo" className="w-9 h-9 object-contain" />
<p className="text-white text-[18px] font-bold cursor-pointer flex">
Malred
<span className="sm:block hidden">| JS Mastery</span>
</p>
</Link>
<ul className="list-none hidden sm:flex flex-row gap-10">
{navLinks.map((link) => (
<li
key={link.id}
className={`${
active === link.title ? "text-white" : "text-secondary"
} hover:text-white text-[18px] font-medium cursor-pointer`}
// 切换页面
onClick={() => setActive(link.title)}
>
<a href={`#${link.id}`}>{link.title}</a>
</li>
))}
</ul>
{/* 如果在小屏幕,就显示一个图标,点击展开页面切换的导航 */}
<div className="sm:hidden flex flex-1 justify-end items-center">
<img
src={toggle ? close : menu}
alt="menu"
className="w-[28px] h-[28px] object-contain cursor-pointer"
onClick={() => setToggle(!toggle)}
/>
<div
className={`${
!toggle ? "hidden" : "flex"
} p-6 black-gradient absolute top-20 right-0 mx-4 my-2 min-w-[140px] z-10 rounded-xl`}
>
<ul className="list-none flex justify-end items-start flex-col gap-4">
{navLinks.map((link) => (
<li
key={link.id}
className={`${
active === link.title ? "text-white" : "text-secondary"
} font-poppins font-medium cursor-pointer text-[16px]`}
// 切换页面
onClick={() => {
setToggle(!toggle);
setActive(link.title);
}}
>
<a href={`#${link.id}`}>{link.title}</a>
</li>
))}
</ul>
</div>
</div>
</div>
</nav>
);
};
export default Navbar;
// 动画
import { motion } from "framer-motion";
import { styles } from "../styles";
import { ComputersCanvas } from "./canvas";
const Hero = () => {
return (
<section className="relative w-full h-screen mx-auto">
<div
className={`${styles.paddingX} absolute inset-0 top-[120px] max-w-7xl mx-auto flex flex-row items-start gap-5`}
>
<div className="flex flex-col justify-center items-center mt-5">
{/* 小圆 */}
<div className="w-5 h-5 rounded-full bg-[#915eff]"></div>
{/* 紫色渐变的线条 */}
<div className="w-1 sm:h-80 h-40 violet-gradient"></div>
</div>
<div>
<h1 className={`${styles.heroHeadText}`}>
Hi I'm <span className="text-[#915eff]">Malred</span>
</h1>
<p className={`${styles.heroSubText} mt-2 text-white-100`}>
I develop 3D visuals, user <br className="sm:block hidden" />{" "}
interfaces and web applications.
</p>
</div>
</div>
{/* 3D模型-电脑 */}
<ComputersCanvas />
<div className="absolute xs:bottom-10 bottom-32 w-full flex justify-center items-center">
<a href="#about">
<div
className="w-[35px] h-[64px] rounded-3xl border-4 border-secondary flex justify-center
items-start p-2"
>
{/* 小球上下滑动的动画 */}
<motion.div
animate={{
y: [0, 24, 0],
}}
transition={{
duration: 1.5,
// 重复到无穷
repeat: Infinity,
repeatType: "loop",
}}
className="w-3 h-3 rounded-full bg-secondary mb-1"
/>
</div>
</a>
</div>
</section>
);
};
export default Hero;
import { Suspense, useEffect, useState } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Preload, useGLTF } from "@react-three/drei";
import CanvasLoader from "../Loader";
const Computers = ({ isMobile }) => {
// 加载模型
const computer = useGLTF("./desktop_pc/scene.gltf");
return (
<mesh>
{/* 半球光 */}
<hemisphereLight intensity={0.15} groundColor="black" />
{/* 点光 */}
<pointLight intensity={1} />
<spotLight
position={[-20, 50, 10]}
angle={0.12}
penumbra={1}
intensity={1}
castShadow
shadow-mapSize={1024}
/>
{/* 加载模型的scene */}
<primitive
object={computer.scene}
scale={isMobile ? 0.7 : 0.75}
position={isMobile ? [0, -3, -2.2] : [0, -3.25, -1.5]}
rotation={[-0.01, -0.2, -0.1]}
/>
</mesh>
);
};
// 画布
const ComputersCanvas = () => {
// 是不是在移动端
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
// 尺寸看是不是符合
const mediaQuery = window.matchMedia("(max-width: 500px)");
// 设置是不是移动端
setIsMobile(mediaQuery.matches);
// 窗口大小改变时,重新判断
const handleMediaQueryChange = (event) => {
setIsMobile(event.matches);
};
// 监听窗口的改变
mediaQuery.addEventListener("change", handleMediaQueryChange);
// 回调
return () => {
mediaQuery.removeEventListener("change", handleMediaQueryChange);
};
}, []);
return (
<Canvas
frameloop="demand"
shadows
// fov 视角
camera={{ position: [20, 3, 5], fov: 25 }}
// 保留视图缓冲区
gl={{ preserveDrawingBuffer: true }}
>
{/* suspense可以等待加载 */}
{/* 加载模型 */}
<Suspense fallback={<CanvasLoader />}>
{/* 轨道控制 */}
<OrbitControls
// 不能缩放
enableZoom={false}
// 只能旋转特定角度
maxPolarAngle={Math.PI / 2}
minPolarAngle={Math.PI / 2}
/>
{/* 3D模型 */}
<Computers isMobile={isMobile} />
</Suspense>
{/* 预加载 */}
<Preload all />
</Canvas>
);
};
export default ComputersCanvas;
import { Html, useProgress } from "@react-three/drei";
const Loader = () => {
// 进度条
const { progress } = useProgress();
return (
<Html>
<span className="canvas-load"></span>
<p
style={{
fontSize: 14,
color: "#f1f1f1",
fontWeight: 800,
marginTop: 40,
}}
>
{/* 显示进度 */}
{progress.toFixed(2)}%
</p>
</Html>
);
};
export default Loader
import React from "react";
// 可倾斜卡片
import Tilt from "react-tilt";
import { motion } from "framer-motion";
import { styles } from "../styles";
import { services } from "../constants";
import { fadeIn, textVariant } from "../utils/motion";
import { SectionWrapper } from "../hoc";
// 小卡片
const ServiceCard = ({ index, title, icon }) => {
return (
<Tilt className="xs:w-[250px] w-full">
<motion.div
variants={fadeIn("right", "spring", 0.5 * index, 0.75)}
className="w-full green-pink-gradient p-[1px] rounded-[20px] shadow-card"
>
<div
options={{
max: 45,
scale: 1,
speed: 450,
}}
className="bg-tertiary rounded-[20px] py-5 px-12 min-h-[280px] flex justify-evenly items-center flex-col"
>
<img src={icon} alt="title" className="w-16 h-16 object-contain" />
<h3 className="text-white text-[20px] font-bold text-center">
{title}
</h3>
</div>
</motion.div>
</Tilt>
);
};
const About = () => {
return (
<>
<motion.div variants={textVariant()}>
<p className={styles.sectionSubText}>Introduction</p>
<h2 className={styles.sectionHeadText}>Overview.</h2>
</motion.div>
{/* 延迟0.1秒,持续1秒 */}
<motion.p
variants={fadeIn("", "", 0.1, 1)}
className="mt-4 text-secondary text-[17px] max-w-3xl leading-[30px]"
>
I'm a skilled software developer with experience in TypeScript and
JavaScript, and expertise in frameworks like React, Node.js, and
Three.js. I'm a quick learner and collaborate closely with clients to
create efficient, scalable, and user-friendly solutions that solve
real-world problems. Let's work together to bring your ideas to life!
</motion.p>
<div className="mt-20 flex flex-wrap gap-10">
{services.map((service, index) => (
<ServiceCard key={service.title} index={index} {...service} />
))}
</div>
</>
);
};
export default SectionWrapper(About, "about");
import { motion } from "framer-motion";
// 时间线组件
import {
VerticalTimeline,
VerticalTimelineElement,
} from "react-vertical-timeline-component";
import "react-vertical-timeline-component/style.min.css";
import { styles } from "../styles";
import { experiences } from "../constants";
import { SectionWrapper } from "../hoc";
import { textVariant } from "../utils/motion";
const ExperienceCard = ({ experience }) => (
<VerticalTimelineElement
contentStyle={{ background: "#1d1836", color: "#fff" }}
contentArrowStyle={{ borderRight: "7px solid #232631" }}
date={experience.date}
iconStyle={{ background: experience.iconBg }}
icon={
<div>
<img
src={experience.icon}
alt={experience.company_name}
className="w-[60%] h-[60%] object-contain"
/>
</div>
}
>
<div>
<h3 className="text-white text-[24px] font-bold">{experience.title}</h3>
<p
className="text-secondary text-[16px] font-semibold"
style={{ margin: 0 }}
>
{experience.company_name}
</p>
</div>
<ul className="mt-5 list-disc ml-5 space-y-2">
{experience.points.map((point, index) => (
<li
key={`experience-point-${index}`}
className="text-white-100 text-[14px] pl-1 tracking-wider"
>
{point}
</li>
))}
</ul>
</VerticalTimelineElement>
);
const Experience = () => {
return (
<>
<motion.div variants={textVariant()}>
<p className={styles.sectionSubText}>What I have done so far</p>
<h2 className={styles.sectionHeadText}>Work Experience.</h2>
</motion.div>
<div className="mt-20 flex flex-col">
<VerticalTimeline>
{experiences.map((experience, index) => (
<ExperienceCard key={index} experience={experience} />
))}
</VerticalTimeline>
</div>
</>
);
};
export default SectionWrapper(Experience, "work");
import React, { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import {
Decal,
Float,
OrbitControls,
Preload,
useTexture,
} from "@react-three/drei";
// 加载进度条
import CanvasLoader from "../Loader";
const Ball = (props) => {
// 纹理
const [decal] = useTexture([props.imgUrl]);
return (
<Float speed={1.75} rotationIntensity={1} floatIntensity={2}>
{/* 环境光 */}
<ambientLight intensity={0.25} />
<directionalLight position={[0, 0, 0.05]} />
<mesh castShadow receiveShadow scale={2.75}>
{/* 20面体 */}
<icosahedronBufferGeometry args={[1, 1]} />
<meshStandardMaterial
color={"#fff8eb"}
polygonOffset
polygonOffsetFactor={-5}
flatShading
/>
<Decal
map={decal}
position={[0, 0, 1]}
rotation={[2 * Math.PI, 0, 6.25]}
flatShading
/>
</mesh>
</Float>
);
};
const BallCanvas = ({ icon }) => {
return (
<Canvas
frameloop="demand"
// 保留视图缓冲区
gl={{ preserveDrawingBuffer: true }}
>
{/* suspense可以等待加载 */}
{/* 加载模型,fallback是没加载完成时执行的回调 */}
<Suspense fallback={<CanvasLoader />}>
{/* 轨道控制 */}
<OrbitControls enableZoom={false} />
{/* 3D模型 */}
<Ball imgUrl={icon} />
</Suspense>
{/* 预加载 */}
<Preload all />
</Canvas>
);
};
export default BallCanvas;
import { BallCanvas } from "./canvas";
import { SectionWrapper } from "../hoc";
import { technologies } from "../constants";
const Tech = () => {
return (
<div className="flex flow-row flex-wrap justify-center gap-10">
{technologies.map((technologie) => (
<div className="w-28 h-28" key={technologie.name}>
{/* 3D模型,小球 */}
<BallCanvas icon={technologie.icon} />
</div>
))}
</div>
);
};
export default SectionWrapper(Tech, "");
// 倾斜卡片
import Tilt from "react-tilt";
import { motion } from "framer-motion";
import { styles } from "../styles";
import { github } from "../assets";
import { SectionWrapper } from "../hoc";
import { projects } from "../constants";
import { fadeIn, textVariant } from "../utils/motion";
const ProjectCard = ({
index,
name,
description,
tags,
image,
source_code_link,
}) => {
return (
// 延迟开始时间->index*0.5,达到一个接一个出现的动画效果
<motion.div variants={fadeIn("up", "spring", index * 0.5, 0.75)}>
<Tilt
options={{ max: 45, scale: 1, speed: 450 }}
className="bg-tertiary p-5 rounded-2xl sm:w-[360px] w-full"
>
<div className="relative w-full h-[230px]">
<img
src={image}
alt={name}
className="w-full h-full object-cover rounded-2xl"
/>
<div className="absolute inset-0 flex justify-end m-3 card-img_hover">
{/* 点击打开连接 */}
<div
onClick={() => window.open(source_code_link, "_blank")}
className="black-gradient w-10 h-10 rounded-full flex justify-center items-center cursor-pointer"
>
{/* 右上角小图标 */}
<img
src={github}
alt="github"
className="w-1/2 h-1/2 object-contain"
/>
</div>
</div>
</div>
{/* 文本内容 */}
<div className="mt-5">
<h3 className="text-white font-bold text-[24px]">{name}</h3>
<p className="mt-2 text-secondary text-[14px]">{description}</p>
</div>
{/* 小标签 */}
<div className="mt-4 flex flex-wrap gap-2">
{tags.map((tag) => (
<p key={tag.name} className={`text-[14px] ${tag.color}`}>
#{tag.name}
</p>
))}
</div>
</Tilt>
</motion.div>
);
};
const Works = () => {
return (
<>
{/* 主副标题 */}
<motion.div variants={textVariant()}>
<p className={styles.sectionSubText}>My work</p>
<h2 className={styles.sectionHeadText}>Projects.</h2>
</motion.div>
<div className="w-full flex">
<motion.p
variants={fadeIn("", "", 0.1, 1)}
className="mt-3 text-secondary text-[17px] max-w-3xl leading-[30px]"
>
Following projects showcases my skills and experience through
real-world examples of my work. Each project is briefly described with
links to code repositories and live demos in it. It reflects my
ability to solve complex problems, work with different technologies,
and manage projects effectively.
</motion.p>
</div>
<div className="mt-20 flex flex-wrap gap-7">
{projects.map((project, index) => (
<ProjectCard key={`project-${index}`} index={index} {...project} />
))}
</div>
</>
);
};
export default SectionWrapper(Works, "");
import { motion } from "framer-motion";
import { styles } from "../styles";
import { SectionWrapper } from "../hoc";
import { fadeIn, textVariant } from "../utils/motion";
import { testimonials } from "../constants";
const FeedbackCard = ({
index,
testimonial,
name,
designation,
company,
image,
}) => {
return (
<motion.div
variants={fadeIn("", "spring", index * 0.5, 0.75)}
className="bg-black-200 p-10 rounded-3xl xs:w-[320px] w-full"
>
<p className="text-white font-black text-[48px]">"</p>
<div className="mt-1">
<p className="text-white tracking-wider text-[18px]">{testimonial}</p>
<div className="mt-7 flex justify-between items-center gap-1">
<div className="flex-1 flex flex-col">
<p className="text-white font-medium text-[16px]">
<span className="blue-text-gradient">@</span> {name}
</p>
<p className="mt-1 text-secondary text-[12px]">
{designation} of {company}
</p>
</div>
{/* 头像 */}
<img
src={image}
alt={`feedback-by-${name}`}
className="w-10 h-10 rounded-full object-cover"
/>
</div>
</div>
</motion.div>
);
};
const Feedbacks = () => {
return (
<div className="mt-12 bg-black-100 rounded-[20px]">
<div
className={`${styles.padding} bg-tertiary rounded-2xl min-h-[300px]`}
>
<motion.div variants={textVariant()}>
<p className={styles.sectionHeadText}>What others say</p>
<h2 className={styles.sectionHeadText}>Testimonails.</h2>
</motion.div>
</div>
<div
// flex-wrap 指定元素多行还是单行显示,wrap表示会多行显示
className={`${styles.paddingX} mt-20 pd-14 flex flex-wrap gap-7`}
>
{testimonials.map((testimonial, index) => (
<FeedbackCard key={testimonial.name} index={index} {...testimonial} />
))}
</div>
</div>
);
};
export default SectionWrapper(Feedbacks, "");
import { useState, useRef } from "react";
import { motion } from "framer-motion";
import emailjs from "@emailjs/browser";
import { styles } from "../styles";
import { EarthCanvas } from "./canvas";
import { SectionWrapper } from "../hoc";
import { slideIn } from "../utils/motion";
const Contact = () => {
// 和form元素关联
const formRef = useRef();
// 表单
const [form, setForm] = useState({
name: "",
email: "",
message: "",
});
const [loading, setLoading] = useState(false);
const handleChange = (e) => {};
const handleSubmit = (e) => {};
return (
<div className="xl:mt-12 xl:flex-row flex-col-reverse flex gap-10 overflow-hidden">
<motion.div
variants={slideIn("left", "tween", 0.2, 1)}
className="flex-[0.75] bg-black-100 p-8 rounded-2xl"
>
<p className={styles.sectionSubText}>Get in touch</p>
<h3 className={styles.sectionHeadText}>Contact.</h3>
<form
ref={formRef}
onSubmit={handleSubmit}
className="mt-12 flex flex-col gap-8"
>
<label className="flex flex-col">
<span className="text-white font-medium mb-4">Your Name</span>
<input
type="text"
name="name"
value={form.name}
onChange={handleChange}
placeholder="What's your name"
className="bg-tertiary py-4 px-6 placeholder:text-secondary
text-white rounded-lg outline-none border-none font-medium"
/>
</label>
<label className="flex flex-col">
<span className="text-white font-medium mb-4">Your Email</span>
<input
type="text"
name="email"
value={form.email}
onChange={handleChange}
placeholder="What's your email"
className="bg-tertiary py-4 px-6 placeholder:text-secondary
text-white rounded-lg outline-none border-none font-medium"
/>
</label>
<label className="flex flex-col">
<span className="text-white font-medium mb-4">Your Message</span>
<textarea
rows="7"
name="message"
value={form.message}
onChange={handleChange}
placeholder="What do you want to say?"
className="bg-tertiary py-4 px-6 placeholder:text-secondary
text-white rounded-lg outline-none border-none font-medium"
/>
</label>
<button
type="submit"
className="bg-tertiary py-3 px-8 outline-none w-fit text-white
font-bold shadow-md shadow-primary rounded-xl"
>
{loading ? "Sending..." : "Send"}
</button>
</form>
</motion.div>
<motion.div
variants={slideIn("right", "tween", 0.2, 1)}
className="xl:flex-1 xl:h-auto md:h-[550px] h-[350px]"
>
{/* 3D模型-地球 */}
<EarthCanvas />
</motion.div>
</div>
);
};
export default SectionWrapper(Contact, "contact");
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Preload, useGLTF } from "@react-three/drei";
import CanvasLoader from "../Loader";
const EarthCanvas = () => {
return (
<Canvas
shadows
frameloop="demand"
gl={{ preserveDrawingBuffer: true }}
camera={{
fov: 45,
near: 0.1,
far: 200,
position: [-4, 3, 6],
}}
>
<Suspense fallback={<CanvasLoader />}>
<OrbitControls
// 自动旋转
autoRotate
enableZoom={false}
maxPolarAngle={Math.PI / 2}
minPolarAngle={Math.PI / 2}
/>
<Earth />
</Suspense>
</Canvas>
);
};
const Earth = () => {
const earth = useGLTF("./planet/scene.gltf");
return (
<primitive scale={2.5} object={earth.scene} position-y={0} rotation-y={0} />
);
};
export default EarthCanvas;
import { useState, useRef, Suspense } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Points, PointMaterial, Preload } from "@react-three/drei";
import * as random from "maath/random/dist/maath-random.esm";
const Stars = (props) => {
const ref = useRef();
// 创建粒子
const sphere = random.inSphere(new Float32Array(5000), { radius: 1.2 });
// 旋转动画
useFrame((state, delta) => {
ref.current.rotation.x -= delta / 10;
ref.current.rotation.y -= delta / 15;
});
return (
<group rotation={[0, 0, Math.PI / 4]}>
<Points ref={ref} positions={sphere} stride={3} frustumCulled {...props}>
<PointMaterial
transparent
color="#f272c8"
size={0.002}
sizeAttenuation={true}
depthWrite={false}
/>
</Points>
</group>
);
};
const StarsCanvas = () => {
return (
<div className="w-full h-auto absolute inset-0 z-[-1]">
<Canvas camera={{ position: [0, 0, 1] }}>
<Suspense fallback={null}>
<Stars />
</Suspense>
<Preload all />
</Canvas>
</div>
);
};
export default StarsCanvas;
const handleChange = (e) => {
const { name, value } = e.target;
// 更新表单
setForm({ ...form, [name]: value });
};
// 发送消息给我们
const handleSubmit = (e) => {
e.preventDefault();
setLoading(true);
emailjs
.send(
"service_5kuryac",
"template_78j9fop",
{
from_name: form.name,
to_name: "Malred",
from_email: form.email,
to_email: "[email protected]",
message: form.message,
},
"F4Njulqs74QkBYH5k"
)
.then(
() => {
setLoading(false);
alert("Thank you. I will get back to you as soon as possible.");
setForm({
name: "",
email: "",
message: "",
});
},
(error) => {
setLoading(false);
console.log(error);
alert("Something went wrong.");
}
);
};