Pretty Ui Update

This commit is contained in:
Space-Banane
2026-03-13 00:04:51 +01:00
parent 855cb4affd
commit 4690084dbe
12 changed files with 515 additions and 265 deletions

View File

@@ -26,9 +26,6 @@
<meta name="robots" content="index, follow" /> <meta name="robots" content="index, follow" />
<link rel="canonical" href="https://space.reversed.dev" /> <link rel="canonical" href="https://space.reversed.dev" />
<script defer src="https://not-a-tracker.reversed.dev/script.js"
data-website-id="7d28af45-d984-428e-af79-fb4dc7e91492"></script>
</head> </head>
<body> <body>

View File

@@ -10,9 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@danielgtmn/umami-react": "^1.1.6",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"lucide-react": "^0.577.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"
}, },
"devDependencies": { "devDependencies": {

72
pnpm-lock.yaml generated
View File

@@ -8,15 +8,24 @@ importers:
.: .:
dependencies: dependencies:
'@danielgtmn/umami-react':
specifier: ^1.1.6
version: 1.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.17 specifier: ^4.1.17
version: 4.1.17(rolldown-vite@7.2.5(@types/node@24.10.1)(jiti@2.6.1)) version: 4.1.17(rolldown-vite@7.2.5(@types/node@24.10.1)(jiti@2.6.1))
lucide-react:
specifier: ^0.577.0
version: 0.577.0(react@19.2.0)
react: react:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.0 version: 19.2.0
react-dom: react-dom:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.0(react@19.2.0) version: 19.2.0(react@19.2.0)
react-router-dom:
specifier: ^7.13.1
version: 7.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
tailwindcss: tailwindcss:
specifier: ^4.1.17 specifier: ^4.1.17
version: 4.1.17 version: 4.1.17
@@ -143,6 +152,13 @@ packages:
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@danielgtmn/umami-react@1.1.6':
resolution: {integrity: sha512-vNk1Q8JOjbE1wx1vM5u2E6jPIDSrZJM/mg++ButiVtD56LNENb1cHvWKcQqC9/camx90PrmdKkWaYhVwU8fCEg==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: ^18.2.0 || ^19.0.0
react-dom: ^18.2.0 || ^19.0.0
'@emnapi/core@1.7.1': '@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
@@ -586,6 +602,10 @@ packages:
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -910,6 +930,11 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.577.0:
resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1001,6 +1026,23 @@ packages:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
react-router-dom@7.13.1:
resolution: {integrity: sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
react-router@7.13.1:
resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
peerDependenciesMeta:
react-dom:
optional: true
react@19.2.0: react@19.2.0:
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1073,6 +1115,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1284,6 +1329,11 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
'@danielgtmn/umami-react@1.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
'@emnapi/core@1.7.1': '@emnapi/core@1.7.1':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.1.0 '@emnapi/wasi-threads': 1.1.0
@@ -1725,6 +1775,8 @@ snapshots:
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
cookie@1.1.1: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -2013,6 +2065,10 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lucide-react@0.577.0(react@19.2.0):
dependencies:
react: 19.2.0
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -2090,6 +2146,20 @@ snapshots:
react-refresh@0.18.0: {} react-refresh@0.18.0: {}
react-router-dom@7.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
react-router: 7.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react-router@7.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies:
cookie: 1.1.1
react: 19.2.0
set-cookie-parser: 2.7.2
optionalDependencies:
react-dom: 19.2.0(react@19.2.0)
react@19.2.0: {} react@19.2.0: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
@@ -2140,6 +2210,8 @@ snapshots:
semver@7.7.3: {} semver@7.7.3: {}
set-cookie-parser@2.7.2: {}
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0

View File

@@ -1,23 +1,22 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import UmamiAnalytics from "@danielgtmn/umami-react";
import type { Experience, MiniProject, Project } from "./types"; import type { Experience, MiniProject, Project } from "./types";
import { MiniProjectModal } from "./components/MiniProjectModal"; import { MiniProjectModal } from "./components/MiniProjectModal";
import { ExperienceModal } from "./components/ExperienceModal"; import { ExperienceModal } from "./components/ExperienceModal";
import { ProjectCard } from "./components/ProjectCard"; import { Navbar } from "./components/Navbar";
import { MiniProjectCard } from "./components/MiniProjectCard";
import { ExperienceCard } from "./components/ExperienceCard";
import { getTypeIcon } from "./components/utils";
import { Hero } from "./sections/Hero";
import { About } from "./sections/About";
import { Goals } from "./sections/Goals";
import { InternshipSection } from "./sections/InternshipSection";
import { Contact } from "./sections/Contact";
import { Footer } from "./sections/Footer"; import { Footer } from "./sections/Footer";
// Pages
import { Home } from "./pages/Home";
import { About } from "./pages/AboutPage";
import { Contact } from "./pages/ContactPage";
function App() { function App() {
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [borderStatus, setBorderStatus] = useState("border-gray-700"); const [borderStatus, setBorderStatus] = useState("border-gray-700");
const [glowColor, setGlowColor] = useState("rgba(55, 65, 81, 0.5)"); const [glowColor, setGlowColor] = useState("rgba(55, 65, 81, 0.5)");
const [statusMessage, setStatusMessage] = useState("[🔃🔃🔃]"); const [statusMessage, setStatusMessage] = useState("[??????]");
const [messageIndex, setMessageIndex] = useState(0); const [messageIndex, setMessageIndex] = useState(0);
const [displayMessage, setDisplayMessage] = useState(""); const [displayMessage, setDisplayMessage] = useState("");
const [isScrambling, setIsScrambling] = useState(false); const [isScrambling, setIsScrambling] = useState(false);
@@ -33,30 +32,28 @@ function App() {
const oldUsernames = [ const oldUsernames = [
"getspaced (ingame)", "getspaced (ingame)",
"Space² (alternative)", "Space<EFBFBD> (alternative)",
"Space-Banane (2022-2024)", "Space-Banane (2022-2024)",
]; ];
const rotatingMessages = [ const rotatingMessages = [
"Yelling at the compiler ⁉️", "Yelling at Claude",
"Shipping bug fixes 📦", "Shipping bugs",
"Waiting for CI ✅", "Compiling typescript files... Please wait.",
"No debugger attached 🐞", "Waiting for CI, this might take a while...",
"Yelling at Gemini 🤖", "No debugger attached, but I'm sure it works ?",
"Writing Readme files 📝", "Seems fine, must be a problem with your machine lol",
"Collecting Spotify hours 🎵", "Waiting for pip, this might take a lifetime...",
"Downloading 10 bajillion dependencies 📥", "Collecting Spotify hours, this might take a while...",
"Fighting with package managers 📦", "Fighting with go modules, i lost",
"Considering nuking python from orbit 🐍", "Nuking Python for the 50th time, it's winning",
"Why is uv so cool? 🧊", "Why is uv so cool?",
"I'm hungry 🍔", "Pulling 20-ish GB of node modules, see you next year"
"I swear this worked on my machine 🖥️",
"Pulling 10GB of docker images 🐳",
]; ];
const characters = const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:',.<>?/`~" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:',.<>?/`~" +
"🧱⁉️🧪📦🦀✅🚧🐞🤖🔄🥹🧹🎵"; "?????????????????????????";
useEffect(() => { useEffect(() => {
if (status === "online") { if (status === "online") {
@@ -78,7 +75,7 @@ function App() {
} else { } else {
setBorderStatus("border-gray-700"); setBorderStatus("border-gray-700");
setGlowColor("rgba(55, 65, 81, 0.5)"); setGlowColor("rgba(55, 65, 81, 0.5)");
setStatusMessage("[🔃🔃🔃]"); setStatusMessage("[??????]");
} }
}, [status]); }, [status]);
@@ -123,8 +120,8 @@ function App() {
setIsScrambling(false); setIsScrambling(false);
} }
iteration += 1 / 3; iteration += 1; // Faster: increment by 1 instead of 1/3
}, 25); }, 15); // Faster: reduce interval to 15ms
return () => clearInterval(scrambleInterval); return () => clearInterval(scrambleInterval);
} else { } else {
@@ -169,77 +166,61 @@ function App() {
}, []); }, []);
return ( return (
<div className="relative min-h-screen bg-black text-white selection:bg-purple-500/30"> <Router>
{/* Background Grid */} <UmamiAnalytics
<div className="fixed inset-0 z-0 pointer-events-none"> websiteId="7d28af45-d984-428e-af79-fb4dc7e91492"
<div url="https://not-a-tracker.reversed.dev/script.js"
className="absolute inset-0 blur-sm" />
style={{ <div className="relative min-h-screen bg-[#020205] text-white selection:bg-blue-500/30 font-sans overflow-x-hidden">
backgroundImage: {/* Modern Background */}
"linear-gradient(#ffffff 1px, transparent 1px), linear-gradient(90deg, #ffffff 1px, transparent 1px)", <div className="fixed inset-0 z-0 pointer-events-none">
backgroundSize: "30px 30px", {/* Main Gradient Glows */}
maskImage: <div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] rounded-full bg-blue-600/10 blur-[120px] animate-pulse"></div>
"radial-gradient(circle at center, black 40%, transparent 80%)", <div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] rounded-full bg-purple-600/10 blur-[120px] animate-pulse" style={{ animationDelay: "1s" }}></div>
}}
/>
</div>
<main className="relative z-10 flex flex-col items-center px-6 py-20 mx-auto max-w-5xl gap-24"> {/* Subtle Grid Pattern */}
<Hero <div
glowColor={glowColor} className="absolute inset-0 opacity-[0.03]"
borderStatus={borderStatus} style={{
displayMessage={displayMessage} backgroundImage: `linear-gradient(#fff 1px, transparent 1px), linear-gradient(90deg, #fff 1px, transparent 1px)`,
rotatingMessages={rotatingMessages} backgroundSize: "40px 40px"
statusMessage={statusMessage} }}
showOldNames={showOldNames} ></div>
setShowOldNames={setShowOldNames}
oldUsernames={oldUsernames}
/>
<About /> {/* Noise Overlay */}
<div className="absolute inset-0 opacity-[0.02] mix-blend-overlay pointer-events-none bg-[url('https://grainy-gradients.vercel.app/noise.svg')]"></div>
</div>
<Goals /> <Navbar />
{/* Projects Section */} <main className="relative z-10 pt-32 pb-20 px-6 mx-auto max-w-6xl">
<section className="w-full space-y-12"> <Routes>
<div className="text-center space-y-2"> <Route
<h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-pink-500 to-orange-400"> path="/"
Favourite Projects element={
</h2> <Home
<p className="text-gray-400 max-w-2xl mx-auto"> glowColor={glowColor}
Personal favourites, projects that I'm proud of, or just things I borderStatus={borderStatus}
find fun to show off. displayMessage={displayMessage}
</p> rotatingMessages={rotatingMessages}
</div> statusMessage={statusMessage}
showOldNames={showOldNames}
setShowOldNames={setShowOldNames}
oldUsernames={oldUsernames}
projects={projects}
miniProjects={miniProjects}
setSelectedMiniProject={setSelectedMiniProject}
experiences={experiences}
setSelectedExperience={setSelectedExperience}
/>
}
/>
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</main>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <Footer />
{projects.map((project, index) => (
<ProjectCard key={index} project={project} />
))}
</div>
</section>
{/* Mini Projects Section */}
<section className="w-full space-y-12">
<div className="text-center space-y-2">
<h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 via-blue-500 to-purple-500">
Mini Projects
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">
Small experiments and fun projects I've built along the way
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{miniProjects.map((project, index) => (
<MiniProjectCard
key={index}
project={project}
onClick={() => setSelectedMiniProject(project)}
/>
))}
</div>
</section>
{selectedMiniProject && ( {selectedMiniProject && (
<MiniProjectModal <MiniProjectModal
@@ -254,101 +235,8 @@ function App() {
onClose={() => setSelectedExperience(null)} onClose={() => setSelectedExperience(null)}
/> />
)} )}
</div>
{/* Experience Section */} </Router>
<section className="w-full space-y-8">
<div className="text-center space-y-2">
<h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 via-red-500 to-pink-500">
Experience
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">
Technologies, platforms, and roles I've worked with
</p>
</div>
<div className="max-w-4xl mx-auto space-y-6">
{Object.entries(
experiences.reduce(
(acc, exp) => {
if (!acc[exp.type]) acc[exp.type] = [];
acc[exp.type].push(exp);
return acc;
},
{} as Record<string, Experience[]>,
),
).map(([type, items]) => (
<div key={type} className="space-y-3">
<h3
className={`text-lg font-semibold flex items-center gap-2 ${
type === "language"
? "text-blue-400"
: type === "service"
? "text-purple-400"
: type === "platform"
? "text-green-400"
: type === "real life experience"
? "text-orange-400"
: type === "roles"
? "text-pink-400"
: "text-gray-400"
}`}
>
{getTypeIcon(type as Experience["type"])}{" "}
{type.charAt(0).toUpperCase() + type.slice(1)}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{items.map((exp, index) => (
<ExperienceCard
key={index}
experience={exp}
onClick={() => setSelectedExperience(exp)}
/>
))}
</div>
</div>
))}
</div>
<p className="text-center text-gray-400 mt-4">
And a bunch of stuff i probably also forgot...
</p>
</section>
<InternshipSection />
<Contact />
<Footer />
</main>
{/* Floating Island: Bottom Left (only on localhost) */}
{typeof window !== "undefined" &&
(window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1") && (
<div
className="fixed bottom-4 left-4 z-50 flex flex-col items-start gap-2 bg-gradient-to-br from-purple-900/80 to-black/80 border border-purple-500/30 rounded-2xl shadow-lg px-5 py-3 text-sm animate-fade-in"
style={{ minWidth: 180 }}
>
<div className="flex items-center gap-2">
<span role="img" aria-label="island">
🏝
</span>
<span className="font-semibold text-purple-300">Loaded</span>
</div>
<div className="flex flex-col gap-1 mt-1">
<span className="text-white/90">
Projects: {projects.length}
</span>
<span className="text-white/90">
Mini Projects: {miniProjects.length}
</span>
<span className="text-white/90">
Experiences: {experiences.length}
</span>
</div>
</div>
)}
</div>
); );
} }

38
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { useUmami } from "@danielgtmn/umami-react";
import { Link, useLocation } from "react-router-dom";
export function Navbar() {
const location = useLocation();
const navItems = [
{ label: "Home", path: "/" },
{ label: "About", path: "/about" },
{ label: "Connect", path: "/contact" },
];
const handleSwitch = (path: string) => {
useUmami().track("nav_to_" + path.replaceAll("/", "_"));
};
return (
<nav className="fixed top-8 left-1/2 -translate-x-1/2 z-[100] w-[min(90%,400px)]">
<div className="bg-black/20 backdrop-blur-xl border border-white/10 rounded-full px-6 py-3 flex items-center justify-between gap-4 shadow-2xl">
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
onClick={() => handleSwitch(item.path)}
className={`text-sm font-semibold transition-all duration-300 px-4 py-2 rounded-full
${
location.pathname === item.path
? "bg-white text-black scale-105 shadow-xl shadow-white/10"
: "text-gray-400 hover:text-white"
}`}
>
{item.label}
</Link>
))}
</div>
</nav>
);
}

