From 515f032f5d507ec647299ecdc61781f328818dfb Mon Sep 17 00:00:00 2001
From: Thomas Bohn
Date: Wed, 8 Jul 2026 21:10:14 -0500
Subject: [PATCH] 10 UI/UX enhancements + cycling role typewriter (#37)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add new UI enhancement components
- RoleTypewriter: cycles through prior role titles before settling on the
current one, with a blinking cursor
- ScrollReveal: fades/slides content in via IntersectionObserver as it
scrolls into view, with a safety-net timeout so content is never
permanently stuck invisible
- BackToTop: floating button that appears after scrolling and smooth-scrolls
to the top of the page
- ReadingProgressBar / ShareButton: blog post reading aids
Co-authored-by: Thomas Bohn
* Wire skip-link, back-to-top, and OG share image into the root layout
- Add a visually-hidden skip-to-content link for keyboard/screen-reader users
- Give an id so the skip-link has a target
- Mount the global BackToTop button
- Point openGraph/twitter metadata at the new social share image
Co-authored-by: Thomas Bohn
* Generate branded OG/Twitter share image
Adds scripts/og-image/generate-og-image.js (sharp-composited from the
profile photo + Tableau Color Blind brand palette) and commits the
generated public/images/og-image.png. Wired into metadata in a prior
commit. Documented regeneration in the README, same pattern as the
existing icon generator.
Co-authored-by: Thomas Bohn
* Default to system color-scheme preference for first-time visitors
Previously the theme always defaulted to light until a visitor made an
explicit choice (persisted in localStorage). Now, when there's no saved
preference, honor prefers-color-scheme: dark before falling back to light.
Co-authored-by: Thomas Bohn
* Add mobile hamburger nav and animated active-link indicator
- Replace the always-expanded mobile nav block with a hamburger toggle that
smoothly expands/collapses (CSS grid-rows trick, no JS height measuring),
closing automatically on route change
- Extract a shared DesktopNavLink with an animated underline that scales in
on hover and stays put in the accent color for the active page
Co-authored-by: Thomas Bohn
* Add reading time, progress bar, share, and scroll-reveal to blog pages
- lib/blog.ts: estimate reading time from rendered word count, exposed as
BlogPost.readingTimeMinutes
- Blog listing: show ' · X min read' per post; reveal each post card
via ScrollReveal (staggered) instead of animating on mount; add the role
typewriter to the page header
- Blog post: mount a top-of-viewport ReadingProgressBar, add a ShareButton
(Web Share API with clipboard fallback) next to 'Back to Blog', and show
reading time alongside the date
Co-authored-by: Thomas Bohn
* Add role typewriter and scroll-reveal animations across all pages
Replace the static 'PRODUCT MANAGER & SOFTWARE DESIGNER' role tag with the
RoleTypewriter component (Home, About, Contact, Certifications, Projects,
Hub, Links, V2ME), and swap each page's on-mount fade-in-up sections for
ScrollReveal so below-the-fold content animates in as it's scrolled into
view rather than all at once on load.
Co-authored-by: Thomas Bohn
---------
Co-authored-by: Cursor Agent
---
README.md | 6 +
app/about/page.tsx | 20 +-
app/blog/[slug]/page.tsx | 22 +-
app/blog/page.tsx | 29 ++-
app/certifications/page.tsx | 10 +-
app/contact/page.tsx | 16 +-
app/layout.tsx | 21 +-
app/page.tsx | 16 +-
app/projects/page.tsx | 10 +-
components/BackToTop.tsx | 48 +++++
components/Header.tsx | 266 ++++++++++++++++---------
components/RoleTypewriter.tsx | 89 +++++++++
components/ScrollReveal.tsx | 69 +++++++
components/ThemeProvider.tsx | 4 +
components/blog/ReadingProgressBar.tsx | 45 +++++
components/blog/ShareButton.tsx | 55 +++++
components/hub/HubPageWrapper.tsx | 25 ++-
components/links/LinksPageWrapper.tsx | 25 ++-
components/v2me/V2MePageWrapper.tsx | 10 +-
lib/blog.ts | 13 ++
package.json | 1 +
public/images/og-image.png | Bin 0 -> 390246 bytes
scripts/og-image/generate-og-image.js | 145 ++++++++++++++
23 files changed, 787 insertions(+), 158 deletions(-)
create mode 100644 components/BackToTop.tsx
create mode 100644 components/RoleTypewriter.tsx
create mode 100644 components/ScrollReveal.tsx
create mode 100644 components/blog/ReadingProgressBar.tsx
create mode 100644 components/blog/ShareButton.tsx
create mode 100644 public/images/og-image.png
create mode 100644 scripts/og-image/generate-og-image.js
diff --git a/README.md b/README.md
index b3b246b..1b7f411 100644
--- a/README.md
+++ b/README.md
@@ -125,6 +125,12 @@ Site icons live in `public/icons/`:
To regenerate PNGs after editing the SVG: `npm run generate:icons` (requires `sharp` as a devDependency).
+## 🖼️ Social Share Image
+
+`public/images/og-image.png` is the Open Graph / Twitter Card image shown when a link to the site is shared. It's generated from `public/images/profile.jpg` plus the site's brand colors.
+
+To regenerate it after updating the profile photo or brand colors: `npm run generate:og-image` (requires `sharp` as a devDependency).
+
## 📝 Content Management
- **Blog Posts**: Written in Markdown and stored in `content/blog/`
diff --git a/app/about/page.tsx b/app/about/page.tsx
index decd1c9..c23d35e 100644
--- a/app/about/page.tsx
+++ b/app/about/page.tsx
@@ -1,5 +1,7 @@
import Image from 'next/image'
import type { Metadata } from 'next'
+import { RoleTypewriter } from '@/components/RoleTypewriter'
+import { ScrollReveal } from '@/components/ScrollReveal'
export const metadata: Metadata = {
title: 'About',
@@ -16,7 +18,9 @@ export default function About() {
{/* Header section with role tag */}
-
PRODUCT MANAGER & SOFTWARE DESIGNER
+
+
+
Thomas Bohn
@@ -54,7 +58,8 @@ export default function About() {
{/* Right column - About content */}
-
+
+
{'⟩'}
@@ -108,7 +113,9 @@ export default function About() {
+
+
{'⟩'}
@@ -145,7 +152,9 @@ export default function About() {
+
+
{'⟩'}
@@ -182,7 +191,9 @@ export default function About() {
+
+
{'⟩'}
@@ -235,7 +246,9 @@ export default function About() {
+
+
{'⟩'}
@@ -270,7 +283,9 @@ export default function About() {
+
+
{'⟩'}
@@ -291,6 +306,7 @@ export default function About() {
Contact Me
+
diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx
index efe7041..25e5521 100644
--- a/app/blog/[slug]/page.tsx
+++ b/app/blog/[slug]/page.tsx
@@ -3,6 +3,8 @@ import { getBlogPost, getBlogPosts } from '@/lib/blog'
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { FaMedium } from 'react-icons/fa'
+import { ReadingProgressBar } from '@/components/blog/ReadingProgressBar'
+import { ShareButton } from '@/components/blog/ShareButton'
export const dynamicParams = false
@@ -30,10 +32,11 @@ export default async function BlogPost({ params }: { params: Promise<{ slug: str
return (
+
{'>'} SOFTWARE ENGINEER
-
+
{'// BLOG'}
@@ -44,6 +47,7 @@ export default async function BlogPost({ params }: { params: Promise<{ slug: str
>
← Back to Blog
+
{post.mediumUrl && (