add initial
This commit is contained in:
59
src/components/Navbar.tsx
Normal file
59
src/components/Navbar.tsx
Normal 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
5
src/components/Title.tsx
Normal 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
76
src/components/contact.ts
Normal 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
15
src/components/fonts.ts
Normal 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
19
src/components/pages.ts
Normal 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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user