Initial commit

This commit is contained in:
Jetomit_Bio 2026-01-23 17:37:28 +01:00
parent 3c7649ef4c
commit 7ada5a5ee1
23 changed files with 1177 additions and 93 deletions

59
install.sh Normal file
View File

@ -0,0 +1,59 @@
#!/bin/bash
# Farby pre krajší terminál
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
echo -e "${BLUE}==========================================${NC}"
echo -e "${BLUE} Jetomit_Bio - Automatická Inštalácia ${NC}"
echo -e "${BLUE}==========================================${NC}"
# 1. Kontrola prostredia
echo -e "\n${YELLOW}🔍 Kontrolujem prostredie...${NC}"
if ! command -v npm &> /dev/null; then
echo -e "${RED}❌ Chyba: NPM nie je nainštalované. Nainštaluj si Node.js.${NC}"
exit 1
fi
# 2. Inštalácia závislostí
echo -e "${GREEN}📦 Inštalujem balíčky (Next.js, Framer Motion, Resend, Lucide, Tailwind)...${NC}"
npm install
# 3. Kontrola .env súboru
echo -e "${GREEN}🔑 Kontrolujem konfiguračné súbory...${NC}"
if [ ! -f .env ]; then
echo -e "${YELLOW}📝 Súbor .env nebol nájdený. Vytváram šablónu...${NC}"
echo "RESEND_API_KEY=re_tvoj_kluc_tu" > .env
echo -e "${RED}⚠ DOPLŇ SI API KĽÚČ DO SÚBORU .env!${NC}"
else
echo -e "${BLUE}✅ Súbor .env už existuje.${NC}"
fi
# 4. Kontrola dátových súborov (JSON)
echo -e "${GREEN}📂 Kontrolujem dátovú štruktúru...${NC}"
FILES=("src/data/navbar.json" "src/data/main_page.json" "src/data/socials.json" "src/data/mainprojects.json" "src/data/contact.json")
for file in "${FILES[@]}"; do
if [ ! -f "$file" ]; then
echo -e "${YELLOW}⚠ Varovanie: Súbor $file chýba. Skontroluj si priečinok src/data/.${NC}"
fi
done
# 5. Príprava priečinkov pre obrázky
echo -e "${GREEN}🖼️ Pripravujem priečinky pre médiá...${NC}"
mkdir -p public/images
if [ ! -f public/images/me1.jpg ] || [ ! -f public/images/me2.jpg ]; then
echo -e "${YELLOW}📸 Nezabudni pridať fotky me1.jpg a me2.jpg do public/images/ pre správne zobrazenie polaroidov.${NC}"
fi
# 6. Finálne nastavenie práv pre skripty
chmod +x install.sh
echo -e "\n${BLUE}==========================================${NC}"
echo -e "${GREEN}✅ Všetko je pripravené!${NC}"
echo -e "${BLUE}Spusti projekt príkazom:${NC} ${YELLOW}npm run dev${NC}"
echo -e "${BLUE}==========================================${NC}"

129
package-lock.json generated
View File

