Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ pnpm-debug.log*

# macOS-specific files
.DS_Store

.contentlayer/
111 changes: 111 additions & 0 deletions app/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
import { RootLayout } from "./layouts/RootLayout";

export function ErrorBoundary() {
const error = useRouteError();

if (isRouteErrorResponse(error)) {
switch (error.status) {
case 404:
return (
<RootLayout
title="Page Not Found"
description="The page you're looking for doesn't exist."
>
<main className="mx-auto max-w-4xl px-6 py-12">
<div className="prose prose-lg">
<h1>Page Not Found</h1>
<p>
The page you're looking for doesn't exist. Please check the
URL and try again.
</p>
<p>
<a href="/" className="text-brand hover:text-brand/90">
← Go back home
</a>
</p>
</div>
</main>
</RootLayout>
);
case 401:
return (
<RootLayout
title="Unauthorized"
description="You don't have permission to access this page."
>
<main className="mx-auto max-w-4xl px-6 py-12">
<div className="prose prose-lg">
<h1>Unauthorized</h1>
<p>
You don't have permission to access this page. Please log in
and try again.
</p>
<p>
<a href="/" className="text-brand hover:text-brand/90">
← Go back home
</a>
</p>
</div>
</main>
</RootLayout>
);
default:
return (
<RootLayout
title="Error"
description="Something went wrong. Please try again later."
>
<main className="mx-auto max-w-4xl px-6 py-12">
<div className="prose prose-lg">
<h1>Error</h1>
<p>
Something went wrong. Please try again later. If the problem
persists, please contact support.
</p>
<p>
<a href="/" className="text-brand hover:text-brand/90">
← Go back home
</a>
</p>
{process.env.NODE_ENV === "development" && (
<pre className="mt-4 rounded-lg bg-gray-100 p-4">
{error.data.message || JSON.stringify(error.data, null, 2)}
</pre>
)}
</div>
</main>
</RootLayout>
);
}
}

return (
<RootLayout
title="Error"
description="Something went wrong. Please try again later."
>
<main className="mx-auto max-w-4xl px-6 py-12">
<div className="prose prose-lg">
<h1>Error</h1>
<p>
Something went wrong. Please try again later. If the problem
persists, please contact support.
</p>
<p>
<a href="/" className="text-brand hover:text-brand/90">
← Go back home
</a>
</p>
{process.env.NODE_ENV === "development" && (
<pre className="mt-4 rounded-lg bg-gray-100 p-4">
{error instanceof Error
? error.message
: "Unknown error occurred"}
</pre>
)}
</div>
</main>
</RootLayout>
);
}
32 changes: 32 additions & 0 deletions app/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Link } from "@remix-run/react";
import { SocialLinks } from "./SocialLinks";

const footerLinks = [
{ name: "Colophon", href: "/colophon" },
{ name: "Privacy Policy", href: "/privacy-policy" },
{ name: "Imprint", href: "/imprint" },
] as const;

export function Footer() {
return (
<footer className="mx-auto flex flex-col gap-5 border-t px-3 py-5">
<div className="mx-auto flex w-full flex-col items-center justify-between gap-3">
<SocialLinks />
<nav>
<ul className="flex flex-row flex-wrap justify-evenly gap-1 text-sm">
{footerLinks.map((link) => (
<li key={link.href} className="px-2">
<Link
to={link.href}
className="cursor-pointer underline-offset-2 notouch:hover:underline"
>
{link.name}
</Link>
</li>
))}
</ul>
</nav>
</div>
</footer>
);
}
74 changes: 74 additions & 0 deletions app/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Link, useLocation } from "@remix-run/react";
import { type ActiveTabType, navigationLinks } from "~/utils/navigation";
import { SocialLinks } from "./SocialLinks";
import clsx from "clsx";

interface HeaderProps {
activeTab?: ActiveTabType;
}

