From 8b0caae14f24143407f05da92ab5d8c2f263ce74 Mon Sep 17 00:00:00 2001
From: Sean O'Connor
Date: Tue, 22 Sep 2026 19:27:54 -0400
Subject: [PATCH] Expand coffee map with Boston guide and real venue photos
---
next.config.js | 1 +
src/app/coffee/coffee.css | 302 ++++++++++++++++++++++++--------
src/app/coffee/page.tsx | 4 +-
src/app/page.tsx | 6 +-
src/app/projects/page.tsx | 2 +-
src/components/CoffeeMirror.tsx | 209 ++++++++++++++--------
src/lib/coffee-shops.ts | 259 +++++++++------------------
src/lib/data.tsx | 8 +-
8 files changed, 458 insertions(+), 333 deletions(-)
diff --git a/next.config.js b/next.config.js
index d4f051c..02911ea 100644
--- a/next.config.js
+++ b/next.config.js
@@ -8,6 +8,7 @@ await import("./src/env.js");
/** @type {import('next').NextConfig} */
const nextConfig = {
+ allowedDevOrigins: ["127.0.0.1"],
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
reactStrictMode: true,
experimental: {
diff --git a/src/app/coffee/coffee.css b/src/app/coffee/coffee.css
index 3969cb9..590c2cb 100644
--- a/src/app/coffee/coffee.css
+++ b/src/app/coffee/coffee.css
@@ -1,83 +1,241 @@
-.coffee-site-main { position: relative; z-index: 0; isolation: isolate; width: 100%; flex: 1; min-height: calc(100dvh - 72px); padding: 0; margin: 0; max-width: none; }
+.coffee-site-main {
+ position: relative;
+ z-index: 0;
+ isolation: isolate;
+ width: 100%;
+ max-width: none;
+ min-height: calc(100dvh - 72px);
+ flex: 1;
+ margin: 0;
+ padding: 0;
+}
body:has(.coffee-mirror) { overflow: hidden; }
body:has(.coffee-mirror) .skip-link { z-index: 3000; }
-.coffee-mirror { position: relative; width: 100%; height: calc(100dvh - 72px); overflow: hidden; background: #eceeea; color: #161b1b; font-family: Georgia, "Times New Roman", serif; --coffee-glass: rgba(255,255,255,.82); --coffee-border: rgba(0,0,0,.12); --coffee-text: #161b1b; --coffee-subtle: #5e6363; --coffee-hover: rgba(0,0,0,.07); }
+
+.coffee-mirror {
+ --coffee-surface: #fbfdfff2;
+ --coffee-border: #d6e1e7;
+ --coffee-text: #173441;
+ --coffee-subtle: #58717d;
+ --coffee-hover: #e9f3f7;
+ --coffee-accent: #0076a0;
+ position: relative;
+ width: 100%;
+ height: calc(100dvh - 72px);
+ overflow: hidden;
+ background: #eaf3f7;
+ color: var(--coffee-text);
+ font-family: var(--font-sans), "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
.coffee-mirror * { box-sizing: border-box; }
.coffee-mirror button { cursor: pointer; }
+.coffee-mirror button:focus-visible,
+.coffee-mirror a:focus-visible,
+.coffee-mirror input:focus-visible { outline: 2px solid var(--coffee-accent); outline-offset: 2px; }
.coffee-mirror-map { position: absolute; inset: 0; z-index: 0; }
-.coffee-mirror-map-error { position: absolute; top: 45%; left: 50%; transform: translate(-50%,-50%); z-index: 3; padding: 16px; color: white; background: #35281e; border-radius: 10px; text-align: center; }
-.coffee-mirror .leaflet-container { background: #eceeea; font-family: Georgia, "Times New Roman", serif; }
-.coffee-mirror .leaflet-control-attribution { display: none; }
-.coffee-mirror-pin { border: 2px solid white; border-radius: 50%; background: #8b4513; box-shadow: 0 2px 10px #0008; }
-.coffee-mirror-pin span { display: block; width: 100%; height: 100%; border-radius: 50%; }
-.coffee-mirror-pin.is-selected { background: #efa83f; box-shadow: 0 0 0 7px #efa83f55, 0 2px 10px #0008; }
-.coffee-mirror-header { position: absolute; top: 16px; left: 16px; right: 16px; z-index: 1000; min-height: 58px; display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; border: 1px solid var(--coffee-border); border-radius: 13px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(22px); box-shadow: 0 8px 28px #0003; }
-.coffee-mirror-icon-button { display: grid; place-items: center; width: 42px; height: 42px; border: 0; border-radius: 9px; color: var(--coffee-text); background: transparent; }
-.coffee-mirror-icon-button:hover, .coffee-mirror-icon-button.is-active { color: #f5ab45; background: var(--coffee-hover); }
-.coffee-mirror-brand { display: flex; align-items: center; gap: 12px; position: absolute; left: 50%; transform: translateX(-50%); border: 0; background: transparent; color: inherit; text-align: left; white-space: nowrap; }
-.coffee-mirror-brand > svg { color: #f1a22e; flex: none; }
-.coffee-mirror-brand strong, .coffee-mirror-brand small { display: block; }
-.coffee-mirror-brand strong { font-size: 17px; line-height: 1.1; }
-.coffee-mirror-brand small { color: var(--coffee-subtle); font-size: 12px; margin-top: 3px; }
-.coffee-mirror-header-spacer { width: 42px; }
-.coffee-mirror-drawer { position: absolute; z-index: 900; top: 92px; bottom: 32px; left: 16px; width: min(400px, calc(100vw - 32px)); overflow: hidden; border: 1px solid var(--coffee-border); border-radius: 13px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(25px); box-shadow: 0 16px 38px #0004; transform: translateX(calc(-100% - 34px)); opacity: 0; pointer-events: none; transition: transform .3s ease, opacity .3s ease; }
+.coffee-mirror .leaflet-container { background: #eaf3f7; font-family: var(--font-sans), Helvetica, Arial, sans-serif; }
+.coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: saturate(.66); }
+.coffee-mirror .leaflet-control-attribution {
+ padding: 3px 7px;
+ background: var(--coffee-surface);
+ color: var(--coffee-subtle);
+ font-size: 10px;
+}
+.coffee-mirror .leaflet-control-attribution a { color: var(--coffee-accent); }
+.coffee-mirror-pin {
+ border: 3px solid #fbfdff;
+ border-radius: 50%;
+ background: #0076a0;
+ box-shadow: 0 2px 8px #17344155;
+}
+.coffee-mirror-pin.is-selected { background: #173b49; box-shadow: 0 0 0 7px #0076a04d, 0 2px 10px #17344166; }
+.coffee-mirror-map-error { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); z-index: 3; max-width: 300px; padding: 16px; border-radius: 10px; background: #173b49; color: #fff; text-align: center; }
+
+.coffee-mirror-header,
+.coffee-mirror-drawer,
+.coffee-mirror-controls > button,
+.coffee-mirror-style-wrap > button,
+.coffee-mirror-style-menu,
+.coffee-mirror-location-error {
+ border: 1px solid var(--coffee-border);
+ background: var(--coffee-surface);
+ -webkit-backdrop-filter: blur(22px);
+ backdrop-filter: blur(22px);
+ box-shadow: 0 10px 32px #1734411f;
+}
+.coffee-mirror-header {
+ position: absolute;
+ top: 16px;
+ left: 16px;
+ right: 16px;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ min-height: 70px;
+ padding: 10px 13px 10px 20px;
+ border-radius: 12px;
+}
+.coffee-mirror-brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
+.coffee-mirror-brand > svg { flex: none; color: var(--coffee-accent); }
+.coffee-mirror-brand strong,
+.coffee-mirror-brand small { display: block; }
+.coffee-mirror-brand strong { font-family: var(--font-heading), Georgia, serif; font-size: 25px; font-weight: 400; letter-spacing: -.035em; line-height: 1.1; white-space: nowrap; }
+.coffee-mirror-brand small { margin-top: 4px; color: var(--coffee-subtle); font-size: 11px; letter-spacing: .03em; }
+.coffee-mirror-regions { display: flex; align-items: center; gap: 3px; margin-left: auto; padding: 4px; border: 1px solid var(--coffee-border); border-radius: 8px; background: #eaf2f670; }
+.coffee-mirror-regions button { padding: 9px 14px; border: 0; border-radius: 5px; background: transparent; color: var(--coffee-subtle); font-size: 12px; font-weight: 600; }
+.coffee-mirror-regions button:hover { color: var(--coffee-text); }
+.coffee-mirror-regions button[aria-current="page"] { background: #173b49; color: #fff; box-shadow: 0 1px 5px #17344124; }
+.coffee-mirror-icon-button { display: grid; place-items: center; width: 42px; height: 42px; flex: none; border: 0; border-radius: 8px; background: transparent; color: var(--coffee-text); }
+.coffee-mirror-icon-button:hover,
+.coffee-mirror-icon-button.is-active { background: var(--coffee-hover); color: var(--coffee-accent); }
+
+.coffee-mirror-drawer {
+ position: absolute;
+ z-index: 900;
+ top: 102px;
+ bottom: 30px;
+ left: 16px;
+ width: min(405px, calc(100vw - 32px));
+ overflow: hidden;
+ border-radius: 12px;
+ transform: translateX(calc(-100% - 36px));
+ opacity: 0;
+ pointer-events: none;
+ transition: transform .3s ease, opacity .3s ease;
+}
.coffee-mirror-drawer.is-open { transform: translateX(0); opacity: 1; pointer-events: auto; }
-.coffee-mirror-discovery, .coffee-mirror-detail { height: 100%; display: flex; flex-direction: column; }
-.coffee-mirror-discovery-top { flex: none; padding: 24px 22px 15px; }
-.coffee-mirror-discovery h2 { display: flex; align-items: center; gap: 9px; margin: 0 0 17px; color: var(--coffee-text); font-size: 23px; line-height: 1.2; }
-.coffee-mirror-discovery h2 svg { color: #efa83f; }
-.coffee-mirror-search { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border: 1px solid var(--coffee-border); border-radius: 8px; background: var(--coffee-hover); color: var(--coffee-subtle); }
-.coffee-mirror-search input { width: 100%; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--coffee-text); font: 14px Arial, sans-serif; }
+.coffee-mirror-detail { display: flex; flex-direction: column; height: 100%; }
+.coffee-mirror-discovery { height: 100%; overflow-y: auto; scrollbar-width: thin; }
+.coffee-mirror-city-art { position: relative; height: 138px; flex: none; overflow: hidden; background: #c8dbe3; }
+.coffee-mirror-city-art img { object-fit: cover; object-position: center; }
+.coffee-mirror-city-art::after { content: ""; position: absolute; inset: 45% 0 0; background: linear-gradient(transparent, #112b3a8c); }
+.coffee-mirror-city-art span { position: absolute; right: 15px; bottom: 11px; z-index: 1; color: #fff; font-size: 10px; letter-spacing: .06em; text-shadow: 0 1px 5px #102630; }
+.coffee-mirror-discovery-top { flex: none; padding: 20px 24px 17px; }
+.coffee-mirror-overline,
+.coffee-mirror-list-label { color: var(--coffee-accent); font-size: 10px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; }
+.coffee-mirror-discovery h2,
+.coffee-mirror-detail-copy h2 { margin: 10px 0 0; font-family: var(--font-heading), Georgia, serif; font-size: 36px; font-weight: 400; letter-spacing: -.035em; line-height: 1.08; }
+.coffee-mirror-discovery-subtitle { margin: 8px 0 17px; color: var(--coffee-subtle); font-size: 12px; line-height: 1.55; }
+.coffee-mirror-search { display: flex; align-items: center; gap: 9px; padding: 11px 12px; border: 1px solid var(--coffee-border); border-radius: 7px; background: #f4f9fb; color: var(--coffee-subtle); }
+.coffee-mirror-search input { width: 100%; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--coffee-text); font: 13px var(--font-sans), Helvetica, Arial, sans-serif; }
.coffee-mirror-search input::placeholder { color: var(--coffee-subtle); }
-.coffee-mirror-list { overflow-y: auto; min-height: 0; padding: 0 14px 24px; scrollbar-width: thin; }
-.coffee-mirror-list-label { color: #eeb05c; text-transform: uppercase; letter-spacing: .12em; font: 700 11px Arial, sans-serif; padding: 8px 12px; }
-.coffee-mirror-list hr { border: 0; border-top: 1px solid var(--coffee-border); margin: 12px; }
-.coffee-mirror-row { display: flex; align-items: center; gap: 5px; width: 100%; border: 1px solid transparent; border-radius: 9px; margin: 3px 0; padding: 8px; }
-.coffee-mirror-row:hover, .coffee-mirror-row:focus-within { background: var(--coffee-hover); border-color: var(--coffee-border); }
-.coffee-mirror-row-main { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; text-align: left; color: var(--coffee-text); border: 0; background: transparent; padding: 0; }
-.coffee-mirror-row-main img { width: 63px; height: 63px; border-radius: 6px; flex: none; object-fit: cover; }
-.coffee-mirror-row-main span { min-width: 0; }
-.coffee-mirror-row-main strong, .coffee-mirror-row-main small { display: block; }
-.coffee-mirror-row-main strong { font-size: 15px; line-height: 1.25; }
-.coffee-mirror-row-main small { margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--coffee-subtle); font: 12px Arial, sans-serif; }
-.coffee-mirror-favorite { flex: none; border: 0; background: transparent; color: var(--coffee-subtle); padding: 8px; }
-.coffee-mirror-favorite:hover, .coffee-mirror-favorite:has(svg[fill="currentColor"]) { color: #eb7d79; }
-.coffee-mirror-empty { padding: 20px; text-align: center; color: var(--coffee-subtle); }
-.coffee-mirror-photo { position: relative; height: 235px; flex: none; overflow: hidden; mask-image: linear-gradient(#000 75%, transparent 100%); }
+.coffee-mirror-featured { flex: none; padding: 0 0 15px; border-bottom: 1px solid var(--coffee-border); }
+.coffee-mirror-featured .coffee-mirror-list-label { margin: 1px 24px 11px; }
+.coffee-mirror-featured-track { display: flex; gap: 10px; overflow-x: auto; padding: 0 24px 5px; scrollbar-width: none; }
+.coffee-mirror-featured-track::-webkit-scrollbar { display: none; }
+.coffee-mirror-feature-card { display: flex; flex: 0 0 145px; flex-direction: column; min-width: 0; padding: 0 0 8px; overflow: hidden; border: 1px solid var(--coffee-border); border-radius: 8px; background: var(--coffee-hover); color: var(--coffee-text); text-align: left; }
+.coffee-mirror-feature-card:hover { border-color: var(--coffee-accent); }
+.coffee-mirror-feature-image { position: relative; display: block; width: 100%; height: 86px; overflow: hidden; background: #c8dbe3; }
+.coffee-mirror-feature-image img { object-fit: cover; }
+.coffee-mirror-feature-card strong { display: block; margin: 8px 9px 3px; font-size: 11px; line-height: 1.25; }
+.coffee-mirror-feature-card small { display: block; margin: auto 9px 0; color: var(--coffee-subtle); font-size: 10px; }
+.coffee-mirror-results-head { display: grid; gap: 16px; padding: 14px 18px 8px; }
+.coffee-mirror-results-head .coffee-mirror-list-label { margin: 0 8px; }
+.coffee-mirror-filters { display: flex; flex-wrap: wrap; gap: 6px; }
+.coffee-mirror-filters button { flex: none; padding: 7px 10px; border: 1px solid var(--coffee-border); border-radius: 100px; background: transparent; color: var(--coffee-subtle); font-size: 10px; font-weight: 600; }
+.coffee-mirror-filters button:hover { background: var(--coffee-hover); color: var(--coffee-text); }
+.coffee-mirror-filters button[aria-pressed="true"] { border-color: #173b49; background: #173b49; color: #fff; }
+.coffee-mirror-list { padding: 0 12px 24px; }
+.coffee-mirror-list-label { margin: 9px 11px 11px; }
+.coffee-mirror-list hr { margin: 14px 10px; border: 0; border-top: 1px solid var(--coffee-border); }
+.coffee-mirror-row { display: flex; align-items: center; gap: 5px; width: 100%; margin: 2px 0; padding: 8px; border: 1px solid transparent; border-radius: 8px; }
+.coffee-mirror-row:hover,
+.coffee-mirror-row:focus-within { border-color: var(--coffee-border); background: var(--coffee-hover); }
+.coffee-mirror-row-main { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1; padding: 0; border: 0; background: transparent; color: var(--coffee-text); text-align: left; }
+.coffee-mirror-row-main img,
+.coffee-mirror-row-icon { width: 62px; height: 62px; flex: none; border-radius: 6px; }
+.coffee-mirror-row-main img { object-fit: cover; }
+.coffee-mirror-row-icon { display: grid; place-items: center; background: linear-gradient(135deg, #d7ebef, #b5d7e0); color: #286074; }
+.coffee-mirror-row-copy { min-width: 0; }
+.coffee-mirror-row-copy strong,
+.coffee-mirror-row-copy small { display: block; }
+.coffee-mirror-row-copy strong { font-size: 13px; line-height: 1.25; }
+.coffee-mirror-row-copy small { overflow: hidden; margin-top: 5px; color: var(--coffee-subtle); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
+.coffee-mirror-favorite { flex: none; padding: 8px; border: 0; background: transparent; color: var(--coffee-subtle); }
+.coffee-mirror-favorite:hover,
+.coffee-mirror-favorite:has(svg[fill="currentColor"]) { color: var(--coffee-accent); }
+.coffee-mirror-empty { padding: 20px; color: var(--coffee-subtle); font-size: 13px; text-align: center; }
+
+.coffee-mirror-detail { overflow-y: auto; }
+.coffee-mirror-photo { position: relative; height: 225px; flex: none; overflow: hidden; background: #d7ebef; }
.coffee-mirror-photo img { width: 100%; height: 100%; object-fit: cover; }
-.coffee-mirror-detail { overflow-y: auto; }
-.coffee-mirror-detail-actions { position: absolute; right: 13px; top: 13px; display: flex; gap: 7px; }
-.coffee-mirror-detail-actions button { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--coffee-border); border-radius: 50%; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(8px); }
-.coffee-mirror-detail-copy { padding: 4px 24px 30px; }
-.coffee-mirror-detail-copy h2 { margin: 0 0 12px; font-size: 29px; line-height: 1.15; }
-.coffee-mirror-detail-copy p { color: var(--coffee-text); font: 14px/1.55 Arial, sans-serif; }
-.coffee-mirror-detail-copy .coffee-mirror-address { display: flex; gap: 7px; align-items: flex-start; color: var(--coffee-subtle); }
-.coffee-mirror-address svg { flex: none; color: #f1a22e; }
-.coffee-mirror-directions { display: block; margin-top: 24px; padding: 12px; border-radius: 8px; background: #151515; color: white; text-align: center; font: 600 14px Arial, sans-serif; }
-.coffee-mirror-detail-links { display: flex; gap: 10px; margin-top: 12px; }
-.coffee-mirror-detail-links a { flex: 1; padding: 10px; border: 1px solid var(--coffee-border); border-radius: 8px; color: var(--coffee-text); text-align: center; font: 13px Arial, sans-serif; }
-.coffee-mirror-controls { position: absolute; right: 16px; bottom: 72px; z-index: 1000; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
-.coffee-mirror-controls > button, .coffee-mirror-style-wrap > button { display: grid; place-items: center; width: 43px; height: 43px; border: 1px solid var(--coffee-border); border-radius: 12px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(20px); box-shadow: 0 4px 14px #0003; }
-.coffee-mirror-controls button:hover { background: var(--coffee-hover); }
+.coffee-mirror-photo-credit { position: absolute; right: 10px; bottom: 10px; z-index: 1; padding: 4px 6px; border-radius: 3px; background: #102630b8; color: #fff; font-size: 10px; }
+.coffee-mirror-photo-credit a { color: inherit; text-decoration: none; }
+.coffee-mirror-photo-credit a:hover { text-decoration: underline; }
+.coffee-mirror-detail-actions { position: absolute; top: 13px; right: 13px; z-index: 2; display: flex; gap: 7px; }
+.coffee-mirror-detail-actions button { display: grid; place-items: center; width: 35px; height: 35px; border: 1px solid var(--coffee-border); border-radius: 50%; background: var(--coffee-surface); color: var(--coffee-text); }
+.coffee-mirror-detail-actions button:hover { color: var(--coffee-accent); }
+.coffee-mirror-detail-copy { padding: 22px 25px 30px; }
+.coffee-mirror-detail-copy h2 { margin: 9px 0 16px; font-size: 34px; }
+.coffee-mirror-detail-copy > p:not(.coffee-mirror-overline) { margin: 15px 0; font-size: 14px; line-height: 1.6; }
+.coffee-mirror-detail-copy .coffee-mirror-address { display: flex; align-items: flex-start; gap: 8px; color: var(--coffee-subtle); font-size: 12px; }
+.coffee-mirror-address svg { flex: none; color: var(--coffee-accent); }
+.coffee-mirror-directions { display: block; margin-top: 24px; padding: 12px; border-radius: 6px; background: #173b49; color: #fff; font-size: 13px; font-weight: 600; text-align: center; }
+.coffee-mirror-directions:hover { background: #0076a0; }
+.coffee-mirror-detail-links { display: flex; gap: 9px; margin-top: 10px; }
+.coffee-mirror-detail-links a { flex: 1; padding: 10px; border: 1px solid var(--coffee-border); border-radius: 6px; color: var(--coffee-text); font-size: 12px; text-align: center; }
+.coffee-mirror-detail-links a:hover { background: var(--coffee-hover); }
+.coffee-mirror-nearby { margin-top: 29px; padding-top: 15px; border-top: 1px solid var(--coffee-border); }
+.coffee-mirror-nearby .coffee-mirror-overline { margin: 0 0 10px; }
+.coffee-mirror-nearby button { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 0; border: 0; background: transparent; color: var(--coffee-text); text-align: left; }
+.coffee-mirror-nearby button:hover strong { color: var(--coffee-accent); }
+.coffee-mirror-nearby-image { position: relative; display: grid; place-items: center; width: 52px; height: 52px; flex: none; overflow: hidden; border-radius: 5px; background: var(--coffee-hover); color: var(--coffee-accent); }
+.coffee-mirror-nearby-image img { object-fit: cover; }
+.coffee-mirror-nearby button > span:nth-child(2) { min-width: 0; flex: 1; }
+.coffee-mirror-nearby strong, .coffee-mirror-nearby small { display: block; }
+.coffee-mirror-nearby strong { font-size: 12px; line-height: 1.2; }
+.coffee-mirror-nearby small { margin-top: 4px; color: var(--coffee-subtle); font-size: 10px; }
+
+.coffee-mirror-controls { position: absolute; right: 17px; bottom: 72px; z-index: 850; display: flex; flex-direction: column; align-items: flex-end; gap: 7px; }
+.coffee-mirror-controls > button,
+.coffee-mirror-style-wrap > button { display: grid; place-items: center; width: 43px; height: 43px; border-radius: 10px; color: var(--coffee-text); }
+.coffee-mirror-controls button:hover { color: var(--coffee-accent); background: var(--coffee-hover); }
.coffee-mirror-style-wrap { position: relative; }
-.coffee-mirror-style-menu { position: absolute; bottom: 0; right: 52px; min-width: 115px; padding: 5px; border: 1px solid var(--coffee-border); border-radius: 10px; background: var(--coffee-glass); backdrop-filter: blur(20px); }
-.coffee-mirror-style-menu button { display: block; width: 100%; padding: 9px; border: 0; border-radius: 6px; background: transparent; color: var(--coffee-text); text-align: left; font: 13px Arial, sans-serif; }
-.coffee-mirror-style-menu button:hover, .coffee-mirror-style-menu button[aria-pressed="true"] { background: var(--coffee-hover); }
-.coffee-mirror-location-error { max-width: 180px; padding: 8px; border-radius: 7px; background: var(--coffee-glass); color: var(--coffee-text); font: 12px Arial, sans-serif; }
-.coffee-mirror-footer { position: absolute; z-index: 800; left: 16px; right: 16px; bottom: 8px; display: flex; justify-content: space-between; color: var(--coffee-text); text-shadow: 0 1px 4px #000; font: 11px Arial, sans-serif; opacity: .75; pointer-events: none; }
-.coffee-mirror-modal-backdrop { position: absolute; z-index: 2000; inset: 0; display: grid; place-items: center; padding: 16px; background: #0009; backdrop-filter: blur(5px); }
-.coffee-mirror-modal { position: relative; width: min(100%, 440px); padding: 32px; border: 1px solid var(--coffee-border); border-radius: 15px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(25px); box-shadow: 0 22px 70px #0007; text-align: center; }
-.coffee-mirror-modal > svg { color: #efa83f; margin: 0 auto 12px; }
-.coffee-mirror-modal h2 { font-size: 27px; line-height: 1.2; margin: 0 0 12px; }
-.coffee-mirror-modal p { margin: 0; color: var(--coffee-subtle); }
-.coffee-mirror-modal small { display: block; color: #efa83f; margin-top: 7px; }
-.coffee-mirror-modal ul { margin: 22px 0; padding: 18px 20px 18px 35px; border: 1px solid var(--coffee-border); border-radius: 9px; background: var(--coffee-hover); color: var(--coffee-subtle); text-align: left; font: 13px/1.8 Arial, sans-serif; }
-.coffee-mirror-start { width: 100%; border: 0; border-radius: 8px; padding: 12px; background: #111; color: #fff; font: 600 14px Arial, sans-serif; }
-.coffee-mirror-modal-close { position: absolute; right: 12px; top: 12px; border: 0; background: transparent; color: var(--coffee-text); }
+.coffee-mirror-style-menu { position: absolute; right: 52px; bottom: 0; min-width: 115px; padding: 5px; border-radius: 8px; }
+.coffee-mirror-style-menu button { display: block; width: 100%; padding: 9px; border: 0; border-radius: 5px; background: transparent; color: var(--coffee-text); font-size: 12px; text-align: left; }
+.coffee-mirror-style-menu button[aria-pressed="true"] { background: var(--coffee-hover); }
+.coffee-mirror-location-error { max-width: 180px; padding: 8px; border-radius: 7px; color: var(--coffee-text); font-size: 12px; }
+.coffee-mirror-footer { position: absolute; z-index: 800; bottom: 8px; left: 16px; display: flex; gap: 15px; color: #173441; font-size: 10px; text-shadow: 0 1px 4px #fff; pointer-events: none; }
+
@media (prefers-color-scheme: dark) {
- .coffee-mirror { color-scheme: dark; background: #182021; color: #f6f5f1; --coffee-glass: rgba(15, 18, 19, .76); --coffee-border: rgba(255,255,255,.16); --coffee-text: #f6f5f1; --coffee-subtle: #bec1c1; --coffee-hover: rgba(255,255,255,.12); }
- .coffee-mirror .leaflet-container { background: #1c2526; }
- .coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: grayscale(1) invert(1) brightness(.8) contrast(1.1) hue-rotate(180deg); }
+ .coffee-mirror { color-scheme: dark; --coffee-surface: #1a2b34ed; --coffee-border: #38505a; --coffee-text: #f2eee4; --coffee-subtle: #afc0c5; --coffee-hover: #294451; --coffee-accent: #83d1e5; background: #10242e; }
+ .coffee-mirror .leaflet-container { background: #10242e; }
+ .coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: grayscale(1) invert(1) brightness(.78) contrast(1.05) hue-rotate(180deg); }
+ .coffee-mirror-pin { border-color: #d5e6ec; background: #46a8c0; }
+ .coffee-mirror-pin.is-selected { background: #e0f2f5; box-shadow: 0 0 0 7px #83d1e55c, 0 2px 10px #0008; }
+ .coffee-mirror-regions { background: #12242e; }
+ .coffee-mirror-regions button[aria-current="page"] { background: #83d1e5; color: #102630; }
+ .coffee-mirror-search { background: #12242e; }
+ .coffee-mirror-filters button[aria-pressed="true"] { border-color: #83d1e5; background: #83d1e5; color: #102630; }
+ .coffee-mirror-row-icon { background: linear-gradient(135deg, #244551, #173641); color: #83d1e5; }
+ .coffee-mirror-directions { background: #83d1e5; color: #102630; }
+ .coffee-mirror-directions:hover { background: #a9e1ef; }
+ .coffee-mirror-footer { color: #e1edf0; text-shadow: 0 1px 4px #000; }
+}
+@media (max-width: 700px) {
+ .coffee-site-main { min-height: calc(100dvh - 64px); }
+ .coffee-mirror { height: calc(100dvh - 64px); }
+}
+@media (max-width: 639px) {
+ .coffee-mirror-header { top: 11px; right: 11px; left: 11px; gap: 7px; min-height: 60px; padding: 7px 8px 7px 12px; }
+ .coffee-mirror-brand { gap: 7px; }
+ .coffee-mirror-brand > svg { width: 18px; }
+ .coffee-mirror-brand strong { font-size: 18px; }
+ .coffee-mirror-brand small { display: none; }
+ .coffee-mirror-regions { gap: 0; padding: 2px; }
+ .coffee-mirror-regions button { padding: 8px 7px; font-size: 10px; }
+ .coffee-mirror-icon-button { width: 34px; height: 38px; }
+ .coffee-mirror-icon-button svg { width: 18px; }
+ .coffee-mirror-drawer { top: 82px; bottom: 20px; left: 11px; width: calc(100vw - 22px); }
+ .coffee-mirror-controls { right: 12px; bottom: 62px; }
+ .coffee-mirror:has(.coffee-mirror-drawer.is-open) .coffee-mirror-controls { display: none; }
+ .coffee-mirror-footer { bottom: 21px; left: 11px; flex-direction: column; gap: 2px; }
+}
+@media (max-width: 370px) {
+ .coffee-mirror-brand > svg { display: none; }
+ .coffee-mirror-brand strong { font-size: 16px; }
+}
+@media (prefers-reduced-motion: reduce) {
+ .coffee-mirror-drawer { transition: none; }
}
-@media (max-width: 700px) { .coffee-site-main { min-height: calc(100dvh - 64px); } .coffee-mirror { height: calc(100dvh - 64px); } }
-@media (max-width: 639px) { .coffee-mirror-header { top: 12px; left: 12px; right: 12px; }.coffee-mirror-brand { gap: 7px; }.coffee-mirror-brand strong { font-size: 14px; }.coffee-mirror-brand small { font-size: 10px; }.coffee-mirror-brand > svg { width: 21px; }.coffee-mirror-drawer { top: 82px; bottom: 16px; left: 12px; width: calc(100vw - 24px); }.coffee-mirror-controls { bottom: 58px; right: 12px; }.coffee-mirror-footer { left: 12px; right: 12px; } }
diff --git a/src/app/coffee/page.tsx b/src/app/coffee/page.tsx
index 977ab85..3295b39 100644
--- a/src/app/coffee/page.tsx
+++ b/src/app/coffee/page.tsx
@@ -3,8 +3,8 @@ import { CoffeeMirror } from "~/components/CoffeeMirror";
import "./coffee.css";
export const metadata: Metadata = {
- title: "Lewisburg Coffee Map",
- description: "Explore coffee shops in Lewisburg, Pennsylvania.",
+ title: "Coffee Map — Lewisburg & Boston",
+ description: "A curated map of coffee shops in Lewisburg, Pennsylvania, and near Boston University's campus and Fenway.",
};
export default function CoffeePage() {
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 635468e..93afa23 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -204,7 +204,7 @@ export default function HomePage() {
Outside of software, I’m into specialty coffee, Formula One, and
travel. I cofounded Bucknell’s Coffee Society—and, naturally, made a{" "}
- website for finding coffee in Lewisburg
+ coffee map for Lewisburg and Boston
.
@@ -241,8 +241,8 @@ export default function HomePage() {
/>
Coffee, usually.
- Cofounding a coffee society came with a map of favorite local
- spots.
+ Cofounding a coffee society led to a map of spots in Lewisburg
+ and around BU.
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index 2099007..bfa673d 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -13,7 +13,7 @@ const order = [
"Puter",
"Manyangles",
"hadlockCMS",
- "Lewisburg, PA Coffee Map",
+ "Coffee Map",
"Real-time Racing Statistics Platform",
"HRIStudio",
];
diff --git a/src/components/CoffeeMirror.tsx b/src/components/CoffeeMirror.tsx
index af9cc6c..1eb7966 100644
--- a/src/components/CoffeeMirror.tsx
+++ b/src/components/CoffeeMirror.tsx
@@ -7,22 +7,11 @@ import {
ChevronLeft, Coffee, Heart, Layers, LocateFixed, MapPin,
Minus, PanelLeft, Plus, Search, X,
} from "lucide-react";
-import { COFFEE_SHOPS, type CoffeeShop } from "~/lib/coffee-shops";
+import { COFFEE_REGIONS, type CoffeeRegionId, type CoffeeShop } from "~/lib/coffee-shops";
import "leaflet/dist/leaflet.css";
type MapStyle = "street" | "satellite";
-const images = [
- "amami-kitchen", "culture-coffee", "7th-st-cafe", "tastecraft-cafe",
- "paris-bakery-caf", "cycleup-coffee", "cornerstone-kitchen", "gram-s-eatery",
- "starbucks-coffee", "barnes-noble-caf", "starbucks-giant", "dunkin",
- "alee-s-cafe", "all-star-bagels", "panera-bread", "street-of-shops",
-];
-
-function shopImage(shop: CoffeeShop) {
- return `/coffee/shops/${images[shop.id - 1]}.jpg`;
-}
-
const tiles: Record = {
street: {
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
@@ -34,28 +23,53 @@ const tiles: Record = {
},
};
+const FAVORITES_KEY = "coffee-map-favorites";
+const categories = ["All", "Coffee first", "Coffee & food", "Campus", "Quick stop"] as const;
+type CategoryFilter = typeof categories[number];
+
+function shopImage(shop: CoffeeShop) {
+ return shop.image ? `/coffee/shops/${shop.image}.jpg` : shop.imageUrl ?? null;
+}
+
+function milesBetween(a: CoffeeShop, b: CoffeeShop) {
+ const lat = ((b.lat - a.lat) * Math.PI) / 180;
+ const lng = ((b.lng - a.lng) * Math.PI) / 180;
+ const arc = Math.sin(lat / 2) ** 2 + Math.cos(a.lat * Math.PI / 180) * Math.cos(b.lat * Math.PI / 180) * Math.sin(lng / 2) ** 2;
+ return 3958.8 * 2 * Math.atan2(Math.sqrt(arc), Math.sqrt(1 - arc));
+}
+
export function CoffeeMirror() {
+ const [regionId, setRegionId] = useState("lewisburg");
const [selected, setSelected] = useState(null);
const [drawerOpen, setDrawerOpen] = useState(true);
const [query, setQuery] = useState("");
+ const [category, setCategory] = useState("All");
const [favorites, setFavorites] = useState([]);
const [style, setStyle] = useState("street");
const [styleOpen, setStyleOpen] = useState(false);
- const [welcomeOpen, setWelcomeOpen] = useState(false);
+ const [mapReady, setMapReady] = useState(false);
const [mapFailed, setMapFailed] = useState(false);
const [locationError, setLocationError] = useState("");
const mapElement = useRef(null);
const map = useRef(null);
const tileLayer = useRef(null);
const markers = useRef(new Map());
+ const region = COFFEE_REGIONS[regionId];
+ const filtered = useMemo(() => region.shops.filter((shop) =>
+ (category === "All" || shop.category === category) &&
+ `${shop.name} ${shop.address} ${shop.description} ${shop.neighborhood} ${shop.category}`.toLowerCase().includes(query.toLowerCase().trim()),
+ ), [category, query, region]);
+ const favoriteShops = filtered.filter((shop) => favorites.includes(shop.id));
+ const otherShops = filtered.filter((shop) => !favorites.includes(shop.id));
+ const nearby = selected ? region.shops.filter((shop) => shop.id !== selected.id).sort((a, b) => milesBetween(selected, a) - milesBetween(selected, b)).slice(0, 3) : [];
useEffect(() => {
const timeout = window.setTimeout(() => {
- setDrawerOpen(window.innerWidth >= 640);
+ const savedRegion = new URLSearchParams(window.location.search).get("city");
+ if (savedRegion === "boston") setRegionId("boston");
try {
- const saved = JSON.parse(localStorage.getItem("lewisburg-coffee-favorites") ?? "[]") as unknown;
+ const saved = JSON.parse(localStorage.getItem(FAVORITES_KEY) ?? localStorage.getItem("lewisburg-coffee-favorites") ?? "[]") as unknown;
if (Array.isArray(saved)) setFavorites(saved.filter((id): id is number => typeof id === "number"));
- setWelcomeOpen(!localStorage.getItem("hasSeenWelcome"));
} catch {
setFavorites([]);
}
@@ -69,28 +83,15 @@ export function CoffeeMirror() {
const savedMarkers = markers.current;
import("leaflet").then((L) => {
if (cancelled || !mapElement.current) return;
+ const initialRegion = COFFEE_REGIONS.lewisburg;
const instance = L.map(mapElement.current, {
- center: [40.9645, -76.8845], zoom: 15, zoomControl: false,
+ center: initialRegion.center, zoom: initialRegion.zoom, zoomControl: false,
});
map.current = instance;
- tileLayer.current = L.tileLayer(tiles.street.url, {
- attribution: tiles.street.attribution, maxZoom: 19,
- }).addTo(instance);
- COFFEE_SHOPS.forEach((shop) => {
- const marker = L.marker([shop.lat, shop.lng], {
- title: shop.name,
- icon: L.divIcon({ className: "coffee-mirror-pin", html: " ", iconSize: [18, 18], iconAnchor: [9, 9] }),
- }).addTo(instance);
- marker.bindTooltip(shop.name);
- marker.on("click", () => {
- setSelected(shop);
- setDrawerOpen(true);
- });
- markers.current.set(shop.id, marker);
- });
resizeObserver = new ResizeObserver(() => instance.invalidateSize());
resizeObserver.observe(mapElement.current);
requestAnimationFrame(() => instance.invalidateSize());
+ setMapReady(true);
}).catch(() => { if (!cancelled) setMapFailed(true); });
return () => {
cancelled = true;
@@ -103,40 +104,72 @@ export function CoffeeMirror() {
}, []);
useEffect(() => {
- if (!map.current || !tileLayer.current) return;
- const L = tileLayer.current;
- L.setUrl(tiles[style].url);
- L.options.attribution = tiles[style].attribution;
- }, [style]);
+ if (!mapReady || !map.current) return;
+ let cancelled = false;
+ const instance = map.current;
+ const existingMarkers = markers.current;
+ existingMarkers.forEach((marker) => marker.remove());
+ existingMarkers.clear();
+ instance.flyTo(region.center, region.zoom, { duration: 0.65 });
+ import("leaflet").then((L) => {
+ if (cancelled) return;
+ filtered.forEach((shop) => {
+ const marker = L.marker([shop.lat, shop.lng], {
+ title: shop.name,
+ icon: L.divIcon({ className: "coffee-mirror-pin", html: " ", iconSize: [19, 19], iconAnchor: [9, 9] }),
+ }).addTo(instance);
+ marker.bindTooltip(shop.name);
+ marker.on("click", () => { setSelected(shop); setDrawerOpen(true); });
+ existingMarkers.set(shop.id, marker);
+ });
+ }).catch(() => { if (!cancelled) setMapFailed(true); });
+ return () => { cancelled = true; };
+ }, [filtered, mapReady, region]);
+
+ useEffect(() => {
+ if (!mapReady || !map.current) return;
+ let cancelled = false;
+ const instance = map.current;
+ import("leaflet").then((L) => {
+ if (cancelled) return;
+ tileLayer.current?.remove();
+ tileLayer.current = L.tileLayer(tiles[style].url, {
+ attribution: tiles[style].attribution, maxZoom: 19,
+ }).addTo(instance);
+ }).catch(() => { if (!cancelled) setMapFailed(true); });
+ return () => { cancelled = true; };
+ }, [mapReady, style]);
useEffect(() => {
if (!selected || !map.current) return;
const instance = map.current;
const target = instance.project([selected.lat, selected.lng], 16);
- const offset = window.innerWidth >= 640 && drawerOpen ? 200 : 0;
- instance.flyTo(instance.unproject(target.subtract([offset, 0]), 16), 16, { duration: 0.8 });
+ const offset = window.innerWidth >= 640 && drawerOpen ? 190 : 0;
+ instance.flyTo(instance.unproject(target.subtract([offset, 0]), 16), 16, { duration: 0.75 });
markers.current.forEach((marker, id) => marker.getElement()?.classList.toggle("is-selected", id === selected.id));
}, [selected, drawerOpen]);
- const filtered = useMemo(() => COFFEE_SHOPS.filter((shop) =>
- `${shop.name} ${shop.address}`.toLowerCase().includes(query.toLowerCase().trim()),
- ), [query]);
- const favoriteShops = filtered.filter((shop) => favorites.includes(shop.id));
- const otherShops = filtered.filter((shop) => !favorites.includes(shop.id));
+ function changeRegion(next: CoffeeRegionId) {
+ if (next === regionId) return;
+ setSelected(null);
+ setQuery("");
+ setCategory("All");
+ setRegionId(next);
+ setDrawerOpen(true);
+ const url = new URL(window.location.href);
+ if (next === "lewisburg") url.searchParams.delete("city");
+ else url.searchParams.set("city", next);
+ window.history.replaceState(null, "", url);
+ }
function toggleFavorite(id: number) {
setFavorites((current) => {
const next = current.includes(id) ? current.filter((item) => item !== id) : [...current, id];
- localStorage.setItem("lewisburg-coffee-favorites", JSON.stringify(next));
+ localStorage.setItem(FAVORITES_KEY, JSON.stringify(next));
return next;
});
}
- function closeWelcome() {
- setWelcomeOpen(false);
- localStorage.setItem("hasSeenWelcome", "true");
- }
-
function locate() {
setLocationError("");
if (!navigator.geolocation) { setLocationError("Location is unavailable in this browser."); return; }
@@ -146,48 +179,84 @@ export function CoffeeMirror() {
);
}
+ const selectedImage = selected && shopImage(selected);
+
return (
-
+
{mapFailed &&
The map could not load. You can still browse the coffee shops.
}
-
-
- © {new Date().getFullYear()} Sean O’Connor Map data © OpenStreetMap contributors
-
- {welcomeOpen && event.stopPropagation()}>
Welcome to the Lewisburg Coffee Map Discover the best coffee spots in Lewisburg, PA.
Created by Sean O’Connor Explore the map and select a coffee shop. Search the list to find your next cup. Use the location button to see what’s nearby. Start Exploring }
+ Curated by Sean O’Connor Locations checked September 2026
);
}
function ShopRow({ shop, favorite, onSelect, onFavorite }: { shop: CoffeeShop; favorite: boolean; onSelect: (shop: CoffeeShop) => void; onFavorite: (id: number) => void }) {
- return onSelect(shop)}>{shop.name} {shop.address} onFavorite(shop.id)} aria-label={favorite ? `Remove ${shop.name} from favorites` : `Add ${shop.name} to favorites`}>
;
+ const image = shopImage(shop);
+ return onSelect(shop)}>{image ? : }{shop.name} {shop.neighborhood} · {shop.category} onFavorite(shop.id)} aria-label={favorite ? `Remove ${shop.name} from favorites` : `Add ${shop.name} to favorites`}>
;
}
diff --git a/src/lib/coffee-shops.ts b/src/lib/coffee-shops.ts
index 5e2c525..34935f9 100644
--- a/src/lib/coffee-shops.ts
+++ b/src/lib/coffee-shops.ts
@@ -1,5 +1,3 @@
-// Shop listings carried over from the original Lewisburg Coffee map.
-
export interface CoffeeShop {
id: number;
name: string;
@@ -7,186 +5,87 @@ export interface CoffeeShop {
lat: number;
lng: number;
address: string;
- phone: string;
website: string;
+ phone?: string;
+ image?: string;
+ imageUrl?: string;
+ photoCredit?: string;
+ photoSource?: string;
+ imageAlt?: string;
+ category: "Coffee first" | "Coffee & food" | "Campus" | "Quick stop";
+ neighborhood: string;
}
-export const COFFEE_SHOPS: CoffeeShop[] = [
- {
- id: 1,
- name: "Amami Kitchen",
- description:
- "A beloved culinary spot offering a blend of high-quality Italian espresso and American coffee bar options, alongside mouthwatering baked goods and paninis.",
- lat: 40.96112008888467,
- lng: -76.88692162812505,
- address: "103 S 6th St, Lewisburg, PA 17837",
- phone: "(570) 490-7857",
- website: "https://www.amamiespresso.com",
+export type CoffeeRegionId = "lewisburg" | "boston";
+
+interface CoffeeRegion {
+ label: string;
+ subtitle: string;
+ introduction: string;
+ coverImage: string;
+ coverAlt: string;
+ coverCaption: string;
+ picks: number[];
+ center: [number, number];
+ zoom: number;
+ shops: CoffeeShop[];
+}
+
+// Locations checked against café, university, and local tourism listings in September 2026.
+// Keep this a curated map; an outdated listing is worse than a shorter list.
+export const COFFEE_REGIONS: Record = {
+ lewisburg: {
+ label: "Lewisburg, PA",
+ subtitle: "Market Street to Bucknell",
+ introduction: "A walkable little coffee town: espresso on Market Street, pastries around the corner, and a campus stop when the day runs long.",
+ coverImage: "/coffee/shops/tastecraft-cafe.jpg",
+ coverAlt: "Tastecraft Coffee in Lewisburg",
+ coverCaption: "Tastecraft Coffee",
+ picks: [4, 1, 5],
+ center: [40.9627, -76.8863],
+ zoom: 15,
+ shops: [
+ { id: 1, name: "Amami Kitchen & Espresso Bar", description: "Italian espresso, all-day breakfast, and paninis a short walk from Market Street.", lat: 40.9611201, lng: -76.8869216, address: "103 S 6th St, Lewisburg, PA 17837", phone: "(570) 490-7857", website: "https://amamiespresso.com/", image: "amami-kitchen", category: "Coffee & food", neighborhood: "South Sixth" },
+ { id: 2, name: "Culture Coffee", description: "Specialty coffee and matcha in its new Market Street home.", lat: 40.9642689, lng: -76.8852364, address: "326 Market St, Lewisburg, PA 17837", phone: "(570) 601-7903", website: "https://www.instagram.com/culturecoffeee_/", imageUrl: "https://pub-ba1a74be17d7442a9f2541946eb9510e.r2.dev/shops/200a519d-d913-4e2b-b4a4-d6c7bed282c0/0.jpg", photoCredit: "via Joe Coffee", photoSource: "https://joe.coffee/locations/pa/lewisburg/culture-coffee-lewisburg/", imageAlt: "Culture Coffee interior", category: "Coffee first", neighborhood: "Market Street" },
+ { id: 3, name: "7th Street Café", description: "Bucknell's campus café for coffee, sweets, and a break between classes.", lat: 40.9577366, lng: -76.8848393, address: "420 S 7th St, Lewisburg, PA 17837", website: "https://www.bucknell.edu/life-bucknell/housing-dining/dining/dining-venues", image: "7th-st-cafe", category: "Campus", neighborhood: "Bucknell" },
+ { id: 4, name: "Tastecraft Coffee", description: "House-roasted coffee, seasonal drinks, French macarons, and a view toward Hufnagle Park.", lat: 40.9629871, lng: -76.8875361, address: "512 Market St, Lewisburg, PA 17837", phone: "(570) 768-5340", website: "https://tastecraftcafe.com/locations/", image: "tastecraft-cafe", category: "Coffee first", neighborhood: "Market Street" },
+ { id: 5, name: "Paris Bakery & Café", description: "Coffee with French pastries and freshly baked bread on Market Street.", lat: 40.9638014, lng: -76.8856132, address: "335 Market St, Lewisburg, PA 17837", phone: "(570) 884-2138", website: "https://parisbakery.cafe/", image: "paris-bakery-caf", category: "Coffee & food", neighborhood: "Market Street" },
+ { id: 6, name: "CycleUp Café", description: "A coffee bar inside the downtown bike shop, with breakfast and lunch too.", lat: 40.9632568, lng: -76.8866329, address: "429 Market St, Lewisburg, PA 17837", phone: "(570) 413-1705", website: "https://store104839915.company.site/", image: "cycleup-coffee", category: "Coffee & food", neighborhood: "Market Street" },
+ { id: 17, name: "World's End Coffee Co.", description: "A Market Street coffee room with espresso, pour-overs, and a space for local events.", lat: 40.9634952, lng: -76.8862402, address: "425 Market St, Lewisburg, PA 17837", phone: "(570) 522-0103", website: "https://www.worldsendcoffee.com/", imageUrl: "https://images.squarespace-cdn.com/content/v1/63c435471c7ce839b38474bd/ba8a1058-6143-4225-abc2-11f4441de46b/IMG_4616.jpeg", photoCredit: "World’s End Coffee Co. · Eagles Mere", photoSource: "https://www.worldsendcoffee.com/best-coffee-house-on-the-mountain", imageAlt: "World’s End Coffee Co. interior in Eagles Mere", category: "Coffee first", neighborhood: "Market Street" },
+ { id: 8, name: "Gram's Eatery", description: "A breakfast and brunch table with coffee just off Market Street.", lat: 40.9648824, lng: -76.8854227, address: "21 N 3rd St, Lewisburg, PA 17837", phone: "(570) 522-0230", website: "https://www.lewisburgpa.com/connections/18441-grams-eatery", image: "gram-s-eatery", category: "Coffee & food", neighborhood: "North Third" },
+ { id: 10, name: "Barnes & Noble Café", description: "Bookstore coffee in the middle of downtown Lewisburg.", lat: 40.9637562, lng: -76.8862202, address: "400 Market St, Lewisburg, PA 17837", website: "https://bucknell.bncollege.com/", image: "barnes-noble-caf", category: "Quick stop", neighborhood: "Market Street" },
+ { id: 14, name: "All Star Bagels", description: "A Market Street bagel stop for breakfast and a cup of coffee.", lat: 40.9644503, lng: -76.8850652, address: "300 Market St, Lewisburg, PA 17837", phone: "(570) 213-4277", website: "https://allstarbagelandcoffee.com/", image: "all-star-bagels", category: "Coffee & food", neighborhood: "Market Street" },
+ { id: 13, name: "Alee's Café", description: "A small Market Street café for a coffee alongside Mediterranean food.", lat: 40.9647375, lng: -76.8840282, address: "235 Market St, Lewisburg, PA 17837", website: "https://www.lewisburgpa.com/business-directory", image: "alee-s-cafe", category: "Coffee & food", neighborhood: "Market Street" },
+ { id: 16, name: "Street of Shops Restaurant", description: "Coffee and a hearty breakfast before browsing the antique mall.", lat: 40.9677070, lng: -76.8826700, address: "100 N Water St, Lewisburg, PA 17837", website: "https://streetofshops.net/", image: "street-of-shops", category: "Coffee & food", neighborhood: "North Water" },
+ { id: 7, name: "Cornerstone Kitchen", description: "A café at the Miller Center; hours vary with events, so check before going.", lat: 40.9606353, lng: -76.8977790, address: "120 Hardwood Dr, Lewisburg, PA 17837", website: "https://millercenterlewisburg.com/cafe/", image: "cornerstone-kitchen", category: "Coffee & food", neighborhood: "Miller Center" },
+ { id: 12, name: "Dunkin' · N Derr Drive", description: "A familiar quick coffee run just north of downtown.", lat: 40.9699564, lng: -76.8924827, address: "600 N Derr Dr, Lewisburg, PA 17837", website: "https://locations.dunkindonuts.com/en/pa/lewisburg/600-n-derr-dr/303602", image: "dunkin", category: "Quick stop", neighborhood: "North Lewisburg" },
+ ],
},
- {
- id: 2,
- name: "Culture Coffee",
- description:
- "A modern coffee shop focusing on unique brewing methods for specialty coffee and traditional techniques for matcha. Known for its 'Red Velvet Latte' and croissants.",
- lat: 40.966214038235066,
- lng: -76.88549177840001,
- address: "216 St. John St, Lewisburg, PA 17837",
- phone: "(570) 601-7903",
- website: "https://www.instagram.com/culturecoffeee_",
+ boston: {
+ label: "Boston, MA",
+ subtitle: "Fenway & BU campus",
+ introduction: "A longer city walk: independent coffee near Kenmore, a café for a sit-down break in Fenway, and plenty of campus-side quick stops.",
+ coverImage: "https://www.phinista.com/fenway-counter.png",
+ coverAlt: "The counter at Phinista in Fenway",
+ coverCaption: "Phinista · Fenway",
+ picks: [103, 101, 111],
+ center: [42.3477, -71.1115],
+ zoom: 14,
+ shops: [
+ { id: 101, name: "Pavement Coffeehouse · BU", description: "An easy campus stop across from Marsh Chapel, known for coffee and house-made bagels.", lat: 42.3498465, lng: -71.1072088, address: "736 Commonwealth Ave, Boston, MA 02215", phone: "(617) 766-6048", website: "https://pavementcoffeehouse.com/pages/boston_university", imageUrl: "https://pavementcoffeehouse.com/cdn/shop/files/BBU_banner_107c7ddb-f0ba-4035-965b-63b960d75d1d_2048x2048.jpg?v=1727315570", photoCredit: "Pavement Coffeehouse", category: "Coffee first", neighborhood: "BU Central" },
+ { id: 102, name: "Pavement Coffeehouse · Fenway", description: "Coffee, tea, and bagels on Boylston Street, a few blocks from Fenway Park.", lat: 42.3441966, lng: -71.0982737, address: "1334 Boylston St, Boston, MA 02215", phone: "(857) 263-7355", website: "https://pavementcoffeehouse.com/pages/fenway", imageUrl: "https://cdn.shopify.com/s/files/1/1797/9423/files/fenway_2048x2048.jpg?v=1653020931", photoCredit: "Pavement Coffeehouse", category: "Coffee first", neighborhood: "Fenway" },
+ { id: 103, name: "Phinista · Fenway", description: "A French-Vietnamese café with phin-drip coffee, house-made syrups, and bánh mì.", lat: 42.3430483, lng: -71.0992983, address: "96 Peterborough St, Boston, MA 02215", phone: "(617) 262-7700", website: "https://www.phinista.com/locations", imageUrl: "https://images.squarespace-cdn.com/content/v1/5d2cf499669038000114edd1/c6f0d447-f891-4246-b2bc-6b4409adb924/DSCF6626.jpg?format=1000w", photoCredit: "Phinista", category: "Coffee first", neighborhood: "Fenway" },
+ { id: 104, name: "Saxbys at BU", description: "Coffee on the ground floor of BU's Center for Computing & Data Sciences.", lat: 42.3500229, lng: -71.1031113, address: "665 Commonwealth Ave, Boston, MA 02215", website: "https://www.bu.edu/dining/location/saxbys/", imageUrl: "https://www.bu.edu/files/2023/02/students-at-saxbys_feat_23-1005-CCDS-034-1200x1200.jpg", photoCredit: "BU Today", photoSource: "https://www.bu.edu/articles/2023/saxbys-new-center-for-computing-data-sciences/", category: "Campus", neighborhood: "BU East" },
+ { id: 105, name: "Tatte Bakery & Café · Fenway", description: "Pastries, specialty coffee, and a sit-down break near Fenway Park.", lat: 42.3438619, lng: -71.0997454, address: "1352 Boylston St, Boston, MA 02115", phone: "(617) 366-1800", website: "https://tattebakery.com/locations/ma/boston/1352-boylston-st/", imageUrl: "https://a.mktgcdn.com/p/y0rtnWeeb59FnyU80DFyZK1hYpZwzD1-IW1pD56mVo4/500x1000.jpg", photoCredit: "Tatte Bakery & Café", photoSource: "https://tattebakery.com/locations/ma/boston/1352-boylston-st/", category: "Coffee & food", neighborhood: "Fenway" },
+ { id: 106, name: "Caffè Nero · West Campus", description: "Coffee and a place to sit at the west end of BU's Commonwealth Avenue stretch.", lat: 42.3525906, lng: -71.1225613, address: "1047 Commonwealth Ave, Boston, MA 02215", website: "https://www.caffenero.com/us/stores", imageUrl: "https://pub-ba1a74be17d7442a9f2541946eb9510e.r2.dev/shops/622acd63-1a25-4e15-a023-04091bac824c/4.jpg", photoCredit: "via Joe Coffee", photoSource: "https://joe.coffee/locations/ma/boston/caff-nero-1047-commonwealth-ave-boston/", category: "Coffee first", neighborhood: "BU West" },
+ { id: 107, name: "Pavement Coffeehouse · Allston", description: "The original corner of Pavement's coffee and bagel story.", lat: 42.3502937, lng: -71.1317983, address: "1243 Commonwealth Ave, Allston, MA 02134", website: "https://pavementcoffeehouse.com/pages/allston", imageUrl: "https://pavementcoffeehouse.com/cdn/shop/files/Allston2_3_1414x.jpg?v=1653020973", photoCredit: "Pavement Coffeehouse", photoSource: "https://pavementcoffeehouse.com/pages/allston", category: "Coffee first", neighborhood: "Allston" },
+ { id: 108, name: "Pavement Coffeehouse · Berklee", description: "Coffee and bagels on Boylston, close to the music school.", lat: 42.3468279, lng: -71.0883896, address: "1096 Boylston St, Boston, MA 02215", website: "https://pavementcoffeehouse.com/pages/locations", imageUrl: "https://pavementcoffeehouse.com/cdn/shop/files/Pavement_Coffeehouse_Berklee_Exteriorjpg_4595x.jpg?v=1773170984", photoCredit: "Pavement Coffeehouse", photoSource: "https://pavementcoffeehouse.com/pages/berklee", category: "Coffee first", neighborhood: "Back Bay" },
+ { id: 109, name: "Pavement Coffeehouse · Symphony", description: "A café around the corner from Symphony Hall and Northeastern.", lat: 42.3416675, lng: -71.0870504, address: "44 Gainsborough St, Boston, MA 02115", website: "https://pavementcoffeehouse.com/pages/locations", imageUrl: "https://pavementcoffeehouse.com/cdn/shop/files/1_917e1b83-079d-4c76-9e3e-d45e7e3c784e_2048x2048.jpg?v=1653020988", photoCredit: "Pavement Coffeehouse", photoSource: "https://pavementcoffeehouse.com/pages/symphony", category: "Coffee first", neighborhood: "Symphony" },
+ { id: 110, name: "Phinista · Allston", description: "Vietnamese coffee and a later café stop near BU West.", lat: 42.3523107, lng: -71.1241009, address: "1092 Commonwealth Ave, Boston, MA 02215", website: "https://www.phinista.com/", imageUrl: "https://www.phinista.com/allston-interior.png", photoCredit: "Phinista", photoSource: "https://www.phinista.com/", category: "Coffee first", neighborhood: "Allston" },
+ { id: 111, name: "George Howell Coffee · Lyrik", description: "Single-origin coffee and art at the Newbury Street end of the walk.", lat: 42.3477243, lng: -71.0886043, address: "400 Newbury St, Boston, MA 02215", website: "https://georgehowellcoffee.com/pages/lyrik", imageUrl: "https://georgehowellcoffee.com/cdn/shop/files/38-DSC01079_resized.jpg?v=1765315754&width=1000", photoCredit: "George Howell Coffee", category: "Coffee first", neighborhood: "Back Bay" },
+ { id: 112, name: "Café Landwer · Audubon Circle", description: "Handcrafted coffee and a Mediterranean brunch at the edge of Kenmore.", lat: 42.3468335, lng: -71.1057590, address: "900 Beacon St, Boston, MA 02215", website: "https://landwercafe.com/locations", imageUrl: "https://pub-ba1a74be17d7442a9f2541946eb9510e.r2.dev/shops/2133d445-333f-46f7-866a-51f4ccde7180/3.jpg", photoCredit: "via Joe Coffee", photoSource: "https://joe.coffee/locations/ma/boston/cafe-landwer-900-beacon-st-boston/", category: "Coffee & food", neighborhood: "Audubon Circle" },
+ { id: 113, name: "Starbucks · Warren Towers", description: "A quick campus coffee run at 700 Commonwealth Avenue.", lat: 42.3492245, lng: -71.1031202, address: "700 Commonwealth Ave, Boston, MA 02215", website: "https://www.bu.edu/dining/location/starbucks/", imageUrl: "https://www.bu.edu/files/2025/02/24-1573-CAMBEAUTY-003_warrentowers-feat-crop-vert-1152x1536.jpg", photoCredit: "BU Today · Warren Towers", photoSource: "https://www.bu.edu/articles/2025/what-to-know-about-warren-towers-renovation/", imageAlt: "Warren Towers exterior, home to the campus Starbucks", category: "Campus", neighborhood: "BU East" },
+ { id: 114, name: "Dunkin' · Buick Street", description: "The no-detour coffee stop inside Buick Street Market.", lat: 42.3523950, lng: -71.1159897, address: "10 Buick St, Boston, MA 02215", website: "https://www.bu.edu/dining/where-to-eat/", imageUrl: "https://www.bu.edu/dining/files/2017/12/dunkin4.jpg", photoCredit: "BU Dining", photoSource: "https://www.bu.edu/dining/location/dunkin-donuts-at-buick-street-market/", category: "Quick stop", neighborhood: "BU West" },
+ ],
},
- {
- id: 3,
- name: "7th St Cafe",
- description:
- "Bucknell's ultimate on-campus dessert and coffee shop. A cozy spot for students and visitors to enjoy milkshakes, ice cream, and specialty espresso drinks.",
- lat: 40.95773659979501,
- lng: -76.88483933057073,
- address: "420 S 7th St, Lewisburg, PA 17837",
- phone: "(570) 577-1240",
- website:
- "https://www.bucknell.edu/life-bucknell/dining-services/places-eat",
- },
- {
- id: 4,
- name: "Tastecraft Cafe",
- description:
- "Downtown cafe known for its in-house roasted coffee and over 20 flavors of French macarons. A perfect spot to relax with a fresh brew and a sweet treat.",
- lat: 40.962987142339166,
- lng: -76.88753610710032,
- address: "512 Market St, Lewisburg, PA 17837",
- phone: "(570) 768-5340",
- website: "https://www.tastecraftcafe.com",
- },
- {
- id: 5,
- name: "Paris Bakery & Café",
- description:
- "Authentic French bakery offering legendary croissants, pain au chocolat, and artisanal breads. Experience a taste of France in the heart of Lewisburg.",
- lat: 40.96380135223123,
- lng: -76.88561321931094,
- address: "335 Market St, Lewisburg, PA 17837",
- phone: "(570) 884-2138",
- website: "https://parisbakery.cafe",
- },
- {
- id: 6,
- name: "CycleUp Coffee",
- description:
- "A unique bike shop cafe serving as a community hub. Enjoy coffee, light bites, and bike services in a cozy, cycling-themed atmosphere.",
- lat: 40.96325677827358,
- lng: -76.88663292782749,
- address: "429 Market St, Lewisburg, PA 17837",
- phone: "(570) 413-1705",
- website: "https://www.cycleupcafe.com",
- },
- {
- id: 7,
- name: "Cornerstone Kitchen",
- description:
- "Located in the Miller Center, offering fresh, healthy, and delicious cafe food. A place to connect over real food and a shared table.",
- lat: 40.96125715212586,
- lng: -76.89719763291119,
- address: "120 Hardwood Dr, Lewisburg, PA 17837",
- phone: "(570) 556-4191",
- website: "https://millercenterlewisburg.com/cornerstone-kitchen",
- },
- {
- id: 8,
- name: "Gram's Eatery",
- description:
- "A local favorite for homestyle breakfast and brunch. Known for generous portions, locally sourced ingredients, and a welcoming atmosphere.",
- lat: 40.9648823532259,
- lng: -76.88542268394536,
- address: "21 N 3rd St, Lewisburg, PA 17837",
- phone: "(570) 522-0230",
- website: "https://www.facebook.com/gramseaterylewisburg",
- },
- {
- id: 9,
- name: "Starbucks Coffee",
- description:
- "Seattle-based coffeehouse chain known for its signature roasts, light bites and WiFi availability.",
- lat: 40.98121817592506,
- lng: -76.88452326979083,
- address: "7431 Westbranch Hwy, Lewisburg, PA 17837",
- phone: "(570) 452-2370",
- website: "https://www.starbucks.com/store-locator/store/1010326/",
- },
- {
- id: 10,
- name: "Barnes & Noble Café",
- description:
- "Barnes & Noble Café at Bucknell University. Serving Starbucks coffee and a variety of pastries and sandwiches in a comfortable bookstore setting.",
- lat: 40.963756201655016,
- lng: -76.88622019668863,
- address: "400 Market St, Lewisburg, PA 17837",
- phone: "(570) 577-3960",
- website: "https://bucknell.bncollege.com",
- },
- {
- id: 11,
- name: "Starbucks (Giant)",
- description:
- "Conveniently located inside Giant Food Store. Perfect for grabbing your favorite coffee while grocery shopping.",
- lat: 40.96169263125371,
- lng: -76.89522292676229,
- address: "224 Hardwood Dr, Lewisburg, PA 17837",
- phone: "(570) 523-3200",
- website: "https://www.starbucks.com",
- },
- {
- id: 12,
- name: "Dunkin'",
- description:
- "Long-running chain serving signature breakfast items & a variety of coffee drinks.",
- lat: 40.969956373480606,
- lng: -76.89248274516008,
- address: "600 N Derr Dr, Lewisburg, PA 17837",
- phone: "(570) 524-4900",
- website: "https://www.dunkindonuts.com",
- },
- {
- id: 13,
- name: "Alee's Cafe",
- description:
- "A cozy local spot offering a variety of coffee drinks and light fare in the heart of downtown.",
- lat: 40.964708961176,
- lng: -76.8846770451187,
- address: "232 Market St, Lewisburg, PA 17837",
- phone: "(570) 523-1234",
- website: "https://www.aleescafe.com",
- },
- {
- id: 14,
- name: "All Star Bagels",
- description:
- "Freshly baked bagels and coffee. A popular morning stop for locals and students.",
- lat: 40.96445033682396,
- lng: -76.88506518736742,
- address: "300 Market St, Lewisburg, PA 17837",
- phone: "(570) 524-1000",
- website: "https://www.allstarbagels.com",
- },
- {
- id: 15,
- name: "Panera Bread",
- description:
- "Counter-serve bakery/cafe chain serving sandwiches, salads & more, known for its bread & free WiFi.",
- lat: 40.97531008164084,
- lng: -76.8897556844414,
- address: "6951 Westbranch Hwy, Lewisburg, PA 17837",
- phone: "(570) 524-0000",
- website: "https://www.panerabread.com",
- },
- {
- id: 16,
- name: "Street of Shops Restaurant",
- description:
- "Part of a shopping village, this retro eatery serves down-home American fare & breakfast.",
- lat: 40.96779250919243,
- lng: -76.88264400489504,
- address: "100 N Water St, Lewisburg, PA 17837",
- phone: "(570) 523-0420",
- website: "https://streetofshops.net",
- },
-];
+};
diff --git a/src/lib/data.tsx b/src/lib/data.tsx
index 434e978..717473d 100644
--- a/src/lib/data.tsx
+++ b/src/lib/data.tsx
@@ -571,12 +571,12 @@ export const projects: Project[] = [
featured: true,
},
{
- title: "Lewisburg, PA Coffee Map",
+ title: "Coffee Map",
description:
- "A website showing off the best coffee shops in Lewisburg, PA.",
+ "A curated coffee map for Lewisburg and the BU/Fenway area.",
longDescription:
- "Aggregates coffee shop data from various sources and displays it in a user-friendly interface. Use it to find your next brew!",
- tags: ["Coffee", "Lewisburg, PA", "Bucknell"],
+ "An interactive guide to coffee stops in Lewisburg and around Boston University's campus, with city switching, search, saved places, and directions.",
+ tags: ["Coffee", "Lewisburg", "Boston", "Maps"],
websiteLink: "/coffee",
gitLink: "https://github.com/soconnor0919/lewisburg-coffee",
image: "/images/lewisburg-coffee.webp",