@ -8,9 +8,13 @@
"name": "jetomit-portfolio",
"version": "0.1.0",
"dependencies": {
"framer-motion": "^12.29.0",
"lucide-react": "^0.562.0",
"matter-js": "^0.20.0",
"next": "16.1.4",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"resend": "^6.8.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -1234,6 +1238,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@stablelib/base64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -3217,6 +3227,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@ -3501,6 +3512,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@ -3591,6 +3608,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/framer-motion": {
"version": "12.29.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.29.0.tgz",
"integrity": "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.29.0",
"motion-utils": "^12.27.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -4839,6 +4883,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.562.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@ -4859,6 +4912,12 @@
"node": ">= 0.4"
}
},
"node_modules/matter-js": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.20.0.tgz",
"integrity": "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA==",
"license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -4906,6 +4965,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/motion-dom": {
"version": "12.29.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.0.tgz",
"integrity": "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.27.2"
}
},
"node_modules/motion-utils": {
"version": "12.27.2",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.27.2.tgz",
"integrity": "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -5458,6 +5532,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resend": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/resend/-/resend-6.8.0.tgz",
"integrity": "sha512-fDOXGqafQfQXl8nXe93wr93pus8tW7YPpowenE3SmG7dJJf0hH3xUWm3xqacnPvhqjCQTJH9xETg07rmUeSuqQ==",
"license": "MIT",
"dependencies": {
"svix": "1.84.1"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@react-email/render": "*"
},
"peerDependenciesMeta": {
"@react-email/render": {
"optional": true
}
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@ -5827,6 +5921,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/standardwebhooks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
"license": "MIT",
"dependencies": {
"@stablelib/base64": "^1.0.0",
"fast-sha256": "^1.3.0"
}
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@ -6026,6 +6130,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svix": {
"version": "1.84.1",
"resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz",
"integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==",
"license": "MIT",
"dependencies": {
"standardwebhooks": "1.0.0",
"uuid": "^10.0.0"
}
},
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
@ -6386,6 +6500,19 @@
"punycode": "^2.1.0"
}
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -9,9 +9,13 @@
"lint": "eslint"
},
"dependencies": {
"framer-motion": "^12.29.0",
"lucide-react": "^0.562.0",
"matter-js": "^0.20.0",
"next": "16.1.4",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"resend": "^6.8.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

BIN
public/images/me1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

BIN
public/images/me2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

53
src/app/api/send/route.ts Normal file
View File

@ -0,0 +1,53 @@
import { NextResponse } from 'next/server';
import { Resend } from 'resend';
import contactData from "@/data/contact.json";
import { promises as fs } from 'fs';
import path from 'path';
// Inicializácia Resend s API kľúčom z .env.local
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
try {
// 1. Získanie dát z prichádzajúceho JSON requestu
const { firstName, lastName, email, subject, body } = await request.json();
// 2. Cesta k HTML šablóne a jej načítanie
const filePath = path.join(process.cwd(), 'src/data/email.html');
let htmlContent = await fs.readFile(filePath, 'utf8');
// 3. Dynamické nahradenie placeholderov v HTML súbore za reálne dáta
// Používame globálny replace (/.../g), ak by sa premenná v HTML opakovala
const finalHtml = htmlContent
.replace(/{{firstName}}/g, firstName)
.replace(/{{lastName}}/g, lastName)
.replace(/{{subject}}/g, subject)
.replace(/{{email}}/g, email)
.replace(/{{body}}/g, body.replace(/\n/g, '<br>')); // Ošetrenie nových riadkov
// 4. Odoslanie e-mailu cez službu Resend
const { data, error } = await resend.emails.send({
from: contactData.config.from_email,
to: [contactData.config.to_email],
subject: `📩 New Message: ${subject}`,
replyTo: email, // Umožní ti odpovedať priamo odosielateľovi kliknutím na "Reply"
html: finalHtml,
});
// 5. Ošetrenie chýb z Resend API
if (error) {
console.error("Resend Error:", error);
return NextResponse.json({ error }, { status: 400 });
}
// 6. Úspešná odpoveď
return NextResponse.json({ success: true, data });
} catch (error: any) {
console.error("Server Error:", error);
return NextResponse.json(
{ error: "Internal Server Error", details: error.message },
{ status: 500 }
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 292 KiB

View File

@ -1,26 +1,96 @@
@import "tailwindcss";
@theme {
--color-pill-gray: #bcbfc7;
--color-pill-dark: #1c1c1e;
}
:root {
--background: #ffffff;
--foreground: #171717;
--background: #0d0d0d;
--foreground: #ffffff;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
/* KĽÚČOVÁ ZMENA TU */
html {
background-color: var(--background);
/* Vynúti stabilný priestor pre scrollbar na všetkých stránkach */
scrollbar-gutter: stable !important;
overflow-x: hidden;
width: 100%;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
margin: 0;
-webkit-font-smoothing: antialiased;
width: 100%;
overflow-x: hidden;
}
/* Zvyšok tvojho CSS zostáva rovnaký */
a {
text-decoration: none !important;
color: inherit;
transition: all 0.2s ease;
}
.force-black {
color: #000000 !important;
}
.btn-pill-primary {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
background-color: #bcbfc7;
color: #000000 !important;
border-radius: 9999px;
font-weight: 700;
transition: transform 0.2s ease;
}
.btn-pill-primary:hover {
transform: scale(1.05);
}
.btn-pill-secondary {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
background-color: #1c1c1e;
color: #ffffff !important;
border-radius: 9999px;
font-weight: 600;
border: 1px solid rgba(255,255,255,0.05);
transition: background-color 0.2s;
}
.btn-pill-secondary:hover {
background-color: #2a2a2c;
}
.polaroid-frame {
background: white;
padding: 12px 12px 48px 12px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0,0,0,0.1);
transition: transform 0.3s ease-in-out;
}
.polaroid-frame:hover {
transform: scale(1.05) rotate(0deg) !important;
z-index: 40;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #0d0d0d;
}
::-webkit-scrollbar-thumb {
background: #1c1c1e;
border-radius: 10px;
}

View File

@ -1,33 +1,39 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Inter } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
// Použijeme font Inter, ktorý je pre "PRPL" štýl ideálny
const inter = Inter({
subsets: ["latin"],
weight: ['400', '700', '900']
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Jetomit_ Bio | Portfolio",
description: "13-year-old Full-stack developer building digital experiences.",
};
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en">
<html lang="en" className="scroll-smooth">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${inter.className} bg-[#0a0a0a] text-[#ededed] antialiased overflow-x-hidden`}
>
{children}
{/* Tento div vytvorí tie jemné kruhy na pozadí pre celú stránku */}
<div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
<div className="absolute top-1/2 left-0 -translate-y-1/2 w-[800px] h-[800px] border border-white/[0.03] rounded-full -translate-x-1/2" />
<div className="absolute top-1/2 left-0 -translate-y-1/2 w-[600px] h-[600px] border border-white/[0.03] rounded-full -translate-x-1/2" />
<div className="absolute top-1/2 left-0 -translate-y-1/2 w-[400px] h-[400px] border border-white/[0.05] rounded-full -translate-x-1/2" />
</div>
{/* Obsah stránky sa vykreslí tu */}
<div className="relative z-10">
{children}
</div>
</body>
</html>
);

56
src/app/not-found.tsx Normal file
View File

@ -0,0 +1,56 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { Home, ArrowLeft } from "lucide-react";
export default function NotFound() {
return (
<main className="min-h-screen flex items-center justify-center bg-[#0d0d0d] text-white px-6 relative overflow-hidden">
{/* Dekoratívne svetlo v pozadí */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-blue-500/10 blur-[120px] rounded-full pointer-events-none" />
<div className="max-w-2xl w-full text-center z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{/* Veľké 404 s glitch efektom v CSS */}
<h1 className="text-[12rem] md:text-[15rem] font-black leading-none tracking-tighter text-white/5 select-none">
404
</h1>
<div className="relative -mt-20 md:-mt-28">
<h2 className="text-4xl md:text-5xl font-black mb-4 tracking-tight">
System Breach? <span className="text-blue-500">Not really.</span>
</h2>
<p className="text-gray-400 text-lg md:text-xl max-w-md mx-auto mb-10 font-medium leading-relaxed">
The page you are looking for has been moved, deleted, or never existed in this terminal.
</p>
</div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link href="/" className="btn-pill-primary w-full sm:w-auto justify-center group">
<Home size={18} className="group-hover:-translate-y-0.5 transition-transform" />
Back Home
</Link>
<button
onClick={() => window.history.back()}
className="btn-pill-secondary w-full sm:w-auto justify-center group"
>
<ArrowLeft size={18} className="group-hover:-translate-x-1 transition-transform" />
Go Back
</button>
</div>
</motion.div>
{/* Spodná línia v štýle kódu */}
<div className="mt-20 pt-8 border-t border-white/5 flex justify-center gap-8 text-xs font-mono text-gray-500 uppercase tracking-[0.2em]">
<span>Error_Code: NULL_POINTER</span>
<span>Status: LOST_IN_SPACE</span>
</div>
</div>
</main>
);
}

View File

@ -1,65 +1,117 @@
import Image from "next/image";
"use client";
import React from "react";
// OPRAVENÉ: Zmenené z "icons-react" na "lucide-react"
import * as Icons from "lucide-react";
import Navbar from "@/components/navbar";
import ContactMe from "@/components/contactme";
// Import dát
import socialsData from "@/data/socials.json";
import projectsData from "@/data/mainprojects.json";
import mainData from "@/data/main_page.json";
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<main className="min-h-screen bg-[#0d0d0d] text-white overflow-x-hidden">
<Navbar />
{/* HERO SEKICA - pridané id="home" pre detekciu v navbare */}
<section id="home" className="max-w-7xl mx-auto px-8 md:px-24 pt-48 grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* BIO */}
<div className="space-y-10">
<div className="flex flex-col items-start gap-4">
<h1 className="text-6xl md:text-8xl font-black tracking-tighter leading-none">
I'm {mainData.hero.name}
</h1>
<div className="px-4 py-1.5 bg-[#2a2a2c] text-gray-400 rounded-full font-bold border border-white/5 text-sm tracking-normal">
{mainData.hero.pronouns}
</div>
<p className="text-xl md:text-2xl text-gray-400 max-w-lg leading-relaxed font-medium pt-2">
{mainData.hero.description} <br />
Specializing in <span className="text-white border-b-2 border-blue-500">{mainData.hero.specialization.tech1}</span> and <span className="text-white border-b-2 border-purple-500">{mainData.hero.specialization.tech2}</span>.
</p>
</div>
{/* SOCIALS */}
<div className="flex flex-wrap gap-4">
{socialsData.map((social, index) => {
// Dynamické priradenie ikony
const LucideIcon = (Icons as any)[social.icon];
return (
<a
key={index}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className={social.primary ? "btn-pill-primary" : "btn-pill-secondary"}
>
{LucideIcon && <LucideIcon size={22} />} {social.name}
</a>
);
})}
</div>
{/* Badges */}
<div className="flex gap-2 pt-4 opacity-50">
{mainData.experience_tags.map(tag => (
<span key={tag} className="px-3 py-1 bg-[#1a1a1c] border border-white/10 rounded-lg text-xs font-mono">{tag}</span>
))}
</div>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
{/* POLAROIDY */}
<div className="relative h-[500px] hidden lg:block">
<div className="absolute top-0 right-20 rotate-[-6deg] polaroid-container w-72 transition-transform hover:rotate-0 hover:z-30 cursor-pointer">
<div className="aspect-square bg-gray-800 w-full mb-4 overflow-hidden shadow-inner">
<img src="/images/me1.jpg" alt="me" className="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-500" />
</div>
</div>
<div className="absolute top-32 right-0 rotate-[8deg] polaroid-container w-72 transition-transform hover:rotate-0 hover:z-30 cursor-pointer">
<div className="aspect-square bg-gray-700 w-full mb-4 overflow-hidden shadow-inner">
<img src="/images/me2.jpg" alt="me" className="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-500" />
</div>
</div>
</div>
</main>
</div>
</section>
{/* PROJEKTY */}
<section id="projects" className="max-w-7xl mx-auto px-8 md:px-24 py-32">
<h2 className="text-4xl font-black mb-12 tracking-tighter">
{mainData.sections.projects_title}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{projectsData.map((project, index) => (
<a
key={index}
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="p-8 bg-[#161618] rounded-[32px] border border-white/5 hover:border-white/20 transition-all group block shadow-xl"
>
<div className="flex justify-between items-start mb-4">
<span className="text-[10px] font-black uppercase text-blue-500 tracking-widest">
{project.tag}
</span>
<Icons.ExternalLink size={20} className="text-gray-600 group-hover:text-white transition-colors" />
</div>
<h3 className="text-3xl font-black mb-2 group-hover:text-white transition-colors">
{project.title}
</h3>
<p className="text-gray-400 font-medium leading-relaxed">
{project.description}
</p>
</a>
))}
</div>
</section>
{/* CONTACT ME SEKICA */}
<ContactMe />
</main>
);
}

95
src/app/projects/page.tsx Normal file
View File

@ -0,0 +1,95 @@
"use client";
import React from "react";
import * as Icons from "lucide-react";
import Navbar from "@/components/navbar";
import projectsData from "@/data/projects.json";
import workedOnData from "@/data/workedon.json";
import projectsPageData from "@/data/projects_page.json"; // Import pre texty stránky
export default function ProjectsPage() {
return (
<main className="min-h-screen bg-[#0d0d0d] text-white overflow-x-hidden">
<Navbar />
<div className="max-w-7xl mx-auto px-8 md:px-24 pt-44">
{/* HLAVIČKA STRÁNKY */}
<div className="mb-20 space-y-4">
<h1 className="text-5xl md:text-7xl font-black tracking-tighter">
{projectsPageData.header.title}
</h1>
<p className="text-xl text-gray-400 max-w-2xl font-medium">
{projectsPageData.header.description}
</p>
</div>
{/* ZOZNAM HLAVNÝCH PROJEKTOV */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pb-20">
{projectsData.map((project, index) => (
<a
key={index}
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="p-8 bg-[#161618] rounded-[32px] border border-white/5 hover:border-white/20 transition-all group block shadow-xl relative overflow-hidden"
>
<div className="flex justify-between items-start mb-4">
<div className="flex gap-3 items-center">
<span className="text-[10px] font-black uppercase text-blue-500 tracking-widest">
{project.category}
</span>
<span className="text-[10px] font-bold text-gray-600">
{project.year}
</span>
</div>
<Icons.ExternalLink size={20} className="text-gray-600 group-hover:text-white transition-colors" />
</div>
<h3 className="text-3xl font-black mb-2 group-hover:text-white transition-colors">
{project.title}
</h3>
<p className="text-gray-400 font-medium leading-relaxed">
{project.description}
</p>
<div className="absolute -right-4 -bottom-4 opacity-[0.02] group-hover:opacity-[0.05] transition-opacity">
<Icons.Layers size={120} />
</div>
</a>
))}
</div>
{/* SEKICA "WORKED ON" */}
<div className="pb-24">
<div className="flex items-center gap-4 mb-10">
<h2 className="text-3xl font-black tracking-tighter">
{projectsPageData.sections.worked_on_title}
</h2>
<div className="h-[1px] flex-1 bg-white/5"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{workedOnData.map((item, index) => (
<a
key={index}
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="p-6 bg-[#111113] rounded-2xl border border-white/5 hover:bg-[#161618] transition-all group flex items-center justify-between"
>
<div>
<h4 className="font-bold text-gray-200 group-hover:text-white transition-colors">
{item.title}
</h4>
<p className="text-xs text-gray-500 font-medium mt-1">
{item.role || projectsPageData.sections.default_role}
</p>
</div>
<Icons.ArrowUpRight size={18} className="text-gray-700 group-hover:text-white transition-all transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</a>
))}
</div>
</div>
</div>
</main>
);
}

View File

@ -0,0 +1,168 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { motion } from "framer-motion";
import { Copy, Check, Send, Loader2 } from "lucide-react";
import contactData from "@/data/contact.json";
export default function ContactMe() {
const [copied, setCopied] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [positionedTags, setPositionedTags] = useState<any[]>([]);
const [status, setStatus] = useState<"idle" | "sending" | "success" | "error">("idle");
const constraintsRef = useRef(null);
// 1. Vyriešenie hydratácie a náhodného spawnu tagov z JSONu
useEffect(() => {
const randomized = contactData.tags.map((tag) => ({
...tag,
initialX: Math.random() * 240 - 120, // Jemne zväčšený rozptyl
initialY: Math.random() * 240 - 120,
initialRotate: Math.random() * 40 - 20,
}));
setPositionedTags(randomized);
setIsMounted(true);
}, []);
const copyToClipboard = () => {
navigator.clipboard.writeText(contactData.config.to_email);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setStatus("sending");
const formData = new FormData(e.currentTarget);
const payload = {
firstName: formData.get("firstName"),
lastName: formData.get("lastName"),
email: formData.get("email"),
subject: formData.get("subject"),
body: formData.get("body"),
};
try {
const res = await fetch("/api/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (res.ok) {
setStatus("success");
(e.target as HTMLFormElement).reset();
setTimeout(() => setStatus("idle"), 5000);
} else {
setStatus("error");
setTimeout(() => setStatus("idle"), 5000);
}
} catch (err) {
setStatus("error");
setTimeout(() => setStatus("idle"), 5000);
}
};
// Prevencia Hydration Mismatch
if (!isMounted) return <div className="min-h-[650px] w-full" />;
return (
<section id="contact" className="max-w-7xl mx-auto px-8 py-24">
<div className="bg-[#161618] border border-white/5 rounded-[40px] overflow-hidden grid grid-cols-1 lg:grid-cols-2 min-h-[650px] shadow-2xl">
{/* L'AVÁ STRANA: Interaktívne Tagy */}
<div
ref={constraintsRef}
className="relative bg-[#0d0d0d]/50 flex items-center justify-center overflow-hidden border-r border-white/5 p-12 touch-none"
>
{/* Dekoratívny text na pozadí */}
<h2 className="absolute text-5xl md:text-7xl font-black text-white/[0.03] select-none text-center leading-none pointer-events-none uppercase tracking-tighter">
Visual Studio <br /> Code
</h2>
{positionedTags.map((tag, i) => (
<motion.div
key={i}
drag
dragConstraints={constraintsRef}
dragElastic={0.2}
whileDrag={{ scale: 1.1, zIndex: 50 }}
initial={{
x: tag.initialX,
y: tag.initialY,
rotate: tag.initialRotate
}}
className={`absolute cursor-grab active:cursor-grabbing px-6 py-3 rounded-full border text-sm font-bold shadow-2xl backdrop-blur-sm select-none ${tag.color}`}
>
{tag.name}
</motion.div>
))}
<p className="absolute bottom-6 text-[10px] uppercase tracking-[0.2em] text-gray-600 font-bold pointer-events-none">
Drag them anywhere
</p>
</div>
{/* PRAVÁ STRANA: Formulár */}
<div className="p-8 md:p-14 flex flex-col justify-between bg-[#161618]">
<div className="space-y-8">
<div className="space-y-2">
<h3 className="text-4xl font-black tracking-tighter text-white">Get in touch</h3>
<p className="text-gray-400 font-medium">Have a project in mind? Let's build it.</p>
</div>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="grid grid-cols-2 gap-4">
<input name="firstName" required type="text" placeholder="First Name" className="bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50 transition-all text-sm text-white" />
<input name="lastName" required type="text" placeholder="Last Name" className="bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50 transition-all text-sm text-white" />
</div>
<input name="email" required type="email" placeholder="E-mail" className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50 transition-all text-sm text-white" />
<input name="subject" required type="text" placeholder="Subject" className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50 transition-all text-sm text-white" />
<textarea name="body" required placeholder="How can I help?" rows={4} className="w-full bg-white/5 border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-blue-500/50 transition-all text-sm resize-none text-white" />
<button
type="submit"
disabled={status === "sending"}
className={`btn-pill-primary w-full justify-center py-4 text-base group mt-2 transition-all flex items-center ${
status === "success" ? "bg-green-600 border-green-500 hover:bg-green-600" :
status === "error" ? "bg-red-600 border-red-500 hover:bg-red-600" : ""
}`}
>
{status === "idle" && (
<>
Send Message
<Send size={18} className="ml-2 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
</>
)}
{status === "sending" && (
<>
<Loader2 size={18} className="mr-2 animate-spin" />
Sending...
</>
)}
{status === "success" && "Message Sent!"}
{status === "error" && "Something went wrong..."}
</button>
</form>
</div>
<div className="mt-12 pt-8 border-t border-white/5 flex flex-col sm:flex-row items-center justify-between gap-4">
<span className="text-gray-500 text-sm font-medium">Or {contactData.config.to_email}</span>
<button
type="button"
onClick={copyToClipboard}
className="flex items-center gap-3 px-6 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-full text-xs font-bold transition-all active:scale-95 text-gray-300"
>
{copied ? (
<> <Check size={14} className="text-green-500" /> Copied! </>
) : (
<> <Copy size={14} /> Copy email </>
)}
</button>
</div>
</div>
</div>
</section>
);
}

113
src/components/navbar.tsx Normal file
View File

@ -0,0 +1,113 @@
"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import navbarData from "@/data/navbar.json";
export default function Navbar() {
const pathname = usePathname();
const [isMounted, setIsMounted] = useState(false);
const [activeTab, setActiveTab] = useState("");
// Funkcia na plynulý scroll
const performScroll = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
setActiveTab(`/#${id}`);
}
};
useEffect(() => {
setIsMounted(true);
// Detekcia počiatočného stavu a scrollu po príchode z inej stránky
if (pathname === "/") {
const hash = window.location.hash.replace("#", "");
if (hash) {
setTimeout(() => performScroll(hash), 400); // Dlhší timeout pre istotu
} else {
setActiveTab("/#home");
}
} else {
setActiveTab(pathname);
}
const handleScroll = () => {
if (pathname !== "/") return;
if (window.scrollY < 100) {
setActiveTab("/#home");
return;
}
// Dynamické sledovanie sekcií z JSONu
navbarData.navItems.forEach((item) => {
if (item.href.includes("#")) {
const id = item.href.replace("/#", "");
const el = document.getElementById(id);
if (el) {
const rect = el.getBoundingClientRect();
if (rect.top <= 200 && rect.bottom >= 200) {
setActiveTab(item.href);
}
}
}
});
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [pathname]);
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
if (href.startsWith("/#") && pathname === "/") {
e.preventDefault();
const id = href.replace("/#", "");
performScroll(id);
window.history.pushState(null, "", href);
}
// Ak ideme na /projects alebo z /projects na Home, necháme Next.js pracovať
};
return (
<nav className="fixed top-8 left-0 w-full z-50 px-8 pointer-events-none">
<div className="max-w-7xl mx-auto flex justify-between items-center">
{/* LOGO Z JSONU */}
<Link href="/" className="text-2xl font-black text-white pointer-events-auto">
{navbarData.logo}
</Link>
<div className="flex bg-[#1a1a1c]/80 backdrop-blur-md p-1.5 rounded-full border border-white/5 relative pointer-events-auto">
{navbarData.navItems.map((item) => {
const isActive = activeTab === item.href;
return (
<Link
key={item.href}
href={item.href}
onClick={(e) => handleLinkClick(e, item.href)}
className="relative px-6 py-2 text-sm font-bold transition-colors duration-500 z-10"
style={{ color: isActive ? "#000000" : "#9ca3af" }} // Explicitná čierna pre aktívny tab
>
<AnimatePresence>
{isActive && isMounted && (
<motion.div
layoutId="active-pill"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ type: "spring", stiffness: 380, damping: 30 }}
className="absolute inset-0 bg-white rounded-full -z-10 shadow-xl"
/>
)}
</AnimatePresence>
{item.name}
</Link>
);
})}
</div>
</div>
</nav>
);
}