39
src/pages/AboutPage.tsx Normal file
View File

@@ -0,0 +1,39 @@
import { About as AboutSection } from "../sections/About";
import { Goals } from "../sections/Goals";
export function About() {
return (
<div className="space-y-24 py-12 max-w-4xl mx-auto px-4">
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
About Me
</h1>
<p className="text-gray-400 max-w-2xl mx-auto">
A little bit about me...
</p>
</div>
<AboutSection />
<Goals />
<section className="bg-white/5 border border-white/10 rounded-3xl p-8 backdrop-blur-sm">
<h3 className="text-2xl font-bold mb-4">Interests Outside of Coding</h3>
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4 text-gray-300">
<li className="flex items-center gap-2">
<span className="text-purple-400"></span> 3D Printing & Design
</li>
<li className="flex items-center gap-2">
<span className="text-purple-400"></span> Graphical Design
</li>
<li className="flex items-center gap-2">
<span className="text-purple-400"></span> Gaming & Hardware
</li>
<li className="flex items-center gap-2">
<span className="text-purple-400"></span> Open Source Contribution
</li>
</ul>
</section>
</div>
);
}

38
src/pages/ContactPage.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { Contact as ContactSection } from "../sections/Contact";
export function Contact() {
return (
<div className="flex flex-col items-center justify-center min-h-[70vh] px-4 space-y-12 w-full max-w-4xl mx-auto">
<div className="text-center space-y-4 w-full">
<h1 className="text-5xl md:text-6xl font-extrabold text-white">
Let's <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Connect</span>
</h1>
<p className="text-gray-400 text-xl max-w-xl mx-auto">
Whatever you've got in mind, I'm just a few clicks away.
</p>
</div>
<div className="w-full max-w-3xl">
<ContactSection />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 w-full max-w-4xl text-center">
<div className="bg-white/5 border border-white/10 p-6 rounded-3xl backdrop-blur-sm group hover:border-blue-500/30 transition-all duration-300">
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">📧</div>
<h3 className="font-bold mb-1">Email</h3>
<p className="text-sm text-gray-500">space@reversed.dev</p>
</div>
<div className="bg-white/5 border border-white/10 p-6 rounded-3xl backdrop-blur-sm group hover:border-purple-500/30 transition-all duration-300">
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">👾</div>
<h3 className="font-bold mb-1">Discord</h3>
<p className="text-sm text-gray-500">@getspaced</p>
</div>
<div className="bg-white/5 border border-white/10 p-6 rounded-3xl backdrop-blur-sm group hover:border-pink-500/30 transition-all duration-300">
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">💻</div>
<h3 className="font-bold mb-1">GitHub</h3>
<p className="text-sm text-gray-500">github.com/Space-Banane</p>
</div>
</div>
</div>
);
}

