Expand coffee map with Boston guide and real venue photos

This commit is contained in:
2026-09-22 19:27:54 -04:00
parent 7758d1dbbc
commit 8b0caae14f
8 changed files with 458 additions and 333 deletions
+1
View File
@@ -8,6 +8,7 @@ await import("./src/env.js");
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
allowedDevOrigins: ["127.0.0.1"],
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"], pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
reactStrictMode: true, reactStrictMode: true,
experimental: { experimental: {
+230 -72
View File
@@ -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) { overflow: hidden; }
body:has(.coffee-mirror) .skip-link { z-index: 3000; } 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 * { box-sizing: border-box; }
.coffee-mirror button { cursor: pointer; } .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 { 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: #eaf3f7; font-family: var(--font-sans), Helvetica, Arial, sans-serif; }
.coffee-mirror .leaflet-container { background: #eceeea; font-family: Georgia, "Times New Roman", serif; } .coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: saturate(.66); }
.coffee-mirror .leaflet-control-attribution { display: none; } .coffee-mirror .leaflet-control-attribution {
.coffee-mirror-pin { border: 2px solid white; border-radius: 50%; background: #8b4513; box-shadow: 0 2px 10px #0008; } padding: 3px 7px;
.coffee-mirror-pin span { display: block; width: 100%; height: 100%; border-radius: 50%; } background: var(--coffee-surface);
.coffee-mirror-pin.is-selected { background: #efa83f; box-shadow: 0 0 0 7px #efa83f55, 0 2px 10px #0008; } color: var(--coffee-subtle);
.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; } font-size: 10px;
.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 .leaflet-control-attribution a { color: var(--coffee-accent); }
.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-pin {
.coffee-mirror-brand > svg { color: #f1a22e; flex: none; } border: 3px solid #fbfdff;
.coffee-mirror-brand strong, .coffee-mirror-brand small { display: block; } border-radius: 50%;
.coffee-mirror-brand strong { font-size: 17px; line-height: 1.1; } background: #0076a0;
.coffee-mirror-brand small { color: var(--coffee-subtle); font-size: 12px; margin-top: 3px; } box-shadow: 0 2px 8px #17344155;
.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-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-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-detail { display: flex; flex-direction: column; height: 100%; }
.coffee-mirror-discovery-top { flex: none; padding: 24px 22px 15px; } .coffee-mirror-discovery { height: 100%; overflow-y: auto; scrollbar-width: thin; }
.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-city-art { position: relative; height: 138px; flex: none; overflow: hidden; background: #c8dbe3; }
.coffee-mirror-discovery h2 svg { color: #efa83f; } .coffee-mirror-city-art img { object-fit: cover; object-position: center; }
.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-city-art::after { content: ""; position: absolute; inset: 45% 0 0; background: linear-gradient(transparent, #112b3a8c); }
.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-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-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-featured { flex: none; padding: 0 0 15px; border-bottom: 1px solid var(--coffee-border); }
.coffee-mirror-list-label { color: #eeb05c; text-transform: uppercase; letter-spacing: .12em; font: 700 11px Arial, sans-serif; padding: 8px 12px; } .coffee-mirror-featured .coffee-mirror-list-label { margin: 1px 24px 11px; }
.coffee-mirror-list hr { border: 0; border-top: 1px solid var(--coffee-border); margin: 12px; } .coffee-mirror-featured-track { display: flex; gap: 10px; overflow-x: auto; padding: 0 24px 5px; scrollbar-width: none; }
.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-featured-track::-webkit-scrollbar { display: none; }
.coffee-mirror-row:hover, .coffee-mirror-row:focus-within { background: var(--coffee-hover); border-color: var(--coffee-border); } .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-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-feature-card:hover { border-color: var(--coffee-accent); }
.coffee-mirror-row-main img { width: 63px; height: 63px; border-radius: 6px; flex: none; object-fit: cover; } .coffee-mirror-feature-image { position: relative; display: block; width: 100%; height: 86px; overflow: hidden; background: #c8dbe3; }
.coffee-mirror-row-main span { min-width: 0; } .coffee-mirror-feature-image img { object-fit: cover; }
.coffee-mirror-row-main strong, .coffee-mirror-row-main small { display: block; } .coffee-mirror-feature-card strong { display: block; margin: 8px 9px 3px; font-size: 11px; line-height: 1.25; }
.coffee-mirror-row-main strong { font-size: 15px; line-height: 1.25; } .coffee-mirror-feature-card small { display: block; margin: auto 9px 0; color: var(--coffee-subtle); font-size: 10px; }
.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-results-head { display: grid; gap: 16px; padding: 14px 18px 8px; }
.coffee-mirror-favorite { flex: none; border: 0; background: transparent; color: var(--coffee-subtle); padding: 8px; } .coffee-mirror-results-head .coffee-mirror-list-label { margin: 0 8px; }
.coffee-mirror-favorite:hover, .coffee-mirror-favorite:has(svg[fill="currentColor"]) { color: #eb7d79; } .coffee-mirror-filters { display: flex; flex-wrap: wrap; gap: 6px; }
.coffee-mirror-empty { padding: 20px; text-align: center; color: var(--coffee-subtle); } .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-photo { position: relative; height: 235px; flex: none; overflow: hidden; mask-image: linear-gradient(#000 75%, transparent 100%); } .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-photo img { width: 100%; height: 100%; object-fit: cover; }
.coffee-mirror-detail { overflow-y: auto; } .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-detail-actions { position: absolute; right: 13px; top: 13px; display: flex; gap: 7px; } .coffee-mirror-photo-credit a { color: inherit; text-decoration: none; }
.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-photo-credit a:hover { text-decoration: underline; }
.coffee-mirror-detail-copy { padding: 4px 24px 30px; } .coffee-mirror-detail-actions { position: absolute; top: 13px; right: 13px; z-index: 2; display: flex; gap: 7px; }
.coffee-mirror-detail-copy h2 { margin: 0 0 12px; font-size: 29px; line-height: 1.15; } .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-copy p { color: var(--coffee-text); font: 14px/1.55 Arial, sans-serif; } .coffee-mirror-detail-actions button:hover { color: var(--coffee-accent); }
.coffee-mirror-detail-copy .coffee-mirror-address { display: flex; gap: 7px; align-items: flex-start; color: var(--coffee-subtle); } .coffee-mirror-detail-copy { padding: 22px 25px 30px; }
.coffee-mirror-address svg { flex: none; color: #f1a22e; } .coffee-mirror-detail-copy h2 { margin: 9px 0 16px; font-size: 34px; }
.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-copy > p:not(.coffee-mirror-overline) { margin: 15px 0; font-size: 14px; line-height: 1.6; }
.coffee-mirror-detail-links { display: flex; gap: 10px; margin-top: 12px; } .coffee-mirror-detail-copy .coffee-mirror-address { display: flex; align-items: flex-start; gap: 8px; color: var(--coffee-subtle); font-size: 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-address svg { flex: none; color: var(--coffee-accent); }
.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-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-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-directions:hover { background: #0076a0; }
.coffee-mirror-controls button:hover { background: var(--coffee-hover); } .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-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 { 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: 6px; background: transparent; color: var(--coffee-text); text-align: left; font: 13px Arial, sans-serif; } .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:hover, .coffee-mirror-style-menu button[aria-pressed="true"] { background: var(--coffee-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-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; 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-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; }
.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); }
@media (prefers-color-scheme: dark) { @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 { 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: #1c2526; } .coffee-mirror .leaflet-container { background: #10242e; }
.coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: grayscale(1) invert(1) brightness(.8) contrast(1.1) hue-rotate(180deg); } .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; } }
+2 -2
View File
@@ -3,8 +3,8 @@ import { CoffeeMirror } from "~/components/CoffeeMirror";
import "./coffee.css"; import "./coffee.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Lewisburg Coffee Map", title: "Coffee Map — Lewisburg & Boston",
description: "Explore coffee shops in Lewisburg, Pennsylvania.", description: "A curated map of coffee shops in Lewisburg, Pennsylvania, and near Boston University's campus and Fenway.",
}; };
export default function CoffeePage() { export default function CoffeePage() {
+3 -3
View File
@@ -204,7 +204,7 @@ export default function HomePage() {
Outside of software, Im into specialty coffee, Formula One, and Outside of software, Im into specialty coffee, Formula One, and
travel. I cofounded Bucknells Coffee Societyand, naturally, made a{" "} travel. I cofounded Bucknells Coffee Societyand, naturally, made a{" "}
<a className="coffee-link" href="/coffee"> <a className="coffee-link" href="/coffee">
website for finding coffee in Lewisburg coffee map for Lewisburg and Boston
</a> </a>
. .
</p> </p>
@@ -241,8 +241,8 @@ export default function HomePage() {
/> />
<span>Coffee, usually.</span> <span>Coffee, usually.</span>
<p> <p>
Cofounding a coffee society came with a map of favorite local Cofounding a coffee society led to a map of spots in Lewisburg
spots. and around BU.
</p> </p>
</a> </a>
<Link className="corner-card" href="/travel"> <Link className="corner-card" href="/travel">
+1 -1
View File
@@ -13,7 +13,7 @@ const order = [
"Puter", "Puter",
"Manyangles", "Manyangles",
"hadlockCMS", "hadlockCMS",
"Lewisburg, PA Coffee Map", "Coffee Map",
"Real-time Racing Statistics Platform", "Real-time Racing Statistics Platform",
"HRIStudio", "HRIStudio",
]; ];
+138 -71
View File
@@ -7,22 +7,11 @@ import {
ChevronLeft, Coffee, Heart, Layers, LocateFixed, MapPin, ChevronLeft, Coffee, Heart, Layers, LocateFixed, MapPin,
Minus, PanelLeft, Plus, Search, X, Minus, PanelLeft, Plus, Search, X,
} from "lucide-react"; } 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"; import "leaflet/dist/leaflet.css";
type MapStyle = "street" | "satellite"; 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<MapStyle, { url: string; attribution: string }> = { const tiles: Record<MapStyle, { url: string; attribution: string }> = {
street: { street: {
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
@@ -34,28 +23,53 @@ const tiles: Record<MapStyle, { url: string; attribution: string }> = {
}, },
}; };
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() { export function CoffeeMirror() {
const [regionId, setRegionId] = useState<CoffeeRegionId>("lewisburg");
const [selected, setSelected] = useState<CoffeeShop | null>(null); const [selected, setSelected] = useState<CoffeeShop | null>(null);
const [drawerOpen, setDrawerOpen] = useState(true); const [drawerOpen, setDrawerOpen] = useState(true);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [category, setCategory] = useState<CategoryFilter>("All");
const [favorites, setFavorites] = useState<number[]>([]); const [favorites, setFavorites] = useState<number[]>([]);
const [style, setStyle] = useState<MapStyle>("street"); const [style, setStyle] = useState<MapStyle>("street");
const [styleOpen, setStyleOpen] = useState(false); const [styleOpen, setStyleOpen] = useState(false);
const [welcomeOpen, setWelcomeOpen] = useState(false); const [mapReady, setMapReady] = useState(false);
const [mapFailed, setMapFailed] = useState(false); const [mapFailed, setMapFailed] = useState(false);
const [locationError, setLocationError] = useState(""); const [locationError, setLocationError] = useState("");
const mapElement = useRef<HTMLDivElement>(null); const mapElement = useRef<HTMLDivElement>(null);
const map = useRef<LeafletMap | null>(null); const map = useRef<LeafletMap | null>(null);
const tileLayer = useRef<TileLayer | null>(null); const tileLayer = useRef<TileLayer | null>(null);
const markers = useRef(new Map<number, Marker>()); const markers = useRef(new Map<number, Marker>());
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(() => { useEffect(() => {
const timeout = window.setTimeout(() => { const timeout = window.setTimeout(() => {
setDrawerOpen(window.innerWidth >= 640); const savedRegion = new URLSearchParams(window.location.search).get("city");
if (savedRegion === "boston") setRegionId("boston");
try { 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")); if (Array.isArray(saved)) setFavorites(saved.filter((id): id is number => typeof id === "number"));
setWelcomeOpen(!localStorage.getItem("hasSeenWelcome"));
} catch { } catch {
setFavorites([]); setFavorites([]);
} }
@@ -69,28 +83,15 @@ export function CoffeeMirror() {
const savedMarkers = markers.current; const savedMarkers = markers.current;
import("leaflet").then((L) => { import("leaflet").then((L) => {
if (cancelled || !mapElement.current) return; if (cancelled || !mapElement.current) return;
const initialRegion = COFFEE_REGIONS.lewisburg;
const instance = L.map(mapElement.current, { 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; 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: "<span></span>", 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 = new ResizeObserver(() => instance.invalidateSize());
resizeObserver.observe(mapElement.current); resizeObserver.observe(mapElement.current);
requestAnimationFrame(() => instance.invalidateSize()); requestAnimationFrame(() => instance.invalidateSize());
setMapReady(true);
}).catch(() => { if (!cancelled) setMapFailed(true); }); }).catch(() => { if (!cancelled) setMapFailed(true); });
return () => { return () => {
cancelled = true; cancelled = true;
@@ -103,40 +104,72 @@ export function CoffeeMirror() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!map.current || !tileLayer.current) return; if (!mapReady || !map.current) return;
const L = tileLayer.current; let cancelled = false;
L.setUrl(tiles[style].url); const instance = map.current;
L.options.attribution = tiles[style].attribution; const existingMarkers = markers.current;
}, [style]); 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: "<span></span>", 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(() => { useEffect(() => {
if (!selected || !map.current) return; if (!selected || !map.current) return;
const instance = map.current; const instance = map.current;
const target = instance.project([selected.lat, selected.lng], 16); const target = instance.project([selected.lat, selected.lng], 16);
const offset = window.innerWidth >= 640 && drawerOpen ? 200 : 0; const offset = window.innerWidth >= 640 && drawerOpen ? 190 : 0;
instance.flyTo(instance.unproject(target.subtract([offset, 0]), 16), 16, { duration: 0.8 }); 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)); markers.current.forEach((marker, id) => marker.getElement()?.classList.toggle("is-selected", id === selected.id));
}, [selected, drawerOpen]); }, [selected, drawerOpen]);
const filtered = useMemo(() => COFFEE_SHOPS.filter((shop) => function changeRegion(next: CoffeeRegionId) {
`${shop.name} ${shop.address}`.toLowerCase().includes(query.toLowerCase().trim()), if (next === regionId) return;
), [query]); setSelected(null);
const favoriteShops = filtered.filter((shop) => favorites.includes(shop.id)); setQuery("");
const otherShops = filtered.filter((shop) => !favorites.includes(shop.id)); 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) { function toggleFavorite(id: number) {
setFavorites((current) => { setFavorites((current) => {
const next = current.includes(id) ? current.filter((item) => item !== id) : [...current, id]; 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; return next;
}); });
} }
function closeWelcome() {
setWelcomeOpen(false);
localStorage.setItem("hasSeenWelcome", "true");
}
function locate() { function locate() {
setLocationError(""); setLocationError("");
if (!navigator.geolocation) { setLocationError("Location is unavailable in this browser."); return; } if (!navigator.geolocation) { setLocationError("Location is unavailable in this browser."); return; }
@@ -146,48 +179,84 @@ export function CoffeeMirror() {
); );
} }
const selectedImage = selected && shopImage(selected);
return ( return (
<div className={`coffee-mirror ${style === "satellite" ? "is-satellite" : ""}`}> <div className={`coffee-mirror ${style === "satellite" ? "is-satellite" : ""}`}>
<div ref={mapElement} className="coffee-mirror-map" aria-label="Interactive map of Lewisburg coffee shops" /> <div ref={mapElement} className="coffee-mirror-map" role="region" aria-label={`Interactive map of coffee shops in ${region.label}`} />
{mapFailed && <p className="coffee-mirror-map-error">The map could not load. You can still browse the coffee shops.</p>} {mapFailed && <p className="coffee-mirror-map-error">The map could not load. You can still browse the coffee shops.</p>}
<header className="coffee-mirror-header"> <header className="coffee-mirror-header">
<button className={`coffee-mirror-icon-button ${drawerOpen ? "is-active" : ""}`} type="button" onClick={() => setDrawerOpen((open) => !open)} aria-label="Toggle coffee shops" aria-expanded={drawerOpen}><PanelLeft size={21} /></button> <div className="coffee-mirror-brand"><Coffee size={22} aria-hidden="true" /><span><strong>Coffee, mapped.</strong><small>{region.subtitle}</small></span></div>
<button className="coffee-mirror-brand" type="button" onClick={() => setWelcomeOpen(true)}> <nav className="coffee-mirror-regions" aria-label="Coffee map city">
<Coffee size={25} /> {(["lewisburg", "boston"] as const).map((id) => (
<span><strong>Lewisburg Coffee Map</strong><small>Find your perfect brew</small></span> <button key={id} type="button" aria-current={regionId === id ? "page" : undefined} onClick={() => changeRegion(id)}>{id === "lewisburg" ? "Lewisburg" : "Boston"}</button>
</button> ))}
<span className="coffee-mirror-header-spacer" /> </nav>
<button className={`coffee-mirror-icon-button ${drawerOpen ? "is-active" : ""}`} type="button" onClick={() => { setDrawerOpen((open) => !open); setSelected(null); }} aria-label="Toggle coffee shop list" aria-expanded={drawerOpen}><PanelLeft size={20} /></button>
</header> </header>
<aside className={`coffee-mirror-drawer ${drawerOpen || selected ? "is-open" : ""}`} aria-label="Discover coffee shops"> <aside className={`coffee-mirror-drawer ${drawerOpen || selected ? "is-open" : ""}`} aria-label={`${region.label} coffee shops`}>
{selected ? ( {selected ? (
<div className="coffee-mirror-detail"> <div className="coffee-mirror-detail">
<div className="coffee-mirror-photo"><Image src={shopImage(selected)} alt={selected.name} fill sizes="(max-width: 640px) 100vw, 400px" /></div> <div className="coffee-mirror-photo">
<Image src={selectedImage ?? region.coverImage} alt={selected.imageAlt ?? selected.name} fill sizes="(max-width: 640px) 100vw, 400px" unoptimized={Boolean(selected.imageUrl)} />
{selected.photoCredit && <span className="coffee-mirror-photo-credit">{selected.photoSource ? <a href={selected.photoSource} target="_blank" rel="noreferrer">Photo: {selected.photoCredit} </a> : `Photo: ${selected.photoCredit}`}</span>}
</div>
<div className="coffee-mirror-detail-actions"> <div className="coffee-mirror-detail-actions">
<button type="button" onClick={() => toggleFavorite(selected.id)} aria-label={favorites.includes(selected.id) ? "Remove favorite" : "Add favorite"}><Heart size={18} fill={favorites.includes(selected.id) ? "currentColor" : "none"} /></button> <button type="button" onClick={() => toggleFavorite(selected.id)} aria-label={favorites.includes(selected.id) ? "Remove favorite" : "Add favorite"}><Heart size={18} fill={favorites.includes(selected.id) ? "currentColor" : "none"} /></button>
<button type="button" onClick={() => setSelected(null)} aria-label="Back to coffee shops"><ChevronLeft size={20} /></button> <button type="button" onClick={() => setSelected(null)} aria-label="Back to coffee shops"><ChevronLeft size={20} /></button>
<button type="button" onClick={() => { setSelected(null); setDrawerOpen(false); }} aria-label="Close panel"><X size={19} /></button> <button type="button" onClick={() => { setSelected(null); setDrawerOpen(false); }} aria-label="Close panel"><X size={19} /></button>
</div> </div>
<div className="coffee-mirror-detail-copy"> <div className="coffee-mirror-detail-copy">
<p className="coffee-mirror-overline">{selected.neighborhood} · {selected.category}</p>
<h2>{selected.name}</h2> <h2>{selected.name}</h2>
<p className="coffee-mirror-address"><MapPin size={17} /> {selected.address}</p> <p className="coffee-mirror-address"><MapPin size={17} /> {selected.address}</p>
<p>{selected.description}</p> <p>{selected.description}</p>
<a className="coffee-mirror-directions" href={`https://www.google.com/maps/dir/?api=1&destination=${selected.lat},${selected.lng}`} target="_blank" rel="noreferrer">Get Directions </a> <a className="coffee-mirror-directions" href={`https://www.google.com/maps/dir/?api=1&destination=${selected.lat},${selected.lng}`} target="_blank" rel="noreferrer">Get directions </a>
<div className="coffee-mirror-detail-links"><a href={selected.website} target="_blank" rel="noreferrer">Website </a><a href={`tel:${selected.phone.replace(/[^+\d]/g, "")}`}>Call</a></div> <div className="coffee-mirror-detail-links"><a href={selected.website} target="_blank" rel="noreferrer">Website </a>{selected.phone && <a href={`tel:${selected.phone.replace(/[^+\d]/g, "")}`}>Call</a>}</div>
<div className="coffee-mirror-nearby">
<p className="coffee-mirror-overline">Around the corner</p>
{nearby.map((shop) => <button key={shop.id} type="button" onClick={() => setSelected(shop)}>
<span className="coffee-mirror-nearby-image">{shopImage(shop) ? <Image src={shopImage(shop)!} alt="" fill sizes="52px" unoptimized={Boolean(shop.imageUrl)} /> : <Coffee size={21} strokeWidth={1.5} aria-hidden="true" />}</span>
<span><strong>{shop.name}</strong><small>{milesBetween(selected, shop).toFixed(1)} mi away · {shop.neighborhood}</small></span><span aria-hidden="true"></span>
</button>)}
</div>
</div> </div>
</div> </div>
) : ( ) : (
<div className="coffee-mirror-discovery"> <div className="coffee-mirror-discovery">
<div className="coffee-mirror-city-art">
<Image src={region.coverImage} alt={region.coverAlt} fill priority sizes="(max-width: 640px) 100vw, 405px" unoptimized={region.coverImage.startsWith("http")} />
<span>{region.coverCaption}</span>
</div>
<div className="coffee-mirror-discovery-top"> <div className="coffee-mirror-discovery-top">
<h2><Coffee size={23} /> Coffee Shops</h2> <p className="coffee-mirror-overline">A little field guide · {region.shops.length} places</p>
<label className="coffee-mirror-search"><Search size={17} /><input type="search" placeholder="Search coffee shops..." value={query} onChange={(event) => setQuery(event.target.value)} aria-label="Search coffee shops" /></label> <h2>{region.label}</h2>
<p className="coffee-mirror-discovery-subtitle">{region.introduction}</p>
<label className="coffee-mirror-search"><Search size={17} /><input type="search" placeholder="Find a coffee shop" value={query} onChange={(event) => setQuery(event.target.value)} aria-label="Search coffee shops" /></label>
</div>
{!query && category === "All" && <div className="coffee-mirror-featured">
<p className="coffee-mirror-list-label">Start here</p>
<div className="coffee-mirror-featured-track">{region.picks.map((id) => {
const shop = region.shops.find((item) => item.id === id);
return shop ? <button key={id} type="button" className="coffee-mirror-feature-card" onClick={() => setSelected(shop)}>
<span className="coffee-mirror-feature-image"><Image src={shopImage(shop) ?? region.coverImage} alt="" fill sizes="155px" unoptimized={Boolean(shop.imageUrl)} /></span>
<strong>{shop.name}</strong><small>{shop.neighborhood} </small>
</button> : null;
})}</div>
</div>}
<div className="coffee-mirror-results-head">
<div className="coffee-mirror-filters" role="group" aria-label="Filter coffee shops by kind">
{categories.map((option) => <button key={option} type="button" aria-pressed={category === option} onClick={() => setCategory(option)}>{option}</button>)}
</div>
<p className="coffee-mirror-list-label">{filtered.length} {filtered.length === 1 ? "place" : "places"} to explore</p>
</div> </div>
<div className="coffee-mirror-list"> <div className="coffee-mirror-list">
{favoriteShops.length > 0 && <><p className="coffee-mirror-list-label"> Favorites</p>{favoriteShops.map((shop) => <ShopRow key={shop.id} shop={shop} favorite onSelect={setSelected} onFavorite={toggleFavorite} />)}<hr /></>} {favoriteShops.length > 0 && <><p className="coffee-mirror-list-label">Saved places</p>{favoriteShops.map((shop) => <ShopRow key={shop.id} shop={shop} favorite onSelect={setSelected} onFavorite={toggleFavorite} />)}<hr /></>}
{favoriteShops.length > 0 && <p className="coffee-mirror-list-label">All Shops</p>} {favoriteShops.length > 0 && otherShops.length > 0 && <p className="coffee-mirror-list-label">Other places</p>}
{otherShops.map((shop) => <ShopRow key={shop.id} shop={shop} favorite={false} onSelect={setSelected} onFavorite={toggleFavorite} />)} {otherShops.map((shop) => <ShopRow key={shop.id} shop={shop} favorite={false} onSelect={setSelected} onFavorite={toggleFavorite} />)}
{filtered.length === 0 && <p className="coffee-mirror-empty">No shops found matching {query}</p>} {filtered.length === 0 && <p className="coffee-mirror-empty">No places match this search. Try another category or term.</p>}
</div> </div>
</div> </div>
)} )}
@@ -203,14 +272,12 @@ export function CoffeeMirror() {
{styleOpen && <div className="coffee-mirror-style-menu">{(["street", "satellite"] as const).map((option) => <button key={option} type="button" aria-pressed={style === option} onClick={() => { setStyle(option); setStyleOpen(false); }}>{option.charAt(0).toUpperCase() + option.slice(1)}</button>)}</div>} {styleOpen && <div className="coffee-mirror-style-menu">{(["street", "satellite"] as const).map((option) => <button key={option} type="button" aria-pressed={style === option} onClick={() => { setStyle(option); setStyleOpen(false); }}>{option.charAt(0).toUpperCase() + option.slice(1)}</button>)}</div>}
</div> </div>
</div> </div>
<footer className="coffee-mirror-footer"><span>Curated by Sean OConnor</span><span>Locations checked September 2026</span></footer>
<footer className="coffee-mirror-footer"><span>© {new Date().getFullYear()} Sean OConnor</span><span>Map data © OpenStreetMap contributors</span></footer>
{welcomeOpen && <div className="coffee-mirror-modal-backdrop" onClick={closeWelcome}><div className="coffee-mirror-modal" role="dialog" aria-modal="true" aria-labelledby="coffee-welcome-title" onClick={(event) => event.stopPropagation()}><button className="coffee-mirror-modal-close" type="button" onClick={closeWelcome} aria-label="Close welcome"><X size={20} /></button><Coffee size={34} /><h2 id="coffee-welcome-title">Welcome to the Lewisburg Coffee Map</h2><p>Discover the best coffee spots in Lewisburg, PA.</p><small>Created by Sean OConnor</small><ul><li>Explore the map and select a coffee shop.</li><li>Search the list to find your next cup.</li><li>Use the location button to see whats nearby.</li></ul><button className="coffee-mirror-start" type="button" onClick={closeWelcome}>Start Exploring</button></div></div>}
</div> </div>
); );
} }
function ShopRow({ shop, favorite, onSelect, onFavorite }: { shop: CoffeeShop; favorite: boolean; onSelect: (shop: CoffeeShop) => void; onFavorite: (id: number) => void }) { function ShopRow({ shop, favorite, onSelect, onFavorite }: { shop: CoffeeShop; favorite: boolean; onSelect: (shop: CoffeeShop) => void; onFavorite: (id: number) => void }) {
return <div className="coffee-mirror-row"><button type="button" className="coffee-mirror-row-main" onClick={() => onSelect(shop)}><Image src={shopImage(shop)} alt="" width={63} height={63} /><span><strong>{shop.name}</strong><small>{shop.address}</small></span></button><button type="button" className="coffee-mirror-favorite" onClick={() => onFavorite(shop.id)} aria-label={favorite ? `Remove ${shop.name} from favorites` : `Add ${shop.name} to favorites`}><Heart size={17} fill={favorite ? "currentColor" : "none"} /></button></div>; const image = shopImage(shop);
return <div className="coffee-mirror-row"><button type="button" className="coffee-mirror-row-main" onClick={() => onSelect(shop)}>{image ? <Image src={image} alt="" width={62} height={62} unoptimized={Boolean(shop.imageUrl)} /> : <span className="coffee-mirror-row-icon"><Coffee size={26} strokeWidth={1.35} aria-hidden="true" /></span>}<span className="coffee-mirror-row-copy"><strong>{shop.name}</strong><small>{shop.neighborhood} · {shop.category}</small></span></button><button type="button" className="coffee-mirror-favorite" onClick={() => onFavorite(shop.id)} aria-label={favorite ? `Remove ${shop.name} from favorites` : `Add ${shop.name} to favorites`}><Heart size={17} fill={favorite ? "currentColor" : "none"} /></button></div>;
} }
+79 -180
View File
@@ -1,5 +1,3 @@
// Shop listings carried over from the original Lewisburg Coffee map.
export interface CoffeeShop { export interface CoffeeShop {
id: number; id: number;
name: string; name: string;
@@ -7,186 +5,87 @@ export interface CoffeeShop {
lat: number; lat: number;
lng: number; lng: number;
address: string; address: string;
phone: string;
website: 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[] = [ export type CoffeeRegionId = "lewisburg" | "boston";
{
id: 1, interface CoffeeRegion {
name: "Amami Kitchen", label: string;
description: subtitle: string;
"A beloved culinary spot offering a blend of high-quality Italian espresso and American coffee bar options, alongside mouthwatering baked goods and paninis.", introduction: string;
lat: 40.96112008888467, coverImage: string;
lng: -76.88692162812505, coverAlt: string;
address: "103 S 6th St, Lewisburg, PA 17837", coverCaption: string;
phone: "(570) 490-7857", picks: number[];
website: "https://www.amamiespresso.com", 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<CoffeeRegionId, CoffeeRegion> = {
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: "Worlds End Coffee Co. · Eagles Mere", photoSource: "https://www.worldsendcoffee.com/best-coffee-house-on-the-mountain", imageAlt: "Worlds 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" },
],
}, },
{ boston: {
id: 2, label: "Boston, MA",
name: "Culture Coffee", subtitle: "Fenway & BU campus",
description: 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.",
"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.", coverImage: "https://www.phinista.com/fenway-counter.png",
lat: 40.966214038235066, coverAlt: "The counter at Phinista in Fenway",
lng: -76.88549177840001, coverCaption: "Phinista · Fenway",
address: "216 St. John St, Lewisburg, PA 17837", picks: [103, 101, 111],
phone: "(570) 601-7903", center: [42.3477, -71.1115],
website: "https://www.instagram.com/culturecoffeee_", 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",
},
];
+4 -4
View File
@@ -571,12 +571,12 @@ export const projects: Project[] = [
featured: true, featured: true,
}, },
{ {
title: "Lewisburg, PA Coffee Map", title: "Coffee Map",
description: description:
"A website showing off the best coffee shops in Lewisburg, PA.", "A curated coffee map for Lewisburg and the BU/Fenway area.",
longDescription: longDescription:
"Aggregates coffee shop data from various sources and displays it in a user-friendly interface. Use it to find your next brew!", "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, PA", "Bucknell"], tags: ["Coffee", "Lewisburg", "Boston", "Maps"],
websiteLink: "/coffee", websiteLink: "/coffee",
gitLink: "https://github.com/soconnor0919/lewisburg-coffee", gitLink: "https://github.com/soconnor0919/lewisburg-coffee",
image: "/images/lewisburg-coffee.webp", image: "/images/lewisburg-coffee.webp",