commit db92ad2f883de6f2d285a2e1e34b2a71347e12d705bbbe1fb23c85461056aeb8 Author: unknown Date: Mon May 18 22:26:10 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b84f8b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/dist +/node_modules +package-lock.json \ No newline at end of file diff --git a/assets/bg.png b/assets/bg.png new file mode 100644 index 0000000..1834525 Binary files /dev/null and b/assets/bg.png differ diff --git a/assets/capi.mp3 b/assets/capi.mp3 new file mode 100644 index 0000000..7a364b7 Binary files /dev/null and b/assets/capi.mp3 differ diff --git a/assets/moan.mp3 b/assets/moan.mp3 new file mode 100644 index 0000000..8eee963 Binary files /dev/null and b/assets/moan.mp3 differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..efe2546 --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ + + + + + + + + + depoliticized. + + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..09fd845 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "depoliticized-portfolio", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "gsap": "^3.12.5", + "lenis": "^1.1.13", + "lucide-react": "^0.460.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.15", + "typescript": "^5.6.3", + "vite": "^6.0.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..f47c293 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/pub-pgp.txt b/pub-pgp.txt new file mode 100644 index 0000000..946811c --- /dev/null +++ b/pub-pgp.txt @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZzigpRYJKwYBBAHaRw8BAQdAt3o7m1YIEwDC+wStOTX4FRbkjEFTJ3KiSEC9 +YLcUk+20HWV4Y3VzM3MgPGNvbnRhY3RAc3RlYWxlci53dGY+iJkEExYKAEEWIQRv +xuv7mSWfs1bTXy2ARacLZTIVdwUCZzigpQIbAwUJBaN1CwULCQgHAgIiAgYVCgkI +CwIEFgIDAQIeBwIXgAAKCRCARacLZTIVd5PjAP49C0bBm5Utku41cNmWJ4H+JcJP +FJyO6VCkPc6RlSpn5gEA7mGcOMkAym9ASFShJ1/o7ZZGSBhnFJ2HQx7H0lDEzAq4 +OARnOKClEgorBgEEAZdVAQUBAQdADUszFmROJl5SKOWrEuth432zT4AXav+lqS2q +fPaSfjsDAQgHiH4EGBYKACYWIQRvxuv7mSWfs1bTXy2ARacLZTIVdwUCZzigpQIb +DAUJBaN1CwAKCRCARacLZTIVd8yWAP4nq3mnf/QKmfAouqTjV6hxaPDjAH3x9axk +hGSQOOnhGgD/Zfoc+wr9YA7CfNIcZMsIalsH4RzKwAXtjyIJRea4OAA= +=LFH8 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/public/media/bg.png b/public/media/bg.png new file mode 100644 index 0000000..1834525 Binary files /dev/null and b/public/media/bg.png differ diff --git a/public/media/capi.mp3 b/public/media/capi.mp3 new file mode 100644 index 0000000..7a364b7 Binary files /dev/null and b/public/media/capi.mp3 differ diff --git a/public/media/moan.mp3 b/public/media/moan.mp3 new file mode 100644 index 0000000..8eee963 Binary files /dev/null and b/public/media/moan.mp3 differ diff --git a/public/pub-pgp.txt b/public/pub-pgp.txt new file mode 100644 index 0000000..946811c --- /dev/null +++ b/public/pub-pgp.txt @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZzigpRYJKwYBBAHaRw8BAQdAt3o7m1YIEwDC+wStOTX4FRbkjEFTJ3KiSEC9 +YLcUk+20HWV4Y3VzM3MgPGNvbnRhY3RAc3RlYWxlci53dGY+iJkEExYKAEEWIQRv +xuv7mSWfs1bTXy2ARacLZTIVdwUCZzigpQIbAwUJBaN1CwULCQgHAgIiAgYVCgkI +CwIEFgIDAQIeBwIXgAAKCRCARacLZTIVd5PjAP49C0bBm5Utku41cNmWJ4H+JcJP +FJyO6VCkPc6RlSpn5gEA7mGcOMkAym9ASFShJ1/o7ZZGSBhnFJ2HQx7H0lDEzAq4 +OARnOKClEgorBgEEAZdVAQUBAQdADUszFmROJl5SKOWrEuth432zT4AXav+lqS2q +fPaSfjsDAQgHiH4EGBYKACYWIQRvxuv7mSWfs1bTXy2ARacLZTIVdwUCZzigpQIb +DAUJBaN1CwAKCRCARacLZTIVd8yWAP4nq3mnf/QKmfAouqTjV6hxaPDjAH3x9axk +hGSQOOnhGgD/Zfoc+wr9YA7CfNIcZMsIalsH4RzKwAXtjyIJRea4OAA= +=LFH8 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..647e7dc --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,111 @@ +import { useState, useEffect, useRef, useCallback } from 'react' +import Lenis from 'lenis' +import { gsap } from 'gsap' +import { ScrollTrigger } from 'gsap/ScrollTrigger' + +import LoadingScreen from './components/LoadingScreen' +import Navigation from './components/Navigation' +import HeroSection from './components/HeroSection' +import AboutSection from './components/AboutSection' +import InterestsSection from './components/InterestsSection' +import ProjectsSection from './components/ProjectsSection' +import ExperienceSection from './components/ExperienceSection' +import TopicsSection from './components/TopicsSection' +import ContactSection from './components/ContactSection' +import Footer from './components/Footer' +import FloatingParticles from './components/FloatingParticles' +import ScrollProgress from './components/ScrollProgress' +import NoiseOverlay from './components/NoiseOverlay' +import { useReducedMotion } from './hooks/useReducedMotion' + +gsap.registerPlugin(ScrollTrigger) + +export default function App() { + const [loaded, setLoaded] = useState(false) + const [showContent, setShowContent] = useState(false) + const lenisRef = useRef(null) + const mainRef = useRef(null) + const capiAudioRef = useRef(null) + const reducedMotion = useReducedMotion() + + useEffect(() => { + capiAudioRef.current = new Audio('./media/capi.mp3') + capiAudioRef.current.preload = 'auto' + capiAudioRef.current.loop = true + capiAudioRef.current.volume = 0.25 + }, []) + + useEffect(() => { + if (!loaded) return + + const lenis = new Lenis({ + duration: 1.2, + easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), + smoothWheel: true, + }) + lenisRef.current = lenis + + lenis.on('scroll', ScrollTrigger.update) + + const rafCallback = (time: number) => { + lenis.raf(time * 1000) + } + gsap.ticker.add(rafCallback) + gsap.ticker.lagSmoothing(0) + + return () => { + gsap.ticker.remove(rafCallback) + lenis.destroy() + } + }, [loaded]) + + const handleLoadComplete = useCallback(() => { + setLoaded(true) + // Small delay before showing content for transition + setTimeout(() => { + setShowContent(true) + }, 100) + }, []) + + useEffect(() => { + if (showContent) { + // Start ambient capi.mp3 loop + capiAudioRef.current?.play().catch(() => {}) + } + }, [showContent]) + + useEffect(() => { + if (showContent && mainRef.current && !reducedMotion) { + gsap.fromTo( + mainRef.current, + { opacity: 0, y: 40 }, + { opacity: 1, y: 0, duration: 1.2, ease: 'expo.out' } + ) + } + }, [showContent, reducedMotion]) + + return ( + <> + + + {showContent && ( + <> + + + + +
+ + + + + + + +
+
+ + )} + + ) +} diff --git a/src/components/AboutSection.tsx b/src/components/AboutSection.tsx new file mode 100644 index 0000000..fbd3aff --- /dev/null +++ b/src/components/AboutSection.tsx @@ -0,0 +1,128 @@ +import { useEffect, useRef } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { MapPin, BookOpen, User } from "lucide-react"; +import { useReducedMotion } from "../hooks/useReducedMotion"; + +gsap.registerPlugin(ScrollTrigger); + +export default function AboutSection() { + const sectionRef = useRef(null); + const reducedMotion = useReducedMotion(); + + useEffect(() => { + if (reducedMotion || !sectionRef.current) return; + + const ctx = gsap.context(() => { + gsap.fromTo( + ".about-reveal", + { y: 50, opacity: 0 }, + { + y: 0, + opacity: 1, + duration: 1, + stagger: 0.15, + ease: "expo.out", + scrollTrigger: { + trigger: sectionRef.current, + start: "top 70%", + toggleActions: "play none none none", + }, + }, + ); + }, sectionRef); + + return () => ctx.revert(); + }, [reducedMotion]); + + return ( +
+
+ {/* Section label */} +
+ + 001 + +
+ + Identity + +
+ + {/* Main heading */} +

+ A young adult from +
+ Europe +

+ + {/* Content grid */} +
+ {/* Left column - main text */} +
+

+ I am a curious mind navigating the intersection of technology, + psychology, and philosophy. My work spans from low-level systems + to sophisticated web platforms, always driven by a desire to + understand how things work beneath the surface. +

+

+ Reading is a core part of who I am - it shapes how I think about + problems and approach solutions. I believe in building things that + are both technically sound and thoughtfully designed. +

+
+ + {/* Right column - details */} +
+
+
+
+ +
+
+

+ Location +

+

Europe

+
+
+
+ +
+
+
+ +
+
+

+ Age +

+

Young adult

+
+
+
+ +
+
+
+ +
+
+

+ Passion +

+

Avid reader

+
+
+
+
+
+
+
+ ); +} diff --git a/src/components/ContactSection.tsx b/src/components/ContactSection.tsx new file mode 100644 index 0000000..0aa3e32 --- /dev/null +++ b/src/components/ContactSection.tsx @@ -0,0 +1,258 @@ +import { useEffect, useRef, useState } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { + MessageSquare, + ExternalLink, + Copy, + Check, + Key, + type LucideIcon, +} from "lucide-react"; +import { useReducedMotion } from "../hooks/useReducedMotion"; + +gsap.registerPlugin(ScrollTrigger); + +interface ContactItem { + label: string; + value: string; + href?: string; + isLink?: boolean; + unavailable?: boolean; + icon?: LucideIcon; +} + +interface ContactGroup { + category: string; + items: ContactItem[]; +} + +const contacts: ContactGroup[] = [ + { + category: "Messaging", + items: [ + { + label: "Simplex", + value: "Open chat", + href: "https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2F3_XOiEnk2Ebup7DqgOwGM5kXlIZX-uJo%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAPJz5X-WHhJk_y1MLRxy7NTfspD-vKF0pmmi_GAJpExY%253D%26q%3Dc%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion", + isLink: true, + }, + { + label: "Discord", + value: + "@sysrand [1408202642588438589] / @forgecadrape [1452113626365169958]", + }, + { label: "Telegram", value: "@forgecadrape (main) / @xorededx" }, + { label: "XMPP", value: "sw@anoxinon.me (main) / me0w@xmpp.jp" }, + { + label: "Session", + value: + "056834db96cedde6012da9d5402c683c0eb260ed866da145a8f86e7f329bf77222", + }, + { + label: "Tox", + value: + "5622A2D2218AF235E39E356792270413BBE7CAEEAA531A20EEF11BF2FD10576E5050F0A55970", + }, + { label: "Potato Chat", value: "@depoliticized" }, + { + label: "Element", + value: "Open profile", + href: "https://matrix.to/#/@depoliticized:tchncs.de", + isLink: true, + }, + { label: "Briar", value: "temporary unavailable", unavailable: true }, + ], + }, + { + category: "Email", + items: [ + { + label: "Primary", + value: "contact@stealer.wtf (temporary unavailable)", + unavailable: true, + }, + ], + }, + { + category: "Git", + items: [ + { + label: "serpent256", + value: "View profile", + href: "https://git.fingeri.ng/serpent256", + isLink: true, + }, + { + label: "raped.cc", + value: "View profile (main)", + href: "https://git.fingeri.ng/raped.cc", + isLink: true, + }, + { + label: "whiskers", + value: "View org", + href: "https://git.fingeri.ng/whiskers", + isLink: true, + }, + ], + }, + { + category: "PGP", + items: [ + { + label: "Public Key", + value: "Download /pub-pgp.txt", + href: "./pub-pgp.txt", + isLink: true, + icon: Key, + }, + ], + }, +]; + +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback + } + }; + + return ( + + ); +} + +export default function ContactSection() { + const sectionRef = useRef(null); + const reducedMotion = useReducedMotion(); + + useEffect(() => { + if (reducedMotion || !sectionRef.current) return; + + const ctx = gsap.context(() => { + gsap.fromTo( + ".contact-reveal", + { y: 40, opacity: 0 }, + { + y: 0, + opacity: 1, + duration: 0.8, + stagger: 0.1, + ease: "expo.out", + scrollTrigger: { + trigger: sectionRef.current, + start: "top 70%", + toggleActions: "play none none none", + }, + }, + ); + }, sectionRef); + + return () => ctx.revert(); + }, [reducedMotion]); + + return ( +
+ {!reducedMotion && ( +
+ )} + +
+ {/* Section label */} +
+ + 006 + +
+ + Contact + +
+ + {/* Header */} +
+

+ Get in touch +

+

+ Reach out through any of these channels. I prefer encrypted + messaging when possible. +

+
+ + {/* Contact grid */} +
+ {contacts.map((group) => ( +
+
+ +

+ {group.category} +

+
+ +
+ {group.items.map((item) => ( +
+
+ + {item.label} + + {item.isLink && item.href ? ( + + {item.icon && } + {item.value} + + + ) : item.unavailable ? ( + + {item.value} + + ) : ( + + {item.value} + + )} +
+ {!item.isLink && !item.unavailable && ( + + )} +
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/ExperienceSection.tsx b/src/components/ExperienceSection.tsx new file mode 100644 index 0000000..4441bb5 --- /dev/null +++ b/src/components/ExperienceSection.tsx @@ -0,0 +1,135 @@ +import { useEffect, useRef } from 'react' +import { gsap } from 'gsap' +import { ScrollTrigger } from 'gsap/ScrollTrigger' +import { Code2, Server, Shield, Cpu, GitBranch, Bot, Cloud, Layers } from 'lucide-react' +import { useReducedMotion } from '../hooks/useReducedMotion' + +gsap.registerPlugin(ScrollTrigger) + +const skills = [ + { icon: Code2, label: 'Languages', items: 'Rust, V, D, Go, TypeScript, Python, PHP, Elixir' }, + { icon: Layers, label: 'Frontend', items: 'Astro, Svelte 5, lightweight & modern UIs' }, + { icon: Server, label: 'Backend & APIs', items: 'REST API design, complex infrastructure' }, + { icon: Cpu, label: 'Low Level', items: 'Systems programming, kernel research' }, + { icon: Shield, label: 'Security Research', items: 'Evasion, exfiltration, cryptography' }, + { icon: GitBranch, label: 'DevOps', items: 'Self-hosting, Docker, monorepo architecture' }, + { icon: Bot, label: 'Automation', items: 'Telegram / Discord bots & scripts' }, + { icon: Cloud, label: 'Networks', items: 'Secure communications, protocol design' }, +] + +const earlyProjects = [ + { name: 'Rose-Stealer_old', url: 'https://github.com/0xRose/Rose-Stealer_old', desc: 'Professional & efficient credential stealer written in python' }, + { name: 'Rose-RAT', url: 'https://github.com/0xRose/Rose-RAT', desc: 'Remote Administration Toolkit Extension to Rose-Stealer with web-host and client controller' }, + { name: 'Rose-Stealer', url: 'https://github.com/0xRose/Rose-Stealer', desc: 'Slightly refined & more modern version of Rosev1' }, + { name: 'Rose-Obf', url: 'https://github.com/gumbobrot/Rose-Obf', desc: 'Rose Python obfuscator & encryptor' }, + { name: 'RoseGuardian', url: 'https://github.com/gumbobrot/RoseGuardian', desc: 'Prior experimental version of Rose obf' }, + { name: 'PyAnalyzer', url: 'https://github.com/gumbobrot/PyAnalyzer', desc: 'Python script utilizing pycdc and pyinstxtractor to decompile pyinstaller packed executables' }, + { name: 'Knight-RAT', url: 'https://github.com/gumbobrot/Knight-RAT', desc: 'Discord-bot managed Remote Access Trojan' }, +] + +export default function ExperienceSection() { + const sectionRef = useRef(null) + const reducedMotion = useReducedMotion() + + useEffect(() => { + if (reducedMotion || !sectionRef.current) return + + const ctx = gsap.context(() => { + gsap.fromTo( + '.exp-reveal', + { y: 40, opacity: 0 }, + { + y: 0, + opacity: 1, + duration: 0.8, + stagger: 0.08, + ease: 'expo.out', + scrollTrigger: { + trigger: sectionRef.current, + start: 'top 70%', + toggleActions: 'play none none none', + }, + } + ) + }, sectionRef) + + return () => ctx.revert() + }, [reducedMotion]) + + return ( +
+ {!reducedMotion && ( +
+ )} + +
+ {/* Section label */} +
+ + 004 + +
+ + Experience + +
+ + {/* Skills grid */} +

+ Capabilities +

+ +
+ {skills.map((skill) => ( +
+ +

{skill.label}

+

{skill.items}

+
+ ))} +
+ + {/* Early history */} +

+ Early History +

+

+ Projects from 2+ years ago that shaped my foundation. These represent my first + serious forays into software development and security research. +

+ +
+ {earlyProjects.map((project) => ( + +
+ +
+ + {project.name} + +

{project.desc}

+
+
+ + github + +
+ ))} +
+
+
+ ) +} diff --git a/src/components/FloatingParticles.tsx b/src/components/FloatingParticles.tsx new file mode 100644 index 0000000..16eb112 --- /dev/null +++ b/src/components/FloatingParticles.tsx @@ -0,0 +1,94 @@ +import { useEffect, useRef } from 'react' +import { useReducedMotion } from '../hooks/useReducedMotion' + +interface Particle { + x: number + y: number + size: number + speedX: number + speedY: number + opacity: number + color: string +} + +export default function FloatingParticles() { + const canvasRef = useRef(null) + const reducedMotion = useReducedMotion() + + useEffect(() => { + if (reducedMotion) return + + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext('2d') + if (!ctx) return + + let animationId: number + let particles: Particle[] = [] + + const resize = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + } + resize() + window.addEventListener('resize', resize) + + const colors = ['rgba(255, 45, 122, 0.15)', 'rgba(255, 107, 157, 0.12)', 'rgba(255, 182, 193, 0.1)'] + + const createParticles = () => { + particles = [] + const count = Math.min(Math.floor(window.innerWidth / 15), 80) + for (let i = 0; i < count; i++) { + particles.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + size: Math.random() * 3 + 1, + speedX: (Math.random() - 0.5) * 0.3, + speedY: (Math.random() - 0.5) * 0.3, + opacity: Math.random() * 0.5 + 0.1, + color: colors[Math.floor(Math.random() * colors.length)], + }) + } + } + createParticles() + + const animate = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height) + + particles.forEach((p) => { + p.x += p.speedX + p.y += p.speedY + + if (p.x < 0) p.x = canvas.width + if (p.x > canvas.width) p.x = 0 + if (p.y < 0) p.y = canvas.height + if (p.y > canvas.height) p.y = 0 + + ctx.beginPath() + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2) + ctx.fillStyle = p.color + ctx.fill() + }) + + animationId = requestAnimationFrame(animate) + } + + animate() + + return () => { + cancelAnimationFrame(animationId) + window.removeEventListener('resize', resize) + } + }, [reducedMotion]) + + if (reducedMotion) return null + + return ( + + ) +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..16be73f --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,23 @@ +import { Heart } from 'lucide-react' + +export default function Footer() { + const year = new Date().getFullYear() + + return ( +
+
+

+ depoliticized. +

+ +

+ Built with in Europe +

+ +

+ {year} +

+
+
+ ) +} diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx new file mode 100644 index 0000000..a32904a --- /dev/null +++ b/src/components/HeroSection.tsx @@ -0,0 +1,101 @@ +import { useEffect, useRef, useState } from 'react' +import { gsap } from 'gsap' +import { ChevronDown } from 'lucide-react' +import { useReducedMotion } from '../hooks/useReducedMotion' + +export default function HeroSection() { + const sectionRef = useRef(null) + const titleRef = useRef(null) + const subtitleRef = useRef(null) + const reducedMotion = useReducedMotion() + const [subtitleText, setSubtitleText] = useState('') + const fullSubtitle = 'identity · research · code' + + useEffect(() => { + if (reducedMotion) { + setSubtitleText(fullSubtitle) + return + } + + // Title entrance animation + if (titleRef.current) { + gsap.fromTo( + titleRef.current, + { y: 60, opacity: 0, scale: 0.95 }, + { y: 0, opacity: 1, scale: 1, duration: 1.4, ease: 'expo.out', delay: 0.3 } + ) + } + + // Typing effect for subtitle + let index = 0 + const interval = setInterval(() => { + if (index <= fullSubtitle.length) { + setSubtitleText(fullSubtitle.slice(0, index)) + index++ + } else { + clearInterval(interval) + } + }, 80) + + return () => clearInterval(interval) + }, [reducedMotion]) + + const handleScrollDown = () => { + const about = document.querySelector('#about') + if (about) about.scrollIntoView({ behavior: 'smooth' }) + } + + return ( +
+ {/* Ambient shapes */} + {!reducedMotion && ( + <> +
+
+
+
+ + )} + + {/* Main content */} +
+

+ depoliticized. +

+ +

+ {subtitleText} +

+ +
+
+ {!reducedMotion && ( +
+ )} +
+
+
+ + {/* Scroll indicator */} + + + {/* Edge gradient fade */} +
+
+ ) +} diff --git a/src/components/InterestsSection.tsx b/src/components/InterestsSection.tsx new file mode 100644 index 0000000..090cc14 --- /dev/null +++ b/src/components/InterestsSection.tsx @@ -0,0 +1,160 @@ +import { useEffect, useRef } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { Music, Brain, Sparkles } from "lucide-react"; +import { useReducedMotion } from "../hooks/useReducedMotion"; + +gsap.registerPlugin(ScrollTrigger); + +const musicGenres = [ + "Nu-Metal", + "Alt-Metal", + "Scenecore", + "Nightcore", + "Russian Hardtekk", + "EDM", + "90's Music", +]; + +export default function InterestsSection() { + const sectionRef = useRef(null); + const reducedMotion = useReducedMotion(); + + useEffect(() => { + if (reducedMotion || !sectionRef.current) return; + + const ctx = gsap.context(() => { + gsap.fromTo( + ".interest-reveal", + { y: 40, opacity: 0 }, + { + y: 0, + opacity: 1, + duration: 0.8, + stagger: 0.1, + ease: "expo.out", + scrollTrigger: { + trigger: sectionRef.current, + start: "top 70%", + toggleActions: "play none none none", + }, + }, + ); + + gsap.fromTo( + ".genre-tag", + { scale: 0.8, opacity: 0 }, + { + scale: 1, + opacity: 1, + duration: 0.6, + stagger: 0.06, + ease: "back.out(1.7)", + scrollTrigger: { + trigger: sectionRef.current, + start: "top 60%", + toggleActions: "play none none none", + }, + }, + ); + }, sectionRef); + + return () => ctx.revert(); + }, [reducedMotion]); + + return ( +
+ {/* Background ambient */} + {!reducedMotion && ( +
+ )} + +
+ {/* Section label */} +
+ + 002 + +
+ + Interests + +
+ +
+ {/* Music */} +
+
+
+ +
+

+ Music +

+
+ +

+ Music is essential to my rhythm. From heavy riffs to high-speed + electronic beats, these are the sounds that fuel my focus and + creative energy. +

+ +
+ {musicGenres.map((genre) => ( + + {genre} + + ))} +
+
+ + {/* Mind */} +
+
+
+ +
+

+ The Mind +

+
+ +

+ Beyond code, I am deeply fascinated by how humans think, feel, and + perceive reality. Psychology and philosophy provide the frameworks + I use to understand complex systems - both technical and human. +

+ +
+
+ +

+ Psychology +

+

+ Cognitive patterns & behavior +

+
+
+ +

+ Philosophy +

+

+ Ethics, logic & existence +

+
+
+
+
+
+
+ ); +} diff --git a/src/components/LoadingScreen.tsx b/src/components/LoadingScreen.tsx new file mode 100644 index 0000000..dda8303 --- /dev/null +++ b/src/components/LoadingScreen.tsx @@ -0,0 +1,187 @@ +import { useState, useRef, useEffect, useCallback } from 'react' +import { gsap } from 'gsap' +import { Volume2, VolumeX, AlertTriangle, Loader2 } from 'lucide-react' +import { useReducedMotion } from '../hooks/useReducedMotion' + +interface LoadingScreenProps { + onLoadComplete: () => void +} + +export default function LoadingScreen({ onLoadComplete }: LoadingScreenProps) { + const [accepted, setAccepted] = useState(false) + const [loading, setLoading] = useState(false) + const [progress, setProgress] = useState(0) + const [audioEnabled, setAudioEnabled] = useState(true) + const [exiting, setExiting] = useState(false) + const containerRef = useRef(null) + const audioRef = useRef(null) + const reducedMotion = useReducedMotion() + + useEffect(() => { + audioRef.current = new Audio('./media/moan.mp3') + audioRef.current.preload = 'auto' + }, []) + + useEffect(() => { + if (!containerRef.current || reducedMotion) return + gsap.fromTo( + containerRef.current.querySelectorAll('.load-item'), + { y: 30, opacity: 0 }, + { y: 0, opacity: 1, duration: 0.8, stagger: 0.12, ease: 'expo.out', delay: 0.2 } + ) + }, [reducedMotion]) + + const handleLoad = useCallback(() => { + if (!accepted || loading) return + setLoading(true) + + // Play moan audio during loading + if (audioEnabled && audioRef.current) { + audioRef.current.volume = 0.6 + audioRef.current.play().catch(() => { + // Audio blocked by browser policy, continue anyway + }) + } + + // Simulate loading progress + const startTime = Date.now() + const duration = 2500 // 2.5s minimum load time + + const updateProgress = () => { + const elapsed = Date.now() - startTime + const p = Math.min((elapsed / duration) * 100, 100) + setProgress(p) + + if (p < 100) { + requestAnimationFrame(updateProgress) + } else { + // Finish loading + setTimeout(() => { + // Stop moan.mp3 before transitioning + if (audioRef.current) { + audioRef.current.pause() + audioRef.current.currentTime = 0 + } + setExiting(true) + setTimeout(() => { + onLoadComplete() + }, 800) + }, 300) + } + } + requestAnimationFrame(updateProgress) + }, [accepted, loading, audioEnabled, onLoadComplete]) + + return ( +
+ {/* Ambient background shapes */} + {!reducedMotion && ( + <> +
+
+ + )} + +
+ {/* Banner image */} +
+
+ Banner +
+ {!reducedMotion && ( +
+ )} +
+ + {/* Load text */} +

+ Load~ +

+ +

+ depoliticized. +

+ + {/* Warning */} +
+
+ +
+ +
+
+
+ + {/* Audio toggle */} +
+ +
+ + {/* Load button */} + + + {/* Progress bar */} + {loading && ( +
+
+
+
+

+ {Math.round(progress)}% +

+
+ )} +
+
+ ) +} diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx new file mode 100644 index 0000000..1f95530 --- /dev/null +++ b/src/components/Navigation.tsx @@ -0,0 +1,133 @@ +import { useState, useEffect } from 'react' +import { Menu, X } from 'lucide-react' + +const navItems = [ + { label: 'About', href: '#about' }, + { label: 'Interests', href: '#interests' }, + { label: 'Projects', href: '#projects' }, + { label: 'Experience', href: '#experience' }, + { label: 'Topics', href: '#topics' }, + { label: 'Contact', href: '#contact' }, +] + +export default function Navigation() { + const [visible, setVisible] = useState(false) + const [mobileOpen, setMobileOpen] = useState(false) + const [activeSection, setActiveSection] = useState('') + + useEffect(() => { + const handleScroll = () => { + setVisible(window.scrollY > window.innerHeight * 0.6) + } + window.addEventListener('scroll', handleScroll, { passive: true }) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveSection(entry.target.id) + } + }) + }, + { rootMargin: '-40% 0px -40% 0px' } + ) + + navItems.forEach((item) => { + const el = document.querySelector(item.href) + if (el) observer.observe(el) + }) + + return () => observer.disconnect() + }, []) + + const handleClick = (e: React.MouseEvent, href: string) => { + e.preventDefault() + const el = document.querySelector(href) + if (el) { + el.scrollIntoView({ behavior: 'smooth' }) + setMobileOpen(false) + } + } + + return ( + <> + + + ) +} diff --git a/src/components/NoiseOverlay.tsx b/src/components/NoiseOverlay.tsx new file mode 100644 index 0000000..be78ada --- /dev/null +++ b/src/components/NoiseOverlay.tsx @@ -0,0 +1,17 @@ +import { useReducedMotion } from '../hooks/useReducedMotion' + +export default function NoiseOverlay() { + const reducedMotion = useReducedMotion() + if (reducedMotion) return null + + return ( +
+ ) +} diff --git a/src/components/ProjectsSection.tsx b/src/components/ProjectsSection.tsx new file mode 100644 index 0000000..9cf7db3 --- /dev/null +++ b/src/components/ProjectsSection.tsx @@ -0,0 +1,243 @@ +import { useEffect, useRef } from "react"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { + Activity, + Rocket, + CheckCircle2, + ExternalLink, + Terminal, + Globe, + Lock, + Database, +} from "lucide-react"; +import { useReducedMotion } from "../hooks/useReducedMotion"; + +gsap.registerPlugin(ScrollTrigger); + +const activeProjects = [ + { + title: "Seraph C2", + description: + "Full fledged, interactive, post-exploitation C2 system with AI-support, E2EE, evasion, cross-platform featuring and large scaled exfiltration features.", + icon: Terminal, + tags: ["Security Research", "AI", "E2EE"], + status: "LIVE", + }, + { + title: "AnonPay (tocador.app-style)", + description: + "Payment gateway that allows paying in hundreds of different cryptocurrencies whilst still receiving only one by utilizing multiple swap aggregators and joining different, rotating wallet pools.", + icon: Globe, + tags: ["Crypto", "Finance", "Payments"], + status: "LIVE", + }, + { + title: "kippenkartell.cc", + description: + "Dockerized Premium SaaS marketplace platform with a clean monorepo architecture: SvelteKit frontend, Rust REST API, MySQL database.", + icon: Database, + tags: ["SaaS", "SvelteKit", "Rust"], + status: "N/A", + }, + { + title: "spying.su", + description: + "Discord, Social Media & Breach Data Aggregation and Web Intelligence Platform.", + icon: Lock, + tags: ["OSINT", "Data", "Intelligence"], + status: "N/A", + }, +]; + +const plannedProjects = [ + { + title: "webforum", + description: + "Suitable & simple breachforum's & WPD-style webforum for anonymous media submission, role assignment systems, localized restriction control & community/topic merging, additionally private chats & E2EE secured chats.", + }, + { + title: "porn page", + description: + "A webforum/archive & user-control system for manageable, quick, fast & secure access/uploading to/of pornography media.", + }, + { + title: "vpn-protocol", + description: + "Low-level interface VPN protocol featuring extremely tiny binary & lightweight byte transmitting using NDISAPI (curve25519 -> serpent(threefish512)).", + }, +]; + +const completedProjects = [ + { + title: "Cryptography Library", + description: + "A threefish512 (customized padding & improved security) implementation in D based off a russian barebones cryptographic algorithm & a ported shamir's secret sharing implementation in python with a bridger written in C.", + link: "https://git.fingeri.ng/whiskers/cryptography", + }, +]; + +export default function ProjectsSection() { + const sectionRef = useRef(null); + const reducedMotion = useReducedMotion(); + + useEffect(() => { + if (reducedMotion || !sectionRef.current) return; + + const ctx = gsap.context(() => { + gsap.fromTo( + ".project-reveal", + { y: 50, opacity: 0 }, + { + y: 0, + opacity: 1, + duration: 0.9, + stagger: 0.12, + ease: "expo.out", + scrollTrigger: { + trigger: sectionRef.current, + start: "top 70%", + toggleActions: "play none none none", + }, + }, + ); + }, sectionRef); + + return () => ctx.revert(); + }, [reducedMotion]); + + return ( +
+
+ {/* Section label */} +
+ + 003 + +
+ + Projects + +
+ + {/* Active Projects */} +
+
+ +

+ Active +

+ + {activeProjects.length} + +
+ +
+ {activeProjects.map((project) => ( +
+
+
+ +
+ + + {project.status} + +
+

+ {project.title} +

+

+ {project.description} +

+
+ {project.tags.map((tag) => ( + + {tag} + + ))} +
+
+ ))} +
+
+ + {/* Planned Projects */} +
+
+ +

+ Planned +

+ + {plannedProjects.length} + +
+ +
+ {plannedProjects.map((project, i) => ( +
+ + {String(i + 1).padStart(2, "0")} + +
+

+ {project.title} +

+

+ {project.description} +

+
+
+ ))} +
+
+ + {/* Completed Projects */} +
+
+ +

+ Completed +

+
+ +
+ {completedProjects.map((project) => ( + +
+

+ {project.title} +

+

+ {project.description} +

+
+ +
+ ))} +
+
+
+
+ ); +} diff --git a/src/components/ScrollProgress.tsx b/src/components/ScrollProgress.tsx new file mode 100644 index 0000000..59b2cfe --- /dev/null +++ b/src/components/ScrollProgress.tsx @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react' +import { useReducedMotion } from '../hooks/useReducedMotion' + +export default function ScrollProgress() { + const [progress, setProgress] = useState(0) + const reducedMotion = useReducedMotion() + + useEffect(() => { + if (reducedMotion) return + + const handleScroll = () => { + const scrollTop = window.scrollY + const docHeight = document.documentElement.scrollHeight - window.innerHeight + const scrollPercent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0 + setProgress(scrollPercent) + } + + window.addEventListener('scroll', handleScroll, { passive: true }) + return () => window.removeEventListener('scroll', handleScroll) + }, [reducedMotion]) + + if (reducedMotion) return null + + return ( +
+
+
+ ) +} diff --git a/src/components/TopicsSection.tsx b/src/components/TopicsSection.tsx new file mode 100644 index 0000000..d88260a --- /dev/null +++ b/src/components/TopicsSection.tsx @@ -0,0 +1,137 @@ +import { useEffect, useRef } from 'react' +import { gsap } from 'gsap' +import { ScrollTrigger } from 'gsap/ScrollTrigger' +import { Telescope, ArrowUpRight } from 'lucide-react' +import { useReducedMotion } from '../hooks/useReducedMotion' + +gsap.registerPlugin(ScrollTrigger) + +const topics = [ + { + title: 'OpenClaw / MCP Servers', + description: 'Exploring modular control plane architectures and automated orchestration systems.', + }, + { + title: 'ML / AI Training', + description: 'Deep diving into model training pipelines, fine-tuning strategies, and deployment patterns.', + }, + { + title: 'LLM Definition & Background', + description: 'Understanding the theoretical foundations and architectural evolution of large language models.', + }, + { + title: 'Low Level & Exploit Development', + description: 'Memory corruption, binary exploitation, and vulnerability research at the systems level.', + }, + { + title: 'Game Security Research', + description: 'External / internal cheat development & kernel level anti-cheat exploitation research.', + }, + { + title: 'Database Engineering', + description: 'Efficiency, management & scaling strategies for high-throughput data systems.', + }, + { + title: 'Stability & Concurrency', + description: 'Improved async handling, fault tolerance, and concurrent system design patterns.', + }, +] + +export default function TopicsSection() { + const sectionRef = useRef(null) + const reducedMotion = useReducedMotion() + + useEffect(() => { + if (reducedMotion || !sectionRef.current) return + + const ctx = gsap.context(() => { + gsap.fromTo( + '.topic-reveal', + { y: 30, opacity: 0 }, + { + y: 0, + opacity: 1, + duration: 0.7, + stagger: 0.1, + ease: 'expo.out', + scrollTrigger: { + trigger: sectionRef.current, + start: 'top 70%', + toggleActions: 'play none none none', + }, + } + ) + }, sectionRef) + + return () => ctx.revert() + }, [reducedMotion]) + + return ( +
+
+ {/* Section label */} +
+ + 005 + +
+ + Research + +
+ +
+ {/* Left sticky heading */} +
+
+
+
+ +
+
+

+ Topics to
+ explore +

+

+ A living list of research directions and technical frontiers + I am actively looking into or plan to investigate. +

+
+
+ + {/* Right topic list */} +
+ {topics.map((topic, i) => ( +
+
+
+ + {String(i + 1).padStart(2, '0')} + +
+

+ {topic.title} +

+

+ {topic.description} +

+
+
+ +
+
+ ))} +
+
+
+
+ ) +} diff --git a/src/hooks/useReducedMotion.ts b/src/hooks/useReducedMotion.ts new file mode 100644 index 0000000..a9ca054 --- /dev/null +++ b/src/hooks/useReducedMotion.ts @@ -0,0 +1,16 @@ +import { useState, useEffect } from 'react' + +export function useReducedMotion(): boolean { + const [reducedMotion, setReducedMotion] = useState(false) + + useEffect(() => { + const mq = window.matchMedia('(prefers-reduced-motion: reduce)') + setReducedMotion(mq.matches) + + const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches) + mq.addEventListener('change', handler) + return () => mq.removeEventListener('change', handler) + }, []) + + return reducedMotion +} diff --git a/src/hooks/useTypingEffect.ts b/src/hooks/useTypingEffect.ts new file mode 100644 index 0000000..36cad6b --- /dev/null +++ b/src/hooks/useTypingEffect.ts @@ -0,0 +1,53 @@ +import { useState, useEffect, useRef, useCallback } from 'react' +import { useReducedMotion } from './useReducedMotion' + +interface UseTypingEffectOptions { + text: string + speed?: number + delay?: number + onComplete?: () => void +} + +export function useTypingEffect({ text, speed = 50, delay = 0, onComplete }: UseTypingEffectOptions) { + const [displayed, setDisplayed] = useState('') + const [started, setStarted] = useState(false) + const [done, setDone] = useState(false) + const reducedMotion = useReducedMotion() + const indexRef = useRef(0) + const timeoutRef = useRef | null>(null) + + const start = useCallback(() => { + if (started) return + setStarted(true) + }, [started]) + + useEffect(() => { + if (!started) return + + if (reducedMotion) { + setDisplayed(text) + setDone(true) + onComplete?.() + return + } + + const type = () => { + if (indexRef.current < text.length) { + setDisplayed(text.slice(0, indexRef.current + 1)) + indexRef.current++ + timeoutRef.current = setTimeout(type, speed) + } else { + setDone(true) + onComplete?.() + } + } + + timeoutRef.current = setTimeout(type, delay) + + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + } + }, [started, text, speed, delay, reducedMotion, onComplete]) + + return { displayed, done, start } +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..c94ddb4 --- /dev/null +++ b/src/index.css @@ -0,0 +1,148 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-skin-50: #FDF8F3; + --color-skin-100: #FAF0E6; + --color-skin-200: #F5E1D0; + --color-skin-300: #EBCDB0; + --color-pink-brand: #FF2D7A; + --color-pink-bright: #FF1B8D; + --color-pink-soft: #FF6B9D; + --color-pink-pale: #FFB6C1; + --color-grey-900: #1A1A1A; + --color-grey-800: #2A2A2A; + --color-grey-700: #3A3A3A; + --color-grey-600: #555555; + --color-grey-500: #777777; + + --font-mono: 'Roboto Mono', monospace; + --font-display: 'Space Grotesk', sans-serif; + --font-body: 'DM Sans', sans-serif; + + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in-out-circ: cubic-bezier(0.85, 0, 0.15, 1); +} + +html { + scroll-behavior: auto; +} + +body { + font-family: var(--font-body); + background-color: var(--color-skin-50); + color: var(--color-grey-900); + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +::selection { + background-color: var(--color-pink-brand); + color: white; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-skin-100); +} + +::-webkit-scrollbar-thumb { + background: var(--color-pink-soft); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-pink-brand); +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* Utility classes */ +.text-gradient { + background: linear-gradient(135deg, var(--color-pink-brand) 0%, var(--color-pink-bright) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.glass { + background: rgba(253, 248, 243, 0.7); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 45, 122, 0.1); +} + +/* Ambient floating shapes */ +@keyframes ambient-float-1 { + 0%, 100% { transform: translate(0, 0) rotate(0deg); } + 33% { transform: translate(30px, -50px) rotate(120deg); } + 66% { transform: translate(-20px, -30px) rotate(240deg); } +} + +@keyframes ambient-float-2 { + 0%, 100% { transform: translate(0, 0) rotate(0deg); } + 33% { transform: translate(-40px, 30px) rotate(-120deg); } + 66% { transform: translate(20px, 50px) rotate(-240deg); } +} + +@keyframes ambient-float-3 { + 0%, 100% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(15px, -25px) scale(1.05); } +} + +.ambient-shape-1 { + animation: ambient-float-1 12s ease-in-out infinite; +} + +.ambient-shape-2 { + animation: ambient-float-2 15s ease-in-out infinite; +} + +.ambient-shape-3 { + animation: ambient-float-3 8s ease-in-out infinite; +} + +/* Typing cursor */ +.typing-cursor::after { + content: '|'; + animation: blink 1s step-end infinite; + color: var(--color-pink-brand); + margin-left: 2px; +} + +@keyframes blink { + 50% { opacity: 0; } +} + +/* Line reveal */ +.line-reveal { + overflow: hidden; +} + +.line-reveal > span { + display: inline-block; + transform: translateY(100%); + opacity: 0; + transition: transform 0.8s var(--ease-out-expo), opacity 0.8s var(--ease-out-expo); +} + +.line-reveal.revealed > span { + transform: translateY(0); + opacity: 1; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..2eccce7 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..b0a273a --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,53 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + skin: { + 50: '#FDF8F3', + 100: '#FAF0E6', + 200: '#F5E1D0', + 300: '#EBCDB0', + }, + pink: { + brand: '#FF2D7A', + bright: '#FF1B8D', + soft: '#FF6B9D', + pale: '#FFB6C1', + }, + grey: { + 900: '#1A1A1A', + 800: '#2A2A2A', + 700: '#3A3A3A', + 600: '#555555', + 500: '#777777', + }, + }, + fontFamily: { + mono: ['"Roboto Mono"', 'monospace'], + display: ['"Space Grotesk"', 'sans-serif'], + body: ['"DM Sans"', 'sans-serif'], + }, + animation: { + 'float': 'float 6s ease-in-out infinite', + 'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite', + 'drift': 'drift 20s linear infinite', + }, + keyframes: { + float: { + '0%, 100%': { transform: 'translateY(0)' }, + '50%': { transform: 'translateY(-20px)' }, + }, + drift: { + '0%': { transform: 'translateX(-100%) translateY(0)' }, + '100%': { transform: 'translateX(100vw) translateY(-40px)' }, + }, + }, + }, + }, + plugins: [], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..352ccd9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..7685ae1 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..a6903be --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + base: './', +})