Skip to content
Merged
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
101 changes: 67 additions & 34 deletions components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,54 @@ import React, { useState, useEffect, useRef } from 'react'
const HOME = 'https://hovanhoa.net'

type FooterLink = { label: string; href: string }
type FooterSection = { title: string; links: FooterLink[] }

const PAGES: FooterLink[] = [
{ label: 'home', href: 'https://hovanhoa.net' },
{ label: 'insight', href: 'https://insight.hovanhoa.net' },
{ label: 'gallery', href: 'https://gallery.hovanhoa.net' },
{ label: 'music', href: 'https://music.hovanhoa.net' },
{ label: 'status', href: 'https://status.hovanhoa.net' },
]

const ELSEWHERE: FooterLink[] = [
{ label: 'github', href: 'https://github.com/hovanhoa' },
{ label: 'twitter', href: 'https://twitter.com/_hovanhoa_' },
{ label: 'linkedin', href: 'https://linkedin.com/in/hovanhoa' },
{ label: 'connect', href: 'https://info.hovanhoa.net' },
const SECTIONS: FooterSection[] = [
{
title: 'apps',
links: [
{ label: 'home', href: 'https://hovanhoa.net' },
{ label: 'insight', href: 'https://insight.hovanhoa.net' },
{ label: 'gallery', href: 'https://gallery.hovanhoa.net' },
{ label: 'music', href: 'https://music.hovanhoa.net' },
{ label: 'status', href: 'https://status.hovanhoa.net' },
],
},
{
title: 'elsewhere',
links: [
{ label: 'github', href: 'https://github.com/hovanhoa' },
{ label: 'twitter', href: 'https://twitter.com/_hovanhoa_' },
{ label: 'linkedin', href: 'https://linkedin.com/in/hovanhoa' },
],
},
{
title: 'more',
links: [
{ label: 'connect', href: 'https://info.hovanhoa.net' },
{ label: 'llms.txt', href: 'https://hovanhoa.net/llms.txt' },
],
},
]

const linkClass =
'font-[family-name:var(--font-mono)] text-sm text-[var(--rd-text-2)] transition-colors duration-200 hover:text-[var(--rd-accent-ink)]'

function LinkRow({ links }: { links: FooterLink[] }) {
function Section({ title, links }: FooterSection) {
return (
<div className="flex flex-wrap gap-x-7 gap-y-3">
{links.map((l) => (
<a key={l.label} href={l.href} className={linkClass}>
{l.label}
</a>
))}
<div>
<h3 className="font-[family-name:var(--font-mono)] text-[11.5px] font-medium uppercase tracking-[0.14em] text-[var(--rd-accent)]">
{title}
</h3>
<ul className="mt-4 space-y-2.5">
{links.map((l) => (
<li key={l.label}>
<a href={l.href} className={linkClass}>
{l.label}
</a>
</li>
))}
</ul>
</div>
)
}
Expand Down Expand Up @@ -74,23 +95,33 @@ export function Footer() {
}}
>
<div className="mx-auto max-w-[var(--rd-maxw)] px-[var(--rd-pad)] py-16 sm:py-20">
<Link
href={HOME}
className="block text-2xl font-bold tracking-tight text-[var(--rd-text)] sm:text-3xl"
>
hovanhoa
<span className="text-[var(--rd-accent)]">.net</span>
</Link>
<p className="rd-lead mt-4">
software engineer — building, writing, and shipping. one
small corner of the internet, a few sites deep.
</p>
<div className="grid gap-12 lg:grid-cols-[1.3fr_2fr]">
{/* wordmark + blurb */}
<div>
<Link
href={HOME}
className="block text-2xl font-bold tracking-tight text-[var(--rd-text)] sm:text-3xl"
>
hovanhoa
<span className="text-[var(--rd-accent)]">
.net
</span>
</Link>
<p className="rd-lead mt-4">
software engineer — building, writing, and shipping.
one small corner of the internet, a few sites deep.
</p>
</div>

<div className="mt-12 space-y-4">
<LinkRow links={PAGES} />
<LinkRow links={ELSEWHERE} />
{/* sectioned links */}
<div className="grid grid-cols-2 gap-x-8 gap-y-10 sm:grid-cols-3">
{SECTIONS.map((s) => (
<Section key={s.title} {...s} />
))}
</div>
</div>

{/* bottom row */}
<div className="mt-14 flex flex-col gap-3 border-t border-[var(--rd-border)] pt-6 font-[family-name:var(--font-mono)] text-xs text-[var(--rd-text-3)] sm:flex-row sm:items-center sm:justify-between">
<p>© 2026 hovanhoa</p>
<p>
Expand Down Expand Up @@ -127,3 +158,5 @@ export function Footer() {
</footer>
)
}

export default Footer
14 changes: 10 additions & 4 deletions utils/generateBlurPlaceholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ export default async function getBase64ImageUrl(
if (url) {
return url;
}
const response = await fetch(
`https://res.cloudinary.com/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/f_jpg,w_8,q_70/${image.public_id}.${image.format}`,
);
const buffer = await response.arrayBuffer();
let buffer: ArrayBuffer;
try {
const response = await fetch(
`https://res.cloudinary.com/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/f_jpg,w_8,q_70/${image.public_id}.${image.format}`,
);
buffer = await response.arrayBuffer();
} catch {
// DIAG: tolerate offline blur fetch so the page can render locally
return "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
}
const minified = await imagemin.buffer(Buffer.from(buffer), {
plugins: [imageminJpegtran()],
});
Expand Down
Loading