Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
1d7c09f539 | |||
f924aac831 | |||
8464df29d7 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.glb filter=lfs diff=lfs merge=lfs -text
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
BIN
Miata_wheel.glb
(Stored with Git LFS)
Normal file
BIN
Miata_wheel.glb
(Stored with Git LFS)
Normal file
Binary file not shown.
36
README.md
36
README.md
@@ -1,36 +0,0 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
@@ -1,16 +0,0 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
29
index.html
Normal file
29
index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Kay Hennig's portfolio" />
|
||||
<meta name="author" content="Kay Hennig" />
|
||||
<meta name="keywords" content="portfolio, web development, design" />
|
||||
<title>Kay Hennig | Portfolio</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#container {
|
||||
width: 100dvw;
|
||||
height: 100dvh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
65
main.js
Normal file
65
main.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as THREE from 'three';
|
||||
import Stats from 'three/addons/libs/stats.module.js';
|
||||
import { RapierPhysics } from 'three/addons/physics/RapierPhysics.js';
|
||||
import { RapierHelper } from 'three/addons/helpers/RapierHelper.js';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
|
||||
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
||||
|
||||
var renderer, camera, scene, car, stats, container;
|
||||
|
||||
(() => {
|
||||
container = document.getElementById('container');
|
||||
|
||||
stats = new Stats();
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100);
|
||||
scene = new THREE.Scene();
|
||||
|
||||
container.appendChild(stats.dom);
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// setup
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
scene.background = new THREE.Color( 0xbfe3dd );
|
||||
|
||||
camera.position.set(0, 1, 5);
|
||||
|
||||
var sun = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||
sun.position.set(10, 10, 10);
|
||||
scene.add(sun);
|
||||
var ambientLight = new THREE.AmbientLight(0x404040);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const loader = new GLTFLoader();
|
||||
loader.load('Miata.glb', (gltf) => {
|
||||
car = gltf.scene;
|
||||
car.position.set(0, 0, 0);
|
||||
car.scale.set(1, 1, 1);
|
||||
scene.add(car);
|
||||
});
|
||||
|
||||
|
||||
window.onresize = function () {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
};
|
||||
|
||||
function loop() {
|
||||
// rotate the car
|
||||
if (car) {
|
||||
car.rotation.y += 0.01;
|
||||
}
|
||||
stats.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
renderer.setAnimationLoop(loop);
|
||||
|
||||
|
||||
})();
|
@@ -1,8 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
6615
package-lock.json
generated
6615
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -1,32 +1,25 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"host": "vite --host"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.kayhennig.de/TheFusion21/Homepage.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"framer-motion": "^12.23.6",
|
||||
"next": "15.4.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
"three": "^0.178.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.2",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
"vite": "^7.0.5"
|
||||
},
|
||||
"author": "Kay Hennig <KayHennig98@gmail.com>",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@@ -1,14 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-gray-900 text-gray-200 antialiased;
|
||||
}
|
||||
html, body {
|
||||
@apply h-full overflow-hidden;
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Kay Hennig | Portfolio",
|
||||
description: "Showcasing the work of Kay Hennig",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { ChevronDoubleDownIcon } from '@heroicons/react/24/solid';
|
||||
import Slide from '@/components/Slide';
|
||||
import Slide1 from '@/components/Slide1';
|
||||
import Slide2 from '@/components/Slide2';
|
||||
import Slide3 from '@/components/Slide3';
|
||||
import Slide4 from '@/components/Slide4';
|
||||
import Slide5 from '@/components/Slide5';
|
||||
|
||||
|
||||
const slides = [
|
||||
<Slide1 key={1} />,
|
||||
<Slide2 key={2} />,
|
||||
//<Slide3 key={3} />,
|
||||
//<Slide4 key={4} />,
|
||||
//<Slide5 key={5} />,
|
||||
]
|
||||
|
||||
export default function Home() {
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [touchStart, setTouchStart] = useState({ x: undefined, y: undefined } as { x: number | undefined, y: number | undefined });
|
||||
const [touchEnd, setTouchEnd] = useState({ x: undefined, y: undefined } as { x: number | undefined, y: number | undefined });
|
||||
|
||||
const nextSlide = useCallback(() => {
|
||||
if (current < slides.length - 1) setCurrent(current + 1);
|
||||
}, [current])
|
||||
|
||||
const prevSlide = useCallback(() => {
|
||||
if (current > 0) setCurrent(current - 1);
|
||||
}, [current])
|
||||
|
||||
const handleTouchStart = useCallback((e: TouchEvent) => {
|
||||
const firstTouch = e.touches[0];
|
||||
setTouchStart({ x: firstTouch.clientX, y: firstTouch.clientY });
|
||||
}, []);
|
||||
|
||||
const handleTouchEnd = useCallback((e: TouchEvent) => {
|
||||
if (!touchStart.x || !touchStart.y || !touchEnd.x || !touchEnd.y) return;
|
||||
|
||||
const xDiff = touchEnd.x - touchStart.x;
|
||||
const yDiff = touchEnd.y - touchStart.y;
|
||||
|
||||
if (Math.abs(xDiff) < Math.abs(yDiff)) {
|
||||
if (yDiff > 0) prevSlide(); // Swipe down
|
||||
else nextSlide(); // Swipe up
|
||||
}
|
||||
|
||||
setTouchStart({ x: undefined, y: undefined });
|
||||
setTouchEnd({ x: undefined, y: undefined });
|
||||
}, [touchStart, touchEnd, prevSlide, nextSlide]);
|
||||
|
||||
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||
setTouchEnd({ x: e.touches[0].clientX, y: e.touches[0].clientY });
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = (e: WheelEvent) => {
|
||||
if (e.deltaY > 0) nextSlide();
|
||||
else prevSlide();
|
||||
}
|
||||
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown') nextSlide();
|
||||
if (e.key === 'ArrowUp') prevSlide();
|
||||
}
|
||||
|
||||
window.addEventListener('wheel', handleScroll);
|
||||
window.addEventListener('keydown', handleKey);
|
||||
window.addEventListener('touchstart', handleTouchStart);
|
||||
window.addEventListener('touchend', handleTouchEnd);
|
||||
window.addEventListener('touchmove', handleTouchMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('wheel', handleScroll);;
|
||||
window.removeEventListener('keydown', handleKey)
|
||||
}
|
||||
}, [current, nextSlide, prevSlide, handleTouchStart, handleTouchEnd, handleTouchMove])
|
||||
|
||||
|
||||
return (
|
||||
<main className="relative h-screen w-screen overflow-hidden">
|
||||
{slides.map((slide, idx) => (
|
||||
<Slide key={idx} index={idx} currentSlide={current}>
|
||||
{slide}
|
||||
</Slide>
|
||||
))}
|
||||
<div className={`absolute bottom-4 left-1/2 transform -translate-y-8 -translate-x-1/2 text-white ${current === slides.length - 1 ? 'hidden' : ''}`}>
|
||||
<ChevronDoubleDownIcon className="w-12 h-12 animate-bounce cursor-pointer" onClick={nextSlide} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type SlideProps = {
|
||||
children: ReactNode
|
||||
index: number
|
||||
currentSlide: number
|
||||
}
|
||||
|
||||
export default function Slide({ children, index, currentSlide }: SlideProps) {
|
||||
const isActive = index === currentSlide
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center text-center gap-2 text-3xl`}
|
||||
initial={{ opacity: 0 }} // Start off-screen
|
||||
// x to left-100% for previous slides, 0 for current, and right-100% for next slides
|
||||
animate={{ y: isActive ? 0 : index < currentSlide ? '-100%' : '100%', opacity: isActive ? 1 : 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
@@ -1,106 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { motion, MotionValue, useTime, useTransform } from 'framer-motion';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const titles = [
|
||||
'Full Stack Developer',
|
||||
'DevOps Engineer',
|
||||
'Game Developer',
|
||||
'Open Source Contributor',
|
||||
'Tech Enthusiast',
|
||||
'Gamer',
|
||||
];
|
||||
|
||||
function Title({ title, progress, range, ys, os }: { title: string, progress: MotionValue<number>, range: number[], ys: string[], os: number[] }) {
|
||||
const y = useTransform(progress, range, ys);
|
||||
const opacity = useTransform(progress, range, os);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="absolute w-full text-center text-4xl font-bold"
|
||||
style={{
|
||||
y,
|
||||
opacity
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Slide1() {
|
||||
const time = useTime();
|
||||
|
||||
|
||||
|
||||
const genRanges = useMemo(() => {
|
||||
const ranges = [];
|
||||
for (let i = 0; i < titles.length+1; i++) {
|
||||
const start = i * 1500 + 100 * i;
|
||||
const end = start + 1500;
|
||||
ranges.push(start, end);
|
||||
}
|
||||
return ranges;
|
||||
}, []);
|
||||
|
||||
const genYs = useMemo(() => {
|
||||
const ys = [];
|
||||
for (let i = 0; i < titles.length; i++) {
|
||||
ys.push([] as string[]);
|
||||
for (let j = 0; j < titles.length+1; j++) {
|
||||
if ((j % titles.length) === i) {
|
||||
ys[i].push('0%', '0%'); // Current title
|
||||
} else if (j < i) {
|
||||
ys[i].push('100%', '100%'); // Previous titles
|
||||
} else if (j == i + 1) {
|
||||
ys[i].push('-100%', '100%'); // Next title
|
||||
} else {
|
||||
ys[i].push('100%', '100%'); // Titles after next
|
||||
}
|
||||
}
|
||||
}
|
||||
return ys;
|
||||
}, []);
|
||||
|
||||
const genOs = useMemo(() => {
|
||||
const os = [];
|
||||
for (let i = 0; i < titles.length; i++) {
|
||||
os.push([] as number[]);
|
||||
for (let j = 0; j < titles.length+1; j++) {
|
||||
if ((j % titles.length) === i) {
|
||||
os[i].push(1, 1); // Current title
|
||||
} else if (j < i) {
|
||||
os[i].push(0, 0); // Previous titles
|
||||
} else if (j == i + 1) {
|
||||
os[i].push(0, 0); // Next title
|
||||
} else {
|
||||
os[i].push(0, 0); // Titles after next
|
||||
}
|
||||
}
|
||||
}
|
||||
return os;
|
||||
}, []);
|
||||
|
||||
const progress = useTransform(time, (t) => ((t * 1) % genRanges[genRanges.length - 2]));
|
||||
|
||||
//
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>I'm Kay</p>
|
||||
<div className="relative flex flex-col h-10 w-full overflow-visible">
|
||||
{titles.map((title, i) => (
|
||||
<Title
|
||||
key={i}
|
||||
title={title}
|
||||
progress={progress}
|
||||
range={genRanges}
|
||||
ys={genYs[i]}
|
||||
os={genOs[i]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
|
||||
export default function Slide2() {
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row items-center lg:items-start justify-center w-full gap-8">
|
||||
<div className="lg:text-right">
|
||||
<h1 className="text-4xl font-bold">Languages</h1>
|
||||
<ul className="mt-4 text-xl">
|
||||
<li>C++</li>
|
||||
<li>C#</li>
|
||||
<li>TypeScript</li>
|
||||
<li>JavaScript</li>
|
||||
<li>SQL</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="lg:text-center">
|
||||
<h1 className="text-4xl font-bold">Frameworks</h1>
|
||||
<ul className="mt-4 text-xl">
|
||||
<li>react/next.js</li>
|
||||
<li>ASP.NET</li>
|
||||
<li>Entity Framework Core</li>
|
||||
<li>Razor</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="lg:text-left">
|
||||
<h1 className="text-4xl font-bold">Tools</h1>
|
||||
<ul className="mt-4 text-xl">
|
||||
<li>Docker</li>
|
||||
<li>Git/Gitea/Github</li>
|
||||
<li>Nginx</li>
|
||||
<li>MySQL/MS SQL</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
|
||||
export default function Slide3() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
|
||||
export default function Slide4() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
|
||||
export default function Slide5() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"next-env.d.ts",
|
||||
"build/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user