feat: add the header

This commit is contained in:
Dorian Niemiec 2024-11-05 21:14:06 +01:00
parent 919c35354d
commit 99e756c508
9 changed files with 324 additions and 26 deletions

31
app/(root)/layout.jsx Normal file
View file

@ -0,0 +1,31 @@
import Header from "@/components/Header";
export default function RootLayout({ children }) {
return (
<div className="flex flex-col min-h-screen">
<Header />
{/*<Header
docLinks={[
{
href: "/",
target: "_self",
label: "Home"
},
{
href: "/docs",
target: "_self",
label: "Docs",
sub: true
},
{
href: "/blog",
target: "_self",
label: "Blog",
sub: true
}
]}
/>*/}
<div className="flex-grow flex-1 overflow-x-hidden">{children}</div>
</div>
);
}

View file

@ -1,5 +0,0 @@
function Home() {
return <div>Placeholder</div>;
}
export default Home;

View file

@ -1,5 +1,3 @@
import { ThemeProvider } from "next-themes";
export const metadata = { export const metadata = {
title: "MERNMail - a MERN stack webmail application", title: "MERNMail - a MERN stack webmail application",
description: description:
@ -15,9 +13,9 @@ export const metadata = {
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/mernmail-cover.png`, url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/mernmail-cover.png`,
width: 2560, width: 2560,
height: 1440, height: 1440,
alt: "MERNMail - a MERN stack webmail application", alt: "MERNMail - a MERN stack webmail application"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
@ -26,12 +24,14 @@ export const metadata = {
description: description:
"MERNMail: Open-source webmail app built with MERN stack. Send, receive, & manage emails easily. Explore and contribute today!", "MERNMail: Open-source webmail app built with MERN stack. Send, receive, & manage emails easily. Explore and contribute today!",
images: [ images: [
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/mernmail-cover.png`, `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/mernmail-cover.png`
], ],
creator: "@MERNMail", creator: "@MERNMail"
}, }
}; };
export default function RootLayout({ children }) { function Home() {
return <>{children}</>; return <div>Placeholder</div>;
} }
export default Home;

View file

@ -4,7 +4,7 @@ import "./globals.css";
const inter = Inter({ const inter = Inter({
weight: ["400", "600", "700", "900"], weight: ["400", "600", "700", "900"],
subsets: ["latin"], subsets: ["latin"]
}); });
export const metadata = { export const metadata = {
@ -22,9 +22,9 @@ export const metadata = {
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/mernmail-cover.png`, url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/mernmail-cover.png`,
width: 2560, width: 2560,
height: 1440, height: 1440,
alt: "MERNMail - a MERN stack webmail application", alt: "MERNMail - a MERN stack webmail application"
}, }
], ]
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
@ -33,10 +33,10 @@ export const metadata = {
description: description:
"MERNMail: Open-source webmail app built with MERN stack. Send, receive, & manage emails easily. Explore and contribute today!", "MERNMail: Open-source webmail app built with MERN stack. Send, receive, & manage emails easily. Explore and contribute today!",
images: [ images: [
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/mernmail-cover.png`, `${process.env.NEXT_PUBLIC_WEBSITE_URL}/metadata/mernmail-cover.png`
], ],
creator: "@MERNMail", creator: "@MERNMail"
}, }
}; };
export default function RootLayout({ children }) { export default function RootLayout({ children }) {

189
components/Header.jsx Normal file
View file

@ -0,0 +1,189 @@
"use client";
import Logo from "@/components/Logo";
import { headerLinks } from "@/constants";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Menu, Moon, Sun, X } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import PropTypes from "prop-types";
function Header({ docLinks }) {
const pathname = usePathname();
const [menuShown, setMenuShown] = useState(false);
const { resolvedTheme, setTheme } = useTheme();
useEffect(() => {
if (menuShown) {
document.documentElement.style.overflow = "hidden";
} else {
document.documentElement.style.overflow = null;
}
}, [menuShown]);
return (
<header className="w-full border-b border-b-border sticky z-40 block h-12">
<div className="flex flex-row h-full px-2 py-2 mx-auto max-w-screen-xl">
<Link href="/" className="mx-1.5">
<span className="sr-only">MERNMail logo</span>
<Logo width={160} height={35} className="inline h-8 w-auto" />
</Link>
<nav className="grow">
<ul className="hidden md:inline mx-2.5 list-none">
{headerLinks.nav.map((navLink) => (
<li className="inline" key={navLink.label}>
<Link
href={navLink.href}
target={navLink.target}
className={`inline-block align-middle ${pathname == navLink.href ? "bg-accent" : ""} text-inherit px-2 py-1 h-8 mx-1 rounded-sm hover:bg-accent/60 transition-colors`}
>
{navLink.label}
</Link>
</li>
))}
</ul>
</nav>
<span className="self-center whitespace-nowrap">
<Link
href={headerLinks.git.href}
target={headerLinks.git.target}
className="hidden md:inline-block align-middle h-8 w-8 mx-0.5 p-1 bg-inherit text-inherit hover:bg-accent/60 rounded-md transition-colors"
>
<span className="sr-only">Git</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width={25}
height={25}
viewBox="0 0 123 123"
fill="none"
className="align-top"
>
<path
fill="currentColor"
d="M120.208 55.953 66.715 2.463a7.885 7.885 0 0 0-11.158 0l-11.109 11.11 14.088 14.088a9.373 9.373 0 0 1 11.87 11.948l13.578 13.579a9.368 9.368 0 0 1 9.704 2.23 9.386 9.386 0 0 1-6.64 16.025 9.393 9.393 0 0 1-9.21-7.547 9.384 9.384 0 0 1 .526-5.416L65.697 45.817v33.33a9.385 9.385 0 0 1 2.48 15.053 9.386 9.386 0 0 1-15.311-3.046A9.388 9.388 0 0 1 54.9 80.923a9.378 9.378 0 0 1 3.078-2.052V45.235a9.336 9.336 0 0 1-3.078-2.047A9.4 9.4 0 0 1 52.88 32.92l-13.89-13.89L2.311 55.703a7.89 7.89 0 0 0 0 11.16l53.495 53.497a7.895 7.895 0 0 0 11.157 0l53.244-53.245a7.9 7.9 0 0 0 0-11.162Z"
/>
</svg>
</Link>
<button
className="inline-block align-middle h-8 w-8 mx-0.5 p-1 bg-inherit text-inherit hover:bg-accent/60 rounded-md transition-colors"
onClick={(e) => {
e.preventDefault();
if (resolvedTheme == "light") {
setTheme("dark");
} else {
setTheme("light");
}
}}
>
<span className="sr-only">Toggle theme</span>
<Moon
width={25}
height={25}
className="hidden dark:inline align-top"
/>
<Sun
width={25}
height={25}
className="inline dark:hidden align-top"
/>
</button>
<button
className="inline-block md:hidden align-middle h-8 w-8 mx-0.5 p-1 bg-inherit text-inherit hover:bg-accent/60 rounded-md transition-colors"
onClick={(e) => {
e.preventDefault();
setMenuShown(true);
}}
>
<span className="sr-only">Toggle menu</span>
<Menu width={25} height={25} />
</button>
</span>
</div>
<nav
className={`block md:hidden bg-background w-full h-full p-2 overflow-auto z-50 fixed top-0 ${menuShown ? "left-0" : "left-full"} transition-[left] shrink-0 duration-1000`}
>
<div className="flex flex-row">
<div className="grow mx-1.5">
<Logo
width={160}
height={35}
className="inline h-8 w-auto self-center"
/>
</div>
<button
onClick={(e) => {
e.preventDefault();
setMenuShown(false);
}}
className="inline-block md:hidden self-center"
>
<X className="inline-block w-8 h-8 py-1 rounded-sm bg-background text-foreground hover:bg-accent/60 hover:text-accent-foreground transition-colors" />
</button>
</div>
<ul
className={`flex ${docLinks ? "flex-row overflow-x-auto" : "flex-col"} my-3 list-none`}
>
{headerLinks.nav.map((navLink) => (
<li
className={`${docLinks ? "inline" : "block my-1 text-center"}`}
key={navLink.label}
>
<Link
href={navLink.href}
target={navLink.target}
className={`inline-block align-middle ${pathname == navLink.href ? "bg-accent" : ""} text-inherit px-2 py-1 h-8 mx-1 rounded-sm hover:bg-accent/60 transition-colors`}
>
{navLink.label}
</Link>
</li>
))}
<li className={`${docLinks ? "inline" : "block my-1 text-center"}`}>
<Link
href={headerLinks.git.href}
target={headerLinks.git.target}
className="whitespace-nowrap inline-block align-middle text-inherit px-2 py-1 h-8 mx-1 rounded-sm hover:bg-accent/60 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width={25}
height={25}
viewBox="0 0 123 123"
fill="none"
className="inline mr-2 align-top"
>
<path
fill="currentColor"
d="M120.208 55.953 66.715 2.463a7.885 7.885 0 0 0-11.158 0l-11.109 11.11 14.088 14.088a9.373 9.373 0 0 1 11.87 11.948l13.578 13.579a9.368 9.368 0 0 1 9.704 2.23 9.386 9.386 0 0 1-6.64 16.025 9.393 9.393 0 0 1-9.21-7.547 9.384 9.384 0 0 1 .526-5.416L65.697 45.817v33.33a9.385 9.385 0 0 1 2.48 15.053 9.386 9.386 0 0 1-15.311-3.046A9.388 9.388 0 0 1 54.9 80.923a9.378 9.378 0 0 1 3.078-2.052V45.235a9.336 9.336 0 0 1-3.078-2.047A9.4 9.4 0 0 1 52.88 32.92l-13.89-13.89L2.311 55.703a7.89 7.89 0 0 0 0 11.16l53.495 53.497a7.895 7.895 0 0 0 11.157 0l53.244-53.245a7.9 7.9 0 0 0 0-11.162Z"
/>
</svg>
<span className="align-middle">Git</span>
</Link>
</li>
</ul>
{docLinks ? (
<ul className={`flex flex-col my-3 list-none`}>
{docLinks.map((docLink) => (
<li className="block my-1" key={docLink.label}>
<Link
href={docLink.href}
target={docLink.target || "_self"}
className={`${docLink.sub ? "ml-4" : ""} block align-middle ${pathname == docLink.href ? "bg-accent" : ""} text-inherit px-2 py-1 h-8 mx-1 rounded-sm hover:bg-accent/60 transition-colors`}
>
{docLink.label}
</Link>
</li>
))}
</ul>
) : (
""
)}
</nav>
</header>
);
}
Header.propTypes = {
docLinks: PropTypes.arrayOf(PropTypes.object)
};
export default Header;

51
components/Logo.jsx Normal file
View file

@ -0,0 +1,51 @@
const Logo = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={460.331}
height={87.284}
viewBox="0 0 121.796 23.094"
{...props}
>
<path
d="m20 17.32-9.999 5.774-10-5.772L0 5.775 9.999 0l10 5.772Z"
style={{
fill: "#ff7f00",
strokeWidth: 0.55943
}}
/>
<path
d="M5.695 6.784a1.386 1.394 0 0 0-1.382 1.341l5.601 2.477 5.622-2.486a1.386 1.394 0 0 0-1.384-1.332Zm9.843 1.977-5.624 2.6L4.31 8.77v5.741a1.386 1.394 0 0 0 1.386 1.395h8.457a1.386 1.394 0 0 0 1.386-1.395z"
style={{
fill: "#fff",
strokeWidth: 4.37786
}}
/>
<g
aria-label="MERNMail"
style={{
fontSize: "19.7556px",
lineHeight: 1.25,
strokeWidth: 0.264583
}}
>
<path
d="M29.855 25.135h2.922v-6.78c0-1.081-.077-3.194-.106-4.94.512 1.93 1.09 3.946 1.428 4.94l2.421 6.78H39l2.382-6.78c.337-1.023.945-3.117 1.428-5.056-.03 1.814-.107 3.975-.107 5.055v6.781h2.962V10.762H41.13l-2.296 6.599c-.29.916-.733 2.836-1.06 4.389-.338-1.563-.782-3.483-1.072-4.39l-2.334-6.598h-4.514zM48.26 25.135h9.636v-2.44h-6.694v-3.617h6.173v-2.402h-6.173v-3.473h6.675v-2.44H48.26zM60.26 25.135h2.942v-5.131h2.315l2.74 5.131h3.26l-3.058-5.604c1.64-.704 2.547-2.122 2.547-4.09 0-2.826-1.872-4.679-5.142-4.679H60.26Zm2.942-7.514v-4.428h2.112c1.804 0 2.653.801 2.653 2.248 0 1.437-.849 2.18-2.643 2.18zM73.234 25.135h3.01v-6.607c0-.85-.087-2.393-.193-4.129.887 1.794 1.418 2.846 2.218 4.138l4.129 6.598h3.29V10.762h-3.01v6.898c0 .945.077 2.488.183 3.897a25.41 25.41 0 0 0-1.804-3.521l-4.543-7.274h-3.28z"
style={{
fill: "#ff7f00"
}}
transform="translate(-4.55 -6.367)"
/>
<path
d="M88.292 25.135h2.923v-6.78c0-1.081-.078-3.194-.107-4.94.512 1.93 1.09 3.946 1.428 4.94l2.421 6.78h2.48l2.382-6.78c.338-1.023.945-3.117 1.428-5.056-.03 1.814-.106 3.975-.106 5.055v6.781h2.961V10.762h-4.534l-2.296 6.599c-.289.916-.733 2.836-1.06 4.389-.338-1.563-.782-3.483-1.071-4.39l-2.335-6.598h-4.514zM109.639 25.348c1.592 0 2.633-.705 3.154-1.708h.097v1.495h2.74v-7.263c0-2.614-2.258-3.656-4.573-3.656-2.412 0-4.148 1.07-4.698 2.942l2.653.444c.232-.695.916-1.245 2.035-1.245 1.08 0 1.689.521 1.689 1.438v.048c0 .704-.763.781-2.615.964-2.122.203-4.07.898-4.07 3.31 0 2.131 1.533 3.23 3.588 3.23zm.82-2.036c-.955 0-1.63-.434-1.63-1.273 0-.868.714-1.283 1.784-1.437.637-.087 1.785-.241 2.142-.492v1.167c0 1.138-.946 2.035-2.296 2.035zM118.099 25.135h2.894V14.351h-2.894zm1.447-12.192c.868 0 1.591-.656 1.591-1.486 0-.82-.723-1.486-1.591-1.486-.878 0-1.592.666-1.592 1.486 0 .83.714 1.486 1.592 1.486zM126.346 10.762h-2.894v14.373h2.894z"
style={{
fill: "currentColor",
fontWeight: 700,
fontFamily: "Inter",
InkscapeFontSpecification: "&quot"
}}
transform="translate(-4.55 -6.367)"
/>
</g>
</svg>
);
export default Logo;

