diff --git a/next.config.js b/next.config.js index d4f051c..02911ea 100644 --- a/next.config.js +++ b/next.config.js @@ -8,6 +8,7 @@ await import("./src/env.js"); /** @type {import('next').NextConfig} */ const nextConfig = { + allowedDevOrigins: ["127.0.0.1"], pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"], reactStrictMode: true, experimental: { diff --git a/src/app/coffee/coffee.css b/src/app/coffee/coffee.css index 3969cb9..590c2cb 100644 --- a/src/app/coffee/coffee.css +++ b/src/app/coffee/coffee.css @@ -1,83 +1,241 @@ -.coffee-site-main { position: relative; z-index: 0; isolation: isolate; width: 100%; flex: 1; min-height: calc(100dvh - 72px); padding: 0; margin: 0; max-width: none; } +.coffee-site-main { + position: relative; + z-index: 0; + isolation: isolate; + width: 100%; + max-width: none; + min-height: calc(100dvh - 72px); + flex: 1; + margin: 0; + padding: 0; +} body:has(.coffee-mirror) { overflow: hidden; } body:has(.coffee-mirror) .skip-link { z-index: 3000; } -.coffee-mirror { position: relative; width: 100%; height: calc(100dvh - 72px); overflow: hidden; background: #eceeea; color: #161b1b; font-family: Georgia, "Times New Roman", serif; --coffee-glass: rgba(255,255,255,.82); --coffee-border: rgba(0,0,0,.12); --coffee-text: #161b1b; --coffee-subtle: #5e6363; --coffee-hover: rgba(0,0,0,.07); } + +.coffee-mirror { + --coffee-surface: #fbfdfff2; + --coffee-border: #d6e1e7; + --coffee-text: #173441; + --coffee-subtle: #58717d; + --coffee-hover: #e9f3f7; + --coffee-accent: #0076a0; + position: relative; + width: 100%; + height: calc(100dvh - 72px); + overflow: hidden; + background: #eaf3f7; + color: var(--coffee-text); + font-family: var(--font-sans), "Helvetica Neue", Helvetica, Arial, sans-serif; +} .coffee-mirror * { box-sizing: border-box; } .coffee-mirror button { cursor: pointer; } +.coffee-mirror button:focus-visible, +.coffee-mirror a:focus-visible, +.coffee-mirror input:focus-visible { outline: 2px solid var(--coffee-accent); outline-offset: 2px; } .coffee-mirror-map { position: absolute; inset: 0; z-index: 0; } -.coffee-mirror-map-error { position: absolute; top: 45%; left: 50%; transform: translate(-50%,-50%); z-index: 3; padding: 16px; color: white; background: #35281e; border-radius: 10px; text-align: center; } -.coffee-mirror .leaflet-container { background: #eceeea; font-family: Georgia, "Times New Roman", serif; } -.coffee-mirror .leaflet-control-attribution { display: none; } -.coffee-mirror-pin { border: 2px solid white; border-radius: 50%; background: #8b4513; box-shadow: 0 2px 10px #0008; } -.coffee-mirror-pin span { display: block; width: 100%; height: 100%; border-radius: 50%; } -.coffee-mirror-pin.is-selected { background: #efa83f; box-shadow: 0 0 0 7px #efa83f55, 0 2px 10px #0008; } -.coffee-mirror-header { position: absolute; top: 16px; left: 16px; right: 16px; z-index: 1000; min-height: 58px; display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; border: 1px solid var(--coffee-border); border-radius: 13px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(22px); box-shadow: 0 8px 28px #0003; } -.coffee-mirror-icon-button { display: grid; place-items: center; width: 42px; height: 42px; border: 0; border-radius: 9px; color: var(--coffee-text); background: transparent; } -.coffee-mirror-icon-button:hover, .coffee-mirror-icon-button.is-active { color: #f5ab45; background: var(--coffee-hover); } -.coffee-mirror-brand { display: flex; align-items: center; gap: 12px; position: absolute; left: 50%; transform: translateX(-50%); border: 0; background: transparent; color: inherit; text-align: left; white-space: nowrap; } -.coffee-mirror-brand > svg { color: #f1a22e; flex: none; } -.coffee-mirror-brand strong, .coffee-mirror-brand small { display: block; } -.coffee-mirror-brand strong { font-size: 17px; line-height: 1.1; } -.coffee-mirror-brand small { color: var(--coffee-subtle); font-size: 12px; margin-top: 3px; } -.coffee-mirror-header-spacer { width: 42px; } -.coffee-mirror-drawer { position: absolute; z-index: 900; top: 92px; bottom: 32px; left: 16px; width: min(400px, calc(100vw - 32px)); overflow: hidden; border: 1px solid var(--coffee-border); border-radius: 13px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(25px); box-shadow: 0 16px 38px #0004; transform: translateX(calc(-100% - 34px)); opacity: 0; pointer-events: none; transition: transform .3s ease, opacity .3s ease; } +.coffee-mirror .leaflet-container { background: #eaf3f7; font-family: var(--font-sans), Helvetica, Arial, sans-serif; } +.coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: saturate(.66); } +.coffee-mirror .leaflet-control-attribution { + padding: 3px 7px; + background: var(--coffee-surface); + color: var(--coffee-subtle); + font-size: 10px; +} +.coffee-mirror .leaflet-control-attribution a { color: var(--coffee-accent); } +.coffee-mirror-pin { + border: 3px solid #fbfdff; + border-radius: 50%; + background: #0076a0; + box-shadow: 0 2px 8px #17344155; +} +.coffee-mirror-pin.is-selected { background: #173b49; box-shadow: 0 0 0 7px #0076a04d, 0 2px 10px #17344166; } +.coffee-mirror-map-error { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); z-index: 3; max-width: 300px; padding: 16px; border-radius: 10px; background: #173b49; color: #fff; text-align: center; } + +.coffee-mirror-header, +.coffee-mirror-drawer, +.coffee-mirror-controls > button, +.coffee-mirror-style-wrap > button, +.coffee-mirror-style-menu, +.coffee-mirror-location-error { + border: 1px solid var(--coffee-border); + background: var(--coffee-surface); + -webkit-backdrop-filter: blur(22px); + backdrop-filter: blur(22px); + box-shadow: 0 10px 32px #1734411f; +} +.coffee-mirror-header { + position: absolute; + top: 16px; + left: 16px; + right: 16px; + z-index: 1000; + display: flex; + align-items: center; + gap: 20px; + min-height: 70px; + padding: 10px 13px 10px 20px; + border-radius: 12px; +} +.coffee-mirror-brand { display: flex; align-items: center; gap: 12px; min-width: 0; } +.coffee-mirror-brand > svg { flex: none; color: var(--coffee-accent); } +.coffee-mirror-brand strong, +.coffee-mirror-brand small { display: block; } +.coffee-mirror-brand strong { font-family: var(--font-heading), Georgia, serif; font-size: 25px; font-weight: 400; letter-spacing: -.035em; line-height: 1.1; white-space: nowrap; } +.coffee-mirror-brand small { margin-top: 4px; color: var(--coffee-subtle); font-size: 11px; letter-spacing: .03em; } +.coffee-mirror-regions { display: flex; align-items: center; gap: 3px; margin-left: auto; padding: 4px; border: 1px solid var(--coffee-border); border-radius: 8px; background: #eaf2f670; } +.coffee-mirror-regions button { padding: 9px 14px; border: 0; border-radius: 5px; background: transparent; color: var(--coffee-subtle); font-size: 12px; font-weight: 600; } +.coffee-mirror-regions button:hover { color: var(--coffee-text); } +.coffee-mirror-regions button[aria-current="page"] { background: #173b49; color: #fff; box-shadow: 0 1px 5px #17344124; } +.coffee-mirror-icon-button { display: grid; place-items: center; width: 42px; height: 42px; flex: none; border: 0; border-radius: 8px; background: transparent; color: var(--coffee-text); } +.coffee-mirror-icon-button:hover, +.coffee-mirror-icon-button.is-active { background: var(--coffee-hover); color: var(--coffee-accent); } + +.coffee-mirror-drawer { + position: absolute; + z-index: 900; + top: 102px; + bottom: 30px; + left: 16px; + width: min(405px, calc(100vw - 32px)); + overflow: hidden; + border-radius: 12px; + transform: translateX(calc(-100% - 36px)); + opacity: 0; + pointer-events: none; + transition: transform .3s ease, opacity .3s ease; +} .coffee-mirror-drawer.is-open { transform: translateX(0); opacity: 1; pointer-events: auto; } -.coffee-mirror-discovery, .coffee-mirror-detail { height: 100%; display: flex; flex-direction: column; } -.coffee-mirror-discovery-top { flex: none; padding: 24px 22px 15px; } -.coffee-mirror-discovery h2 { display: flex; align-items: center; gap: 9px; margin: 0 0 17px; color: var(--coffee-text); font-size: 23px; line-height: 1.2; } -.coffee-mirror-discovery h2 svg { color: #efa83f; } -.coffee-mirror-search { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border: 1px solid var(--coffee-border); border-radius: 8px; background: var(--coffee-hover); color: var(--coffee-subtle); } -.coffee-mirror-search input { width: 100%; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--coffee-text); font: 14px Arial, sans-serif; } +.coffee-mirror-detail { display: flex; flex-direction: column; height: 100%; } +.coffee-mirror-discovery { height: 100%; overflow-y: auto; scrollbar-width: thin; } +.coffee-mirror-city-art { position: relative; height: 138px; flex: none; overflow: hidden; background: #c8dbe3; } +.coffee-mirror-city-art img { object-fit: cover; object-position: center; } +.coffee-mirror-city-art::after { content: ""; position: absolute; inset: 45% 0 0; background: linear-gradient(transparent, #112b3a8c); } +.coffee-mirror-city-art span { position: absolute; right: 15px; bottom: 11px; z-index: 1; color: #fff; font-size: 10px; letter-spacing: .06em; text-shadow: 0 1px 5px #102630; } +.coffee-mirror-discovery-top { flex: none; padding: 20px 24px 17px; } +.coffee-mirror-overline, +.coffee-mirror-list-label { color: var(--coffee-accent); font-size: 10px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; } +.coffee-mirror-discovery h2, +.coffee-mirror-detail-copy h2 { margin: 10px 0 0; font-family: var(--font-heading), Georgia, serif; font-size: 36px; font-weight: 400; letter-spacing: -.035em; line-height: 1.08; } +.coffee-mirror-discovery-subtitle { margin: 8px 0 17px; color: var(--coffee-subtle); font-size: 12px; line-height: 1.55; } +.coffee-mirror-search { display: flex; align-items: center; gap: 9px; padding: 11px 12px; border: 1px solid var(--coffee-border); border-radius: 7px; background: #f4f9fb; color: var(--coffee-subtle); } +.coffee-mirror-search input { width: 100%; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--coffee-text); font: 13px var(--font-sans), Helvetica, Arial, sans-serif; } .coffee-mirror-search input::placeholder { color: var(--coffee-subtle); } -.coffee-mirror-list { overflow-y: auto; min-height: 0; padding: 0 14px 24px; scrollbar-width: thin; } -.coffee-mirror-list-label { color: #eeb05c; text-transform: uppercase; letter-spacing: .12em; font: 700 11px Arial, sans-serif; padding: 8px 12px; } -.coffee-mirror-list hr { border: 0; border-top: 1px solid var(--coffee-border); margin: 12px; } -.coffee-mirror-row { display: flex; align-items: center; gap: 5px; width: 100%; border: 1px solid transparent; border-radius: 9px; margin: 3px 0; padding: 8px; } -.coffee-mirror-row:hover, .coffee-mirror-row:focus-within { background: var(--coffee-hover); border-color: var(--coffee-border); } -.coffee-mirror-row-main { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; text-align: left; color: var(--coffee-text); border: 0; background: transparent; padding: 0; } -.coffee-mirror-row-main img { width: 63px; height: 63px; border-radius: 6px; flex: none; object-fit: cover; } -.coffee-mirror-row-main span { min-width: 0; } -.coffee-mirror-row-main strong, .coffee-mirror-row-main small { display: block; } -.coffee-mirror-row-main strong { font-size: 15px; line-height: 1.25; } -.coffee-mirror-row-main small { margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--coffee-subtle); font: 12px Arial, sans-serif; } -.coffee-mirror-favorite { flex: none; border: 0; background: transparent; color: var(--coffee-subtle); padding: 8px; } -.coffee-mirror-favorite:hover, .coffee-mirror-favorite:has(svg[fill="currentColor"]) { color: #eb7d79; } -.coffee-mirror-empty { padding: 20px; text-align: center; color: var(--coffee-subtle); } -.coffee-mirror-photo { position: relative; height: 235px; flex: none; overflow: hidden; mask-image: linear-gradient(#000 75%, transparent 100%); } +.coffee-mirror-featured { flex: none; padding: 0 0 15px; border-bottom: 1px solid var(--coffee-border); } +.coffee-mirror-featured .coffee-mirror-list-label { margin: 1px 24px 11px; } +.coffee-mirror-featured-track { display: flex; gap: 10px; overflow-x: auto; padding: 0 24px 5px; scrollbar-width: none; } +.coffee-mirror-featured-track::-webkit-scrollbar { display: none; } +.coffee-mirror-feature-card { display: flex; flex: 0 0 145px; flex-direction: column; min-width: 0; padding: 0 0 8px; overflow: hidden; border: 1px solid var(--coffee-border); border-radius: 8px; background: var(--coffee-hover); color: var(--coffee-text); text-align: left; } +.coffee-mirror-feature-card:hover { border-color: var(--coffee-accent); } +.coffee-mirror-feature-image { position: relative; display: block; width: 100%; height: 86px; overflow: hidden; background: #c8dbe3; } +.coffee-mirror-feature-image img { object-fit: cover; } +.coffee-mirror-feature-card strong { display: block; margin: 8px 9px 3px; font-size: 11px; line-height: 1.25; } +.coffee-mirror-feature-card small { display: block; margin: auto 9px 0; color: var(--coffee-subtle); font-size: 10px; } +.coffee-mirror-results-head { display: grid; gap: 16px; padding: 14px 18px 8px; } +.coffee-mirror-results-head .coffee-mirror-list-label { margin: 0 8px; } +.coffee-mirror-filters { display: flex; flex-wrap: wrap; gap: 6px; } +.coffee-mirror-filters button { flex: none; padding: 7px 10px; border: 1px solid var(--coffee-border); border-radius: 100px; background: transparent; color: var(--coffee-subtle); font-size: 10px; font-weight: 600; } +.coffee-mirror-filters button:hover { background: var(--coffee-hover); color: var(--coffee-text); } +.coffee-mirror-filters button[aria-pressed="true"] { border-color: #173b49; background: #173b49; color: #fff; } +.coffee-mirror-list { padding: 0 12px 24px; } +.coffee-mirror-list-label { margin: 9px 11px 11px; } +.coffee-mirror-list hr { margin: 14px 10px; border: 0; border-top: 1px solid var(--coffee-border); } +.coffee-mirror-row { display: flex; align-items: center; gap: 5px; width: 100%; margin: 2px 0; padding: 8px; border: 1px solid transparent; border-radius: 8px; } +.coffee-mirror-row:hover, +.coffee-mirror-row:focus-within { border-color: var(--coffee-border); background: var(--coffee-hover); } +.coffee-mirror-row-main { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1; padding: 0; border: 0; background: transparent; color: var(--coffee-text); text-align: left; } +.coffee-mirror-row-main img, +.coffee-mirror-row-icon { width: 62px; height: 62px; flex: none; border-radius: 6px; } +.coffee-mirror-row-main img { object-fit: cover; } +.coffee-mirror-row-icon { display: grid; place-items: center; background: linear-gradient(135deg, #d7ebef, #b5d7e0); color: #286074; } +.coffee-mirror-row-copy { min-width: 0; } +.coffee-mirror-row-copy strong, +.coffee-mirror-row-copy small { display: block; } +.coffee-mirror-row-copy strong { font-size: 13px; line-height: 1.25; } +.coffee-mirror-row-copy small { overflow: hidden; margin-top: 5px; color: var(--coffee-subtle); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.coffee-mirror-favorite { flex: none; padding: 8px; border: 0; background: transparent; color: var(--coffee-subtle); } +.coffee-mirror-favorite:hover, +.coffee-mirror-favorite:has(svg[fill="currentColor"]) { color: var(--coffee-accent); } +.coffee-mirror-empty { padding: 20px; color: var(--coffee-subtle); font-size: 13px; text-align: center; } + +.coffee-mirror-detail { overflow-y: auto; } +.coffee-mirror-photo { position: relative; height: 225px; flex: none; overflow: hidden; background: #d7ebef; } .coffee-mirror-photo img { width: 100%; height: 100%; object-fit: cover; } -.coffee-mirror-detail { overflow-y: auto; } -.coffee-mirror-detail-actions { position: absolute; right: 13px; top: 13px; display: flex; gap: 7px; } -.coffee-mirror-detail-actions button { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--coffee-border); border-radius: 50%; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(8px); } -.coffee-mirror-detail-copy { padding: 4px 24px 30px; } -.coffee-mirror-detail-copy h2 { margin: 0 0 12px; font-size: 29px; line-height: 1.15; } -.coffee-mirror-detail-copy p { color: var(--coffee-text); font: 14px/1.55 Arial, sans-serif; } -.coffee-mirror-detail-copy .coffee-mirror-address { display: flex; gap: 7px; align-items: flex-start; color: var(--coffee-subtle); } -.coffee-mirror-address svg { flex: none; color: #f1a22e; } -.coffee-mirror-directions { display: block; margin-top: 24px; padding: 12px; border-radius: 8px; background: #151515; color: white; text-align: center; font: 600 14px Arial, sans-serif; } -.coffee-mirror-detail-links { display: flex; gap: 10px; margin-top: 12px; } -.coffee-mirror-detail-links a { flex: 1; padding: 10px; border: 1px solid var(--coffee-border); border-radius: 8px; color: var(--coffee-text); text-align: center; font: 13px Arial, sans-serif; } -.coffee-mirror-controls { position: absolute; right: 16px; bottom: 72px; z-index: 1000; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; } -.coffee-mirror-controls > button, .coffee-mirror-style-wrap > button { display: grid; place-items: center; width: 43px; height: 43px; border: 1px solid var(--coffee-border); border-radius: 12px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(20px); box-shadow: 0 4px 14px #0003; } -.coffee-mirror-controls button:hover { background: var(--coffee-hover); } +.coffee-mirror-photo-credit { position: absolute; right: 10px; bottom: 10px; z-index: 1; padding: 4px 6px; border-radius: 3px; background: #102630b8; color: #fff; font-size: 10px; } +.coffee-mirror-photo-credit a { color: inherit; text-decoration: none; } +.coffee-mirror-photo-credit a:hover { text-decoration: underline; } +.coffee-mirror-detail-actions { position: absolute; top: 13px; right: 13px; z-index: 2; display: flex; gap: 7px; } +.coffee-mirror-detail-actions button { display: grid; place-items: center; width: 35px; height: 35px; border: 1px solid var(--coffee-border); border-radius: 50%; background: var(--coffee-surface); color: var(--coffee-text); } +.coffee-mirror-detail-actions button:hover { color: var(--coffee-accent); } +.coffee-mirror-detail-copy { padding: 22px 25px 30px; } +.coffee-mirror-detail-copy h2 { margin: 9px 0 16px; font-size: 34px; } +.coffee-mirror-detail-copy > p:not(.coffee-mirror-overline) { margin: 15px 0; font-size: 14px; line-height: 1.6; } +.coffee-mirror-detail-copy .coffee-mirror-address { display: flex; align-items: flex-start; gap: 8px; color: var(--coffee-subtle); font-size: 12px; } +.coffee-mirror-address svg { flex: none; color: var(--coffee-accent); } +.coffee-mirror-directions { display: block; margin-top: 24px; padding: 12px; border-radius: 6px; background: #173b49; color: #fff; font-size: 13px; font-weight: 600; text-align: center; } +.coffee-mirror-directions:hover { background: #0076a0; } +.coffee-mirror-detail-links { display: flex; gap: 9px; margin-top: 10px; } +.coffee-mirror-detail-links a { flex: 1; padding: 10px; border: 1px solid var(--coffee-border); border-radius: 6px; color: var(--coffee-text); font-size: 12px; text-align: center; } +.coffee-mirror-detail-links a:hover { background: var(--coffee-hover); } +.coffee-mirror-nearby { margin-top: 29px; padding-top: 15px; border-top: 1px solid var(--coffee-border); } +.coffee-mirror-nearby .coffee-mirror-overline { margin: 0 0 10px; } +.coffee-mirror-nearby button { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 0; border: 0; background: transparent; color: var(--coffee-text); text-align: left; } +.coffee-mirror-nearby button:hover strong { color: var(--coffee-accent); } +.coffee-mirror-nearby-image { position: relative; display: grid; place-items: center; width: 52px; height: 52px; flex: none; overflow: hidden; border-radius: 5px; background: var(--coffee-hover); color: var(--coffee-accent); } +.coffee-mirror-nearby-image img { object-fit: cover; } +.coffee-mirror-nearby button > span:nth-child(2) { min-width: 0; flex: 1; } +.coffee-mirror-nearby strong, .coffee-mirror-nearby small { display: block; } +.coffee-mirror-nearby strong { font-size: 12px; line-height: 1.2; } +.coffee-mirror-nearby small { margin-top: 4px; color: var(--coffee-subtle); font-size: 10px; } + +.coffee-mirror-controls { position: absolute; right: 17px; bottom: 72px; z-index: 850; display: flex; flex-direction: column; align-items: flex-end; gap: 7px; } +.coffee-mirror-controls > button, +.coffee-mirror-style-wrap > button { display: grid; place-items: center; width: 43px; height: 43px; border-radius: 10px; color: var(--coffee-text); } +.coffee-mirror-controls button:hover { color: var(--coffee-accent); background: var(--coffee-hover); } .coffee-mirror-style-wrap { position: relative; } -.coffee-mirror-style-menu { position: absolute; bottom: 0; right: 52px; min-width: 115px; padding: 5px; border: 1px solid var(--coffee-border); border-radius: 10px; background: var(--coffee-glass); backdrop-filter: blur(20px); } -.coffee-mirror-style-menu button { display: block; width: 100%; padding: 9px; border: 0; border-radius: 6px; background: transparent; color: var(--coffee-text); text-align: left; font: 13px Arial, sans-serif; } -.coffee-mirror-style-menu button:hover, .coffee-mirror-style-menu button[aria-pressed="true"] { background: var(--coffee-hover); } -.coffee-mirror-location-error { max-width: 180px; padding: 8px; border-radius: 7px; background: var(--coffee-glass); color: var(--coffee-text); font: 12px Arial, sans-serif; } -.coffee-mirror-footer { position: absolute; z-index: 800; left: 16px; right: 16px; bottom: 8px; display: flex; justify-content: space-between; color: var(--coffee-text); text-shadow: 0 1px 4px #000; font: 11px Arial, sans-serif; opacity: .75; pointer-events: none; } -.coffee-mirror-modal-backdrop { position: absolute; z-index: 2000; inset: 0; display: grid; place-items: center; padding: 16px; background: #0009; backdrop-filter: blur(5px); } -.coffee-mirror-modal { position: relative; width: min(100%, 440px); padding: 32px; border: 1px solid var(--coffee-border); border-radius: 15px; background: var(--coffee-glass); color: var(--coffee-text); backdrop-filter: blur(25px); box-shadow: 0 22px 70px #0007; text-align: center; } -.coffee-mirror-modal > svg { color: #efa83f; margin: 0 auto 12px; } -.coffee-mirror-modal h2 { font-size: 27px; line-height: 1.2; margin: 0 0 12px; } -.coffee-mirror-modal p { margin: 0; color: var(--coffee-subtle); } -.coffee-mirror-modal small { display: block; color: #efa83f; margin-top: 7px; } -.coffee-mirror-modal ul { margin: 22px 0; padding: 18px 20px 18px 35px; border: 1px solid var(--coffee-border); border-radius: 9px; background: var(--coffee-hover); color: var(--coffee-subtle); text-align: left; font: 13px/1.8 Arial, sans-serif; } -.coffee-mirror-start { width: 100%; border: 0; border-radius: 8px; padding: 12px; background: #111; color: #fff; font: 600 14px Arial, sans-serif; } -.coffee-mirror-modal-close { position: absolute; right: 12px; top: 12px; border: 0; background: transparent; color: var(--coffee-text); } +.coffee-mirror-style-menu { position: absolute; right: 52px; bottom: 0; min-width: 115px; padding: 5px; border-radius: 8px; } +.coffee-mirror-style-menu button { display: block; width: 100%; padding: 9px; border: 0; border-radius: 5px; background: transparent; color: var(--coffee-text); font-size: 12px; text-align: left; } +.coffee-mirror-style-menu button[aria-pressed="true"] { background: var(--coffee-hover); } +.coffee-mirror-location-error { max-width: 180px; padding: 8px; border-radius: 7px; color: var(--coffee-text); font-size: 12px; } +.coffee-mirror-footer { position: absolute; z-index: 800; bottom: 8px; left: 16px; display: flex; gap: 15px; color: #173441; font-size: 10px; text-shadow: 0 1px 4px #fff; pointer-events: none; } + @media (prefers-color-scheme: dark) { - .coffee-mirror { color-scheme: dark; background: #182021; color: #f6f5f1; --coffee-glass: rgba(15, 18, 19, .76); --coffee-border: rgba(255,255,255,.16); --coffee-text: #f6f5f1; --coffee-subtle: #bec1c1; --coffee-hover: rgba(255,255,255,.12); } - .coffee-mirror .leaflet-container { background: #1c2526; } - .coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: grayscale(1) invert(1) brightness(.8) contrast(1.1) hue-rotate(180deg); } + .coffee-mirror { color-scheme: dark; --coffee-surface: #1a2b34ed; --coffee-border: #38505a; --coffee-text: #f2eee4; --coffee-subtle: #afc0c5; --coffee-hover: #294451; --coffee-accent: #83d1e5; background: #10242e; } + .coffee-mirror .leaflet-container { background: #10242e; } + .coffee-mirror:not(.is-satellite) .leaflet-tile-pane { filter: grayscale(1) invert(1) brightness(.78) contrast(1.05) hue-rotate(180deg); } + .coffee-mirror-pin { border-color: #d5e6ec; background: #46a8c0; } + .coffee-mirror-pin.is-selected { background: #e0f2f5; box-shadow: 0 0 0 7px #83d1e55c, 0 2px 10px #0008; } + .coffee-mirror-regions { background: #12242e; } + .coffee-mirror-regions button[aria-current="page"] { background: #83d1e5; color: #102630; } + .coffee-mirror-search { background: #12242e; } + .coffee-mirror-filters button[aria-pressed="true"] { border-color: #83d1e5; background: #83d1e5; color: #102630; } + .coffee-mirror-row-icon { background: linear-gradient(135deg, #244551, #173641); color: #83d1e5; } + .coffee-mirror-directions { background: #83d1e5; color: #102630; } + .coffee-mirror-directions:hover { background: #a9e1ef; } + .coffee-mirror-footer { color: #e1edf0; text-shadow: 0 1px 4px #000; } +} +@media (max-width: 700px) { + .coffee-site-main { min-height: calc(100dvh - 64px); } + .coffee-mirror { height: calc(100dvh - 64px); } +} +@media (max-width: 639px) { + .coffee-mirror-header { top: 11px; right: 11px; left: 11px; gap: 7px; min-height: 60px; padding: 7px 8px 7px 12px; } + .coffee-mirror-brand { gap: 7px; } + .coffee-mirror-brand > svg { width: 18px; } + .coffee-mirror-brand strong { font-size: 18px; } + .coffee-mirror-brand small { display: none; } + .coffee-mirror-regions { gap: 0; padding: 2px; } + .coffee-mirror-regions button { padding: 8px 7px; font-size: 10px; } + .coffee-mirror-icon-button { width: 34px; height: 38px; } + .coffee-mirror-icon-button svg { width: 18px; } + .coffee-mirror-drawer { top: 82px; bottom: 20px; left: 11px; width: calc(100vw - 22px); } + .coffee-mirror-controls { right: 12px; bottom: 62px; } + .coffee-mirror:has(.coffee-mirror-drawer.is-open) .coffee-mirror-controls { display: none; } + .coffee-mirror-footer { bottom: 21px; left: 11px; flex-direction: column; gap: 2px; } +} +@media (max-width: 370px) { + .coffee-mirror-brand > svg { display: none; } + .coffee-mirror-brand strong { font-size: 16px; } +} +@media (prefers-reduced-motion: reduce) { + .coffee-mirror-drawer { transition: none; } } -@media (max-width: 700px) { .coffee-site-main { min-height: calc(100dvh - 64px); } .coffee-mirror { height: calc(100dvh - 64px); } } -@media (max-width: 639px) { .coffee-mirror-header { top: 12px; left: 12px; right: 12px; }.coffee-mirror-brand { gap: 7px; }.coffee-mirror-brand strong { font-size: 14px; }.coffee-mirror-brand small { font-size: 10px; }.coffee-mirror-brand > svg { width: 21px; }.coffee-mirror-drawer { top: 82px; bottom: 16px; left: 12px; width: calc(100vw - 24px); }.coffee-mirror-controls { bottom: 58px; right: 12px; }.coffee-mirror-footer { left: 12px; right: 12px; } } diff --git a/src/app/coffee/page.tsx b/src/app/coffee/page.tsx index 977ab85..3295b39 100644 --- a/src/app/coffee/page.tsx +++ b/src/app/coffee/page.tsx @@ -3,8 +3,8 @@ import { CoffeeMirror } from "~/components/CoffeeMirror"; import "./coffee.css"; export const metadata: Metadata = { - title: "Lewisburg Coffee Map", - description: "Explore coffee shops in Lewisburg, Pennsylvania.", + title: "Coffee Map — Lewisburg & Boston", + description: "A curated map of coffee shops in Lewisburg, Pennsylvania, and near Boston University's campus and Fenway.", }; export default function CoffeePage() { diff --git a/src/app/page.tsx b/src/app/page.tsx index 635468e..93afa23 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -204,7 +204,7 @@ export default function HomePage() { Outside of software, I’m into specialty coffee, Formula One, and travel. I cofounded Bucknell’s Coffee Society—and, naturally, made a{" "} - website for finding coffee in Lewisburg + coffee map for Lewisburg and Boston .

@@ -241,8 +241,8 @@ export default function HomePage() { /> Coffee, usually.

- Cofounding a coffee society came with a map of favorite local - spots. + Cofounding a coffee society led to a map of spots in Lewisburg + and around BU.

diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index 2099007..bfa673d 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -13,7 +13,7 @@ const order = [ "Puter", "Manyangles", "hadlockCMS", - "Lewisburg, PA Coffee Map", + "Coffee Map", "Real-time Racing Statistics Platform", "HRIStudio", ]; diff --git a/src/components/CoffeeMirror.tsx b/src/components/CoffeeMirror.tsx index af9cc6c..1eb7966 100644 --- a/src/components/CoffeeMirror.tsx +++ b/src/components/CoffeeMirror.tsx @@ -7,22 +7,11 @@ import { ChevronLeft, Coffee, Heart, Layers, LocateFixed, MapPin, Minus, PanelLeft, Plus, Search, X, } from "lucide-react"; -import { COFFEE_SHOPS, type CoffeeShop } from "~/lib/coffee-shops"; +import { COFFEE_REGIONS, type CoffeeRegionId, type CoffeeShop } from "~/lib/coffee-shops"; import "leaflet/dist/leaflet.css"; type MapStyle = "street" | "satellite"; -const images = [ - "amami-kitchen", "culture-coffee", "7th-st-cafe", "tastecraft-cafe", - "paris-bakery-caf", "cycleup-coffee", "cornerstone-kitchen", "gram-s-eatery", - "starbucks-coffee", "barnes-noble-caf", "starbucks-giant", "dunkin", - "alee-s-cafe", "all-star-bagels", "panera-bread", "street-of-shops", -]; - -function shopImage(shop: CoffeeShop) { - return `/coffee/shops/${images[shop.id - 1]}.jpg`; -} - const tiles: Record = { street: { url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", @@ -34,28 +23,53 @@ const tiles: Record = { }, }; +const FAVORITES_KEY = "coffee-map-favorites"; +const categories = ["All", "Coffee first", "Coffee & food", "Campus", "Quick stop"] as const; +type CategoryFilter = typeof categories[number]; + +function shopImage(shop: CoffeeShop) { + return shop.image ? `/coffee/shops/${shop.image}.jpg` : shop.imageUrl ?? null; +} + +function milesBetween(a: CoffeeShop, b: CoffeeShop) { + const lat = ((b.lat - a.lat) * Math.PI) / 180; + const lng = ((b.lng - a.lng) * Math.PI) / 180; + const arc = Math.sin(lat / 2) ** 2 + Math.cos(a.lat * Math.PI / 180) * Math.cos(b.lat * Math.PI / 180) * Math.sin(lng / 2) ** 2; + return 3958.8 * 2 * Math.atan2(Math.sqrt(arc), Math.sqrt(1 - arc)); +} + export function CoffeeMirror() { + const [regionId, setRegionId] = useState("lewisburg"); const [selected, setSelected] = useState(null); const [drawerOpen, setDrawerOpen] = useState(true); const [query, setQuery] = useState(""); + const [category, setCategory] = useState("All"); const [favorites, setFavorites] = useState([]); const [style, setStyle] = useState("street"); const [styleOpen, setStyleOpen] = useState(false); - const [welcomeOpen, setWelcomeOpen] = useState(false); + const [mapReady, setMapReady] = useState(false); const [mapFailed, setMapFailed] = useState(false); const [locationError, setLocationError] = useState(""); const mapElement = useRef(null); const map = useRef(null); const tileLayer = useRef(null); const markers = useRef(new Map()); + const region = COFFEE_REGIONS[regionId]; + const filtered = useMemo(() => region.shops.filter((shop) => + (category === "All" || shop.category === category) && + `${shop.name} ${shop.address} ${shop.description} ${shop.neighborhood} ${shop.category}`.toLowerCase().includes(query.toLowerCase().trim()), + ), [category, query, region]); + const favoriteShops = filtered.filter((shop) => favorites.includes(shop.id)); + const otherShops = filtered.filter((shop) => !favorites.includes(shop.id)); + const nearby = selected ? region.shops.filter((shop) => shop.id !== selected.id).sort((a, b) => milesBetween(selected, a) - milesBetween(selected, b)).slice(0, 3) : []; useEffect(() => { const timeout = window.setTimeout(() => { - setDrawerOpen(window.innerWidth >= 640); + const savedRegion = new URLSearchParams(window.location.search).get("city"); + if (savedRegion === "boston") setRegionId("boston"); try { - const saved = JSON.parse(localStorage.getItem("lewisburg-coffee-favorites") ?? "[]") as unknown; + const saved = JSON.parse(localStorage.getItem(FAVORITES_KEY) ?? localStorage.getItem("lewisburg-coffee-favorites") ?? "[]") as unknown; if (Array.isArray(saved)) setFavorites(saved.filter((id): id is number => typeof id === "number")); - setWelcomeOpen(!localStorage.getItem("hasSeenWelcome")); } catch { setFavorites([]); } @@ -69,28 +83,15 @@ export function CoffeeMirror() { const savedMarkers = markers.current; import("leaflet").then((L) => { if (cancelled || !mapElement.current) return; + const initialRegion = COFFEE_REGIONS.lewisburg; const instance = L.map(mapElement.current, { - center: [40.9645, -76.8845], zoom: 15, zoomControl: false, + center: initialRegion.center, zoom: initialRegion.zoom, zoomControl: false, }); map.current = instance; - tileLayer.current = L.tileLayer(tiles.street.url, { - attribution: tiles.street.attribution, maxZoom: 19, - }).addTo(instance); - COFFEE_SHOPS.forEach((shop) => { - const marker = L.marker([shop.lat, shop.lng], { - title: shop.name, - icon: L.divIcon({ className: "coffee-mirror-pin", html: "", iconSize: [18, 18], iconAnchor: [9, 9] }), - }).addTo(instance); - marker.bindTooltip(shop.name); - marker.on("click", () => { - setSelected(shop); - setDrawerOpen(true); - }); - markers.current.set(shop.id, marker); - }); resizeObserver = new ResizeObserver(() => instance.invalidateSize()); resizeObserver.observe(mapElement.current); requestAnimationFrame(() => instance.invalidateSize()); + setMapReady(true); }).catch(() => { if (!cancelled) setMapFailed(true); }); return () => { cancelled = true; @@ -103,40 +104,72 @@ export function CoffeeMirror() { }, []); useEffect(() => { - if (!map.current || !tileLayer.current) return; - const L = tileLayer.current; - L.setUrl(tiles[style].url); - L.options.attribution = tiles[style].attribution; - }, [style]); + if (!mapReady || !map.current) return; + let cancelled = false; + const instance = map.current; + const existingMarkers = markers.current; + existingMarkers.forEach((marker) => marker.remove()); + existingMarkers.clear(); + instance.flyTo(region.center, region.zoom, { duration: 0.65 }); + import("leaflet").then((L) => { + if (cancelled) return; + filtered.forEach((shop) => { + const marker = L.marker([shop.lat, shop.lng], { + title: shop.name, + icon: L.divIcon({ className: "coffee-mirror-pin", html: "", iconSize: [19, 19], iconAnchor: [9, 9] }), + }).addTo(instance); + marker.bindTooltip(shop.name); + marker.on("click", () => { setSelected(shop); setDrawerOpen(true); }); + existingMarkers.set(shop.id, marker); + }); + }).catch(() => { if (!cancelled) setMapFailed(true); }); + return () => { cancelled = true; }; + }, [filtered, mapReady, region]); + + useEffect(() => { + if (!mapReady || !map.current) return; + let cancelled = false; + const instance = map.current; + import("leaflet").then((L) => { + if (cancelled) return; + tileLayer.current?.remove(); + tileLayer.current = L.tileLayer(tiles[style].url, { + attribution: tiles[style].attribution, maxZoom: 19, + }).addTo(instance); + }).catch(() => { if (!cancelled) setMapFailed(true); }); + return () => { cancelled = true; }; + }, [mapReady, style]); useEffect(() => { if (!selected || !map.current) return; const instance = map.current; const target = instance.project([selected.lat, selected.lng], 16); - const offset = window.innerWidth >= 640 && drawerOpen ? 200 : 0; - instance.flyTo(instance.unproject(target.subtract([offset, 0]), 16), 16, { duration: 0.8 }); + const offset = window.innerWidth >= 640 && drawerOpen ? 190 : 0; + instance.flyTo(instance.unproject(target.subtract([offset, 0]), 16), 16, { duration: 0.75 }); markers.current.forEach((marker, id) => marker.getElement()?.classList.toggle("is-selected", id === selected.id)); }, [selected, drawerOpen]); - const filtered = useMemo(() => COFFEE_SHOPS.filter((shop) => - `${shop.name} ${shop.address}`.toLowerCase().includes(query.toLowerCase().trim()), - ), [query]); - const favoriteShops = filtered.filter((shop) => favorites.includes(shop.id)); - const otherShops = filtered.filter((shop) => !favorites.includes(shop.id)); + function changeRegion(next: CoffeeRegionId) { + if (next === regionId) return; + setSelected(null); + setQuery(""); + setCategory("All"); + setRegionId(next); + setDrawerOpen(true); + const url = new URL(window.location.href); + if (next === "lewisburg") url.searchParams.delete("city"); + else url.searchParams.set("city", next); + window.history.replaceState(null, "", url); + } function toggleFavorite(id: number) { setFavorites((current) => { const next = current.includes(id) ? current.filter((item) => item !== id) : [...current, id]; - localStorage.setItem("lewisburg-coffee-favorites", JSON.stringify(next)); + localStorage.setItem(FAVORITES_KEY, JSON.stringify(next)); return next; }); } - function closeWelcome() { - setWelcomeOpen(false); - localStorage.setItem("hasSeenWelcome", "true"); - } - function locate() { setLocationError(""); if (!navigator.geolocation) { setLocationError("Location is unavailable in this browser."); return; } @@ -146,48 +179,84 @@ export function CoffeeMirror() { ); } + const selectedImage = selected && shopImage(selected); + return (
-
+
{mapFailed &&

The map could not load. You can still browse the coffee shops.

}
- - - +
+ +
-
- -
© {new Date().getFullYear()} Sean O’ConnorMap data © OpenStreetMap contributors
- - {welcomeOpen &&
event.stopPropagation()}>

Welcome to the Lewisburg Coffee Map

Discover the best coffee spots in Lewisburg, PA.

Created by Sean O’Connor
  • Explore the map and select a coffee shop.
  • Search the list to find your next cup.
  • Use the location button to see what’s nearby.
} +
Curated by Sean O’ConnorLocations checked September 2026
); } function ShopRow({ shop, favorite, onSelect, onFavorite }: { shop: CoffeeShop; favorite: boolean; onSelect: (shop: CoffeeShop) => void; onFavorite: (id: number) => void }) { - return
; + const image = shopImage(shop); + return
; } diff --git a/src/lib/coffee-shops.ts b/src/lib/coffee-shops.ts index 5e2c525..34935f9 100644 --- a/src/lib/coffee-shops.ts +++ b/src/lib/coffee-shops.ts @@ -1,5 +1,3 @@ -// Shop listings carried over from the original Lewisburg Coffee map. - export interface CoffeeShop { id: number; name: string; @@ -7,186 +5,87 @@ export interface CoffeeShop { lat: number; lng: number; address: string; - phone: string; website: string; + phone?: string; + image?: string; + imageUrl?: string; + photoCredit?: string; + photoSource?: string; + imageAlt?: string; + category: "Coffee first" | "Coffee & food" | "Campus" | "Quick stop"; + neighborhood: string; } -export const COFFEE_SHOPS: CoffeeShop[] = [ - { - id: 1, - name: "Amami Kitchen", - description: - "A beloved culinary spot offering a blend of high-quality Italian espresso and American coffee bar options, alongside mouthwatering baked goods and paninis.", - lat: 40.96112008888467, - lng: -76.88692162812505, - address: "103 S 6th St, Lewisburg, PA 17837", - phone: "(570) 490-7857", - website: "https://www.amamiespresso.com", +export type CoffeeRegionId = "lewisburg" | "boston"; + +interface CoffeeRegion { + label: string; + subtitle: string; + introduction: string; + coverImage: string; + coverAlt: string; + coverCaption: string; + picks: number[]; + center: [number, number]; + zoom: number; + shops: CoffeeShop[]; +} + +// Locations checked against café, university, and local tourism listings in September 2026. +// Keep this a curated map; an outdated listing is worse than a shorter list. +export const COFFEE_REGIONS: Record = { + lewisburg: { + label: "Lewisburg, PA", + subtitle: "Market Street to Bucknell", + introduction: "A walkable little coffee town: espresso on Market Street, pastries around the corner, and a campus stop when the day runs long.", + coverImage: "/coffee/shops/tastecraft-cafe.jpg", + coverAlt: "Tastecraft Coffee in Lewisburg", + coverCaption: "Tastecraft Coffee", + picks: [4, 1, 5], + center: [40.9627, -76.8863], + zoom: 15, + shops: [ + { id: 1, name: "Amami Kitchen & Espresso Bar", description: "Italian espresso, all-day breakfast, and paninis a short walk from Market Street.", lat: 40.9611201, lng: -76.8869216, address: "103 S 6th St, Lewisburg, PA 17837", phone: "(570) 490-7857", website: "https://amamiespresso.com/", image: "amami-kitchen", category: "Coffee & food", neighborhood: "South Sixth" }, + { id: 2, name: "Culture Coffee", description: "Specialty coffee and matcha in its new Market Street home.", lat: 40.9642689, lng: -76.8852364, address: "326 Market St, Lewisburg, PA 17837", phone: "(570) 601-7903", website: "https://www.instagram.com/culturecoffeee_/", imageUrl: "https://pub-ba1a74be17d7442a9f2541946eb9510e.r2.dev/shops/200a519d-d913-4e2b-b4a4-d6c7bed282c0/0.jpg", photoCredit: "via Joe Coffee", photoSource: "https://joe.coffee/locations/pa/lewisburg/culture-coffee-lewisburg/", imageAlt: "Culture Coffee interior", category: "Coffee first", neighborhood: "Market Street" }, + { id: 3, name: "7th Street Café", description: "Bucknell's campus café for coffee, sweets, and a break between classes.", lat: 40.9577366, lng: -76.8848393, address: "420 S 7th St, Lewisburg, PA 17837", website: "https://www.bucknell.edu/life-bucknell/housing-dining/dining/dining-venues", image: "7th-st-cafe", category: "Campus", neighborhood: "Bucknell" }, + { id: 4, name: "Tastecraft Coffee", description: "House-roasted coffee, seasonal drinks, French macarons, and a view toward Hufnagle Park.", lat: 40.9629871, lng: -76.8875361, address: "512 Market St, Lewisburg, PA 17837", phone: "(570) 768-5340", website: "https://tastecraftcafe.com/locations/", image: "tastecraft-cafe", category: "Coffee first", neighborhood: "Market Street" }, + { id: 5, name: "Paris Bakery & Café", description: "Coffee with French pastries and freshly baked bread on Market Street.", lat: 40.9638014, lng: -76.8856132, address: "335 Market St, Lewisburg, PA 17837", phone: "(570) 884-2138", website: "https://parisbakery.cafe/", image: "paris-bakery-caf", category: "Coffee & food", neighborhood: "Market Street" }, + { id: 6, name: "CycleUp Café", description: "A coffee bar inside the downtown bike shop, with breakfast and lunch too.", lat: 40.9632568, lng: -76.8866329, address: "429 Market St, Lewisburg, PA 17837", phone: "(570) 413-1705", website: "https://store104839915.company.site/", image: "cycleup-coffee", category: "Coffee & food", neighborhood: "Market Street" }, + { id: 17, name: "World's End Coffee Co.", description: "A Market Street coffee room with espresso, pour-overs, and a space for local events.", lat: 40.9634952, lng: -76.8862402, address: "425 Market St, Lewisburg, PA 17837", phone: "(570) 522-0103", website: "https://www.worldsendcoffee.com/", imageUrl: "https://images.squarespace-cdn.com/content/v1/63c435471c7ce839b38474bd/ba8a1058-6143-4225-abc2-11f4441de46b/IMG_4616.jpeg", photoCredit: "World’s End Coffee Co. · Eagles Mere", photoSource: "https://www.worldsendcoffee.com/best-coffee-house-on-the-mountain", imageAlt: "World’s End Coffee Co. interior in Eagles Mere", category: "Coffee first", neighborhood: "Market Street" }, + { id: 8, name: "Gram's Eatery", description: "A breakfast and brunch table with coffee just off Market Street.", lat: 40.9648824, lng: -76.8854227, address: "21 N 3rd St, Lewisburg, PA 17837", phone: "(570) 522-0230", website: "https://www.lewisburgpa.com/connections/18441-grams-eatery", image: "gram-s-eatery", category: "Coffee & food", neighborhood: "North Third" }, + { id: 10, name: "Barnes & Noble Café", description: "Bookstore coffee in the middle of downtown Lewisburg.", lat: 40.9637562, lng: -76.8862202, address: "400 Market St, Lewisburg, PA 17837", website: "https://bucknell.bncollege.com/", image: "barnes-noble-caf", category: "Quick stop", neighborhood: "Market Street" }, + { id: 14, name: "All Star Bagels", description: "A Market Street bagel stop for breakfast and a cup of coffee.", lat: 40.9644503, lng: -76.8850652, address: "300 Market St, Lewisburg, PA 17837", phone: "(570) 213-4277", website: "https://allstarbagelandcoffee.com/", image: "all-star-bagels", category: "Coffee & food", neighborhood: "Market Street" }, + { id: 13, name: "Alee's Café", description: "A small Market Street café for a coffee alongside Mediterranean food.", lat: 40.9647375, lng: -76.8840282, address: "235 Market St, Lewisburg, PA 17837", website: "https://www.lewisburgpa.com/business-directory", image: "alee-s-cafe", category: "Coffee & food", neighborhood: "Market Street" }, + { id: 16, name: "Street of Shops Restaurant", description: "Coffee and a hearty breakfast before browsing the antique mall.", lat: 40.9677070, lng: -76.8826700, address: "100 N Water St, Lewisburg, PA 17837", website: "https://streetofshops.net/", image: "street-of-shops", category: "Coffee & food", neighborhood: "North Water" }, + { id: 7, name: "Cornerstone Kitchen", description: "A café at the Miller Center; hours vary with events, so check before going.", lat: 40.9606353, lng: -76.8977790, address: "120 Hardwood Dr, Lewisburg, PA 17837", website: "https://millercenterlewisburg.com/cafe/", image: "cornerstone-kitchen", category: "Coffee & food", neighborhood: "Miller Center" }, + { id: 12, name: "Dunkin' · N Derr Drive", description: "A familiar quick coffee run just north of downtown.", lat: 40.9699564, lng: -76.8924827, address: "600 N Derr Dr, Lewisburg, PA 17837", website: "https://locations.dunkindonuts.com/en/pa/lewisburg/600-n-derr-dr/303602", image: "dunkin", category: "Quick stop", neighborhood: "North Lewisburg" }, + ], }, - { - id: 2, - name: "Culture Coffee", - description: - "A modern coffee shop focusing on unique brewing methods for specialty coffee and traditional techniques for matcha. Known for its 'Red Velvet Latte' and croissants.", - lat: 40.966214038235066, - lng: -76.88549177840001, - address: "216 St. John St, Lewisburg, PA 17837", - phone: "(570) 601-7903", - website: "https://www.instagram.com/culturecoffeee_", + boston: { + label: "Boston, MA", + subtitle: "Fenway & BU campus", + introduction: "A longer city walk: independent coffee near Kenmore, a café for a sit-down break in Fenway, and plenty of campus-side quick stops.", + coverImage: "https://www.phinista.com/fenway-counter.png", + coverAlt: "The counter at Phinista in Fenway", + coverCaption: "Phinista · Fenway", + picks: [103, 101, 111], + center: [42.3477, -71.1115], + zoom: 14, + shops: [ + { id: 101, name: "Pavement Coffeehouse · BU", description: "An easy campus stop across from Marsh Chapel, known for coffee and house-made bagels.", lat: 42.3498465, lng: -71.1072088, address: "736 Commonwealth Ave, Boston, MA 02215", phone: "(617) 766-6048", website: "https://pavementcoffeehouse.com/pages/boston_university", imageUrl: "https://pavementcoffeehouse.com/cdn/shop/files/BBU_banner_107c7ddb-f0ba-4035-965b-63b960d75d1d_2048x2048.jpg?v=1727315570", photoCredit: "Pavement Coffeehouse", category: "Coffee first", neighborhood: "BU Central" }, + { id: 102, name: "Pavement Coffeehouse · Fenway", description: "Coffee, tea, and bagels on Boylston Street, a few blocks from Fenway Park.", lat: 42.3441966, lng: -71.0982737, address: "1334 Boylston St, Boston, MA 02215", phone: "(857) 263-7355", website: "https://pavementcoffeehouse.com/pages/fenway", imageUrl: "https://cdn.shopify.com/s/files/1/1797/9423/files/fenway_2048x2048.jpg?v=1653020931", photoCredit: "Pavement Coffeehouse", category: "Coffee first", neighborhood: "Fenway" }, + { id: 103, name: "Phinista · Fenway", description: "A French-Vietnamese café with phin-drip coffee, house-made syrups, and bánh mì.", lat: 42.3430483, lng: -71.0992983, address: "96 Peterborough St, Boston, MA 02215", phone: "(617) 262-7700", website: "https://www.phinista.com/locations", imageUrl: "https://images.squarespace-cdn.com/content/v1/5d2cf499669038000114edd1/c6f0d447-f891-4246-b2bc-6b4409adb924/DSCF6626.jpg?format=1000w", photoCredit: "Phinista", category: "Coffee first", neighborhood: "Fenway" }, + { id: 104, name: "Saxbys at BU", description: "Coffee on the ground floor of BU's Center for Computing & Data Sciences.", lat: 42.3500229, lng: -71.1031113, address: "665 Commonwealth Ave, Boston, MA 02215", website: "https://www.bu.edu/dining/location/saxbys/", imageUrl: "https://www.bu.edu/files/2023/02/students-at-saxbys_feat_23-1005-CCDS-034-1200x1200.jpg", photoCredit: "BU Today", photoSource: "https://www.bu.edu/articles/2023/saxbys-new-center-for-computing-data-sciences/", category: "Campus", neighborhood: "BU East" }, + { id: 105, name: "Tatte Bakery & Café · Fenway", description: "Pastries, specialty coffee, and a sit-down break near Fenway Park.", lat: 42.3438619, lng: -71.0997454, address: "1352 Boylston St, Boston, MA 02115", phone: "(617) 366-1800", website: "https://tattebakery.com/locations/ma/boston/1352-boylston-st/", imageUrl: "https://a.mktgcdn.com/p/y0rtnWeeb59FnyU80DFyZK1hYpZwzD1-IW1pD56mVo4/500x1000.jpg", photoCredit: "Tatte Bakery & Café", photoSource: "https://tattebakery.com/locations/ma/boston/1352-boylston-st/", category: "Coffee & food", neighborhood: "Fenway" }, + { id: 106, name: "Caffè Nero · West Campus", description: "Coffee and a place to sit at the west end of BU's Commonwealth Avenue stretch.", lat: 42.3525906, lng: -71.1225613, address: "1047 Commonwealth Ave, Boston, MA 02215", website: "https://www.caffenero.com/us/stores", imageUrl: "https://pub-ba1a74be17d7442a9f2541946eb9510e.r2.dev/shops/622acd63-1a25-4e15-a023-04091bac824c/4.jpg", photoCredit: "via Joe Coffee", photoSource: "https://joe.coffee/locations/ma/boston/caff-nero-1047-commonwealth-ave-boston/", category: "Coffee first", neighborhood: "BU West" }, + { id: 107, name: "Pavement Coffeehouse · Allston", description: "The original corner of Pavement's coffee and bagel story.", lat: 42.3502937, lng: -71.1317983, address: "1243 Commonwealth Ave, Allston, MA 02134", website: "https://pavementcoffeehouse.com/pages/allston", imageUrl: "https://pavementcoffeehouse.com/cdn/shop/files/Allston2_3_1414x.jpg?v=1653020973", photoCredit: "Pavement Coffeehouse", photoSource: "https://pavementcoffeehouse.com/pages/allston", category: "Coffee first", neighborhood: "Allston" }, + { id: 108, name: "Pavement Coffeehouse · Berklee", description: "Coffee and bagels on Boylston, close to the music school.", lat: 42.3468279, lng: -71.0883896, address: "1096 Boylston St, Boston, MA 02215", website: "https://pavementcoffeehouse.com/pages/locations", imageUrl: "https://pavementcoffeehouse.com/cdn/shop/files/Pavement_Coffeehouse_Berklee_Exteriorjpg_4595x.jpg?v=1773170984", photoCredit: "Pavement Coffeehouse", photoSource: "https://pavementcoffeehouse.com/pages/berklee", category: "Coffee first", neighborhood: "Back Bay" }, + { id: 109, name: "Pavement Coffeehouse · Symphony", description: "A café around the corner from Symphony Hall and Northeastern.", lat: 42.3416675, lng: -71.0870504, address: "44 Gainsborough St, Boston, MA 02115", website: "https://pavementcoffeehouse.com/pages/locations", imageUrl: "https://pavementcoffeehouse.com/cdn/shop/files/1_917e1b83-079d-4c76-9e3e-d45e7e3c784e_2048x2048.jpg?v=1653020988", photoCredit: "Pavement Coffeehouse", photoSource: "https://pavementcoffeehouse.com/pages/symphony", category: "Coffee first", neighborhood: "Symphony" }, + { id: 110, name: "Phinista · Allston", description: "Vietnamese coffee and a later café stop near BU West.", lat: 42.3523107, lng: -71.1241009, address: "1092 Commonwealth Ave, Boston, MA 02215", website: "https://www.phinista.com/", imageUrl: "https://www.phinista.com/allston-interior.png", photoCredit: "Phinista", photoSource: "https://www.phinista.com/", category: "Coffee first", neighborhood: "Allston" }, + { id: 111, name: "George Howell Coffee · Lyrik", description: "Single-origin coffee and art at the Newbury Street end of the walk.", lat: 42.3477243, lng: -71.0886043, address: "400 Newbury St, Boston, MA 02215", website: "https://georgehowellcoffee.com/pages/lyrik", imageUrl: "https://georgehowellcoffee.com/cdn/shop/files/38-DSC01079_resized.jpg?v=1765315754&width=1000", photoCredit: "George Howell Coffee", category: "Coffee first", neighborhood: "Back Bay" }, + { id: 112, name: "Café Landwer · Audubon Circle", description: "Handcrafted coffee and a Mediterranean brunch at the edge of Kenmore.", lat: 42.3468335, lng: -71.1057590, address: "900 Beacon St, Boston, MA 02215", website: "https://landwercafe.com/locations", imageUrl: "https://pub-ba1a74be17d7442a9f2541946eb9510e.r2.dev/shops/2133d445-333f-46f7-866a-51f4ccde7180/3.jpg", photoCredit: "via Joe Coffee", photoSource: "https://joe.coffee/locations/ma/boston/cafe-landwer-900-beacon-st-boston/", category: "Coffee & food", neighborhood: "Audubon Circle" }, + { id: 113, name: "Starbucks · Warren Towers", description: "A quick campus coffee run at 700 Commonwealth Avenue.", lat: 42.3492245, lng: -71.1031202, address: "700 Commonwealth Ave, Boston, MA 02215", website: "https://www.bu.edu/dining/location/starbucks/", imageUrl: "https://www.bu.edu/files/2025/02/24-1573-CAMBEAUTY-003_warrentowers-feat-crop-vert-1152x1536.jpg", photoCredit: "BU Today · Warren Towers", photoSource: "https://www.bu.edu/articles/2025/what-to-know-about-warren-towers-renovation/", imageAlt: "Warren Towers exterior, home to the campus Starbucks", category: "Campus", neighborhood: "BU East" }, + { id: 114, name: "Dunkin' · Buick Street", description: "The no-detour coffee stop inside Buick Street Market.", lat: 42.3523950, lng: -71.1159897, address: "10 Buick St, Boston, MA 02215", website: "https://www.bu.edu/dining/where-to-eat/", imageUrl: "https://www.bu.edu/dining/files/2017/12/dunkin4.jpg", photoCredit: "BU Dining", photoSource: "https://www.bu.edu/dining/location/dunkin-donuts-at-buick-street-market/", category: "Quick stop", neighborhood: "BU West" }, + ], }, - { - id: 3, - name: "7th St Cafe", - description: - "Bucknell's ultimate on-campus dessert and coffee shop. A cozy spot for students and visitors to enjoy milkshakes, ice cream, and specialty espresso drinks.", - lat: 40.95773659979501, - lng: -76.88483933057073, - address: "420 S 7th St, Lewisburg, PA 17837", - phone: "(570) 577-1240", - website: - "https://www.bucknell.edu/life-bucknell/dining-services/places-eat", - }, - { - id: 4, - name: "Tastecraft Cafe", - description: - "Downtown cafe known for its in-house roasted coffee and over 20 flavors of French macarons. A perfect spot to relax with a fresh brew and a sweet treat.", - lat: 40.962987142339166, - lng: -76.88753610710032, - address: "512 Market St, Lewisburg, PA 17837", - phone: "(570) 768-5340", - website: "https://www.tastecraftcafe.com", - }, - { - id: 5, - name: "Paris Bakery & Café", - description: - "Authentic French bakery offering legendary croissants, pain au chocolat, and artisanal breads. Experience a taste of France in the heart of Lewisburg.", - lat: 40.96380135223123, - lng: -76.88561321931094, - address: "335 Market St, Lewisburg, PA 17837", - phone: "(570) 884-2138", - website: "https://parisbakery.cafe", - }, - { - id: 6, - name: "CycleUp Coffee", - description: - "A unique bike shop cafe serving as a community hub. Enjoy coffee, light bites, and bike services in a cozy, cycling-themed atmosphere.", - lat: 40.96325677827358, - lng: -76.88663292782749, - address: "429 Market St, Lewisburg, PA 17837", - phone: "(570) 413-1705", - website: "https://www.cycleupcafe.com", - }, - { - id: 7, - name: "Cornerstone Kitchen", - description: - "Located in the Miller Center, offering fresh, healthy, and delicious cafe food. A place to connect over real food and a shared table.", - lat: 40.96125715212586, - lng: -76.89719763291119, - address: "120 Hardwood Dr, Lewisburg, PA 17837", - phone: "(570) 556-4191", - website: "https://millercenterlewisburg.com/cornerstone-kitchen", - }, - { - id: 8, - name: "Gram's Eatery", - description: - "A local favorite for homestyle breakfast and brunch. Known for generous portions, locally sourced ingredients, and a welcoming atmosphere.", - lat: 40.9648823532259, - lng: -76.88542268394536, - address: "21 N 3rd St, Lewisburg, PA 17837", - phone: "(570) 522-0230", - website: "https://www.facebook.com/gramseaterylewisburg", - }, - { - id: 9, - name: "Starbucks Coffee", - description: - "Seattle-based coffeehouse chain known for its signature roasts, light bites and WiFi availability.", - lat: 40.98121817592506, - lng: -76.88452326979083, - address: "7431 Westbranch Hwy, Lewisburg, PA 17837", - phone: "(570) 452-2370", - website: "https://www.starbucks.com/store-locator/store/1010326/", - }, - { - id: 10, - name: "Barnes & Noble Café", - description: - "Barnes & Noble Café at Bucknell University. Serving Starbucks coffee and a variety of pastries and sandwiches in a comfortable bookstore setting.", - lat: 40.963756201655016, - lng: -76.88622019668863, - address: "400 Market St, Lewisburg, PA 17837", - phone: "(570) 577-3960", - website: "https://bucknell.bncollege.com", - }, - { - id: 11, - name: "Starbucks (Giant)", - description: - "Conveniently located inside Giant Food Store. Perfect for grabbing your favorite coffee while grocery shopping.", - lat: 40.96169263125371, - lng: -76.89522292676229, - address: "224 Hardwood Dr, Lewisburg, PA 17837", - phone: "(570) 523-3200", - website: "https://www.starbucks.com", - }, - { - id: 12, - name: "Dunkin'", - description: - "Long-running chain serving signature breakfast items & a variety of coffee drinks.", - lat: 40.969956373480606, - lng: -76.89248274516008, - address: "600 N Derr Dr, Lewisburg, PA 17837", - phone: "(570) 524-4900", - website: "https://www.dunkindonuts.com", - }, - { - id: 13, - name: "Alee's Cafe", - description: - "A cozy local spot offering a variety of coffee drinks and light fare in the heart of downtown.", - lat: 40.964708961176, - lng: -76.8846770451187, - address: "232 Market St, Lewisburg, PA 17837", - phone: "(570) 523-1234", - website: "https://www.aleescafe.com", - }, - { - id: 14, - name: "All Star Bagels", - description: - "Freshly baked bagels and coffee. A popular morning stop for locals and students.", - lat: 40.96445033682396, - lng: -76.88506518736742, - address: "300 Market St, Lewisburg, PA 17837", - phone: "(570) 524-1000", - website: "https://www.allstarbagels.com", - }, - { - id: 15, - name: "Panera Bread", - description: - "Counter-serve bakery/cafe chain serving sandwiches, salads & more, known for its bread & free WiFi.", - lat: 40.97531008164084, - lng: -76.8897556844414, - address: "6951 Westbranch Hwy, Lewisburg, PA 17837", - phone: "(570) 524-0000", - website: "https://www.panerabread.com", - }, - { - id: 16, - name: "Street of Shops Restaurant", - description: - "Part of a shopping village, this retro eatery serves down-home American fare & breakfast.", - lat: 40.96779250919243, - lng: -76.88264400489504, - address: "100 N Water St, Lewisburg, PA 17837", - phone: "(570) 523-0420", - website: "https://streetofshops.net", - }, -]; +}; diff --git a/src/lib/data.tsx b/src/lib/data.tsx index 434e978..717473d 100644 --- a/src/lib/data.tsx +++ b/src/lib/data.tsx @@ -571,12 +571,12 @@ export const projects: Project[] = [ featured: true, }, { - title: "Lewisburg, PA Coffee Map", + title: "Coffee Map", description: - "A website showing off the best coffee shops in Lewisburg, PA.", + "A curated coffee map for Lewisburg and the BU/Fenway area.", longDescription: - "Aggregates coffee shop data from various sources and displays it in a user-friendly interface. Use it to find your next brew!", - tags: ["Coffee", "Lewisburg, PA", "Bucknell"], + "An interactive guide to coffee stops in Lewisburg and around Boston University's campus, with city switching, search, saved places, and directions.", + tags: ["Coffee", "Lewisburg", "Boston", "Maps"], websiteLink: "/coffee", gitLink: "https://github.com/soconnor0919/lewisburg-coffee", image: "/images/lewisburg-coffee.webp",