Initial commit
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.npmrc
|
||||
|
||||
backup/
|
||||
.env
|
||||
.vercel
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Moncy Yohannan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/Logo/image.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Phong Pham Portfolio - QA & Automation Engineer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4266
package-lock.json
generated
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "moncy-portfolio",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gsap/react": "^2.1.1",
|
||||
"@react-three/cannon": "^6.6.0",
|
||||
"@react-three/drei": "^9.120.4",
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"@react-three/postprocessing": "^2.16.3",
|
||||
"@react-three/rapier": "^1.5.0",
|
||||
"@types/three": "^0.168.0",
|
||||
"@vercel/analytics": "^1.4.1",
|
||||
"gsap": "^3.13.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-icons": "^5.3.0",
|
||||
"three": "^0.168.0",
|
||||
"three-stdlib": "^2.33.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"globals": "^15.9.0",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
BIN
public/Logo/image.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
34
public/draco/draco_decoder.js
Normal file
BIN
public/draco/draco_decoder.wasm
Normal file
BIN
public/images/express.webp
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
public/images/javascript.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/images/mongo.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/images/mysql.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/images/next.webp
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
public/images/next1.webp
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
public/images/next2.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/nextBL.webp
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/images/node.webp
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
public/images/node2.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/placeholder.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/images/react.webp
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
public/images/react2.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/typescript.webp
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
1
public/models/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.glb filter=lfs diff=lfs merge=lfs -text
|
||||
BIN
public/models/char_enviorment.hdr
Normal file
BIN
public/models/character.enc
Normal file
3
public/models/character.glb
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c287a4d10bbbbc4c779f7b0a68b666b530ef9754142843aa048b4d5976c0d052
|
||||
size 1537316
|
||||
16
public/models/encrypt.cjs
Normal file
@@ -0,0 +1,16 @@
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
|
||||
const encryptFile = (inputFile, outputFile, password) => {
|
||||
const key = crypto.createHash("sha256").update(password).digest();
|
||||
const iv = crypto.randomBytes(16);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||
const input = fs.createReadStream(inputFile);
|
||||
const output = fs.createWriteStream(outputFile);
|
||||
|
||||
output.write(iv);
|
||||
input.pipe(cipher).pipe(output);
|
||||
};
|
||||
|
||||
encryptFile("character.glb", "character.enc", "Character3D#@");
|
||||
BIN
public/project_img/CSED.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
public/project_img/Laca City.png
Normal file
|
After Width: | Height: | Size: 1016 KiB |
BIN
public/project_img/NASA Space app.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
public/project_img/UVIita.png
Normal file
|
After Width: | Height: | Size: 900 KiB |
BIN
public/project_img/VSIG .png
Normal file
|
After Width: | Height: | Size: 533 KiB |
28
src/App.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.section-container {
|
||||
width: 1300px;
|
||||
}
|
||||
.title,
|
||||
.para {
|
||||
font-kerning: none;
|
||||
-webkit-text-rendering: optimizeSpeed;
|
||||
text-rendering: optimizeSpeed;
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
@media only screen and (max-width: 1600px) {
|
||||
.section-container {
|
||||
width: 1200px;
|
||||
max-width: calc(100% - 160px);
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.section-container {
|
||||
width: 900px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 900px) {
|
||||
.section-container {
|
||||
width: 500px;
|
||||
max-width: var(--cWidth);
|
||||
}
|
||||
}
|
||||
24
src/App.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import "./App.css";
|
||||
|
||||
const CharacterModel = lazy(() => import("./components/Character"));
|
||||
const MainContainer = lazy(() => import("./components/MainContainer"));
|
||||
import { LoadingProvider } from "./context/LoadingProvider";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<LoadingProvider>
|
||||
<Suspense>
|
||||
<MainContainer>
|
||||
<Suspense>
|
||||
<CharacterModel />
|
||||
</Suspense>
|
||||
</MainContainer>
|
||||
</Suspense>
|
||||
</LoadingProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
18
src/components/About.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import "./styles/About.css";
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<div className="about-section" id="about">
|
||||
<div className="about-me">
|
||||
<h3 className="title">About Me</h3>
|
||||
<p className="para">
|
||||
QA & Automation Engineer specializing in manual, API, and database testing.
|
||||
Experienced in improving release stability and building automated workflows.
|
||||
Physics student with a CS minor, focused on creating robust testing frameworks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
65
src/components/Career.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import "./styles/Career.css";
|
||||
|
||||
const Career = () => {
|
||||
return (
|
||||
<div className="career-section section-container">
|
||||
<div className="career-container">
|
||||
<h2>
|
||||
My career <span>&</span>
|
||||
<br /> experience
|
||||
</h2>
|
||||
<div className="career-info">
|
||||
<div className="career-timeline">
|
||||
<div className="career-dot"></div>
|
||||
</div>
|
||||
<div className="career-info-box">
|
||||
<div className="career-info-in">
|
||||
<div className="career-role">
|
||||
<h4>Software Testing Engineer</h4>
|
||||
<h5>LACA CITY COMPANY</h5>
|
||||
</div>
|
||||
<h3>2024</h3>
|
||||
</div>
|
||||
<p>
|
||||
Performed API and database testing to ensure system stability for a parking management
|
||||
platform serving 5,000+ users. Detected 40+ functional/UI issues prior to release,
|
||||
contributing to a 20% improvement in release stability. Collaborated with developers
|
||||
using Jira, Postman, and Chrome DevTools.
|
||||
</p>
|
||||
</div>
|
||||
<div className="career-info-box">
|
||||
<div className="career-info-in">
|
||||
<div className="career-role">
|
||||
<h4>Head of Software Development</h4>
|
||||
<h5>VIETNAM SPACE INDUSTRY JSC</h5>
|
||||
</div>
|
||||
<h3>2025</h3>
|
||||
</div>
|
||||
<p>
|
||||
Designed and executed 150+ test cases in TestRail achieving >98% release stability.
|
||||
Tracked 70+ bug tickets on Jira, reducing issue resolution time by 25%. Proactively
|
||||
adopted automation workflows using n8n, Postman, and Swagger to improve test efficiency
|
||||
and coverage.
|
||||
</p>
|
||||
</div>
|
||||
<div className="career-info-box">
|
||||
<div className="career-info-in">
|
||||
<div className="career-role">
|
||||
<h4>QA & Automation Engineer</h4>
|
||||
<h5>CURRENT ROLE</h5>
|
||||
</div>
|
||||
<h3>NOW</h3>
|
||||
</div>
|
||||
<p>
|
||||
Continuing to grow as an Automation Testing specialist, designing robust frameworks
|
||||
and accelerating release cycles. Leading quality control initiatives for NASA International
|
||||
Space Apps Challenge and Conference Earth Sciences events with 500+ participants.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Career;
|
||||
160
src/components/Character/Scene.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import setCharacter from "./utils/character";
|
||||
import setLighting from "./utils/lighting";
|
||||
import { useLoading } from "../../context/LoadingProvider";
|
||||
import handleResize from "./utils/resizeUtils";
|
||||
import {
|
||||
handleMouseMove,
|
||||
handleTouchEnd,
|
||||
handleHeadRotation,
|
||||
handleTouchMove,
|
||||
} from "./utils/mouseUtils";
|
||||
import setAnimations from "./utils/animationUtils";
|
||||
import { setProgress } from "../Loading";
|
||||
|
||||
const Scene = () => {
|
||||
const canvasDiv = useRef<HTMLDivElement | null>(null);
|
||||
const hoverDivRef = useRef<HTMLDivElement>(null);
|
||||
const sceneRef = useRef(new THREE.Scene());
|
||||
const { setLoading } = useLoading();
|
||||
|
||||
const [character, setChar] = useState<THREE.Object3D | null>(null);
|
||||
useEffect(() => {
|
||||
if (canvasDiv.current) {
|
||||
let rect = canvasDiv.current.getBoundingClientRect();
|
||||
let container = { width: rect.width, height: rect.height };
|
||||
const aspect = container.width / container.height;
|
||||
const scene = sceneRef.current;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
});
|
||||
renderer.setSize(container.width, container.height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1;
|
||||
canvasDiv.current.appendChild(renderer.domElement);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(14.5, aspect, 0.1, 1000);
|
||||
camera.position.z = 10;
|
||||
camera.position.set(0, 13.1, 24.7);
|
||||
camera.zoom = 1.1;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
let headBone: THREE.Object3D | null = null;
|
||||
let screenLight: any | null = null;
|
||||
let mixer: THREE.AnimationMixer;
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
|
||||
const light = setLighting(scene);
|
||||
let progress = setProgress((value) => setLoading(value));
|
||||
const { loadCharacter } = setCharacter(renderer, scene, camera);
|
||||
|
||||
loadCharacter().then((gltf) => {
|
||||
if (gltf) {
|
||||
const animations = setAnimations(gltf);
|
||||
hoverDivRef.current && animations.hover(gltf, hoverDivRef.current);
|
||||
mixer = animations.mixer;
|
||||
let character = gltf.scene;
|
||||
setChar(character);
|
||||
scene.add(character);
|
||||
headBone = character.getObjectByName("spine006") || null;
|
||||
screenLight = character.getObjectByName("screenlight") || null;
|
||||
progress.loaded().then(() => {
|
||||
setTimeout(() => {
|
||||
light.turnOnLights();
|
||||
animations.startIntro();
|
||||
}, 2500);
|
||||
});
|
||||
window.addEventListener("resize", () =>
|
||||
handleResize(renderer, camera, canvasDiv, character)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let mouse = { x: 0, y: 0 },
|
||||
interpolation = { x: 0.1, y: 0.2 };
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
handleMouseMove(event, (x, y) => (mouse = { x, y }));
|
||||
};
|
||||
let debounce: number | undefined;
|
||||
const onTouchStart = (event: TouchEvent) => {
|
||||
const element = event.target as HTMLElement;
|
||||
debounce = setTimeout(() => {
|
||||
element?.addEventListener("touchmove", (e: TouchEvent) =>
|
||||
handleTouchMove(e, (x, y) => (mouse = { x, y }))
|
||||
);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
handleTouchEnd((x, y, interpolationX, interpolationY) => {
|
||||
mouse = { x, y };
|
||||
interpolation = { x: interpolationX, y: interpolationY };
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", (event) => {
|
||||
onMouseMove(event);
|
||||
});
|
||||
const landingDiv = document.getElementById("landingDiv");
|
||||
if (landingDiv) {
|
||||
landingDiv.addEventListener("touchstart", onTouchStart);
|
||||
landingDiv.addEventListener("touchend", onTouchEnd);
|
||||
}
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
if (headBone) {
|
||||
handleHeadRotation(
|
||||
headBone,
|
||||
mouse.x,
|
||||
mouse.y,
|
||||
interpolation.x,
|
||||
interpolation.y,
|
||||
THREE.MathUtils.lerp
|
||||
);
|
||||
light.setPointLight(screenLight);
|
||||
}
|
||||
const delta = clock.getDelta();
|
||||
if (mixer) {
|
||||
mixer.update(delta);
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
animate();
|
||||
return () => {
|
||||
clearTimeout(debounce);
|
||||
scene.clear();
|
||||
renderer.dispose();
|
||||
window.removeEventListener("resize", () =>
|
||||
handleResize(renderer, camera, canvasDiv, character!)
|
||||
);
|
||||
if (canvasDiv.current) {
|
||||
canvasDiv.current.removeChild(renderer.domElement);
|
||||
}
|
||||
if (landingDiv) {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
landingDiv.removeEventListener("touchstart", onTouchStart);
|
||||
landingDiv.removeEventListener("touchend", onTouchEnd);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="character-container">
|
||||
<div className="character-model" ref={canvasDiv}>
|
||||
<div className="character-rim"></div>
|
||||
<div className="character-hover" ref={hoverDivRef}></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scene;
|
||||
0
src/components/Character/exports.ts
Normal file
7
src/components/Character/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import Scene from "./Scene";
|
||||
|
||||
const CharacterModel = () => {
|
||||
return <Scene />;
|
||||
};
|
||||
|
||||
export default CharacterModel;
|
||||
118
src/components/Character/utils/animationUtils.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import * as THREE from "three";
|
||||
import { GLTF } from "three-stdlib";
|
||||
import { eyebrowBoneNames, typingBoneNames } from "../../../data/boneData";
|
||||
|
||||
const setAnimations = (gltf: GLTF) => {
|
||||
let character = gltf.scene;
|
||||
let mixer = new THREE.AnimationMixer(character);
|
||||
if (gltf.animations) {
|
||||
const introClip = gltf.animations.find(
|
||||
(clip) => clip.name === "introAnimation"
|
||||
);
|
||||
const introAction = mixer.clipAction(introClip!);
|
||||
introAction.setLoop(THREE.LoopOnce, 1);
|
||||
introAction.clampWhenFinished = true;
|
||||
introAction.play();
|
||||
const clipNames = ["key1", "key2", "key5", "key6"];
|
||||
clipNames.forEach((name) => {
|
||||
const clip = THREE.AnimationClip.findByName(gltf.animations, name);
|
||||
if (clip) {
|
||||
const action = mixer?.clipAction(clip);
|
||||
action!.play();
|
||||
action!.timeScale = 1.2;
|
||||
} else {
|
||||
console.error(`Animation "${name}" not found`);
|
||||
}
|
||||
});
|
||||
let typingAction: THREE.AnimationAction | null = null;
|
||||
typingAction = createBoneAction(gltf, mixer, "typing", typingBoneNames);
|
||||
if (typingAction) {
|
||||
typingAction.enabled = true;
|
||||
typingAction.play();
|
||||
typingAction.timeScale = 1.2;
|
||||
}
|
||||
}
|
||||
function startIntro() {
|
||||
const introClip = gltf.animations.find(
|
||||
(clip) => clip.name === "introAnimation"
|
||||
);
|
||||
const introAction = mixer.clipAction(introClip!);
|
||||
introAction.clampWhenFinished = true;
|
||||
introAction.reset().play();
|
||||
setTimeout(() => {
|
||||
const blink = gltf.animations.find((clip) => clip.name === "Blink");
|
||||
mixer.clipAction(blink!).play().fadeIn(0.5);
|
||||
}, 2500);
|
||||
}
|
||||
function hover(gltf: GLTF, hoverDiv: HTMLDivElement) {
|
||||
let eyeBrowUpAction = createBoneAction(
|
||||
gltf,
|
||||
mixer,
|
||||
"browup",
|
||||
eyebrowBoneNames
|
||||
);
|
||||
let isHovering = false;
|
||||
if (eyeBrowUpAction) {
|
||||
eyeBrowUpAction.setLoop(THREE.LoopOnce, 1);
|
||||
eyeBrowUpAction.clampWhenFinished = true;
|
||||
eyeBrowUpAction.enabled = true;
|
||||
}
|
||||
const onHoverFace = () => {
|
||||
if (eyeBrowUpAction && !isHovering) {
|
||||
isHovering = true;
|
||||
eyeBrowUpAction.reset();
|
||||
eyeBrowUpAction.enabled = true;
|
||||
eyeBrowUpAction.setEffectiveWeight(4);
|
||||
eyeBrowUpAction.fadeIn(0.5).play();
|
||||
}
|
||||
};
|
||||
const onLeaveFace = () => {
|
||||
if (eyeBrowUpAction && isHovering) {
|
||||
isHovering = false;
|
||||
eyeBrowUpAction.fadeOut(0.6);
|
||||
}
|
||||
};
|
||||
if (!hoverDiv) return;
|
||||
hoverDiv.addEventListener("mouseenter", onHoverFace);
|
||||
hoverDiv.addEventListener("mouseleave", onLeaveFace);
|
||||
return () => {
|
||||
hoverDiv.removeEventListener("mouseenter", onHoverFace);
|
||||
hoverDiv.removeEventListener("mouseleave", onLeaveFace);
|
||||
};
|
||||
}
|
||||
return { mixer, startIntro, hover };
|
||||
};
|
||||
|
||||
const createBoneAction = (
|
||||
gltf: GLTF,
|
||||
mixer: THREE.AnimationMixer,
|
||||
clip: string,
|
||||
boneNames: string[]
|
||||
): THREE.AnimationAction | null => {
|
||||
const AnimationClip = THREE.AnimationClip.findByName(gltf.animations, clip);
|
||||
if (!AnimationClip) {
|
||||
console.error(`Animation "${clip}" not found in GLTF file.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const filteredClip = filterAnimationTracks(AnimationClip, boneNames);
|
||||
|
||||
return mixer.clipAction(filteredClip);
|
||||
};
|
||||
|
||||
const filterAnimationTracks = (
|
||||
clip: THREE.AnimationClip,
|
||||
boneNames: string[]
|
||||
): THREE.AnimationClip => {
|
||||
const filteredTracks = clip.tracks.filter((track) =>
|
||||
boneNames.some((boneName) => track.name.includes(boneName))
|
||||
);
|
||||
|
||||
return new THREE.AnimationClip(
|
||||
clip.name + "_filtered",
|
||||
clip.duration,
|
||||
filteredTracks
|
||||
);
|
||||
};
|
||||
|
||||
export default setAnimations;
|
||||
62
src/components/Character/utils/character.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as THREE from "three";
|
||||
import { DRACOLoader, GLTF, GLTFLoader } from "three-stdlib";
|
||||
import { setCharTimeline, setAllTimeline } from "../../utils/GsapScroll";
|
||||
import { decryptFile } from "./decrypt";
|
||||
|
||||
const setCharacter = (
|
||||
renderer: THREE.WebGLRenderer,
|
||||
scene: THREE.Scene,
|
||||
camera: THREE.PerspectiveCamera
|
||||
) => {
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath("/draco/");
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
const loadCharacter = () => {
|
||||
return new Promise<GLTF | null>(async (resolve, reject) => {
|
||||
try {
|
||||
const encryptedBlob = await decryptFile(
|
||||
"/models/character.enc",
|
||||
"Character3D#@"
|
||||
);
|
||||
const blobUrl = URL.createObjectURL(new Blob([encryptedBlob]));
|
||||
|
||||
let character: THREE.Object3D;
|
||||
loader.load(
|
||||
blobUrl,
|
||||
async (gltf) => {
|
||||
character = gltf.scene;
|
||||
await renderer.compileAsync(character, camera, scene);
|
||||
character.traverse((child: any) => {
|
||||
if (child.isMesh) {
|
||||
const mesh = child as THREE.Mesh;
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
mesh.frustumCulled = true;
|
||||
}
|
||||
});
|
||||
resolve(gltf);
|
||||
setCharTimeline(character, camera);
|
||||
setAllTimeline();
|
||||
character!.getObjectByName("footR")!.position.y = 3.36;
|
||||
character!.getObjectByName("footL")!.position.y = 3.36;
|
||||
dracoLoader.dispose();
|
||||
},
|
||||
undefined,
|
||||
(error) => {
|
||||
console.error("Error loading GLTF model:", error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return { loadCharacter };
|
||||
};
|
||||
|
||||
export default setCharacter;
|
||||
23
src/components/Character/utils/decrypt.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
async function generateAESKey(password: string): Promise<CryptoKey> {
|
||||
const passwordBuffer = new TextEncoder().encode(password);
|
||||
const hashedPassword = await crypto.subtle.digest("SHA-256", passwordBuffer);
|
||||
return crypto.subtle.importKey(
|
||||
"raw",
|
||||
hashedPassword.slice(0, 32),
|
||||
{ name: "AES-CBC" },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
|
||||
export const decryptFile = async (
|
||||
url: string,
|
||||
password: string
|
||||
): Promise<ArrayBuffer> => {
|
||||
const response = await fetch(url);
|
||||
const encryptedData = await response.arrayBuffer();
|
||||
const iv = new Uint8Array(encryptedData.slice(0, 16));
|
||||
const data = encryptedData.slice(16);
|
||||
const key = await generateAESKey(password);
|
||||
return crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, data);
|
||||
};
|
||||
61
src/components/Character/utils/lighting.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as THREE from "three";
|
||||
import { RGBELoader } from "three-stdlib";
|
||||
import { gsap } from "gsap";
|
||||
|
||||
const setLighting = (scene: THREE.Scene) => {
|
||||
const directionalLight = new THREE.DirectionalLight(0xc7a9ff, 0);
|
||||
directionalLight.intensity = 0;
|
||||
directionalLight.position.set(-0.47, -0.32, -1);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.shadow.mapSize.width = 1024;
|
||||
directionalLight.shadow.mapSize.height = 1024;
|
||||
directionalLight.shadow.camera.near = 0.5;
|
||||
directionalLight.shadow.camera.far = 50;
|
||||
scene.add(directionalLight);
|
||||
|
||||
const pointLight = new THREE.PointLight(0xc2a4ff, 0, 100, 3);
|
||||
pointLight.position.set(3, 12, 4);
|
||||
pointLight.castShadow = true;
|
||||
scene.add(pointLight);
|
||||
|
||||
new RGBELoader()
|
||||
.setPath("/models/")
|
||||
.load("char_enviorment.hdr", function (texture) {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
scene.environment = texture;
|
||||
scene.environmentIntensity = 0;
|
||||
scene.environmentRotation.set(5.76, 85.85, 1);
|
||||
});
|
||||
|
||||
function setPointLight(screenLight: any) {
|
||||
if (screenLight.material.opacity > 0.9) {
|
||||
pointLight.intensity = screenLight.material.emissiveIntensity * 20;
|
||||
} else {
|
||||
pointLight.intensity = 0;
|
||||
}
|
||||
}
|
||||
const duration = 2;
|
||||
const ease = "power2.inOut";
|
||||
function turnOnLights() {
|
||||
gsap.to(scene, {
|
||||
environmentIntensity: 0.64,
|
||||
duration: duration,
|
||||
ease: ease,
|
||||
});
|
||||
gsap.to(directionalLight, {
|
||||
intensity: 1,
|
||||
duration: duration,
|
||||
ease: ease,
|
||||
});
|
||||
gsap.to(".character-rim", {
|
||||
y: "55%",
|
||||
opacity: 1,
|
||||
delay: 0.2,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
|
||||
return { setPointLight, turnOnLights };
|
||||
};
|
||||
|
||||
export default setLighting;
|
||||
82
src/components/Character/utils/mouseUtils.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
export const handleMouseMove = (
|
||||
event: MouseEvent,
|
||||
setMousePosition: (x: number, y: number) => void
|
||||
) => {
|
||||
const mouseX = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
const mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
setMousePosition(mouseX, mouseY);
|
||||
};
|
||||
|
||||
export const handleTouchMove = (
|
||||
event: TouchEvent,
|
||||
setMousePosition: (x: number, y: number) => void
|
||||
) => {
|
||||
const mouseX = (event.touches[0].clientX / window.innerWidth) * 2 - 1;
|
||||
const mouseY = -(event.touches[0].clientY / window.innerHeight) * 2 + 1;
|
||||
setMousePosition(mouseX, mouseY);
|
||||
};
|
||||
|
||||
export const handleTouchEnd = (
|
||||
setMousePosition: (
|
||||
x: number,
|
||||
y: number,
|
||||
interpolationX: number,
|
||||
interpolationY: number
|
||||
) => void
|
||||
) => {
|
||||
setTimeout(() => {
|
||||
setMousePosition(0, 0, 0.03, 0.03);
|
||||
setTimeout(() => {
|
||||
setMousePosition(0, 0, 0.1, 0.2);
|
||||
}, 1000);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
export const handleHeadRotation = (
|
||||
headBone: THREE.Object3D,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
interpolationX: number,
|
||||
interpolationY: number,
|
||||
lerp: (x: number, y: number, t: number) => number
|
||||
) => {
|
||||
if (!headBone) return;
|
||||
if (window.scrollY < 200) {
|
||||
const maxRotation = Math.PI / 6;
|
||||
headBone.rotation.y = lerp(
|
||||
headBone.rotation.y,
|
||||
mouseX * maxRotation,
|
||||
interpolationY
|
||||
);
|
||||
let minRotationX = -0.3;
|
||||
let maxRotationX = 0.4;
|
||||
if (mouseY > minRotationX) {
|
||||
if (mouseY < maxRotationX) {
|
||||
headBone.rotation.x = lerp(
|
||||
headBone.rotation.x,
|
||||
-mouseY - 0.5 * maxRotation,
|
||||
interpolationX
|
||||
);
|
||||
} else {
|
||||
headBone.rotation.x = lerp(
|
||||
headBone.rotation.x,
|
||||
-maxRotation - 0.5 * maxRotation,
|
||||
interpolationX
|
||||
);
|
||||
}
|
||||
} else {
|
||||
headBone.rotation.x = lerp(
|
||||
headBone.rotation.x,
|
||||
-minRotationX - 0.5 * maxRotation,
|
||||
interpolationX
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (window.innerWidth > 1024) {
|
||||
headBone.rotation.x = lerp(headBone.rotation.x, -0.4, 0.03);
|
||||
headBone.rotation.y = lerp(headBone.rotation.y, -0.3, 0.03);
|
||||
}
|
||||
}
|
||||
};
|
||||
26
src/components/Character/utils/resizeUtils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as THREE from "three";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import { setCharTimeline, setAllTimeline } from "../../utils/GsapScroll";
|
||||
|
||||
export default function handleResize(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
canvasDiv: React.RefObject<HTMLDivElement>,
|
||||
character: THREE.Object3D
|
||||
) {
|
||||
if (!canvasDiv.current) return;
|
||||
let canvas3d = canvasDiv.current.getBoundingClientRect();
|
||||
const width = canvas3d.width;
|
||||
const height = canvas3d.height;
|
||||
renderer.setSize(width, height);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
const workTrigger = ScrollTrigger.getById("work");
|
||||
ScrollTrigger.getAll().forEach((trigger) => {
|
||||
if (trigger != workTrigger) {
|
||||
trigger.kill();
|
||||
}
|
||||
});
|
||||
setCharTimeline(character, camera);
|
||||
setAllTimeline();
|
||||
}
|
||||
65
src/components/Contact.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { MdArrowOutward, MdCopyright } from "react-icons/md";
|
||||
import "./styles/Contact.css";
|
||||
|
||||
const Contact = () => {
|
||||
return (
|
||||
<div className="contact-section section-container" id="contact">
|
||||
<div className="contact-container">
|
||||
<h3>Contact</h3>
|
||||
<div className="contact-flex">
|
||||
<div className="contact-box">
|
||||
<h4>Email</h4>
|
||||
<p>
|
||||
<a href="mailto:phongpt.working@gmail.com" data-cursor="disable">
|
||||
phongpt.working@gmail.com
|
||||
</a>
|
||||
</p>
|
||||
<h4>Phone</h4>
|
||||
<p>
|
||||
<a href="tel:+84867524445" data-cursor="disable">
|
||||
+84 86 752 4445
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="contact-box">
|
||||
<h4>Social</h4>
|
||||
<a
|
||||
href="https://gitea.phongprojects.id.vn/phongpham"
|
||||
target="_blank"
|
||||
data-cursor="disable"
|
||||
className="contact-social"
|
||||
>
|
||||
Gitea <MdArrowOutward />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/in/phong-pham"
|
||||
target="_blank"
|
||||
data-cursor="disable"
|
||||
className="contact-social"
|
||||
>
|
||||
Linkedin <MdArrowOutward />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/phongpham"
|
||||
target="_blank"
|
||||
data-cursor="disable"
|
||||
className="contact-social"
|
||||
>
|
||||
Github <MdArrowOutward />
|
||||
</a>
|
||||
</div>
|
||||
<div className="contact-box">
|
||||
<h2>
|
||||
Designed and Developed <br /> by <span>Phong Pham</span>
|
||||
</h2>
|
||||
<h5>
|
||||
<MdCopyright /> 2025
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
||||
54
src/components/Cursor.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import "./styles/Cursor.css";
|
||||
import gsap from "gsap";
|
||||
|
||||
const Cursor = () => {
|
||||
const cursorRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
let hover = false;
|
||||
const cursor = cursorRef.current!;
|
||||
const mousePos = { x: 0, y: 0 };
|
||||
const cursorPos = { x: 0, y: 0 };
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
mousePos.x = e.clientX;
|
||||
mousePos.y = e.clientY;
|
||||
});
|
||||
requestAnimationFrame(function loop() {
|
||||
if (!hover) {
|
||||
const delay = 6;
|
||||
cursorPos.x += (mousePos.x - cursorPos.x) / delay;
|
||||
cursorPos.y += (mousePos.y - cursorPos.y) / delay;
|
||||
gsap.to(cursor, { x: cursorPos.x, y: cursorPos.y, duration: 0.1 });
|
||||
// cursor.style.transform = `translate(${cursorPos.x}px, ${cursorPos.y}px)`;
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
});
|
||||
document.querySelectorAll("[data-cursor]").forEach((item) => {
|
||||
const element = item as HTMLElement;
|
||||
element.addEventListener("mouseover", (e: MouseEvent) => {
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
if (element.dataset.cursor === "icons") {
|
||||
cursor.classList.add("cursor-icons");
|
||||
|
||||
gsap.to(cursor, { x: rect.left, y: rect.top, duration: 0.1 });
|
||||
// cursor.style.transform = `translate(${rect.left}px,${rect.top}px)`;
|
||||
cursor.style.setProperty("--cursorH", `${rect.height}px`);
|
||||
hover = true;
|
||||
}
|
||||
if (element.dataset.cursor === "disable") {
|
||||
cursor.classList.add("cursor-disable");
|
||||
}
|
||||
});
|
||||
element.addEventListener("mouseout", () => {
|
||||
cursor.classList.remove("cursor-disable", "cursor-icons");
|
||||
hover = false;
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <div className="cursor-main" ref={cursorRef}></div>;
|
||||
};
|
||||
|
||||
export default Cursor;
|
||||
13
src/components/HoverLinks.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import "./styles/style.css";
|
||||
|
||||
const HoverLinks = ({ text, cursor }: { text: string; cursor?: boolean }) => {
|
||||
return (
|
||||
<div className="hover-link" data-cursor={!cursor && `disable`}>
|
||||
<div className="hover-in">
|
||||
{text} <div>{text}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HoverLinks;
|
||||
35
src/components/Landing.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import "./styles/Landing.css";
|
||||
|
||||
const Landing = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<>
|
||||
<div className="landing-section" id="landingDiv">
|
||||
<div className="landing-container">
|
||||
<div className="landing-intro">
|
||||
<h2>Hello! I'm</h2>
|
||||
<h1>
|
||||
PHONG
|
||||
<br />
|
||||
<span>PHAM</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="landing-info">
|
||||
<h3>QA & Automation</h3>
|
||||
<h2 className="landing-info-h2">
|
||||
<div className="landing-h2-1">Engineer</div>
|
||||
<div className="landing-h2-2">Tester</div>
|
||||
</h2>
|
||||
<h2>
|
||||
<div className="landing-h2-info">Tester</div>
|
||||
<div className="landing-h2-info-1">Engineer</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Landing;
|
||||
135
src/components/Loading.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import "./styles/Loading.css";
|
||||
import { useLoading } from "../context/LoadingProvider";
|
||||
|
||||
import Marquee from "react-fast-marquee";
|
||||
|
||||
const Loading = ({ percent }: { percent: number }) => {
|
||||
const { setIsLoading } = useLoading();
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [clicked, setClicked] = useState(false);
|
||||
|
||||
if (percent >= 100) {
|
||||
setTimeout(() => {
|
||||
setLoaded(true);
|
||||
setTimeout(() => {
|
||||
setIsLoaded(true);
|
||||
}, 1000);
|
||||
}, 600);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
import("./utils/initialFX").then((module) => {
|
||||
if (isLoaded) {
|
||||
setClicked(true);
|
||||
setTimeout(() => {
|
||||
if (module.initialFX) {
|
||||
module.initialFX();
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, 900);
|
||||
}
|
||||
});
|
||||
}, [isLoaded]);
|
||||
|
||||
function handleMouseMove(e: React.MouseEvent<HTMLElement>) {
|
||||
const { currentTarget: target } = e;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
target.style.setProperty("--mouse-x", `${x}px`);
|
||||
target.style.setProperty("--mouse-y", `${y}px`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="loading-header">
|
||||
<a href="/#" className="loader-title" data-cursor="disable">
|
||||
Logo
|
||||
</a>
|
||||
<div className={`loaderGame ${clicked && "loader-out"}`}>
|
||||
<div className="loaderGame-container">
|
||||
<div className="loaderGame-in">
|
||||
{[...Array(27)].map((_, index) => (
|
||||
<div className="loaderGame-line" key={index}></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="loaderGame-ball"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="loading-screen">
|
||||
<div className="loading-marquee">
|
||||
<Marquee>
|
||||
<span> A Creative Developer</span> <span>A Creative Designer</span>
|
||||
<span> A Creative Developer</span> <span>A Creative Designer</span>
|
||||
</Marquee>
|
||||
</div>
|
||||
<div
|
||||
className={`loading-wrap ${clicked && "loading-clicked"}`}
|
||||
onMouseMove={(e) => handleMouseMove(e)}
|
||||
>
|
||||
<div className="loading-hover"></div>
|
||||
<div className={`loading-button ${loaded && "loading-complete"}`}>
|
||||
<div className="loading-container">
|
||||
<div className="loading-content">
|
||||
<div className="loading-content-in">
|
||||
Loading <span>{percent}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="loading-box"></div>
|
||||
</div>
|
||||
<div className="loading-content2">
|
||||
<span>Welcome</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
||||
export const setProgress = (setLoading: (value: number) => void) => {
|
||||
let percent: number = 0;
|
||||
|
||||
let interval = setInterval(() => {
|
||||
if (percent <= 50) {
|
||||
let rand = Math.round(Math.random() * 5);
|
||||
percent = percent + rand;
|
||||
setLoading(percent);
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
interval = setInterval(() => {
|
||||
percent = percent + Math.round(Math.random());
|
||||
setLoading(percent);
|
||||
if (percent > 91) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
function clear() {
|
||||
clearInterval(interval);
|
||||
setLoading(100);
|
||||
}
|
||||
|
||||
function loaded() {
|
||||
return new Promise<number>((resolve) => {
|
||||
clearInterval(interval);
|
||||
interval = setInterval(() => {
|
||||
if (percent < 100) {
|
||||
percent++;
|
||||
setLoading(percent);
|
||||
} else {
|
||||
resolve(percent);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 2);
|
||||
});
|
||||
}
|
||||
return { loaded, percent, clear };
|
||||
};
|
||||
59
src/components/MainContainer.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { lazy, PropsWithChildren, Suspense, useEffect, useState } from "react";
|
||||
import About from "./About";
|
||||
import Career from "./Career";
|
||||
import Contact from "./Contact";
|
||||
import Cursor from "./Cursor";
|
||||
import Landing from "./Landing";
|
||||
import Navbar from "./Navbar";
|
||||
import SocialIcons from "./SocialIcons";
|
||||
import WhatIDo from "./WhatIDo";
|
||||
import Work from "./Work";
|
||||
import setSplitText from "./utils/splitText";
|
||||
|
||||
const TechStack = lazy(() => import("./TechStack"));
|
||||
|
||||
const MainContainer = ({ children }: PropsWithChildren) => {
|
||||
const [isDesktopView, setIsDesktopView] = useState<boolean>(
|
||||
window.innerWidth > 1024
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const resizeHandler = () => {
|
||||
setSplitText();
|
||||
setIsDesktopView(window.innerWidth > 1024);
|
||||
};
|
||||
resizeHandler();
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, [isDesktopView]);
|
||||
|
||||
return (
|
||||
<div className="container-main">
|
||||
<Cursor />
|
||||
<Navbar />
|
||||
<SocialIcons />
|
||||
{isDesktopView && children}
|
||||
<div id="smooth-wrapper">
|
||||
<div id="smooth-content">
|
||||
<div className="container-main">
|
||||
<Landing>{!isDesktopView && children}</Landing>
|
||||
<About />
|
||||
<WhatIDo />
|
||||
<Career />
|
||||
<Work />
|
||||
{isDesktopView && (
|
||||
<Suspense fallback={<div>Loading....</div>}>
|
||||
<TechStack />
|
||||
</Suspense>
|
||||
)}
|
||||
<Contact />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainContainer;
|
||||
81
src/components/Navbar.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useEffect } from "react";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import HoverLinks from "./HoverLinks";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollSmoother } from "gsap/dist/ScrollSmoother";
|
||||
import "./styles/Navbar.css";
|
||||
|
||||
gsap.registerPlugin(ScrollSmoother, ScrollTrigger);
|
||||
export let smoother: ScrollSmoother;
|
||||
|
||||
const Navbar = () => {
|
||||
useEffect(() => {
|
||||
smoother = ScrollSmoother.create({
|
||||
wrapper: "#smooth-wrapper",
|
||||
content: "#smooth-content",
|
||||
smooth: 1.7,
|
||||
speed: 1.7,
|
||||
effects: true,
|
||||
autoResize: true,
|
||||
ignoreMobileResize: true,
|
||||
});
|
||||
|
||||
smoother.scrollTop(0);
|
||||
smoother.paused(true);
|
||||
|
||||
let links = document.querySelectorAll(".header ul a");
|
||||
links.forEach((elem) => {
|
||||
let element = elem as HTMLAnchorElement;
|
||||
element.addEventListener("click", (e) => {
|
||||
if (window.innerWidth > 1024) {
|
||||
e.preventDefault();
|
||||
let elem = e.currentTarget as HTMLAnchorElement;
|
||||
let section = elem.getAttribute("data-href");
|
||||
smoother.scrollTo(section, true, "top top");
|
||||
}
|
||||
});
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
ScrollSmoother.refresh(true);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<div className="header">
|
||||
<a href="/#" className="navbar-title" data-cursor="disable">
|
||||
PhongPham.DEV
|
||||
</a>
|
||||
<a
|
||||
href="mailto:phongpt.working@gmail.com"
|
||||
className="navbar-connect"
|
||||
data-cursor="disable"
|
||||
>
|
||||
phongpt.working@gmail.com
|
||||
</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a data-href="#about" href="#about">
|
||||
<HoverLinks text="ABOUT" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-href="#work" href="#work">
|
||||
<HoverLinks text="WORK" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-href="#contact" href="#contact">
|
||||
<HoverLinks text="CONTACT" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="landing-circle1"></div>
|
||||
<div className="landing-circle2"></div>
|
||||
<div className="nav-fade"></div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
93
src/components/SocialIcons.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
FaGithub,
|
||||
FaLinkedinIn,
|
||||
FaFacebookF,
|
||||
} from "react-icons/fa6";
|
||||
import { SiGitea } from "react-icons/si";
|
||||
import "./styles/SocialIcons.css";
|
||||
import { TbNotes } from "react-icons/tb";
|
||||
import { useEffect } from "react";
|
||||
import HoverLinks from "./HoverLinks";
|
||||
|
||||
const SocialIcons = () => {
|
||||
useEffect(() => {
|
||||
const social = document.getElementById("social") as HTMLElement;
|
||||
|
||||
social.querySelectorAll("span").forEach((item) => {
|
||||
const elem = item as HTMLElement;
|
||||
const link = elem.querySelector("a") as HTMLElement;
|
||||
|
||||
const rect = elem.getBoundingClientRect();
|
||||
let mouseX = rect.width / 2;
|
||||
let mouseY = rect.height / 2;
|
||||
let currentX = 0;
|
||||
let currentY = 0;
|
||||
|
||||
const updatePosition = () => {
|
||||
currentX += (mouseX - currentX) * 0.1;
|
||||
currentY += (mouseY - currentY) * 0.1;
|
||||
|
||||
link.style.setProperty("--siLeft", `${currentX}px`);
|
||||
link.style.setProperty("--siTop", `${currentY}px`);
|
||||
|
||||
requestAnimationFrame(updatePosition);
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
if (x < 40 && x > 10 && y < 40 && y > 5) {
|
||||
mouseX = x;
|
||||
mouseY = y;
|
||||
} else {
|
||||
mouseX = rect.width / 2;
|
||||
mouseY = rect.height / 2;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
|
||||
updatePosition();
|
||||
|
||||
return () => {
|
||||
elem.removeEventListener("mousemove", onMouseMove);
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="icons-section">
|
||||
<div className="social-icons" data-cursor="icons" id="social">
|
||||
<span>
|
||||
<a href="https://github.com/Phong12HexDockwork" target="_blank">
|
||||
<FaGithub />
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<a href="https://gitea.phongprojects.id.vn/phongpham" target="_blank">
|
||||
<SiGitea />
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<a href="https://www.facebook.com" target="_blank">
|
||||
<FaFacebookF />
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<a href="https://www.linkedin.com/in/phong-pham" target="_blank">
|
||||
<FaLinkedinIn />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<a className="resume-button" href="https://docs.google.com/document/d/1JizajaaBrOGiEnE7mvA_CrjxwyWW82oJtfToM5QpTaI/edit?usp=sharing" target="_blank" rel="noopener noreferrer">
|
||||
<HoverLinks text="RESUME" />
|
||||
<span>
|
||||
<TbNotes />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialIcons;
|
||||
214
src/components/TechStack.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import * as THREE from "three";
|
||||
import { useRef, useMemo, useState, useEffect } from "react";
|
||||
import { Canvas, useFrame } from "@react-three/fiber";
|
||||
import { Environment } from "@react-three/drei";
|
||||
import { EffectComposer, N8AO } from "@react-three/postprocessing";
|
||||
import {
|
||||
BallCollider,
|
||||
Physics,
|
||||
RigidBody,
|
||||
CylinderCollider,
|
||||
RapierRigidBody,
|
||||
} from "@react-three/rapier";
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const imageUrls = [
|
||||
"/images/react2.webp",
|
||||
"/images/next2.webp",
|
||||
"/images/node2.webp",
|
||||
"/images/express.webp",
|
||||
"/images/mongo.webp",
|
||||
"/images/mysql.webp",
|
||||
"/images/typescript.webp",
|
||||
"/images/javascript.webp",
|
||||
];
|
||||
const textures = imageUrls.map((url) => textureLoader.load(url));
|
||||
|
||||
const sphereGeometry = new THREE.SphereGeometry(1, 28, 28);
|
||||
|
||||
const spheres = [...Array(30)].map(() => ({
|
||||
scale: [0.7, 1, 0.8, 1, 1][Math.floor(Math.random() * 5)],
|
||||
}));
|
||||
|
||||
type SphereProps = {
|
||||
vec?: THREE.Vector3;
|
||||
scale: number;
|
||||
r?: typeof THREE.MathUtils.randFloatSpread;
|
||||
material: THREE.MeshPhysicalMaterial;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
function SphereGeo({
|
||||
vec = new THREE.Vector3(),
|
||||
scale,
|
||||
r = THREE.MathUtils.randFloatSpread,
|
||||
material,
|
||||
isActive,
|
||||
}: SphereProps) {
|
||||
const api = useRef<RapierRigidBody | null>(null);
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!isActive) return;
|
||||
delta = Math.min(0.1, delta);
|
||||
const impulse = vec
|
||||
.copy(api.current!.translation())
|
||||
.normalize()
|
||||
.multiply(
|
||||
new THREE.Vector3(
|
||||
-50 * delta * scale,
|
||||
-150 * delta * scale,
|
||||
-50 * delta * scale
|
||||
)
|
||||
);
|
||||
|
||||
api.current?.applyImpulse(impulse, true);
|
||||
});
|
||||
|
||||
return (
|
||||
<RigidBody
|
||||
linearDamping={0.75}
|
||||
angularDamping={0.15}
|
||||
friction={0.2}
|
||||
position={[r(20), r(20) - 25, r(20) - 10]}
|
||||
ref={api}
|
||||
colliders={false}
|
||||
>
|
||||
<BallCollider args={[scale]} />
|
||||
<CylinderCollider
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
position={[0, 0, 1.2 * scale]}
|
||||
args={[0.15 * scale, 0.275 * scale]}
|
||||
/>
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
scale={scale}
|
||||
geometry={sphereGeometry}
|
||||
material={material}
|
||||
rotation={[0.3, 1, 1]}
|
||||
/>
|
||||
</RigidBody>
|
||||
);
|
||||
}
|
||||
|
||||
type PointerProps = {
|
||||
vec?: THREE.Vector3;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
function Pointer({ vec = new THREE.Vector3(), isActive }: PointerProps) {
|
||||
const ref = useRef<RapierRigidBody>(null);
|
||||
|
||||
useFrame(({ pointer, viewport }) => {
|
||||
if (!isActive) return;
|
||||
const targetVec = vec.lerp(
|
||||
new THREE.Vector3(
|
||||
(pointer.x * viewport.width) / 2,
|
||||
(pointer.y * viewport.height) / 2,
|
||||
0
|
||||
),
|
||||
0.2
|
||||
);
|
||||
ref.current?.setNextKinematicTranslation(targetVec);
|
||||
});
|
||||
|
||||
return (
|
||||
<RigidBody
|
||||
position={[100, 100, 100]}
|
||||
type="kinematicPosition"
|
||||
colliders={false}
|
||||
ref={ref}
|
||||
>
|
||||
<BallCollider args={[2]} />
|
||||
</RigidBody>
|
||||
);
|
||||
}
|
||||
|
||||
const TechStack = () => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollY = window.scrollY || document.documentElement.scrollTop;
|
||||
const threshold = document
|
||||
.getElementById("work")!
|
||||
.getBoundingClientRect().top;
|
||||
setIsActive(scrollY > threshold);
|
||||
};
|
||||
document.querySelectorAll(".header a").forEach((elem) => {
|
||||
const element = elem as HTMLAnchorElement;
|
||||
element.addEventListener("click", () => {
|
||||
const interval = setInterval(() => {
|
||||
handleScroll();
|
||||
}, 10);
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
const materials = useMemo(() => {
|
||||
return textures.map(
|
||||
(texture) =>
|
||||
new THREE.MeshPhysicalMaterial({
|
||||
map: texture,
|
||||
emissive: "#ffffff",
|
||||
emissiveMap: texture,
|
||||
emissiveIntensity: 0.3,
|
||||
metalness: 0.5,
|
||||
roughness: 1,
|
||||
clearcoat: 0.1,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="techstack">
|
||||
<h2> My Techstack</h2>
|
||||
|
||||
<Canvas
|
||||
shadows
|
||||
gl={{ alpha: true, stencil: false, depth: false, antialias: false }}
|
||||
camera={{ position: [0, 0, 20], fov: 32.5, near: 1, far: 100 }}
|
||||
onCreated={(state) => (state.gl.toneMappingExposure = 1.5)}
|
||||
className="tech-canvas"
|
||||
>
|
||||
<ambientLight intensity={1} />
|
||||
<spotLight
|
||||
position={[20, 20, 25]}
|
||||
penumbra={1}
|
||||
angle={0.2}
|
||||
color="white"
|
||||
castShadow
|
||||
shadow-mapSize={[512, 512]}
|
||||
/>
|
||||
<directionalLight position={[0, 5, -4]} intensity={2} />
|
||||
<Physics gravity={[0, 0, 0]}>
|
||||
<Pointer isActive={isActive} />
|
||||
{spheres.map((props, i) => (
|
||||
<SphereGeo
|
||||
key={i}
|
||||
{...props}
|
||||
material={materials[Math.floor(Math.random() * materials.length)]}
|
||||
isActive={isActive}
|
||||
/>
|
||||
))}
|
||||
</Physics>
|
||||
<Environment
|
||||
files="/models/char_enviorment.hdr"
|
||||
environmentIntensity={0.5}
|
||||
environmentRotation={[0, 4, 2]}
|
||||
/>
|
||||
<EffectComposer enableNormalPass={false}>
|
||||
<N8AO color="#0f002c" aoRadius={2} intensity={1.15} />
|
||||
</EffectComposer>
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TechStack;
|
||||
172
src/components/WhatIDo.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import "./styles/WhatIDo.css";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
const WhatIDo = () => {
|
||||
const containerRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const setRef = (el: HTMLDivElement | null, index: number) => {
|
||||
containerRef.current[index] = el;
|
||||
};
|
||||
useEffect(() => {
|
||||
if (ScrollTrigger.isTouch) {
|
||||
containerRef.current.forEach((container) => {
|
||||
if (container) {
|
||||
container.classList.remove("what-noTouch");
|
||||
container.addEventListener("click", () => handleClick(container));
|
||||
}
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
containerRef.current.forEach((container) => {
|
||||
if (container) {
|
||||
container.removeEventListener("click", () => handleClick(container));
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="whatIDO">
|
||||
<div className="what-box">
|
||||
<h2 className="title">
|
||||
W<span className="hat-h2">HAT</span>
|
||||
<div>
|
||||
I<span className="do-h2"> DO</span>
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="what-box">
|
||||
<div className="what-box-in">
|
||||
<div className="what-border2">
|
||||
<svg width="100%">
|
||||
<line
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="7,7"
|
||||
/>
|
||||
<line
|
||||
x1="100%"
|
||||
y1="0"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="7,7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="what-content what-noTouch"
|
||||
ref={(el) => setRef(el, 0)}
|
||||
>
|
||||
<div className="what-border1">
|
||||
<svg height="100%">
|
||||
<line
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="100%"
|
||||
y2="0"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="6,6"
|
||||
/>
|
||||
<line
|
||||
x1="0"
|
||||
y1="100%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="6,6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="what-corner"></div>
|
||||
|
||||
<div className="what-content-in">
|
||||
<h3>QA TESTING</h3>
|
||||
<h4>Description</h4>
|
||||
<p>
|
||||
Manual, API and database testing to ensure system stability and data integrity.
|
||||
Designing test cases, tracking bugs, and performing regression testing across platforms.
|
||||
</p>
|
||||
<h5>Skillset & tools</h5>
|
||||
<div className="what-content-flex">
|
||||
<div className="what-tags">Jira</div>
|
||||
<div className="what-tags">TestRail</div>
|
||||
<div className="what-tags">Postman</div>
|
||||
<div className="what-tags">Swagger</div>
|
||||
<div className="what-tags">SQL</div>
|
||||
<div className="what-tags">API Testing</div>
|
||||
<div className="what-tags">Regression Testing</div>
|
||||
<div className="what-tags">Chrome DevTools</div>
|
||||
<div className="what-tags">Bug Tracking</div>
|
||||
<div className="what-tags">Agile</div>
|
||||
</div>
|
||||
<div className="what-arrow"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="what-content what-noTouch"
|
||||
ref={(el) => setRef(el, 1)}
|
||||
>
|
||||
<div className="what-border1">
|
||||
<svg height="100%">
|
||||
<line
|
||||
x1="0"
|
||||
y1="100%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="6,6"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="what-corner"></div>
|
||||
<div className="what-content-in">
|
||||
<h3>AUTOMATION</h3>
|
||||
<h4>Description</h4>
|
||||
<p>
|
||||
Workflow automation and test efficiency improvement using modern tools.
|
||||
Building automated processes for testing, data management, and system integration.
|
||||
</p>
|
||||
<h5>Skillset & tools</h5>
|
||||
<div className="what-content-flex">
|
||||
<div className="what-tags">n8n</div>
|
||||
<div className="what-tags">JavaScript</div>
|
||||
<div className="what-tags">Python</div>
|
||||
<div className="what-tags">Google App Scripts</div>
|
||||
<div className="what-tags">Git</div>
|
||||
<div className="what-tags">Workflow Automation</div>
|
||||
<div className="what-tags">CI/CD</div>
|
||||
<div className="what-tags">Test Frameworks</div>
|
||||
</div>
|
||||
<div className="what-arrow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhatIDo;
|
||||
|
||||
function handleClick(container: HTMLDivElement) {
|
||||
container.classList.toggle("what-content-active");
|
||||
container.classList.remove("what-sibling");
|
||||
if (container.parentElement) {
|
||||
const siblings = Array.from(container.parentElement.children);
|
||||
|
||||
siblings.forEach((sibling) => {
|
||||
if (sibling !== container) {
|
||||
sibling.classList.remove("what-content-active");
|
||||
sibling.classList.toggle("what-sibling");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
128
src/components/Work.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import "./styles/Work.css";
|
||||
import WorkImage from "./WorkImage";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import { useEffect } from "react";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const Work = () => {
|
||||
useEffect(() => {
|
||||
let translateX: number = 0;
|
||||
|
||||
function setTranslateX() {
|
||||
const box = document.getElementsByClassName("work-box");
|
||||
const rectLeft = document
|
||||
.querySelector(".work-container")!
|
||||
.getBoundingClientRect().left;
|
||||
const rect = box[0].getBoundingClientRect();
|
||||
const parentWidth = box[0].parentElement!.getBoundingClientRect().width;
|
||||
let padding: number =
|
||||
parseInt(window.getComputedStyle(box[0]).padding) / 2;
|
||||
translateX = rect.width * box.length - (rectLeft + parentWidth) + padding;
|
||||
}
|
||||
|
||||
setTranslateX();
|
||||
|
||||
let timeline = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".work-section",
|
||||
start: "top top",
|
||||
end: `+=${translateX}`, // Use actual scroll width
|
||||
scrub: true,
|
||||
pin: true,
|
||||
id: "work",
|
||||
},
|
||||
});
|
||||
|
||||
timeline.to(".work-flex", {
|
||||
x: -translateX,
|
||||
ease: "none",
|
||||
});
|
||||
|
||||
// Clean up (optional, good practice)
|
||||
return () => {
|
||||
timeline.kill();
|
||||
ScrollTrigger.getById("work")?.kill();
|
||||
};
|
||||
}, []);
|
||||
const projects = [
|
||||
{
|
||||
title: "UVita - UV Application",
|
||||
category: "QA & Development",
|
||||
description: "Led QC activities and data integrity testing for real-time UV-index app. Managed bug life cycle within 48-hour hackathon deadline.",
|
||||
tools: "Postman, TestRail, Git, Agile Workflow",
|
||||
image: "/project_img/UVIita.png",
|
||||
link: "https://github.com/Phong12HexDockwork/UVita"
|
||||
},
|
||||
{
|
||||
title: "LACA City Platform",
|
||||
category: "Software Testing",
|
||||
description: "API and database testing for parking management platform serving 5,000+ users. Improved release stability by 20%.",
|
||||
tools: "Jira, Postman, Chrome DevTools, SQL",
|
||||
image: "/project_img/Laca City.png",
|
||||
link: "https://new.laca.city"
|
||||
},
|
||||
{
|
||||
title: "NASA Space Apps Challenge",
|
||||
category: "Quality Control & Operations",
|
||||
description: "Designed QC processes for event with 500+ participants. Increased participation by 126% compared to previous year.",
|
||||
tools: "n8n, TestRail, Meta Console, API Testing",
|
||||
image: "/project_img/NASA Space app.jpg",
|
||||
link: "https://www.facebook.com/spaceappshochiminh"
|
||||
},
|
||||
{
|
||||
title: "VSIG Platform",
|
||||
category: "Testing & Automation",
|
||||
description: "Executed 150+ test cases achieving >98% release stability. Reduced issue resolution time by 25%.",
|
||||
tools: "TestRail, Postman, Swagger, Jira, n8n",
|
||||
image: "/project_img/VSIG .png",
|
||||
link: ""
|
||||
},
|
||||
{
|
||||
title: "CSED Conference 2024",
|
||||
category: "Development & Automation",
|
||||
description: "Developed automated submission system reducing processing time by 40%. Supported 200+ submissions with workflow automation.",
|
||||
tools: "Google App Scripts, n8n, JavaScript",
|
||||
image: "/project_img/CSED.jpg",
|
||||
link: ""
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="work-section" id="work">
|
||||
<div className="work-container section-container">
|
||||
<h2>
|
||||
My <span>Work</span>
|
||||
</h2>
|
||||
<div className="work-flex">
|
||||
{projects.map((project, index) => (
|
||||
<div className="work-box" key={index}>
|
||||
<div className="work-info">
|
||||
<div className="work-title">
|
||||
<h3>0{index + 1}</h3>
|
||||
|
||||
<div>
|
||||
<h4>{project.title}</h4>
|
||||
<p>{project.category}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h4>{project.description}</h4>
|
||||
<p>{project.tools}</p>
|
||||
</div>
|
||||
{project.link ? (
|
||||
<a href={project.link} target="_blank" rel="noopener noreferrer">
|
||||
<WorkImage image={project.image} alt={project.title} />
|
||||
</a>
|
||||
) : (
|
||||
<WorkImage image={project.image} alt={project.title} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Work;
|
||||
46
src/components/WorkImage.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import { MdArrowOutward } from "react-icons/md";
|
||||
|
||||
interface Props {
|
||||
image: string;
|
||||
alt?: string;
|
||||
video?: string;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
const WorkImage = (props: Props) => {
|
||||
const [isVideo, setIsVideo] = useState(false);
|
||||
const [video, setVideo] = useState("");
|
||||
const handleMouseEnter = async () => {
|
||||
if (props.video) {
|
||||
setIsVideo(true);
|
||||
const response = await fetch(`src/assets/${props.video}`);
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
setVideo(blobUrl);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="work-image">
|
||||
<a
|
||||
className="work-image-in"
|
||||
href={props.link}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={() => setIsVideo(false)}
|
||||
target="_blank"
|
||||
data-cursor={"disable"}
|
||||
>
|
||||
{props.link && (
|
||||
<div className="work-link">
|
||||
<MdArrowOutward />
|
||||
</div>
|
||||
)}
|
||||
<img src={props.image} alt={props.alt} />
|
||||
{isVideo && <video src={video} autoPlay muted playsInline loop></video>}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkImage;
|
||||
71
src/components/styles/About.css
Normal file
@@ -0,0 +1,71 @@
|
||||
.about-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
width: var(--cWidth);
|
||||
margin: auto;
|
||||
}
|
||||
.about-me {
|
||||
padding: 50px 0px;
|
||||
padding-bottom: 0;
|
||||
width: 500px;
|
||||
max-width: calc(100% - 15px);
|
||||
}
|
||||
.about-me h3 {
|
||||
font-size: 25px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 7px;
|
||||
font-weight: 400;
|
||||
color: var(--accentColor);
|
||||
}
|
||||
|
||||
.about-me p {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.about-section {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 768px) {
|
||||
.about-me {
|
||||
width: 500px;
|
||||
max-width: calc(100% - 70px);
|
||||
transform: translateY(0%);
|
||||
}
|
||||
.about-section {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1025px) {
|
||||
.about-section {
|
||||
width: var(--cWidth);
|
||||
justify-content: right;
|
||||
max-width: 1920px;
|
||||
height: var(--vh);
|
||||
padding: 0px;
|
||||
opacity: 1;
|
||||
}
|
||||
.about-me {
|
||||
padding: 0px;
|
||||
width: 50%;
|
||||
}
|
||||
.about-me p {
|
||||
font-size: 1.2vw;
|
||||
line-height: 1.8vw;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1950px) {
|
||||
.about-me p {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
}
|
||||
216
src/components/styles/Career.css
Normal file
@@ -0,0 +1,216 @@
|
||||
.career-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
margin: auto;
|
||||
margin-bottom: 250px;
|
||||
padding: 120px 0px;
|
||||
}
|
||||
|
||||
.career-section h2 {
|
||||
font-size: 70px;
|
||||
line-height: 70px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
background: linear-gradient(0deg, #7f40ff, #ffffff);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 90px;
|
||||
}
|
||||
.career-section h2 > span {
|
||||
font-family: "Geist", sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
.career-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0px auto;
|
||||
}
|
||||
.career-info-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.career-info-box p {
|
||||
width: 40%;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
}
|
||||
.career-info-in {
|
||||
display: flex;
|
||||
width: 40%;
|
||||
justify-content: space-between;
|
||||
gap: 50px;
|
||||
}
|
||||
.career-info h3 {
|
||||
font-size: 48px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
line-height: 45px;
|
||||
}
|
||||
|
||||
.career-info h4 {
|
||||
font-size: 33px;
|
||||
line-height: 30px;
|
||||
letter-spacing: 0.8px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.career-info h5 {
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.7px;
|
||||
font-size: 20px;
|
||||
text-transform: capitalize;
|
||||
margin: 10px 0px;
|
||||
color: var(--accentColor);
|
||||
}
|
||||
.career-timeline {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(
|
||||
to top,
|
||||
#aa42ff 20%,
|
||||
var(--accentColor) 50%,
|
||||
transparent 95%
|
||||
);
|
||||
max-height: 0%;
|
||||
}
|
||||
.career-dot {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
background-color: #aa42ff;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0px 0px 5px 2px #d29bff, 0px 0px 15px 8px #d097ff,
|
||||
0px 0px 110px 20px #f2c0ff;
|
||||
animation: timeline 0.8s linear infinite forwards;
|
||||
}
|
||||
|
||||
@keyframes timeline {
|
||||
10%,
|
||||
20%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
box-shadow: 0px 0px 5px 2px #d29bff;
|
||||
}
|
||||
10%,
|
||||
30%,
|
||||
0%,
|
||||
100%,
|
||||
64%,
|
||||
80% {
|
||||
box-shadow: 0px 0px 5px 2px #d29bff, 0px 0px 15px 5px #d097ff,
|
||||
0px 0px 110px 20px #f2c0ff;
|
||||
}
|
||||
}
|
||||
@keyframes timeline2 {
|
||||
0% {
|
||||
box-shadow: 0px 0px 5px 2px #d29bff;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 5px 2px #d29bff, 0px 0px 15px 5px #d097ff,
|
||||
0px 0px 110px 20px #f2c0ff;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.career-section h2 {
|
||||
font-size: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
.career-info h4 {
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
width: 180px;
|
||||
}
|
||||
.career-info h5 {
|
||||
font-size: 17px;
|
||||
}
|
||||
.career-info h3 {
|
||||
font-size: 40px;
|
||||
}
|
||||
.career-info-box p {
|
||||
font-size: 14px;
|
||||
}
|
||||
.career-info-in {
|
||||
width: 45%;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.career-info-box p {
|
||||
width: 45%;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1025px) {
|
||||
.career-section {
|
||||
padding: 70px 0px;
|
||||
padding-top: 220px;
|
||||
margin-top: -200px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 900px) {
|
||||
.career-info-box {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 70px;
|
||||
}
|
||||
.career-info-in,
|
||||
.career-info-box p {
|
||||
width: 100%;
|
||||
padding-left: 10%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.career-timeline {
|
||||
left: 0%;
|
||||
}
|
||||
.career-container {
|
||||
width: calc(100% - 25px);
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
.career-info {
|
||||
margin: 0;
|
||||
}
|
||||
.career-section h2 {
|
||||
width: 100%;
|
||||
font-size: 45px;
|
||||
line-height: 45px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.career-info-in {
|
||||
gap: 0px;
|
||||
}
|
||||
.career-info h3 {
|
||||
font-size: 33px;
|
||||
}
|
||||
.career-info-in,
|
||||
.career-info-box p {
|
||||
padding-left: 5%;
|
||||
}
|
||||
.career-section {
|
||||
padding-top: 90px;
|
||||
margin-top: -70px;
|
||||
|
||||
align-items: start;
|
||||
place-items: inherit;
|
||||
justify-content: left;
|
||||
}
|
||||
}
|
||||
92
src/components/styles/Contact.css
Normal file
@@ -0,0 +1,92 @@
|
||||
.contact-section {
|
||||
margin: auto;
|
||||
padding-bottom: 100px;
|
||||
margin-top: 100px;
|
||||
}
|
||||
.contact-section h3 {
|
||||
font-size: 60px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
.contact-flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.contact-flex h4 {
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.contact-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.contact-flex p {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a.contact-social {
|
||||
font-size: 25px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.contact-box h2 {
|
||||
font-weight: 400;
|
||||
font-size: 23px;
|
||||
margin: 0;
|
||||
}
|
||||
.contact-box h2 > span {
|
||||
color: var(--accentColor);
|
||||
}
|
||||
.contact-box h5 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@media only screen and (max-width: 1600px) {
|
||||
.contact-section h3 {
|
||||
font-size: 50px;
|
||||
}
|
||||
.contact-box h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
a.contact-social {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1300px) {
|
||||
.contact-section h3 {
|
||||
font-size: 40px;
|
||||
}
|
||||
.contact-box h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
a.contact-social {
|
||||
font-size: 20px;
|
||||
}
|
||||
.contact-flex p {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 900px) {
|
||||
.contact-flex {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
.contact-flex p {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.contact-flex h4 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.contact-section {
|
||||
margin-top: 50px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.contact-container {
|
||||
width: calc(100% - 25px);
|
||||
}
|
||||
}
|
||||
33
src/components/styles/Cursor.css
Normal file
@@ -0,0 +1,33 @@
|
||||
.cursor-main {
|
||||
--size: 0px;
|
||||
position: fixed;
|
||||
top: calc(var(--size) / -2);
|
||||
left: calc(var(--size) / -2);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50px;
|
||||
pointer-events: none;
|
||||
z-index: 99;
|
||||
background-color: #e6c3ff;
|
||||
box-shadow: 0px 0px 30px 0px rgb(175, 131, 255);
|
||||
mix-blend-mode: difference;
|
||||
transition: top 0.3s ease-out, left 0.3s ease-out, width 0.3s ease-out,
|
||||
height 0.3s ease-out;
|
||||
}
|
||||
.cursor-icons {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
height: calc(var(--cursorH) - 20px);
|
||||
transition: all 0.5s ease-out, height 0.5s ease-in-out;
|
||||
}
|
||||
.cursor-disable {
|
||||
--size: 0px;
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
.cursor-main {
|
||||
--size: 50px;
|
||||
}
|
||||
.cursor-disable {
|
||||
--size: 0px;
|
||||
}
|
||||
}
|
||||
358
src/components/styles/Landing.css
Normal file
@@ -0,0 +1,358 @@
|
||||
.landing-section {
|
||||
width: 100%;
|
||||
max-width: var(--cMaxWidth);
|
||||
margin: auto;
|
||||
position: relative;
|
||||
height: var(--vh);
|
||||
}
|
||||
.landing-container {
|
||||
width: var(--cWidth);
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
max-width: var(--cMaxWidth);
|
||||
}
|
||||
.landing-circle1 {
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
z-index: 15;
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background-color: #fb8dff;
|
||||
box-shadow: inset -50px 40px 50px rgba(84, 0, 255, 0.6);
|
||||
filter: blur(60px);
|
||||
border-radius: 50%;
|
||||
animation: loadingCircle 5s linear infinite;
|
||||
}
|
||||
.nav-fade {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
var(--backgroundColor) 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 12;
|
||||
opacity: 0;
|
||||
left: 0;
|
||||
}
|
||||
@keyframes loadingCircle {
|
||||
0% {
|
||||
transform: translate(-95%, -75%) rotateZ(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-95%, -75%) rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.landing-circle2 {
|
||||
top: 50%;
|
||||
right: 0%;
|
||||
transform: translate(calc(100% - 2px), -50%);
|
||||
z-index: 9;
|
||||
position: fixed;
|
||||
display: none;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background-color: #fb8dff;
|
||||
box-shadow: inset -50px 40px 50px rgba(84, 0, 255, 0.6);
|
||||
filter: blur(50px);
|
||||
border-radius: 50%;
|
||||
animation: loadingCircle2 5s linear infinite;
|
||||
}
|
||||
@keyframes loadingCircle2 {
|
||||
100% {
|
||||
transform: translate(calc(100% - 2px), -50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
.landing-video,
|
||||
.landing-image {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 95%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.landing-image img {
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
.character-rim {
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
z-index: 1;
|
||||
background-color: #f59bf8;
|
||||
transform: translate(-50%, 36%) scaleX(1.4);
|
||||
box-shadow: inset 66px 35px 85px 0px rgba(85, 0, 255, 0.65);
|
||||
filter: blur(50px);
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 100%) scale(1.4);
|
||||
opacity: 0;
|
||||
}
|
||||
.character-model {
|
||||
height: 80%;
|
||||
height: 80vh;
|
||||
position: absolute;
|
||||
max-width: 1920px;
|
||||
max-height: 1080px;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
left: 50%;
|
||||
z-index: 0;
|
||||
bottom: 50px;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
.character-model::after {
|
||||
content: "";
|
||||
width: 100vw;
|
||||
height: 250px;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
var(--backgroundColor) 70%
|
||||
);
|
||||
bottom: -50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9;
|
||||
position: absolute;
|
||||
}
|
||||
.character-model::before {
|
||||
content: "";
|
||||
width: 100vw;
|
||||
height: 700px;
|
||||
background-color: var(--backgroundColor);
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9;
|
||||
position: absolute;
|
||||
}
|
||||
.character-loaded .character-rim {
|
||||
animation: backlight 3s forwards;
|
||||
animation-delay: 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
.character-model canvas {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.character-hover {
|
||||
position: absolute;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 3;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.landing-intro {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
top: 12%;
|
||||
left: 0;
|
||||
}
|
||||
.landing-intro h2 {
|
||||
margin: 0;
|
||||
color: var(--accentColor);
|
||||
font-size: 22px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.landing-intro h1 {
|
||||
margin: 0;
|
||||
letter-spacing: 2px;
|
||||
font-size: 28px;
|
||||
line-height: 28px;
|
||||
font-weight: 500;
|
||||
font-family: "Geist", sans-serif;
|
||||
}
|
||||
|
||||
/* .landing-intro h1 span {
|
||||
font-weight: 200;
|
||||
} */
|
||||
|
||||
.landing-info {
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
bottom: 40px;
|
||||
top: inherit;
|
||||
z-index: 9;
|
||||
}
|
||||
.landing-info h3 {
|
||||
font-size: 22px;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 300;
|
||||
color: var(--accentColor);
|
||||
margin: 0;
|
||||
}
|
||||
.landing-info h2 {
|
||||
margin: 0;
|
||||
margin-top: -20px;
|
||||
margin-left: 20px;
|
||||
font-family: "Geist", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.landing-h2-info-1 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
h2.landing-info-h2 {
|
||||
color: #c481ff;
|
||||
font-size: 42px;
|
||||
width: 120%;
|
||||
margin: 0;
|
||||
font-family: "Geist", sans-serif;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
margin-left: -5px;
|
||||
}
|
||||
.landing-h2-2 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.landing-info-h2::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 120%;
|
||||
z-index: 3;
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
var(--backgroundColor) 40%,
|
||||
rgba(0, 0, 0, 0) 110%
|
||||
);
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
@media screen and (min-width: 500px) {
|
||||
.landing-circle2 {
|
||||
display: block;
|
||||
}
|
||||
.character-model {
|
||||
z-index: 0;
|
||||
}
|
||||
.landing-info h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.landing-intro h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
.landing-intro h1 {
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.landing-info h2 {
|
||||
font-size: 35px;
|
||||
line-height: 40px;
|
||||
}
|
||||
h2.landing-info-h2 {
|
||||
font-size: 38px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
.character-model {
|
||||
height: 80vh;
|
||||
}
|
||||
.landing-intro h2 {
|
||||
font-size: 25px;
|
||||
}
|
||||
.landing-intro h1 {
|
||||
font-size: 40px;
|
||||
line-height: 35px;
|
||||
}
|
||||
.landing-info h3 {
|
||||
font-size: 25px;
|
||||
}
|
||||
.landing-info h2 {
|
||||
font-size: 45px;
|
||||
line-height: 42px;
|
||||
}
|
||||
h2.landing-info-h2 {
|
||||
font-size: 55px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1025px) {
|
||||
.character-model {
|
||||
height: 100vh;
|
||||
bottom: 0;
|
||||
z-index: 11;
|
||||
position: fixed;
|
||||
}
|
||||
.character-model::after,
|
||||
.character-model::before {
|
||||
display: none;
|
||||
}
|
||||
.landing-intro {
|
||||
top: 50%;
|
||||
left: auto;
|
||||
right: 66%;
|
||||
transform: translate(0%, -50%);
|
||||
}
|
||||
|
||||
.landing-info {
|
||||
bottom: auto;
|
||||
top: 51%;
|
||||
z-index: inherit;
|
||||
text-align: left;
|
||||
transform: translate(0%, -50%);
|
||||
right: auto;
|
||||
left: 66%;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1200px) {
|
||||
.landing-intro {
|
||||
top: 50%;
|
||||
left: auto;
|
||||
right: 70%;
|
||||
transform: translate(0%, -50%);
|
||||
}
|
||||
|
||||
.landing-info {
|
||||
bottom: auto;
|
||||
top: 51%;
|
||||
z-index: inherit;
|
||||
text-align: left;
|
||||
transform: translate(0%, -50%);
|
||||
right: auto;
|
||||
left: 70%;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1600px) {
|
||||
.landing-intro h2 {
|
||||
font-size: 35px;
|
||||
}
|
||||
.landing-intro h1 {
|
||||
font-size: 60px;
|
||||
line-height: 55px;
|
||||
}
|
||||
.landing-info h3 {
|
||||
font-size: 35px;
|
||||
}
|
||||
.landing-info h2 {
|
||||
font-size: 65px;
|
||||
line-height: 62px;
|
||||
}
|
||||
h2.landing-info-h2 {
|
||||
font-size: 75px;
|
||||
}
|
||||
}
|
||||
363
src/components/styles/Loading.css
Normal file
@@ -0,0 +1,363 @@
|
||||
.loading-screen {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: var(--vh);
|
||||
/* background-image: linear-gradient(#cbb1ff, #d8c4ff); */
|
||||
background-color: #eae5ec;
|
||||
z-index: 999999999;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading-button {
|
||||
padding: 20px 50px;
|
||||
border-radius: 100px;
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
}
|
||||
.loading-button::before {
|
||||
content: "";
|
||||
background-color: #ffffff;
|
||||
top: var(--mouse-y);
|
||||
left: var(--mouse-x);
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
filter: blur(60px);
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.loading-button:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.loading-clicked .loading-button::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
--Lsize: 145px;
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
min-width: 0px;
|
||||
min-height: 0px;
|
||||
border-radius: 100px;
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
transition: 0.8s ease-in-out;
|
||||
transition-delay: 0.2s;
|
||||
box-shadow: 0px 15px 15px 0px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.loading-clicked {
|
||||
transition-delay: 0ms;
|
||||
transition-timing-function: cubic-bezier(0.33, 0.11, 1, 0.72);
|
||||
transform: scale(1);
|
||||
min-width: calc(100vw + 5000px);
|
||||
border-radius: 5000px;
|
||||
min-height: calc(100vh + 500px);
|
||||
box-shadow: none;
|
||||
}
|
||||
.loading-clicked .loading-button {
|
||||
overflow: visible;
|
||||
}
|
||||
.loading-hover {
|
||||
background-color: #a87cff;
|
||||
width: 250px;
|
||||
height: 120px;
|
||||
position: absolute;
|
||||
top: var(--mouse-y);
|
||||
left: var(--mouse-x);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
filter: blur(30px);
|
||||
opacity: 1;
|
||||
transition: opacity 500ms;
|
||||
}
|
||||
|
||||
.loading-wrap:hover .loading-hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.loading-clicked:hover .loading-hover,
|
||||
.loading-clicked .loading-hover {
|
||||
opacity: 0;
|
||||
}
|
||||
.loading-content {
|
||||
position: relative;
|
||||
background-color: #000;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
transition: 0.6s;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.loading-content-in {
|
||||
position: relative;
|
||||
width: var(--Lsize);
|
||||
overflow: hidden;
|
||||
}
|
||||
.loading-content2 {
|
||||
position: relative;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
width: var(--Lsize);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
text-align: center;
|
||||
transition: 1s;
|
||||
max-width: var(--Lsize);
|
||||
}
|
||||
.loading-clicked .loading-content2 {
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.loading-content span {
|
||||
font-weight: 300;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.loading-box {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translate(100%, -50%);
|
||||
width: 15px;
|
||||
height: 25px;
|
||||
background-color: white;
|
||||
animation: blink 1s linear infinite;
|
||||
}
|
||||
.loading-icon {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
transition-delay: 0.5s;
|
||||
}
|
||||
|
||||
.loading-complete .loading-icon {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
.loading-clicked .loading-icon {
|
||||
transition-delay: 0s;
|
||||
transition: 1s;
|
||||
transform: translateX(200px);
|
||||
}
|
||||
.loading-clicked .loading-content2 {
|
||||
overflow: visible;
|
||||
}
|
||||
.loading-clicked .loading-content2 span {
|
||||
transition: 1s;
|
||||
transform: translateY(100px);
|
||||
opacity: 0;
|
||||
}
|
||||
.loading-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: var(--Lsize);
|
||||
/* height: 45px; */
|
||||
top: 50%;
|
||||
transition: 1s;
|
||||
left: 50px;
|
||||
z-index: 9;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.loading-complete .loading-container {
|
||||
max-width: 0px;
|
||||
}
|
||||
.loading-header {
|
||||
width: var(--cWidth);
|
||||
max-width: var(--cMaxWidth);
|
||||
position: fixed;
|
||||
z-index: 9999999999;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 0px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 0;
|
||||
color: var(--backgroundColor);
|
||||
}
|
||||
.loader-title {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
75% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.loading-complete .loading-box {
|
||||
animation: blinkDone 0.3s forwards;
|
||||
animation-delay: 1s;
|
||||
opacity: 1;
|
||||
}
|
||||
@keyframes blinkDone {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loaderGame-container {
|
||||
width: 200px;
|
||||
transition: 0.3s;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transform: scale(0.4);
|
||||
transform-origin: top right;
|
||||
}
|
||||
.loader-out .loaderGame-container {
|
||||
opacity: 0;
|
||||
}
|
||||
.loaderGame-in {
|
||||
width: 1200px;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
left: 0;
|
||||
animation: loaderGame 7s linear infinite;
|
||||
}
|
||||
@keyframes loaderGame {
|
||||
0% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
}
|
||||
.loaderGame-line {
|
||||
float: left;
|
||||
margin: 0px 20px;
|
||||
margin-bottom: 40px;
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 60px;
|
||||
background-color: #000;
|
||||
display: block;
|
||||
}
|
||||
.loaderGame-line:nth-child(2n) {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.loaderGame-ball {
|
||||
position: absolute;
|
||||
left: 20%;
|
||||
top: 0%;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
background-color: #a87cff;
|
||||
animation: ball25 7s infinite;
|
||||
transform: translateY(10px);
|
||||
animation-timing-function: cubic-bezier(0.3, 1.18, 0.63, 1.28);
|
||||
}
|
||||
.loading-marquee {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--backgroundColor);
|
||||
font-size: 60px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.loading-marquee span {
|
||||
padding: 0px 50px;
|
||||
position: relative;
|
||||
}
|
||||
.loading-marquee span::before {
|
||||
content: "";
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--backgroundColor);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
border-radius: 50px;
|
||||
left: 0px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@keyframes ball25 {
|
||||
0% {
|
||||
transform: translateY(70px);
|
||||
}
|
||||
15% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
30% {
|
||||
transform: translateY(70px);
|
||||
}
|
||||
45% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
67% {
|
||||
transform: translateY(70px);
|
||||
}
|
||||
80% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
90% {
|
||||
transform: translateY(70px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(70px);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1400px) {
|
||||
.loading-wrap {
|
||||
--Lsize: 210px;
|
||||
}
|
||||
.loading-button {
|
||||
padding: 30px 70px;
|
||||
font-size: 25px;
|
||||
}
|
||||
.loading-container {
|
||||
left: 70px;
|
||||
}
|
||||
.loading-marquee {
|
||||
font-size: 100px;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 500px) {
|
||||
.loading-header {
|
||||
padding: 20px 0px;
|
||||
}
|
||||
.loader-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1200px) {
|
||||
.loading-header {
|
||||
padding: 35px 0px;
|
||||
}
|
||||
.loader-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
87
src/components/styles/Navbar.css
Normal file
@@ -0,0 +1,87 @@
|
||||
.header {
|
||||
display: flex;
|
||||
max-width: var(--cMaxWidth);
|
||||
width: var(--cWidth);
|
||||
justify-content: space-between;
|
||||
padding: 20px 0px;
|
||||
margin-bottom: -100px;
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
.header ul {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
column-gap: 40px;
|
||||
row-gap: 8px;
|
||||
align-items: end;
|
||||
}
|
||||
.header ul li {
|
||||
margin-left: 0px;
|
||||
letter-spacing: 1px;
|
||||
color: #ccc;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.navbar-connect {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.navbar-title {
|
||||
font-family: 'Silkscreen', 'Courier New', monospace;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
@media only screen and (min-width: 500px) {
|
||||
.header {
|
||||
padding: 20px 0px;
|
||||
}
|
||||
.header ul {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
.header ul li {
|
||||
color: #eae5ec;
|
||||
}
|
||||
.navbar-title {
|
||||
font-family: 'Silkscreen', 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 900px) {
|
||||
.navbar-connect {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1200px) {
|
||||
.header {
|
||||
padding: 35px 0px;
|
||||
}
|
||||
.header ul {
|
||||
column-gap: 80px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.navbar-connect {
|
||||
font-size: 16px;
|
||||
}
|
||||
.navbar-title {
|
||||
font-family: 'Silkscreen', 'Courier New', monospace;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
103
src/components/styles/SocialIcons.css
Normal file
@@ -0,0 +1,103 @@
|
||||
.icons-section {
|
||||
position: fixed;
|
||||
max-width: var(--cMaxWidth);
|
||||
width: var(--cWidth);
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.social-icons {
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
bottom: 20px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 999;
|
||||
padding: 10px;
|
||||
}
|
||||
.social-icons:hover {
|
||||
transition: 0.3s;
|
||||
color: var(--backgroundColor);
|
||||
}
|
||||
.social-icons a:hover {
|
||||
color: var(--backgroundColor);
|
||||
/* transform: scale(1.2); */
|
||||
}
|
||||
.social-icons span {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
.social-icons a {
|
||||
--siLeft: 50%;
|
||||
--siTop: 50%;
|
||||
position: absolute;
|
||||
left: var(--siLeft, 50%);
|
||||
top: var(--siTop, 50%);
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
font-size: 23px;
|
||||
will-change: left, top;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
.resume-button {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
bottom: 40px;
|
||||
right: 0;
|
||||
width: auto;
|
||||
text-wrap: nowrap;
|
||||
letter-spacing: 4px;
|
||||
font-size: 15px;
|
||||
line-height: 15px;
|
||||
font-weight: 600;
|
||||
color: #5e5e5e;
|
||||
cursor: pointer;
|
||||
transition: 0.5s;
|
||||
transform-origin: left bottom;
|
||||
transform: translateX(100%) rotateZ(-90deg);
|
||||
}
|
||||
.resume-button:hover {
|
||||
color: #fff;
|
||||
}
|
||||
div.resume-button span {
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
margin-top: -1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.check-line {
|
||||
position: fixed;
|
||||
top: 655px;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
z-index: 99999;
|
||||
}
|
||||
@media only screen and (min-width: 900px) {
|
||||
.social-icons {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.social-icons a {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 768px) {
|
||||
.resume-button {
|
||||
transform: none;
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
div.resume-button span {
|
||||
font-size: 23px;
|
||||
margin-top: -1.5px;
|
||||
}
|
||||
}
|
||||
393
src/components/styles/WhatIDo.css
Normal file
@@ -0,0 +1,393 @@
|
||||
.whatIDO {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
opacity: 1;
|
||||
height: 100vh;
|
||||
width: var(--cWidth);
|
||||
max-width: 1920px;
|
||||
margin: auto;
|
||||
z-index: 9;
|
||||
}
|
||||
.what-box {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 9;
|
||||
}
|
||||
.what-box h2 {
|
||||
font-size: calc(4vw + 25px);
|
||||
line-height: calc(4vw + 20px);
|
||||
font-weight: 600;
|
||||
margin-right: 10%;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.hat-h2 {
|
||||
font-style: italic;
|
||||
}
|
||||
.do-h2 {
|
||||
color: var(--accentColor);
|
||||
}
|
||||
.what-box-in {
|
||||
flex-direction: column;
|
||||
height: 500px;
|
||||
margin-left: 200px;
|
||||
position: relative;
|
||||
display: none;
|
||||
}
|
||||
.what-content {
|
||||
width: 450px;
|
||||
height: 33%;
|
||||
min-height: 50%;
|
||||
transition: 0.5s;
|
||||
/* border: 0.5px dashed rgba(255, 255, 255, 0.3); */
|
||||
position: relative;
|
||||
padding: 50px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.what-noTouch:hover,
|
||||
.what-content-active {
|
||||
min-height: 67%;
|
||||
padding: 40px 50px;
|
||||
}
|
||||
|
||||
.what-noTouch:hover ~ .what-content,
|
||||
.what-box-in:hover .what-noTouch:not(:hover),
|
||||
.what-content.what-sibling {
|
||||
min-height: 33%;
|
||||
padding: 10px 50px;
|
||||
}
|
||||
.what-content h3 {
|
||||
font-size: 35px;
|
||||
letter-spacing: 1px;
|
||||
margin: 0;
|
||||
}
|
||||
.what-content p {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-weight: 200;
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
.what-content h4 {
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
margin: 0px;
|
||||
font-size: 14px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.what-content-in {
|
||||
opacity: 0;
|
||||
animation: whatFlicker 0.5s 1 forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes whatFlicker {
|
||||
0%,
|
||||
25%,
|
||||
35%,
|
||||
60% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
40%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.what-content::before,
|
||||
.what-corner::before,
|
||||
.what-content::after,
|
||||
.what-corner::after {
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
border: 4px solid #fff;
|
||||
opacity: 0;
|
||||
animation: whatCorners 0.2s 1 forwards;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
@keyframes whatCorners {
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.what-content::before {
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
.what-corner::before {
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
border-left: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
.what-content::after {
|
||||
bottom: -2px;
|
||||
left: -2px;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
}
|
||||
.what-corner::after {
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
.what-arrow {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
.what-arrow::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
border-left: 1px solid #fff;
|
||||
border-bottom: 1px solid #fff;
|
||||
transition: 0.5s;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
.what-noTouch:hover .what-arrow::before,
|
||||
.what-content-active .what-arrow::before {
|
||||
transform: translate(-50%, -20%) rotate(-225deg);
|
||||
}
|
||||
.what-border1 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
height: 100%;
|
||||
transition: 0.5s;
|
||||
max-width: 0%;
|
||||
overflow: hidden;
|
||||
opacity: 0.8;
|
||||
animation: whatBorders 1.2s 1 forwards;
|
||||
}
|
||||
.what-border1 svg {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 450px;
|
||||
}
|
||||
.what-border2 {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
height: 100%;
|
||||
max-height: 0%;
|
||||
overflow: hidden;
|
||||
transition: 0.5s;
|
||||
opacity: 0.8;
|
||||
animation: whatBorders 1.2s 1 forwards;
|
||||
}
|
||||
.what-border2 svg {
|
||||
height: 500px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
position: absolute;
|
||||
}
|
||||
.what-content-in {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.what-content-in h5 {
|
||||
font-weight: 300;
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
font-family: "Geist", sans-serif;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@keyframes whatBorders {
|
||||
80% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
.what-content-flex {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.what-tags {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
padding: 2px 7px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid #ffffff50;
|
||||
border-radius: 30px;
|
||||
}
|
||||
@media only screen and (max-width: 1600px) {
|
||||
.what-box h2 {
|
||||
margin-right: 18%;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.what-box h2 {
|
||||
margin-right: 20%;
|
||||
}
|
||||
.what-box-in {
|
||||
height: 400px;
|
||||
}
|
||||
.what-content h3 {
|
||||
font-size: 28px;
|
||||
}
|
||||
.what-content {
|
||||
padding: 30px;
|
||||
width: 400px;
|
||||
}
|
||||
.what-content p {
|
||||
font-size: 13px;
|
||||
}
|
||||
.what-noTouch:hover,
|
||||
.what-content-active {
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
.what-noTouch:hover ~ .what-content,
|
||||
.what-box-in:hover .what-noTouch:not(:hover),
|
||||
.what-content.what-sibling {
|
||||
padding: 10px 30px;
|
||||
}
|
||||
.what-tags {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.what-box-in {
|
||||
margin-left: 50px;
|
||||
}
|
||||
.what-content {
|
||||
width: 380px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1024px) {
|
||||
.whatIDO {
|
||||
height: auto;
|
||||
padding: 50px 0px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.what-box-in {
|
||||
height: 500px;
|
||||
margin-left: -50px;
|
||||
}
|
||||
.what-content {
|
||||
padding: 50px;
|
||||
width: 500px;
|
||||
}
|
||||
.what-content p {
|
||||
font-size: 14px;
|
||||
}
|
||||
.what-noTouch:hover,
|
||||
.what-content-active {
|
||||
min-height: 67%;
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
.what-noTouch:hover ~ .what-content,
|
||||
.what-box-in:hover .what-noTouch:not(:hover),
|
||||
.what-content.what-sibling {
|
||||
min-height: 33%;
|
||||
padding: 10px 50px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 900px) {
|
||||
.whatIDO {
|
||||
flex-direction: column;
|
||||
}
|
||||
.what-box h2 {
|
||||
margin: 50px 0;
|
||||
font-size: 55px;
|
||||
line-height: 53px;
|
||||
}
|
||||
.what-box:first-child {
|
||||
justify-content: left;
|
||||
}
|
||||
.what-box:last-child {
|
||||
height: 500px;
|
||||
}
|
||||
.what-box {
|
||||
width: 500px;
|
||||
max-width: calc(100% - 50px);
|
||||
margin: auto;
|
||||
}
|
||||
.what-content {
|
||||
width: 100%;
|
||||
}
|
||||
.what-box-in {
|
||||
margin-left: 0px;
|
||||
height: 450px;
|
||||
}
|
||||
.what-content h5,
|
||||
.what-content-flex {
|
||||
opacity: 0;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.what-noTouch:hover h5,
|
||||
.what-content-active h5,
|
||||
.what-noTouch:hover .what-content-flex,
|
||||
.what-content-active .what-content-flex {
|
||||
opacity: 1;
|
||||
}
|
||||
.what-content {
|
||||
padding: 30px;
|
||||
}
|
||||
.what-content p {
|
||||
font-size: 11px;
|
||||
}
|
||||
.what-noTouch:hover,
|
||||
.what-content-active {
|
||||
padding: 10px 30px;
|
||||
}
|
||||
.what-tags {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.what-noTouch:hover ~ .what-content,
|
||||
.what-box-in:hover .what-noTouch:not(:hover),
|
||||
.what-content.what-sibling {
|
||||
padding: 5px 30px;
|
||||
}
|
||||
.what-content h3 {
|
||||
font-size: 25px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 550px) {
|
||||
.whatIDO {
|
||||
place-items: inherit;
|
||||
align-items: start;
|
||||
justify-content: left;
|
||||
}
|
||||
.what-box {
|
||||
max-width: calc(100% - 25px);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1950px) {
|
||||
.what-box h2 {
|
||||
font-size: 7rem;
|
||||
line-height: 6.8rem;
|
||||
}
|
||||
}
|
||||
241
src/components/styles/Work.css
Normal file
@@ -0,0 +1,241 @@
|
||||
.work-section h2 {
|
||||
margin-top: 100px;
|
||||
font-size: 70px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.work-section h2 > span {
|
||||
color: var(--accentColor);
|
||||
}
|
||||
.work-section {
|
||||
transition: 0s;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
will-change: transform;
|
||||
}
|
||||
.work-container {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
align-content: stretch;
|
||||
}
|
||||
.work-flex {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
margin-left: -80px;
|
||||
padding-right: 120px;
|
||||
position: relative;
|
||||
}
|
||||
.work-box {
|
||||
padding: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 600px;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #363636;
|
||||
flex-shrink: 0;
|
||||
gap: 50px;
|
||||
justify-content: start;
|
||||
}
|
||||
.work-flex .work-box:nth-child(even) {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.work-flex::before,
|
||||
.work-flex::after {
|
||||
content: "";
|
||||
width: calc(50000vw);
|
||||
height: 1px;
|
||||
background-color: #363636;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.work-flex::after {
|
||||
top: 100%;
|
||||
}
|
||||
.work-title {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.work-title > div {
|
||||
text-align: right;
|
||||
}
|
||||
.work-title h3 {
|
||||
font-size: 50px;
|
||||
line-height: 50px;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.work-info h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
.work-info p {
|
||||
font-weight: 200;
|
||||
color: #adacac;
|
||||
margin: 0;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.work-info > p {
|
||||
width: 90%;
|
||||
}
|
||||
.work-image {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.work-image::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
background: radial-gradient(circle at center, rgba(162, 124, 255, 0.6), transparent 60%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
filter: blur(20px);
|
||||
}
|
||||
.work-image:hover::before,
|
||||
.work-image a:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.work-image-in {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.work-link {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
background-color: var(--backgroundColor);
|
||||
width: 50px;
|
||||
border-radius: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 25px;
|
||||
box-shadow: 0px 0px 10px 0px rgba(255, 255, 255, 0.5),
|
||||
inset 0px 0px 10px 0px #393939;
|
||||
transition: 0.3s;
|
||||
opacity: 0;
|
||||
}
|
||||
.work-image a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.work-image a:hover .work-link {
|
||||
opacity: 1;
|
||||
}
|
||||
.work-image video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #000;
|
||||
object-fit: cover;
|
||||
}
|
||||
.work-image img {
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
object-fit: fill;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.work-image:hover img,
|
||||
.work-image a:hover img {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
@media only screen and (max-height: 900px) {
|
||||
.work-image img {
|
||||
height: 250px;
|
||||
}
|
||||
.work-box {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.work-section h2 {
|
||||
font-size: 60px;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 70px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.work-title h3 {
|
||||
font-size: 35px;
|
||||
}
|
||||
.work-info p {
|
||||
font-size: 13px;
|
||||
}
|
||||
.work-info h4 {
|
||||
font-size: 15px;
|
||||
}
|
||||
.work-box {
|
||||
width: 450px;
|
||||
padding: 50px;
|
||||
}
|
||||
.work-flex {
|
||||
margin-left: -50px;
|
||||
padding-right: 75px;
|
||||
}
|
||||
.work-section h2 {
|
||||
font-size: 50px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.work-box {
|
||||
width: 350px;
|
||||
padding: 30px;
|
||||
}
|
||||
.work-flex {
|
||||
margin-left: -30px;
|
||||
padding-right: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 650px) {
|
||||
.work-image img {
|
||||
height: 200px;
|
||||
}
|
||||
.work-section h2 {
|
||||
font-size: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.work-box {
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
/* @media only screen and (max-width: 900px) {
|
||||
.work-image img {
|
||||
max-height: 200px;
|
||||
}
|
||||
.work-section h2 {
|
||||
font-size: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.work-box {
|
||||
gap: 20px;
|
||||
}
|
||||
} */
|
||||
@media only screen and (max-width: 1025px) {
|
||||
.work-container {
|
||||
align-content: normal;
|
||||
}
|
||||
.work-flex {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
20
src/components/styles/style.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.hover-link {
|
||||
position: relative;
|
||||
display: flex;
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hover-in {
|
||||
position: relative;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.hover-in div {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
}
|
||||
.hover-link:hover .hover-in {
|
||||
transform: translateY(-100%);
|
||||
color: var(--accentColor);
|
||||
}
|
||||
191
src/components/utils/GsapScroll.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import * as THREE from "three";
|
||||
import gsap from "gsap";
|
||||
|
||||
export function setCharTimeline(
|
||||
character: THREE.Object3D<THREE.Object3DEventMap> | null,
|
||||
camera: THREE.PerspectiveCamera
|
||||
) {
|
||||
let intensity: number = 0;
|
||||
setInterval(() => {
|
||||
intensity = Math.random();
|
||||
}, 200);
|
||||
const tl1 = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".landing-section",
|
||||
start: "top top",
|
||||
end: "bottom top",
|
||||
scrub: true,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
const tl2 = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".about-section",
|
||||
start: "center 55%",
|
||||
end: "bottom top",
|
||||
scrub: true,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
const tl3 = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".whatIDO",
|
||||
start: "top top",
|
||||
end: "bottom top",
|
||||
scrub: true,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
let screenLight: any, monitor: any;
|
||||
character?.children.forEach((object: any) => {
|
||||
if (object.name === "Plane004") {
|
||||
object.children.forEach((child: any) => {
|
||||
child.material.transparent = true;
|
||||
child.material.opacity = 0;
|
||||
if (child.material.name === "Material.027") {
|
||||
monitor = child;
|
||||
child.material.color.set("#FFFFFF");
|
||||
}
|
||||
});
|
||||
}
|
||||
if (object.name === "screenlight") {
|
||||
object.material.transparent = true;
|
||||
object.material.opacity = 0;
|
||||
object.material.emissive.set("#C8BFFF");
|
||||
gsap.timeline({ repeat: -1, repeatRefresh: true }).to(object.material, {
|
||||
emissiveIntensity: () => intensity * 8,
|
||||
duration: () => Math.random() * 0.6,
|
||||
delay: () => Math.random() * 0.1,
|
||||
});
|
||||
screenLight = object;
|
||||
}
|
||||
});
|
||||
let neckBone = character?.getObjectByName("spine005");
|
||||
if (window.innerWidth > 1024) {
|
||||
if (character) {
|
||||
tl1
|
||||
.fromTo(character.rotation, { y: 0 }, { y: 0.7, duration: 1 }, 0)
|
||||
.to(camera.position, { z: 22 }, 0)
|
||||
.fromTo(".character-model", { x: 0 }, { x: "-25%", duration: 1 }, 0)
|
||||
.to(".landing-container", { opacity: 0, duration: 0.4 }, 0)
|
||||
.to(".landing-container", { y: "40%", duration: 0.8 }, 0)
|
||||
.fromTo(".about-me", { y: "-50%" }, { y: "0%" }, 0);
|
||||
|
||||
tl2
|
||||
.to(
|
||||
camera.position,
|
||||
{ z: 75, y: 8.4, duration: 6, delay: 2, ease: "power3.inOut" },
|
||||
0
|
||||
)
|
||||
.to(".about-section", { y: "30%", duration: 6 }, 0)
|
||||
.to(".about-section", { opacity: 0, delay: 3, duration: 2 }, 0)
|
||||
.fromTo(
|
||||
".character-model",
|
||||
{ pointerEvents: "inherit" },
|
||||
{ pointerEvents: "none", x: "-12%", delay: 2, duration: 5 },
|
||||
0
|
||||
)
|
||||
.to(character.rotation, { y: 0.92, x: 0.12, delay: 3, duration: 3 }, 0)
|
||||
.to(neckBone!.rotation, { x: 0.6, delay: 2, duration: 3 }, 0)
|
||||
.to(monitor.material, { opacity: 1, duration: 0.8, delay: 3.2 }, 0)
|
||||
.to(screenLight.material, { opacity: 1, duration: 0.8, delay: 4.5 }, 0)
|
||||
.fromTo(
|
||||
".what-box-in",
|
||||
{ display: "none" },
|
||||
{ display: "flex", duration: 0.1, delay: 6 },
|
||||
0
|
||||
)
|
||||
.fromTo(
|
||||
monitor.position,
|
||||
{ y: -10, z: 2 },
|
||||
{ y: 0, z: 0, delay: 1.5, duration: 3 },
|
||||
0
|
||||
)
|
||||
.fromTo(
|
||||
".character-rim",
|
||||
{ opacity: 1, scaleX: 1.4 },
|
||||
{ opacity: 0, scale: 0, y: "-70%", duration: 5, delay: 2 },
|
||||
0.3
|
||||
);
|
||||
|
||||
tl3
|
||||
.fromTo(
|
||||
".character-model",
|
||||
{ y: "0%" },
|
||||
{ y: "-100%", duration: 4, ease: "none", delay: 1 },
|
||||
0
|
||||
)
|
||||
.fromTo(".whatIDO", { y: 0 }, { y: "15%", duration: 2 }, 0)
|
||||
.to(character.rotation, { x: -0.04, duration: 2, delay: 1 }, 0);
|
||||
}
|
||||
} else {
|
||||
if (character) {
|
||||
const tM2 = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".what-box-in",
|
||||
start: "top 70%",
|
||||
end: "bottom top",
|
||||
},
|
||||
});
|
||||
tM2.to(".what-box-in", { display: "flex", duration: 0.1, delay: 0 }, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAllTimeline() {
|
||||
const careerTimeline = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".career-section",
|
||||
start: "top 30%",
|
||||
end: "100% center",
|
||||
scrub: true,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
careerTimeline
|
||||
.fromTo(
|
||||
".career-timeline",
|
||||
{ maxHeight: "10%" },
|
||||
{ maxHeight: "100%", duration: 0.5 },
|
||||
0
|
||||
)
|
||||
|
||||
.fromTo(
|
||||
".career-timeline",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, duration: 0.1 },
|
||||
0
|
||||
)
|
||||
.fromTo(
|
||||
".career-info-box",
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1, stagger: 0.1, duration: 0.5 },
|
||||
0
|
||||
)
|
||||
.fromTo(
|
||||
".career-dot",
|
||||
{ animationIterationCount: "infinite" },
|
||||
{
|
||||
animationIterationCount: "1",
|
||||
delay: 0.3,
|
||||
duration: 0.1,
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
if (window.innerWidth > 1024) {
|
||||
careerTimeline.fromTo(
|
||||
".career-section",
|
||||
{ y: 0 },
|
||||
{ y: "20%", duration: 0.5, delay: 0.2 },
|
||||
0
|
||||
);
|
||||
} else {
|
||||
careerTimeline.fromTo(
|
||||
".career-section",
|
||||
{ y: 0 },
|
||||
{ y: 0, duration: 0.5, delay: 0.2 },
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
136
src/components/utils/initialFX.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { SplitText } from "gsap/dist/SplitText";
|
||||
import gsap from "gsap";
|
||||
import { smoother } from "../Navbar";
|
||||
|
||||
export function initialFX() {
|
||||
document.body.style.overflowY = "auto";
|
||||
smoother.paused(false);
|
||||
document.getElementsByTagName("main")[0].classList.add("main-active");
|
||||
gsap.to("body", {
|
||||
backgroundColor: "#0b080c",
|
||||
duration: 0.5,
|
||||
delay: 1,
|
||||
});
|
||||
|
||||
var landingText = new SplitText(
|
||||
[".landing-info h3", ".landing-intro h2", ".landing-intro h1"],
|
||||
{
|
||||
type: "chars,lines",
|
||||
linesClass: "split-line",
|
||||
}
|
||||
);
|
||||
gsap.fromTo(
|
||||
landingText.chars,
|
||||
{ opacity: 0, y: 80, filter: "blur(5px)" },
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 1.2,
|
||||
filter: "blur(0px)",
|
||||
ease: "power3.inOut",
|
||||
y: 0,
|
||||
stagger: 0.025,
|
||||
delay: 0.3,
|
||||
}
|
||||
);
|
||||
|
||||
let TextProps = { type: "chars,lines", linesClass: "split-h2" };
|
||||
|
||||
var landingText2 = new SplitText(".landing-h2-info", TextProps);
|
||||
gsap.fromTo(
|
||||
landingText2.chars,
|
||||
{ opacity: 0, y: 80, filter: "blur(5px)" },
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 1.2,
|
||||
filter: "blur(0px)",
|
||||
ease: "power3.inOut",
|
||||
y: 0,
|
||||
stagger: 0.025,
|
||||
delay: 0.3,
|
||||
}
|
||||
);
|
||||
|
||||
gsap.fromTo(
|
||||
".landing-info-h2",
|
||||
{ opacity: 0, y: 30 },
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 1.2,
|
||||
ease: "power1.inOut",
|
||||
y: 0,
|
||||
delay: 0.8,
|
||||
}
|
||||
);
|
||||
gsap.fromTo(
|
||||
[".header", ".icons-section", ".nav-fade"],
|
||||
{ opacity: 0 },
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 1.2,
|
||||
ease: "power1.inOut",
|
||||
delay: 0.1,
|
||||
}
|
||||
);
|
||||
|
||||
var landingText3 = new SplitText(".landing-h2-info-1", TextProps);
|
||||
var landingText4 = new SplitText(".landing-h2-1", TextProps);
|
||||
var landingText5 = new SplitText(".landing-h2-2", TextProps);
|
||||
|
||||
LoopText(landingText2, landingText3);
|
||||
LoopText(landingText4, landingText5);
|
||||
}
|
||||
|
||||
function LoopText(Text1: SplitText, Text2: SplitText) {
|
||||
var tl = gsap.timeline({ repeat: -1, repeatDelay: 1 });
|
||||
const delay = 4;
|
||||
const delay2 = delay * 2 + 1;
|
||||
|
||||
tl.fromTo(
|
||||
Text2.chars,
|
||||
{ opacity: 0, y: 80 },
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 1.2,
|
||||
ease: "power3.inOut",
|
||||
y: 0,
|
||||
stagger: 0.1,
|
||||
delay: delay,
|
||||
},
|
||||
0
|
||||
)
|
||||
.fromTo(
|
||||
Text1.chars,
|
||||
{ y: 80 },
|
||||
{
|
||||
duration: 1.2,
|
||||
ease: "power3.inOut",
|
||||
y: 0,
|
||||
stagger: 0.1,
|
||||
delay: delay2,
|
||||
},
|
||||
1
|
||||
)
|
||||
.fromTo(
|
||||
Text1.chars,
|
||||
{ y: 0 },
|
||||
{
|
||||
y: -80,
|
||||
duration: 1.2,
|
||||
ease: "power3.inOut",
|
||||
stagger: 0.1,
|
||||
delay: delay,
|
||||
},
|
||||
0
|
||||
)
|
||||
.to(
|
||||
Text2.chars,
|
||||
{
|
||||
y: -80,
|
||||
duration: 1.2,
|
||||
ease: "power3.inOut",
|
||||
stagger: 0.1,
|
||||
delay: delay2,
|
||||
},
|
||||
1
|
||||
);
|
||||
}
|
||||
80
src/components/utils/splitText.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import { ScrollSmoother } from "gsap/dist/ScrollSmoother";
|
||||
import { SplitText } from "gsap/dist/SplitText";
|
||||
|
||||
interface ParaElement extends HTMLElement {
|
||||
anim?: gsap.core.Animation;
|
||||
split?: SplitText;
|
||||
}
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger, ScrollSmoother, SplitText);
|
||||
|
||||
export default function setSplitText() {
|
||||
ScrollTrigger.config({ ignoreMobileResize: true });
|
||||
if (window.innerWidth < 900) return;
|
||||
const paras: NodeListOf<ParaElement> = document.querySelectorAll(".para");
|
||||
const titles: NodeListOf<ParaElement> = document.querySelectorAll(".title");
|
||||
|
||||
const TriggerStart = window.innerWidth <= 1024 ? "top 60%" : "20% 60%";
|
||||
const ToggleAction = "play pause resume reverse";
|
||||
|
||||
paras.forEach((para: ParaElement) => {
|
||||
para.classList.add("visible");
|
||||
if (para.anim) {
|
||||
para.anim.progress(1).kill();
|
||||
para.split?.revert();
|
||||
}
|
||||
|
||||
para.split = new SplitText(para, {
|
||||
type: "lines,words",
|
||||
linesClass: "split-line",
|
||||
});
|
||||
|
||||
para.anim = gsap.fromTo(
|
||||
para.split.words,
|
||||
{ autoAlpha: 0, y: 80 },
|
||||
{
|
||||
autoAlpha: 1,
|
||||
scrollTrigger: {
|
||||
trigger: para.parentElement?.parentElement,
|
||||
toggleActions: ToggleAction,
|
||||
start: TriggerStart,
|
||||
},
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
y: 0,
|
||||
stagger: 0.02,
|
||||
}
|
||||
);
|
||||
});
|
||||
titles.forEach((title: ParaElement) => {
|
||||
if (title.anim) {
|
||||
title.anim.progress(1).kill();
|
||||
title.split?.revert();
|
||||
}
|
||||
title.split = new SplitText(title, {
|
||||
type: "chars,lines",
|
||||
linesClass: "split-line",
|
||||
});
|
||||
title.anim = gsap.fromTo(
|
||||
title.split.chars,
|
||||
{ autoAlpha: 0, y: 80, rotate: 10 },
|
||||
{
|
||||
autoAlpha: 1,
|
||||
scrollTrigger: {
|
||||
trigger: title.parentElement?.parentElement,
|
||||
toggleActions: ToggleAction,
|
||||
start: TriggerStart,
|
||||
},
|
||||
duration: 0.8,
|
||||
ease: "power2.inOut",
|
||||
y: 0,
|
||||
rotate: 0,
|
||||
stagger: 0.03,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
ScrollTrigger.addEventListener("refresh", () => setSplitText());
|
||||
}
|
||||
43
src/context/LoadingProvider.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import Loading from "../components/Loading";
|
||||
|
||||
interface LoadingType {
|
||||
isLoading: boolean;
|
||||
setIsLoading: (state: boolean) => void;
|
||||
setLoading: (percent: number) => void;
|
||||
}
|
||||
|
||||
export const LoadingContext = createContext<LoadingType | null>(null);
|
||||
|
||||
export const LoadingProvider = ({ children }: PropsWithChildren) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(0);
|
||||
|
||||
const value = {
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
setLoading,
|
||||
};
|
||||
useEffect(() => {}, [loading]);
|
||||
|
||||
return (
|
||||
<LoadingContext.Provider value={value as LoadingType}>
|
||||
{isLoading && <Loading percent={loading} />}
|
||||
<main className="main-body">{children}</main>
|
||||
</LoadingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLoading = () => {
|
||||
const context = useContext(LoadingContext);
|
||||
if (!context) {
|
||||
throw new Error("useLoading must be used within a LoadingProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
58
src/data/boneData.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export const typingBoneNames = [
|
||||
"thighL",
|
||||
"thighR",
|
||||
// "footL",
|
||||
// "footR",
|
||||
"shinL",
|
||||
"shinR",
|
||||
"forearmL",
|
||||
"forearmR",
|
||||
"handL",
|
||||
"handR",
|
||||
"f_pinky03R",
|
||||
"f_pinky02L",
|
||||
"f_pinky02R",
|
||||
"f_pinky01L",
|
||||
"f_pinky01R",
|
||||
"palm04L",
|
||||
"palm04R",
|
||||
"f_ring01L",
|
||||
"thumb01L",
|
||||
"thumb01R",
|
||||
"thumb03L",
|
||||
"thumb03R",
|
||||
"palm02L",
|
||||
"palm02R",
|
||||
"palm01L",
|
||||
"palm01R",
|
||||
"f_index01L",
|
||||
"f_index01R",
|
||||
"palm03L",
|
||||
"palm03R",
|
||||
"f_ring02L",
|
||||
"f_ring02R",
|
||||
"f_ring01R",
|
||||
"f_ring03L",
|
||||
"f_ring03R",
|
||||
"f_middle01L",
|
||||
"f_middle02L",
|
||||
"f_middle03L",
|
||||
"f_middle01R",
|
||||
"f_middle02R",
|
||||
"f_middle03R",
|
||||
"f_index02L",
|
||||
"f_index03L",
|
||||
"f_index02R",
|
||||
"f_index03R",
|
||||
"thumb02L",
|
||||
"f_pinky03L",
|
||||
"upper_armL",
|
||||
"upper_armR",
|
||||
"thumb02R",
|
||||
"toeL",
|
||||
"heel02L",
|
||||
"toeR",
|
||||
"heel02R",
|
||||
];
|
||||
|
||||
export const eyebrowBoneNames = ["eyebrow_L", "eyebrow_R"];
|
||||
124
src/index.css
Normal file
@@ -0,0 +1,124 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Silkscreen&display=swap");
|
||||
|
||||
:root {
|
||||
font-family: "Geist", sans-serif;
|
||||
|
||||
font-optical-sizing: auto;
|
||||
font-style: normal;
|
||||
line-height: 1.5;
|
||||
scroll-behavior: smooth;
|
||||
color-scheme: light dark;
|
||||
color: #eae5ec;
|
||||
background-color: var(--backgroundColor);
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
--accentColor: #c2a4ff;
|
||||
--backgroundColor: #0b080c;
|
||||
--vh: 100vh;
|
||||
--vh: 100svh;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: "Geist", sans-serif;
|
||||
}
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--accentColor);
|
||||
}
|
||||
main {
|
||||
opacity: 1;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.main-active {
|
||||
opacity: 0;
|
||||
animation: fadeIn 1s 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
background-color: #000;
|
||||
flex-grow: 1;
|
||||
--cWidth: calc(100% - 30px);
|
||||
--cMaxWidth: 1920px;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.main-body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container-main {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
.container1 {
|
||||
width: var(--cWidth);
|
||||
height: var(--vh);
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
.split-line {
|
||||
overflow: hidden;
|
||||
}
|
||||
.split-h2 {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.techstack {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: var(--vh);
|
||||
margin: auto;
|
||||
margin-top: 50px;
|
||||
margin-bottom: -100px;
|
||||
}
|
||||
|
||||
.techstack h2 {
|
||||
font-size: 80px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 120px;
|
||||
left: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
body {
|
||||
--cWidth: 94%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.techstack h2 {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
17
test.js
Normal file
@@ -0,0 +1,17 @@
|
||||
let endListener = false;
|
||||
|
||||
clickElement.addEventListener("click", () => {
|
||||
if (endListener) {
|
||||
endListener = false;
|
||||
} else {
|
||||
endListener = true;
|
||||
}
|
||||
});
|
||||
|
||||
video.addEventListener("ended", () => {
|
||||
if (endListener) return console.log("listener ended");
|
||||
|
||||
//endListener =false then below code runs
|
||||
|
||||
console.log("Video has ended");
|
||||
});
|
||||
24
tsconfig.app.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
1
tsconfig.app.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/about.tsx","./src/components/career.tsx","./src/components/contact.tsx","./src/components/cursor.tsx","./src/components/hoverlinks.tsx","./src/components/landing.tsx","./src/components/loading.tsx","./src/components/maincontainer.tsx","./src/components/navbar.tsx","./src/components/socialicons.tsx","./src/components/techstack.tsx","./src/components/whatido.tsx","./src/components/work.tsx","./src/components/workimage.tsx","./src/components/character/scene.tsx","./src/components/character/exports.ts","./src/components/character/index.tsx","./src/components/character/utils/animationutils.ts","./src/components/character/utils/character.ts","./src/components/character/utils/decrypt.ts","./src/components/character/utils/lighting.ts","./src/components/character/utils/mouseutils.ts","./src/components/character/utils/resizeutils.ts","./src/components/utils/gsapscroll.ts","./src/components/utils/initialfx.ts","./src/components/utils/splittext.ts","./src/context/loadingprovider.tsx","./src/data/bonedata.ts"],"version":"5.6.2"}
|
||||
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
22
tsconfig.node.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
1
tsconfig.node.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./vite.config.ts"],"version":"5.6.2"}
|
||||
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||