add initial

This commit is contained in:
2025-04-02 00:17:25 +02:00
commit 792d71523c
39 changed files with 7525 additions and 0 deletions

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

@@ -0,0 +1,59 @@
"use client";
import { useState } from "react";
import React from "react";
import { navBarFont } from "./fonts";
import {
PAGES,
Pages,
pathNameFromSelectedPage,
selectedPageFromPathName,
} from "./pages";
import { usePathname, useRouter } from "next/navigation";
export default function Navbar() {
const [hoveredPage, setHoveredPage] = useState<Pages | null>(null);
const pathName = usePathname();
const selectedPage = selectedPageFromPathName(pathName);
const router = useRouter();
const navbarItem = (page: Pages): React.CSSProperties => ({
padding: "0.3rem 0.7rem",
cursor: "pointer",
backgroundColor:
selectedPage == page || hoveredPage === page ? "#eee" : "transparent",
color: selectedPage == page || hoveredPage == page ? "#222" : "#eee",
transition: "all 0.3s ease",
});
return (
<nav
className={navBarFont.className}
style={{
fontWeight: "700",
display: "flex",
flexDirection: "row",
gap: "1rem",
fontSize: "1.1rem",
flexWrap: "wrap",
maxWidth: "90vw",
justifyContent: "center",
}}
>
{PAGES.map((page, index) => (
<React.Fragment key={page}>
<div
style={navbarItem(page)}
onClick={() => router.push(pathNameFromSelectedPage(page))}
onMouseEnter={() => setHoveredPage(page)}
onMouseLeave={() => setHoveredPage(null)}
>
<div style={{ userSelect: "none" }}>{page}</div>
</div>
{index < PAGES.length - 1 && <span>|</span>}
</React.Fragment>
))}
</nav>
);
}

5
src/components/Title.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { titleFont } from "./fonts";
export default function Title() {
return <h1 className={titleFont.className}>nrx.sh</h1>;
}

76
src/components/contact.ts Normal file
View File

@@ -0,0 +1,76 @@
"use server";
import { createTransport } from "nodemailer";
const RECAPTCHA_SECRET_KEY = process.env.RECAPTCHA_SECRET_KEY;
export type SubmitContactReturn = { error: string } | { success: true };
export default async function SubmitContact(
prevState: unknown,
data: FormData
): Promise<SubmitContactReturn> {
if (!RECAPTCHA_SECRET_KEY) {
console.error(
"RECAPTCHA_SECRET_KEY is not set. Please check your environment variables."
);
throw new Error("Server error: RECAPTCHA not configure correctly.");
}
const name = data.get("name");
const email = data.get("email");
const message = data.get("message");
const recaptcha = data.get("g-recaptcha-response");
if (!name || !email || !message) {
return { error: "All fields are required." };
}
if (!recaptcha) {
return { error: "Please complete the reCAPTCHA." };
}
try {
const recaptchaResponse = await fetch(
`https://www.google.com/recaptcha/api/siteverify`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `secret=${RECAPTCHA_SECRET_KEY}&response=${recaptcha}`,
}
);
const recaptchaData = await recaptchaResponse.json();
if (!recaptchaData.success) {
return { error: "reCAPTCHA verification failed." };
}
} catch (e) {
console.error(e);
return { error: "Could not reach reCAPTCHA for verification." };
}
try {
const transporter = createTransport({
host: process.env.MAIL_HOST ?? "localhost",
port: process.env.MAIL_PORT ? parseInt(process.env.MAIL_PORT) : undefined,
secure: true,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
});
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: process.env.MAIL_TO,
subject: `From ${name}`,
replyTo: email.toString(),
text: `Name:${name}\nEmail: ${email}\nMessage:\n ${message}`,
});
} catch (e) {
console.error(e);
return { error: "Failed to send email." };
}
return { success: true };
}

15
src/components/fonts.ts Normal file
View File

@@ -0,0 +1,15 @@
import { Doto, Sixtyfour, Space_Grotesk } from "next/font/google";
export const titleFont = Sixtyfour({
subsets: ["latin"],
});
export const navBarFont = Doto({
subsets: ["latin"],
weight: "900",
});
export const bodyFont = Space_Grotesk({
subsets: ["latin"],
weight: "400",
});

19
src/components/pages.ts Normal file
View File

@@ -0,0 +1,19 @@
export type Pages = "home" | "about" | "links" | "contact" | "blog";
export const PAGES: Pages[] = ["home", "about", "blog", "links", "contact"];
export function selectedPageFromPathName(pathName: string): Pages {
if (pathName === "/") {
return "home";
}
if (pathName.includes("/blog/")) {
return "blog";
}
return pathName.replace("/", "") as Pages;
}
export function pathNameFromSelectedPage(page: Pages): string {
if (page === "home") {
return "/";
}
return `/${page}`;
}