151
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,151 @@
import { Hero } from "../sections/Hero";
import type { MiniProject, Experience, Project } from "../types";
import { ProjectCard } from "../components/ProjectCard";
import { MiniProjectCard } from "../components/MiniProjectCard";
import { ExperienceCard } from "../components/ExperienceCard";
interface HomeProps {
glowColor: string;
borderStatus: string;
displayMessage: string;
rotatingMessages: string[];
statusMessage: string;
showOldNames: boolean;
setShowOldNames: (show: boolean) => void;
oldUsernames: string[];
projects: Project[];
miniProjects: MiniProject[];
setSelectedMiniProject: (p: MiniProject) => void;
experiences: Experience[];
setSelectedExperience: (e: Experience) => void;
}
export function Home({
glowColor,
borderStatus,
displayMessage,
rotatingMessages,
statusMessage,
showOldNames,
setShowOldNames,
oldUsernames,
projects,
miniProjects,
setSelectedMiniProject,
experiences,
setSelectedExperience,
}: HomeProps) {
const groupedExperiences = experiences.reduce(
(acc, exp) => {
if (!acc[exp.type]) {
acc[exp.type] = [];
}
acc[exp.type].push(exp);
return acc;
},
{} as Record<string, Experience[]>,
);
const rewriteType = (type: string) => {
switch (type) {
case "language":
return "Languages";
case "service":
return "Services";
case "platform":
return "Platforms";
case "real life experience":
return "Real Life Experience";
case "roles":
return "Roles";
default:
return type.charAt(0).toUpperCase() + type.slice(1);
}
};
return (
<div className="space-y-24 pb-20">
<Hero
glowColor={glowColor}
borderStatus={borderStatus}
displayMessage={displayMessage}
rotatingMessages={rotatingMessages}
statusMessage={statusMessage}
showOldNames={showOldNames}
setShowOldNames={setShowOldNames}
oldUsernames={oldUsernames}
/>
<section className="w-full max-w-6xl space-y-12">
<div className="text-center space-y-4">
<h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
Featured Projects
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">
A selection of my personal favorites. Many more on my GitHub.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 px-4">
{projects.map((project, index) => (
<ProjectCard key={index} project={project} />
))}
</div>
</section>
<section className="w-full max-w-6xl space-y-12">
<div className="text-center space-y-4">
<h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-500">
Mini Projects
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">
Small tools, experiments, and fun little things I've built.
</p>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 px-4">
{miniProjects.map((mini, index) => (
<MiniProjectCard
key={index}
project={mini}
onClick={() => setSelectedMiniProject(mini)}
/>
))}
</div>
</section>
<section className="w-full max-w-6xl space-y-16">
<div className="text-center space-y-4">
<h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-blue-500">
Experience
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">
My journey through the tech world so far.
</p>
</div>
<div className="space-y-12 px-4">
{Object.entries(groupedExperiences)
.sort(([typeA], [typeB]) => typeA.localeCompare(typeB))
.map(([type, items]) => (
<div key={type} className="space-y-6">
<div className="flex items-center gap-4">
<h3 className="text-xl font-semibold text-gray-200 capitalize whitespace-nowrap">
{rewriteType(type)}
</h3>
<div className="h-px w-full bg-gradient-to-r from-gray-500/20 via-gray-500/10 to-transparent" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{items.map((exp, index) => (
<ExperienceCard
key={index}
experience={exp}
onClick={() => setSelectedExperience(exp)}
/>
))}
</div>
</div>
))}
</div>
</section>
</div>
);
}

