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 data delete button

+146 -19
+23
www/models/app.ts
··· 157 157 } 158 158 } 159 159 160 + async deleteAllData(): Promise<{ success: boolean; error?: string }> { 161 + try { 162 + localStorage.removeItem('maps-data') 163 + await new Promise<void>((resolve, reject) => { 164 + const req = indexedDB.deleteDatabase('maps-offline') 165 + req.onsuccess = () => resolve() 166 + req.onerror = () => reject(req.error) 167 + req.onblocked = () => 168 + reject( 169 + new Error( 170 + 'Database deletion blocked — close other tabs and try again', 171 + ), 172 + ) 173 + }) 174 + return { success: true } 175 + } catch (error) { 176 + return { 177 + success: false, 178 + error: error instanceof Error ? error.message : 'Delete failed', 179 + } 180 + } 181 + } 182 + 160 183 dispose(): void { 161 184 this.store.dispose() 162 185 }
+9 -2
www/models/schema/v0.ts
··· 96 96 }) 97 97 export type LastView = z.infer<typeof LastView> 98 98 99 + const now = new Date().toISOString() 100 + 99 101 export const StoreState = z.object({ 100 102 version: z.optional(z.string()), 101 103 searchHistory: z._default(z.array(SearchHistoryEntry), []), 102 104 bookmarks: z._default(z.array(Bookmark), []), 103 - bookmarkCollections: z._default(z.array(BookmarkCollection), []), 104 - geocodingBookmarksEnabled: z._default(z.boolean(), false), 105 + bookmarkCollections: z._default(z.array(BookmarkCollection), [{ 106 + id: 'favorites', 107 + name: 'Favorites', 108 + createdAt: now, 109 + updatedAt: now, 110 + }]), 111 + geocodingBookmarksEnabled: z._default(z.boolean(), true), 105 112 onlineSearchEnabled: z._default(z.boolean(), true), 106 113 lastView: z._default(z.nullable(LastView), null), 107 114 })
+99 -16
www/routes/settings.ts
··· 7 7 } 8 8 9 9 #onAppUpdate = () => this.requestUpdate() 10 + #showDeleteConfirm = false 10 11 11 12 override connectedCallback() { 12 13 super.connectedCallback() ··· 47 48 </section> 48 49 49 50 <section> 50 - <h2>Data</h2> 51 - <p>Export your data to a file, or import a previously exported file.</p> 52 - <div class="settings-data-actions"> 53 - <button class="action" id="settings-export" @click="${this 54 - .#handleExport}"> 55 - Export 56 - </button> 57 - <button class="action" id="settings-import" @click="${this 58 - .#handleImport}"> 59 - Import 60 - </button> 61 - </div> 62 - <p id="settings-data-status" class="settings-data-status" hidden></p> 63 - </section> 64 - 65 - <section> 66 51 <h2>Privacy</h2> 67 52 <label class="settings-toggle-label"> 68 53 <div> ··· 95 80 </section> 96 81 97 82 <section> 83 + <h2>Data</h2> 84 + <p>Export your data to a file, or import a previously exported file.</p> 85 + <div class="settings-data-actions"> 86 + <button class="action" id="settings-export" @click="${this 87 + .#handleExport}"> 88 + Export 89 + </button> 90 + <button class="action" id="settings-import" @click="${this 91 + .#handleImport}"> 92 + Import 93 + </button> 94 + </div> 95 + 96 + <button class="action action--danger" @click="${this 97 + .#handleDeleteAllData}"> 98 + Delete All Data 99 + </button> 100 + <p id="settings-data-status" class="settings-data-status" hidden></p> 101 + </section> 102 + 103 + <section> 98 104 <h2>Info</h2> 99 105 <nav class="settings-nav-list"> 100 106 <a class="settings-nav-link" href="#!/settings/about"> ··· 111 117 </a> 112 118 </nav> 113 119 </section> 120 + 121 + <ui-dialog 122 + ?open="${this.#showDeleteConfirm}" 123 + @dismiss="${this.#handleDeleteCancel}" 124 + > 125 + <dialog> 126 + <article class="bm-dialog"> 127 + <h2 class="bm-dialog-title">Delete All Data</h2> 128 + <p> 129 + This will permanently delete all your bookmarks, search history, offline 130 + maps, and preferences. This cannot be undone. 131 + </p> 132 + <p>Type <strong>Delete my data</strong> to confirm:</p> 133 + <input 134 + id="settings-delete-confirm-input" 135 + type="text" 136 + placeholder="Delete my data" 137 + autocomplete="off" 138 + > 139 + <p id="settings-delete-status" class="settings-data-status" hidden></p> 140 + <div class="bm-form-actions"> 141 + <button 142 + id="settings-delete-confirm-btn" 143 + class="bm-btn-danger" 144 + @click="${this.#handleDeleteConfirm}" 145 + > 146 + Delete 147 + </button> 148 + <button type="button" @click="${this.#handleDeleteCancel}"> 149 + Cancel 150 + </button> 151 + </div> 152 + </article> 153 + </dialog> 154 + </ui-dialog> 114 155 ` 115 156 } 116 157 ··· 141 182 this.#setStatus('Data exported successfully.') 142 183 } else { 143 184 this.#setStatus(result.error ?? 'Export failed.', true) 185 + } 186 + } 187 + 188 + #handleDeleteAllData(): void { 189 + this.#showDeleteConfirm = true 190 + this.requestUpdate() 191 + } 192 + 193 + #handleDeleteCancel = (): void => { 194 + this.#showDeleteConfirm = false 195 + this.requestUpdate() 196 + } 197 + 198 + #setDeleteStatus(msg: string): void { 199 + const el = this.querySelector<HTMLElement>('#settings-delete-status') 200 + if (!el) return 201 + el.textContent = msg 202 + el.hidden = false 203 + el.className = 'settings-data-status settings-data-status--error' 204 + } 205 + 206 + async #handleDeleteConfirm(): Promise<void> { 207 + const input = this.querySelector<HTMLInputElement>( 208 + '#settings-delete-confirm-input', 209 + ) 210 + if (input?.value !== 'Delete my data') { 211 + this.#setDeleteStatus('Please type "Delete my data" exactly to confirm.') 212 + return 213 + } 214 + const btn = this.querySelector<HTMLButtonElement>( 215 + '#settings-delete-confirm-btn', 216 + ) 217 + if (btn) btn.disabled = true 218 + const result = await app.deleteAllData() 219 + if (result.success) { 220 + this.#showDeleteConfirm = false 221 + this.requestUpdate() 222 + this.#setStatus('All data deleted.') 223 + globalThis.location.reload() 224 + } else { 225 + if (btn) btn.disabled = false 226 + this.#setDeleteStatus(result.error ?? 'Delete failed.') 144 227 } 145 228 } 146 229
+6
www/static/styles/theme.css
··· 225 225 transform: none; 226 226 } 227 227 228 + .action--danger { 229 + color: var(--error); 230 + border-color: var(--error); 231 + } 232 + 228 233 .settings-data-actions { 229 234 display: flex; 230 235 gap: var(--s3); 236 + margin-bottom: var(--s3); 231 237 } 232 238 233 239 .settings-data-status {
+9 -1
www/worker.ts
··· 1 1 import { 2 2 init, 3 + withBackgroundPrecache, 3 4 withCleanup, 4 5 withFetchStrategy, 5 6 withPrecache, ··· 9 10 init([ 10 11 withPrecache([ 11 12 '/', 13 + 'https://bpev.me/civility.min.css', 14 + '/dist/icons/128x128.png', 15 + '/dist/icons/192x192.png', 16 + '/dist/icons/256x256.png', 17 + '/dist/icons/512x512.png', 18 + '/dist/icons/icon.png', 19 + '/dist/icons/icon.ico', 12 20 '/index.html', 13 21 '/static/styles/maplibre-gl.css', 14 - '/static/styles/civility.min.css', 15 22 '/static/styles/theme.css', 16 23 '/static/icons/home.svg', 17 24 '/static/icons/navigation.svg', ··· 23 30 '/dist/index.js', 24 31 '/manifest.json', 25 32 ]), 33 + withBackgroundPrecache(), 26 34 withCleanup(), 27 35 withUpdatePolling(), 28 36 withFetchStrategy(),