Offline-capable geomap, meant for storing location bookmarks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add screenshot

+25 -362
+2
README.md
··· 4 4 5 5 It is a PWA built on [MapLibre GL](https://maplibre.org) and [PMTiles](https://protomaps.github.io), and serves as a proof-of-concept for the data flow behind something like [Organic Maps](https://organicmaps.app). A world basemap is fetched on first load, users can download regional tiles on demand with additional detail, and everything renders offline after that. Bookmarks, collections, and search history are stored locally. 6 6 7 + ![Screenshot](./www/static/brand/screenshot.png) 8 + 7 9 ## Technologies 8 10 9 11 **App**
+1 -1
deno.json
··· 1 1 { 2 - "version": "0.2.0", 2 + "version": "0.3.1", 3 3 "workspace": ["./data"], 4 4 "tasks": { 5 5 "data": "deno run -A ./data/cli/main.ts",
+5 -3
www/models/schema/v0.ts
··· 70 70 /** Resolves a human-readable name for display, falling through available fields. */ 71 71 export function bookmarkDisplayName(b: Bookmark): string { 72 72 return ( 73 - b.properties.displayName ?? 74 - b.properties.name ?? 73 + b.properties.name ?? 74 + b.properties.displayName ?? 75 75 b.properties.address?.displayText ?? 76 76 `${b.geometry.coordinates[1].toFixed(4)}, ${ 77 77 b.geometry.coordinates[0].toFixed(4) ··· 98 98 99 99 const now = new Date().toISOString() 100 100 101 + const appVersion: string = globalThis.__APP_VERSION__ 102 + 101 103 export const StoreState = z.object({ 102 - version: z.optional(z.string()), 104 + version: z._default(z.string(), appVersion), 103 105 searchHistory: z._default(z.array(SearchHistoryEntry), []), 104 106 bookmarks: z._default(z.array(Bookmark), []), 105 107 bookmarkCollections: z._default(z.array(BookmarkCollection), [{
+1
www/routes/bookmarks.ts
··· 76 76 77 77 #navigateTo(b: Bookmark) { 78 78 const [lng, lat] = b.geometry.coordinates 79 + console.log(b) 79 80 setMapNav({ lat, lng, zoom: b.zoom, bookmarkId: b.id }) 80 81 location.hash = '#!/' 81 82 }
www/static/brand/screenshot.png

This is a binary file and will not be displayed.

+16 -1
www/static/styles/theme.css
··· 138 138 r-bookmarks { 139 139 display: block; 140 140 padding: var(--s4) var(--s3); 141 + width: 100%; 141 142 max-width: 600px; 142 143 margin: 0 auto; 143 144 box-sizing: border-box; ··· 324 325 display: flex; 325 326 flex-direction: column; 326 327 gap: var(--s2); 328 + max-width: 95vw; 329 + margin: auto; 327 330 } 328 331 329 332 .search-history-item { ··· 379 382 380 383 .bm-toolbar { 381 384 display: flex; 385 + flex-wrap: wrap; 382 386 gap: var(--s2); 383 387 margin-bottom: var(--s4); 384 388 } ··· 395 399 } 396 400 397 401 .bm-folder { 398 - margin-bottom: var(--s2); 399 402 border: 1px solid currentColor; 400 403 border-radius: var(--br-base); 401 404 overflow: hidden; ··· 450 453 padding: 0 var(--s3); 451 454 } 452 455 456 + .bm-folder, 457 + .bm-section { 458 + max-width: 90vw; 459 + margin: var(--s2) auto var(--s2) auto; 460 + } 461 + 453 462 .bm-item { 454 463 display: flex; 455 464 align-items: center; ··· 641 650 text-transform: uppercase; 642 651 letter-spacing: 0.05em; 643 652 opacity: 0.7; 653 + } 654 + 655 + @media (max-width: 768px) { 656 + input, textarea, select { 657 + font-size: 1rem; 658 + } 644 659 } 645 660 }
-357
www/static/styles/utilities.min.css
··· 1 - @layer utilities { 2 - .flex { 3 - display: flex; 4 - } 5 - 6 - .flex-column { 7 - flex-direction: column; 8 - } 9 - 10 - .flex-row { 11 - flex-direction: row; 12 - } 13 - 14 - .items-center { 15 - align-items: center; 16 - } 17 - 18 - .items-start { 19 - align-items: flex-start; 20 - } 21 - 22 - .items-end { 23 - align-items: flex-end; 24 - } 25 - 26 - .justify-center { 27 - justify-content: center; 28 - } 29 - 30 - .justify-start { 31 - justify-content: flex-start; 32 - } 33 - 34 - .justify-end { 35 - justify-content: flex-end; 36 - } 37 - 38 - .justify-between { 39 - justify-content: space-between; 40 - } 41 - 42 - .w-100 { 43 - width: 100%; 44 - } 45 - 46 - .mw6 { 47 - max-width: var(--max-width-sm); 48 - } 49 - 50 - .measure { 51 - max-width: 30em; 52 - } 53 - 54 - .container-sm { 55 - max-width: var(--max-width-sm); 56 - } 57 - 58 - .container-md, 59 - .container-sm { 60 - margin: 0 auto; 61 - padding: 0 var(--container-padding); 62 - } 63 - 64 - .container-md { 65 - max-width: var(--max-width-md); 66 - } 67 - 68 - .container-lg { 69 - max-width: var(--max-width-lg); 70 - } 71 - 72 - .container-lg, 73 - .container-xl { 74 - margin: 0 auto; 75 - padding: 0 var(--container-padding); 76 - } 77 - 78 - .container-xl { 79 - max-width: var(--max-width-xl); 80 - } 81 - 82 - .container-fluid { 83 - padding: 0 var(--container-padding); 84 - width: 100%; 85 - } 86 - 87 - .pa0 { 88 - padding: 0; 89 - } 90 - 91 - .pa1 { 92 - padding: var(--s1); 93 - } 94 - 95 - .pa2 { 96 - padding: var(--s2); 97 - } 98 - 99 - .pa3 { 100 - padding: var(--s3); 101 - } 102 - 103 - .pa4 { 104 - padding: var(--s4); 105 - } 106 - 107 - .ma0 { 108 - margin: 0; 109 - } 110 - 111 - .ma1 { 112 - margin: var(--s1); 113 - } 114 - 115 - .ma2 { 116 - margin: var(--s2); 117 - } 118 - 119 - .ma3 { 120 - margin: var(--s3); 121 - } 122 - 123 - .mh-auto { 124 - margin-left: auto; 125 - margin-right: auto; 126 - } 127 - 128 - .mv3 { 129 - margin-bottom: var(--s3); 130 - margin-top: var(--s3); 131 - } 132 - 133 - .mb2 { 134 - margin-bottom: var(--s2); 135 - } 136 - 137 - .mb0 { 138 - margin-bottom: 0; 139 - } 140 - 141 - .mt3 { 142 - margin-top: var(--s3); 143 - } 144 - 145 - .mt2 { 146 - margin-top: var(--s2); 147 - } 148 - 149 - .mr3 { 150 - margin-right: var(--s3); 151 - } 152 - 153 - .pr3 { 154 - padding-right: var(--s3); 155 - } 156 - 157 - .pl2 { 158 - padding-left: var(--s2); 159 - } 160 - 161 - .pt2 { 162 - padding-top: var(--s2); 163 - } 164 - 165 - .pb4 { 166 - padding-bottom: var(--s4); 167 - } 168 - 169 - .pb0 { 170 - padding-bottom: 0; 171 - } 172 - 173 - .pb3 { 174 - padding-bottom: var(--s3); 175 - } 176 - 177 - .f3 { 178 - font-size: var(--f3); 179 - } 180 - 181 - .f4 { 182 - font-size: var(--f4); 183 - } 184 - 185 - .f5 { 186 - font-size: var(--f5); 187 - } 188 - 189 - .f6 { 190 - font-size: var(--f6); 191 - } 192 - 193 - .tc { 194 - text-align: center; 195 - } 196 - 197 - .tr { 198 - text-align: right; 199 - } 200 - 201 - .b { 202 - font-weight: var(--fw-bold); 203 - } 204 - 205 - .br2 { 206 - border-radius: var(--br2); 207 - } 208 - 209 - .ba { 210 - border: 1px solid; 211 - } 212 - 213 - .b1 { 214 - border-width: 1px; 215 - } 216 - 217 - .t-border { 218 - border: 1px solid transparent; 219 - } 220 - 221 - .border { 222 - border: 1px solid; 223 - } 224 - 225 - .list { 226 - list-style: none; 227 - } 228 - 229 - .cursor, 230 - .pointer { 231 - cursor: pointer; 232 - } 233 - 234 - .o-30 { 235 - opacity: 0.3; 236 - } 237 - 238 - .o-40 { 239 - opacity: 0.4; 240 - } 241 - 242 - .o-60 { 243 - opacity: 0.6; 244 - } 245 - 246 - .underline { 247 - text-decoration: underline; 248 - } 249 - 250 - .link { 251 - text-decoration: none; 252 - } 253 - 254 - .db { 255 - display: block; 256 - } 257 - 258 - .h2 { 259 - height: 2rem; 260 - } 261 - 262 - .h3 { 263 - height: 3rem; 264 - } 265 - 266 - .input { 267 - border: 1px solid; 268 - border-radius: var(--br2); 269 - padding: var(--s2); 270 - } 271 - 272 - .input-reset { 273 - -webkit-appearance: none; 274 - -moz-appearance: none; 275 - appearance: none; 276 - background: transparent; 277 - } 278 - 279 - .shadow { 280 - box-shadow: var(--shadow-base); 281 - } 282 - 283 - .pv2 { 284 - padding-bottom: var(--s2); 285 - padding-top: var(--s2); 286 - } 287 - 288 - .pv1 { 289 - padding-bottom: var(--s1); 290 - padding-top: var(--s1); 291 - } 292 - 293 - .ph2 { 294 - padding-left: var(--s2); 295 - padding-right: var(--s2); 296 - } 297 - 298 - .lh-copy { 299 - line-height: var(--lh-copy); 300 - } 301 - 302 - .center { 303 - text-align: center; 304 - } 305 - 306 - .flex-wrap { 307 - flex-wrap: wrap; 308 - } 309 - 310 - .no-select { 311 - cursor: default; 312 - -webkit-user-select: none; 313 - -moz-user-select: none; 314 - user-select: none; 315 - } 316 - 317 - .relative { 318 - position: relative; 319 - } 320 - 321 - .absolute { 322 - position: absolute; 323 - } 324 - 325 - .fixed { 326 - position: fixed; 327 - } 328 - 329 - .absolute-center { 330 - left: 50%; 331 - position: fixed; 332 - top: 50%; 333 - transform: translate(-50%, -50%); 334 - } 335 - 336 - .overflow-hidden, 337 - .text-ellipsis { 338 - overflow: hidden; 339 - } 340 - 341 - .text-ellipsis { 342 - text-overflow: ellipsis; 343 - white-space: nowrap; 344 - } 345 - 346 - .z-dropdown { 347 - z-index: var(--z-dropdown); 348 - } 349 - 350 - .z-modal { 351 - z-index: var(--z-modal); 352 - } 353 - 354 - .z-tooltip { 355 - z-index: var(--z-tooltip); 356 - } 357 - }