23
constants/index.jsx Normal file
View file

@ -0,0 +1,23 @@
export const headerLinks = {
nav: [
{
href: "/",
target: "_self",
label: "Home"
},
{
href: "/docs",
target: "_self",
label: "Docs"
},
{
href: "/blog",
target: "_self",
label: "Blog"
}
],
git: {
href: "https://git.svrjs.org",
target: "_blank"
}
};

15
package-lock.json generated
View file

@ -8,8 +8,10 @@
"name": "mernmail-website", "name": "mernmail-website",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"lucide-react": "^0.454.0",
"next": "^15.0.2", "next": "^15.0.2",
"next-themes": "^0.4.3", "next-themes": "^0.4.3",
"prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },
@ -6822,6 +6824,14 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true "dev": true
}, },
"node_modules/lucide-react": {
"version": "0.454.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz",
"integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
}
},
"node_modules/meow": { "node_modules/meow": {
"version": "12.1.1", "version": "12.1.1",
"resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz",
@ -7133,7 +7143,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -7749,7 +7758,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
@ -7839,8 +7847,7 @@
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
"dev": true
}, },
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",

View file

@ -12,8 +12,10 @@
"cz": "cz" "cz": "cz"
}, },
"dependencies": { "dependencies": {
"lucide-react": "^0.454.0",
"next": "^15.0.2", "next": "^15.0.2",
"next-themes": "^0.4.3", "next-themes": "^0.4.3",
"prop-types": "^15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },