Initial commit

This commit is contained in:
2026-01-30 03:53:38 +07:00
commit 42abc57430
85 changed files with 8922 additions and 0 deletions

29
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
public/images/express.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/images/mongo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/images/mysql.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/images/next.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
public/images/next1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
public/images/next2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/images/nextBL.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
public/images/node.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

BIN
public/images/node2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/images/react.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

BIN
public/images/react2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

1
public/models/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.glb filter=lfs diff=lfs merge=lfs -text

Binary file not shown.

BIN
public/models/character.enc Normal file

Binary file not shown.

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

28
src/App.css Normal file
View 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
View 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
View 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
View 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
View 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 &gt;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;

View 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;

View File

View File

@@ -0,0 +1,7 @@
import Scene from "./Scene";
const CharacterModel = () => {
return <Scene />;
};
export default CharacterModel;

View 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;

View 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;

View 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);
};

View 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;

View 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);
}
}
};

View 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();
}

View 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
View 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;

View 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;

View 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
View 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 };
};

View 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
View 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;

View 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;

View 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
View 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
View 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;

View 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;

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}

View 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
);
}
}

View 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
);
}

View 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());
}

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

17
test.js Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

22
tsconfig.node.json Normal file
View 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"]
}

View File

@@ -0,0 +1 @@
{"root":["./vite.config.ts"],"version":"5.6.2"}

7
vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});