Expand coffee map with Boston guide and real venue photos
This commit is contained in:
@@ -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: {
|
||||
|
||||
+230
-72
@@ -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; } }
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+3
-3
@@ -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{" "}
|
||||
<a className="coffee-link" href="/coffee">
|
||||
website for finding coffee in Lewisburg
|
||||
coffee map for Lewisburg and Boston
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
@@ -241,8 +241,8 @@ export default function HomePage() {
|
||||
/>
|
||||
<span>Coffee, usually.</span>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</a>
|
||||
<Link className="corner-card" href="/travel">
|
||||
|
||||
@@ -13,7 +13,7 @@ const order = [
|
||||
"Puter",
|
||||
"Manyangles",
|
||||
"hadlockCMS",
|
||||
"Lewisburg, PA Coffee Map",
|
||||
"Coffee Map",
|
||||
"Real-time Racing Statistics Platform",
|
||||
"HRIStudio",
|
||||
];
|
||||
|
||||
+138
-71
@@ -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<MapStyle, { url: string; attribution: string }> = {
|
||||
street: {
|
||||
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() {
|
||||
const [regionId, setRegionId] = useState<CoffeeRegionId>("lewisburg");
|
||||
const [selected, setSelected] = useState<CoffeeShop | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(true);
|
||||
const [query, setQuery] = useState("");
|
||||
const [category, setCategory] = useState<CategoryFilter>("All");
|
||||
const [favorites, setFavorites] = useState<number[]>([]);
|
||||
const [style, setStyle] = useState<MapStyle>("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<HTMLDivElement>(null);
|
||||
const map = useRef<LeafletMap | null>(null);
|
||||
const tileLayer = useRef<TileLayer | null>(null);
|
||||
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(() => {
|
||||
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: "<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.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: "<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(() => {
|
||||
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 (
|
||||
<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>}
|
||||
|
||||
<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>
|
||||
<button className="coffee-mirror-brand" type="button" onClick={() => setWelcomeOpen(true)}>
|
||||
<Coffee size={25} />
|
||||
<span><strong>Lewisburg Coffee Map</strong><small>Find your perfect brew</small></span>
|
||||
</button>
|
||||
<span className="coffee-mirror-header-spacer" />
|
||||
<div className="coffee-mirror-brand"><Coffee size={22} aria-hidden="true" /><span><strong>Coffee, mapped.</strong><small>{region.subtitle}</small></span></div>
|
||||
<nav className="coffee-mirror-regions" aria-label="Coffee map city">
|
||||
{(["lewisburg", "boston"] as const).map((id) => (
|
||||
<button key={id} type="button" aria-current={regionId === id ? "page" : undefined} onClick={() => changeRegion(id)}>{id === "lewisburg" ? "Lewisburg" : "Boston"}</button>
|
||||
))}
|
||||
</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>
|
||||
|
||||
<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 ? (
|
||||
<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">
|
||||
<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); setDrawerOpen(false); }} aria-label="Close panel"><X size={19} /></button>
|
||||
</div>
|
||||
<div className="coffee-mirror-detail-copy">
|
||||
<p className="coffee-mirror-overline">{selected.neighborhood} · {selected.category}</p>
|
||||
<h2>{selected.name}</h2>
|
||||
<p className="coffee-mirror-address"><MapPin size={17} /> {selected.address}</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>
|
||||
<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>
|
||||
<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>{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 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">
|
||||
<h2><Coffee size={23} /> Coffee Shops</h2>
|
||||
<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>
|
||||
<p className="coffee-mirror-overline">A little field guide · {region.shops.length} places</p>
|
||||
<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 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">All Shops</p>}
|
||||
{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 && 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} />)}
|
||||
{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>
|
||||
)}
|
||||
@@ -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>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="coffee-mirror-footer"><span>© {new Date().getFullYear()} Sean O’Connor</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 O’Connor</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 what’s nearby.</li></ul><button className="coffee-mirror-start" type="button" onClick={closeWelcome}>Start Exploring</button></div></div>}
|
||||
<footer className="coffee-mirror-footer"><span>Curated by Sean O’Connor</span><span>Locations checked September 2026</span></footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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
@@ -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<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: "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",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
+4
-4
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user