export function Header({ activeTab }: HeaderProps) {
const location = useLocation();

return (
<header className="flex w-full flex-grow flex-col items-center justify-between gap-5 px-4 py-5 sm:flex-row lg:flex-col lg:gap-10 lg:px-0">
<Link to="/" className="relative block cursor-pointer lg:mt-10">
<img
className="relative z-20 aspect-[200/243] h-auto max-w-[100px] lg:max-w-[175px] xl:max-w-[200px]"
width={200}
src="/images/cut-out.png"
alt="Image of Christian Cito, a 26 year old white man. I'm wearing a loose blue shirt and am looking to the side."
/>
</Link>

<nav className="flex w-fit flex-col gap-2 italic lg:w-full lg:flex-grow">
<SocialLinks
size={20}
className="justify-center sm:justify-end lg:hidden"
/>
<ul className="flex flex-wrap items-center justify-center gap-2 overflow-auto lg:flex-col lg:items-start lg:justify-start xxs:flex-nowrap">
{navigationLinks.map((link) => {
const isActive = activeTab
? activeTab === link.href
: location.pathname === link.href;

return (
<li
key={link.href}
className="pb-6 pt-4 lg:border-b group-first:lg:border-t"
>
<Link
className={clsx(
"group relative block w-full cursor-pointer break-keep px-3 text-[#444] underline-offset-2 lg:px-5",
isActive && "text-brand",
"notouch:hover:text-brand",
)}
to={link.href}
data-active={isActive}
>
<span
className={clsx(
"absolute left-0 top-1/2 -translate-y-[calc(50%+0.2rem)] text-3xl xl:text-5xl",
isActive && "text-brand",
"notouch:group-hover:text-brand",
)}
>
/
</span>
<span
className={clsx(
"ml-2 text-xl font-medium text-[#444] xl:text-2xl notouch:group-hover:!text-brand",
isActive && "!text-brand",
)}
>
{link.name}
</span>
</Link>
</li>
);
})}
</ul>
</nav>
</header>
);
}
85 changes: 85 additions & 0 deletions app/components/Particles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useCallback } from "react";
import type { Container, Engine } from "tsparticles-engine";
import { loadFull } from "tsparticles";
import Particles from "react-tsparticles";

export function Particles({ className }: { className?: string }) {
const particlesInit = useCallback(async (engine: Engine) => {
await loadFull(engine);
}, []);

const particlesLoaded = useCallback(
async (container: Container | undefined) => {
await container?.refresh();
},
[],
);

return (
<Particles
className={className}
init={particlesInit}
loaded={particlesLoaded}
options={{
fpsLimit: 120,
interactivity: {
events: {
onHover: {
enable: true,
mode: "repulse",
},
resize: true,
},
modes: {
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
color: {
value: "#8B1717",
},
links: {
color: "#8B1717",
distance: 150,
enable: true,
opacity: 0.5,
width: 1,
},
collisions: {
enable: true,
},
move: {
direction: "none",
enable: true,
outModes: {
default: "bounce",
},
random: false,
speed: 1,
straight: false,
},
number: {
density: {
enable: true,
area: 800,
},
value: 80,
},
opacity: {
value: 0.5,
},
shape: {
type: "circle",
},
size: {
value: { min: 1, max: 5 },
},
},
detectRetina: true,
}}
/>
);
}
55 changes: 55 additions & 0 deletions app/components/RelatedContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Link } from "@remix-run/react";

interface RelatedContentItem {
title: string;
description: string;
slug: string;
type: "article" | "book" | "project";
tags: string[];
}

interface RelatedContentProps {
items: RelatedContentItem[];
title?: string;
}

export function RelatedContent({
items,
title = "Related Content",
}: RelatedContentProps) {
if (items.length === 0) return null;

return (
<section className="mt-12 border-t border-gray-200 pt-12">
<h2 className="mb-6 text-2xl font-bold">{title}</h2>
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{items.map((item) => (
<li key={`${item.type}-${item.slug}`}>
<Link
to={`/${item.type}s/${item.slug}`}
className="group block h-full space-y-2 rounded-lg border border-gray-200 p-4 no-underline transition-all hover:border-brand"
>
<div className="flex flex-wrap gap-2">
<span className="inline-block rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-700">
{item.type.charAt(0).toUpperCase() + item.type.slice(1)}
</span>
{item.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="inline-block rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-700"
>
{tag}
</span>
))}
</div>
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-brand">
{item.title}
</h3>
<p className="text-sm text-gray-600">{item.description}</p>
</Link>
</li>
))}
</ul>
</section>
);
}
Loading