diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..d497edc --- /dev/null +++ b/install.sh @@ -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}" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4ca9b35..33e3762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c741151..042e138 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/images/me1.jpg b/public/images/me1.jpg new file mode 100644 index 0000000..05f03fd Binary files /dev/null and b/public/images/me1.jpg differ diff --git a/public/images/me2.jpg b/public/images/me2.jpg new file mode 100644 index 0000000..f95732c Binary files /dev/null and b/public/images/me2.jpg differ diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts new file mode 100644 index 0000000..dc75d66 --- /dev/null +++ b/src/app/api/send/route.ts @@ -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, '
')); // 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 } + ); + } +} \ No newline at end of file diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fe..a713067 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..c136ea0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..6383a96 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,34 +1,40 @@ 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 ( - + - {children} + {/* Tento div vytvorí tie jemné kruhy na pozadí pre celú stránku */} +
+
+
+
+
+ + {/* Obsah stránky sa vykreslí tu */} +
+ {children} +
); -} +} \ No newline at end of file diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..87fb6b8 --- /dev/null +++ b/src/app/not-found.tsx @@ -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 ( +
+ {/* Dekoratívne svetlo v pozadí */} +
+ +
+ + {/* Veľké 404 s glitch efektom v CSS */} +

+ 404 +

+ +
+

+ System Breach? Not really. +

+

+ The page you are looking for has been moved, deleted, or never existed in this terminal. +

+
+ +
+ + + Back Home + + + +
+
+ + {/* Spodná línia v štýle kódu */} +
+ Error_Code: NULL_POINTER + Status: LOST_IN_SPACE +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 295f8fd..533d5e6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+ + + + {/* HERO SEKICA - pridané id="home" pre detekciu v navbare */} +
+ + {/* BIO */} +
+
+

+ I'm {mainData.hero.name} +

+ +
+ {mainData.hero.pronouns} +
+ +

+ {mainData.hero.description}
+ Specializing in {mainData.hero.specialization.tech1} and {mainData.hero.specialization.tech2}. +

+
+ + {/* SOCIALS */} +
+ {socialsData.map((social, index) => { + // Dynamické priradenie ikony + const LucideIcon = (Icons as any)[social.icon]; + return ( + + {LucideIcon && } {social.name} + + ); + })} +
+ + {/* Badges */} +
+ {mainData.experience_tags.map(tag => ( + {tag} + ))} +
-
- - Vercel logomark - Deploy Now - - - Documentation - + + {/* POLAROIDY */} +
+
+
+ me +
+
+
+
+ me +
+
-
-
+ + + {/* PROJEKTY */} +
+

+ {mainData.sections.projects_title} +

+ +
+ + {/* CONTACT ME SEKICA */} + + +
); -} +} \ No newline at end of file diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx new file mode 100644 index 0000000..0941ba3 --- /dev/null +++ b/src/app/projects/page.tsx @@ -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 ( +
+ + +
+ {/* HLAVIČKA STRÁNKY */} +
+

+ {projectsPageData.header.title} +

+

+ {projectsPageData.header.description} +

+
+ + {/* ZOZNAM HLAVNÝCH PROJEKTOV */} + + + {/* SEKICA "WORKED ON" */} +
+
+

+ {projectsPageData.sections.worked_on_title} +

+
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/contactme.tsx b/src/components/contactme.tsx new file mode 100644 index 0000000..90999d6 --- /dev/null +++ b/src/components/contactme.tsx @@ -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([]); + 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) => { + 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
; + + return ( +
+
+ + {/* L'AVÁ STRANA: Interaktívne Tagy */} +
+ {/* Dekoratívny text na pozadí */} +

+ Visual Studio
Code +

+ + {positionedTags.map((tag, i) => ( + + {tag.name} + + ))} + +

+ Drag them anywhere +

+
+ + {/* PRAVÁ STRANA: Formulár */} +
+
+
+

Get in touch

+

Have a project in mind? Let's build it.

+
+ +
+
+ + +
+ + +