View File

@@ -1,6 +1,6 @@
export function Contact() { export function Contact() {
return ( return (
<section className="w-full max-w-2xl text-center space-y-4 pb-8"> <section className="w-full max-w-2xl text-center space-y-4 pb-8 mx-auto">
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 via-blue-500 to-purple-500"> <h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 via-blue-500 to-purple-500">
Want to talk? Want to talk?
</h2> </h2>

View File

@@ -1,38 +1,90 @@
import { useUmami } from "@danielgtmn/umami-react";
import { Github, Mail, MessageSquare } from "lucide-react";
export function Footer() { export function Footer() {
const currentYear = new Date().getFullYear();
const handleLinkClick = (name: string, url: string) => {
useUmami().track(`footer_click_${name.toLowerCase()}`, { url });
};
return ( return (
<footer className="w-full border-t border-white/10 pt-4"> <footer className="w-full border-t border-white/5 py-8 mt-20">
<div className="text-center space-y-3"> <div className="max-w-7xl mx-auto px-6 flex flex-col items-center gap-6">
<div className="flex items-center justify-center gap-2"> {/* Social Icons Row */}
<img <div className="flex items-center gap-6 text-gray-400">
src="http://shsf.reversed.dev/SHSF%20SMALL%20TRANSPARENT.png" <a
alt="SHSF Logo" href="https://github.com/Space-Banane"
className="w-6 h-6" target="_blank"
/> rel="noreferrer"
<p className="text-gray-400 text-sm"> className="hover:text-purple-400 transition-colors"
Partly powered by{" "} aria-label="GitHub"
onClick={() =>
handleLinkClick("GitHub", "https://github.com/Space-Banane")
}
>
<Github className="w-5 h-5" />
</a>
<a
href="mailto:space@reversed.dev"
className="hover:text-purple-400 transition-colors"
aria-label="Email"
onClick={() =>
handleLinkClick("Email", "mailto:space@reversed.dev")
}
>
<Mail className="w-5 h-5" />
</a>
<a
href="https://discord.com/users/456443941169004545"
target="_blank"
rel="noreferrer"
className="hover:text-purple-400 transition-colors"
aria-label="Discord"
onClick={() =>
handleLinkClick(
"Discord",
"https://discord.com/users/456443941169004545",
)
}
>
<MessageSquare className="w-5 h-5" />
</a>
</div>
{/* Branding & Attribution */}
<div className="flex flex-col items-center gap-2 text-center">
<p className="text-gray-500 text-xs tracking-wide">
&copy; {currentYear} &bull; Space-Banane &bull; Love from Space
</p>
<div className="flex items-center gap-3 text-[10px] text-gray-600 uppercase tracking-[0.2em]">
<a <a
href="https://github.com/Space-Banane/shsf" href="https://github.com/Space-Banane/shsf"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-purple-400 hover:text-purple-300 transition-colors" className="hover:text-gray-400 transition-colors"
onClick={() =>
handleLinkClick("SHSF", "https://github.com/Space-Banane/shsf")
}
> >
SHSF SHSF
</a> </a>
</p> <span>&bull;</span>
<a
href="https://gradienty.codes/"
target="_blank"
rel="noreferrer"
className="hover:text-gray-400 transition-colors"
onClick={() =>
handleLinkClick("Gradienty", "https://gradienty.codes/")
}
>
Gradienty
</a>
<span>&bull;</span>
<span>React</span>
</div>
</div> </div>
<p className="text-gray-400 text-sm">Love from Space </p>
<p className="text-gray-500 text-xs">
Designed with Copilot &{" "}
<a
href="https://gradienty.codes/"
target="_blank"
rel="noreferrer"
className="text-purple-400 hover:text-purple-300 transition-colors underline"
>
gradienty.codes
</a>
, Tailwind and React
</p>
</div> </div>
</footer> </footer>
); );

View File

@@ -69,6 +69,22 @@ export function Goals() {
</div> </div>
</div> </div>
</div> </div>
<div className="md:col-span-2 p-6 rounded-2xl bg-gradient-to-br from-yellow-500/10 to-orange-500/5 backdrop-blur-sm border border-yellow-500/20">
<div className="flex items-start gap-4">
<div className="p-3 rounded-lg bg-yellow-500/20 text-3xl">
🏠
</div>
<div>
<h3 className="text-xl font-bold text-white mb-2">
Embrace Self-Hosting
</h3>
<p className="text-gray-400 leading-relaxed">
I want to learn more about self-hosting my own services and reduce reliance on cloud providers for personal projects.
</p>
</div>
</div>
</div>
</div> </div>
</section> </section>
); );

View File

@@ -1,44 +0,0 @@
export function InternshipSection() {
return (
<section className="w-full space-y-8">
<div className="text-center space-y-2">
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-orange-500">
Real Life Experience (ABB Internship)
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">
My internship experience at ABB and what i learned from it
</p>
</div>
<div className="max-w-3xl mx-auto">
<div className="p-6 rounded-2xl bg-gradient-to-br from-yellow-500/10 to-orange-500/5 backdrop-blur-sm border border-yellow-500/20">
<div className="space-y-4 text-gray-300">
<p className="leading-relaxed">
I did an internship at{" "}
<a
href="https://www.abb.com/global/en"
target="_blank"
rel="noreferrer"
className="text-red-500 hover:text-red-600 transition-colors font-semibold"
>
ABB{" "}
</a>
where i got to work on a project that involved internal management
software. It was a great experience that taught me a lot about
working in a professional environment and collaborating with a
team of developers.
</p>
<p className="leading-relaxed">
This was the first propper time i worked with a Framework (Angular) and it was a great learning experience. Which helped me understand why
frameworks are a thing and how they can help structure and organize codebases, especially as they grow larger.
</p>
<p className="leading-relaxed">
Also, this was the first time submitting a PR, which was scary but super rewarding when they merged it. From that point on
i was hooked on Frameworks and OSS contribution. I want to do more of both in the future.
</p>
</div>
</div>
</div>
</section>
);
}