15
src/data/contact.json Normal file
View File

@ -0,0 +1,15 @@
{
"tags": [
{ "name": "React", "color": "bg-blue-500/20 text-blue-400 border-blue-500/20" },
{ "name": "Lead Dev.", "color": "bg-white/10 text-white border-white/20" },
{ "name": "TypeScript", "color": "bg-purple-500/20 text-purple-400 border-purple-500/20" },
{ "name": "Node.Js", "color": "bg-indigo-500/20 text-indigo-400 border-indigo-500/20" },
{ "name": "Developer", "color": "bg-purple-600/20 text-purple-300 border-purple-600/20" },
{ "name": "Minecraft Server", "color": "bg-green-500/20 text-green-400 border-green-500/20" },
{ "name": "UI/UX", "color": "bg-orange-500/20 text-orange-400 border-orange-500/20" }
],
"config": {
"from_email": "test@kadernictvotama.sk",
"to_email": "hello@jetomit.bio"
}
}

109
src/data/email.html Normal file
View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
background-color: #0d0d0d;
margin: 0;
padding: 40px 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #161618;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 32px;
padding: 40px;
}
.header {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 24px;
margin-bottom: 32px;
}
.name-label {
color: #3b82f6;
text-transform: uppercase;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.1em;
margin-bottom: 4px;
}
.name-value {
color: #ffffff;
font-size: 28px;
font-weight: 900;
margin: 0;
letter-spacing: -0.02em;
}
.section-title {
color: #666666;
text-transform: uppercase;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.2em;
margin-bottom: 12px;
}
.content-box {
background-color: rgba(255, 255, 255, 0.03);
border-radius: 20px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.03);
color: #cccccc;
font-size: 16px;
line-height: 1.6;
}
.footer {
margin-top: 40px;
padding-top: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
text-align: center;
}
.pill {
display: inline-block;
padding: 8px 16px;
background-color: #2a2a2c;
border-radius: 100px;
font-size: 11px;
font-weight: 700;
color: #888888;
text-decoration: none;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.reply-hint {
font-size: 12px;
color: #555555;
margin-bottom: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="name-label">New message from</div>
<h1 class="name-value">{{firstName}} {{lastName}}</h1>
</div>
<div style="margin-bottom: 32px;">
<div class="section-title">Subject</div>
<div style="color: #ffffff; font-size: 18px; font-weight: 600;">{{subject}}</div>
</div>
<div style="margin-bottom: 32px;">
<div class="section-title">Message Body</div>
<div class="content-box">
{{body}}
</div>
</div>
<div class="footer">
<p class="reply-hint">
To reply, simply hit <b>Reply</b> in your email client.<br>
Sender: {{email}}
</p>
<div class="pill">SENT VIA JETOMIT.BIO</div>
</div>
</div>
</body>
</html>

15
src/data/main_page.json Normal file
View File

@ -0,0 +1,15 @@
{
"hero": {
"name": "Jetomit_Bio",
"pronouns": "he/him",
"description": "13y/o developer. I make and break things.",
"specialization": {
"tech1": "React",
"tech2": "Node.js"
}
},
"sections": {
"projects_title": "Main Projects"
},
"experience_tags": ["HotHost", "UI/UX", "Dev Lead", "Hosting CEO"]
}

View File

@ -0,0 +1,29 @@
[
{
"title": "HotHost.org",
"description": "High-performance hosting services for modern apps.",
"url": "https://hothost.org/",
"tag": "Hosting"
},
{
"title": "TimRodina.online",
"description": "High-performance webhosting.",
"url": "https://timrodina.online/",
"tag": "Web-Hosting"
},
{
"title": "Surafino",
"description": "Open-Source blog system.",
"url": "https://surafino.xyz/",
"tag": "Creative"
},
{
"title": "Tajnr.eu",
"description": "The best Minecraft Server",
"url": "https://tajnr.eu/",
"tag": "Minecraft Server"
}
]

8
src/data/navbar.json Normal file
View File

@ -0,0 +1,8 @@
{
"logo": "Jetomit_Bio",
"navItems": [
{ "name": "Home", "href": "/#home" },
{ "name": "Projects", "href": "/projects" },
{ "name": "Contact", "href": "/#contact" }
]
}

51
src/data/projects.json Normal file
View File

@ -0,0 +1,51 @@
[
{
"title": "HotHost.org",
"description": "High-performance hosting services for modern applications and developers.",
"url": "https://hothost.org/",
"category": "Hosting",
"year": "2025"
},
{
"title": "TimRodina.online",
"description": "Personal and family-oriented website with custom design and functionality.",
"url": "https://timrodina.online/",
"category": "Hosting",
"year": "2025"
},
{
"title": "AppGuessr.space",
"description": "Interactive guessing game focused on apps and digital platforms.",
"url": "https://appguessr.space/",
"category": "Game",
"year": "2025"
},
{
"title": "NebulaCloud",
"description": "Cloud platform project focused on storage, hosting and custom services.",
"url": "https://nebulacloud.buzz/",
"category": "Cloud",
"year": "2026"
},
{
"title": "tajnr.eu",
"description": "Experimental web project with custom UI and modern frontend technologies.",
"url": "https://tajnr.eu/",
"category": "Minecraft Server",
"year": "2025"
},
{
"title": "surafino.xyz",
"description": "Creative web project with a focus on design, branding and presentation.",
"url": "https://surafino.xyz/",
"category": "Creative",
"year": "2026"
},
{
"title": "Discord DM Ticket System",
"description": "A production-ready Discord ticket bot that operates entirely through Direct Messages, while keeping ticket channels private on the server for support staff only.",
"url": "https://github.com/Jetomit-Bio/ticket-system",
"category": "Support",
"year": "2026"
}
]

View File

@ -0,0 +1,10 @@
{
"header": {
"title": "Projects",
"description": "A collection of my projects, experiments, and things I've built over the years."
},
"sections": {
"worked_on_title": "Worked on",
"default_role": "Contributor"
}
}

27
src/data/socials.json Normal file
View File

@ -0,0 +1,27 @@
[
{
"name": "GitHub",
"url": "https://github.com/Jetomit-Bio",
"icon": "Github",
"primary": true
},
{
"name": "Discord",
"url": "https://discord.gg/gpRAgrYxP2",
"icon": "MessageSquare",
"primary": false
},
{
"name": "YouTube",
"url": "https://www.youtube.com/@Jetomit_Bio_Offi",
"icon": "Youtube",
"primary": false
},
{
"name": "PayPal",
"url": "https://www.paypal.com/paypalme/JetomitBio",
"icon": "Wallet",
"primary": false
}
]

17
src/data/workedon.json Normal file
View File

@ -0,0 +1,17 @@
[
{
"title": "PopiHout",
"role": "Dev Lead",
"url": "https://popihout.com/"
},
{
"title": "Kedernictvo Tama",
"role": "Dev Lead, Backend Dev",
"url": "https://kadernictvotama.sk/"
},
{
"title": "Nighty Scripts",
"role": "Distribution, UI",
"url": "https://nighty.timrodina.online/"
}
]