Skip to content

Commit 32aca2e

Browse files
feat(map): WIP GeoJSON overlay, cross-project map mgmt, download history (#129)
In-progress work preserved as draft PR. Includes GeoJSONOverlay component, useAllMaps hook, DownloadHistory screen, and MapScreen authoring/mgmt wiring. Not yet CI-green or review-complete. Refs #129
1 parent 3fdb8bb commit 32aca2e

11 files changed

Lines changed: 979 additions & 35 deletions

File tree

src/hooks/useAllMaps.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
3+
import type { Project, SavedMap } from '@/lib/db';
4+
import { getDb } from '@/lib/db';
5+
6+
export const ALL_MAPS_QUERY_KEY = ['maps', 'all'] as const;
7+
8+
export interface SavedMapWithProject extends SavedMap {
9+
/** Origin project name, or `null` if the project no longer exists. */
10+
originProjectName: string | null;
11+
}
12+
13+
/**
14+
* Query every saved map across all projects, sorted by `updatedAt` desc.
15+
* Each map is joined with its origin project name for cross-project display.
16+
*
17+
* Used by the "All projects" view in MapScreen (#16).
18+
*/
19+
export function useAllMaps() {
20+
return useQuery({
21+
queryKey: ALL_MAPS_QUERY_KEY,
22+
queryFn: async (): Promise<SavedMapWithProject[]> => {
23+
const db = getDb();
24+
const [maps, projects] = await Promise.all([
25+
db.maps.toArray(),
26+
db.projects.toArray(),
27+
]);
28+
const projectMap = new Map<string, Project>();
29+
for (const project of projects) {
30+
projectMap.set(project.localId, project);
31+
}
32+
const enriched = maps.map((map) => ({
33+
...map,
34+
originProjectName: projectMap.get(map.projectLocalId)?.name ?? null,
35+
}));
36+
return enriched.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
37+
},
38+
});
39+
}

src/hooks/useMaps.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
22

3+
import { ALL_MAPS_QUERY_KEY } from '@/hooks/useAllMaps';
34
import type { SavedMap } from '@/lib/db';
45
import { getDb } from '@/lib/db';
56
import type { DownloadProgress } from '@/lib/map/smp-download';
@@ -39,6 +40,7 @@ export function useCreateMap() {
3940
void queryClient.invalidateQueries({
4041
queryKey: mapsQueryKey(map.projectLocalId),
4142
});
43+
void queryClient.invalidateQueries({ queryKey: ALL_MAPS_QUERY_KEY });
4244
},
4345
});
4446
}
@@ -56,6 +58,7 @@ export function useRenameMap(projectLocalId: string | null) {
5658
queryKey: mapsQueryKey(projectLocalId),
5759
});
5860
void queryClient.invalidateQueries({ queryKey: ['map', mapId] });
61+
void queryClient.invalidateQueries({ queryKey: ALL_MAPS_QUERY_KEY });
5962
},
6063
});
6164
}
@@ -92,6 +95,7 @@ export function useDeleteMap(projectLocalId: string | null) {
9295
});
9396
void queryClient.invalidateQueries({ queryKey: ['map', mapId] });
9497
void queryClient.invalidateQueries({ queryKey: ['projects'] });
98+
void queryClient.invalidateQueries({ queryKey: ALL_MAPS_QUERY_KEY });
9599
},
96100
});
97101
}
@@ -143,11 +147,13 @@ export function useDownloadMap() {
143147
queryKey: mapsQueryKey(map.projectLocalId),
144148
});
145149
void queryClient.invalidateQueries({ queryKey: ['map', map.id] });
150+
void queryClient.invalidateQueries({ queryKey: ALL_MAPS_QUERY_KEY });
146151
},
147152
onError: (_error, { map }) => {
148153
void queryClient.invalidateQueries({
149154
queryKey: mapsQueryKey(map.projectLocalId),
150155
});
156+
void queryClient.invalidateQueries({ queryKey: ALL_MAPS_QUERY_KEY });
151157
},
152158
});
153159
}

src/i18n/messages/en.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,27 @@
10941094
"map.download.unknownError": {
10951095
"defaultMessage": "Unknown error"
10961096
},
1097+
"map.history.downloaded": {
1098+
"defaultMessage": "Downloaded"
1099+
},
1100+
"map.history.empty": {
1101+
"defaultMessage": "No downloads yet"
1102+
},
1103+
"map.history.retry": {
1104+
"defaultMessage": "Retry"
1105+
},
1106+
"map.history.size": {
1107+
"defaultMessage": "Size: {size}"
1108+
},
1109+
"map.history.statusError": {
1110+
"defaultMessage": "Failed"
1111+
},
1112+
"map.history.statusReady": {
1113+
"defaultMessage": "Ready"
1114+
},
1115+
"map.history.title": {
1116+
"defaultMessage": "Download history"
1117+
},
10971118
"map.nameDialog.cancel": {
10981119
"defaultMessage": "Cancel"
10991120
},
@@ -1118,6 +1139,36 @@
11181139
"map.nameDialog.title": {
11191140
"defaultMessage": "Save map"
11201141
},
1142+
"map.overlay.clear": {
1143+
"defaultMessage": "Clear all overlays"
1144+
},
1145+
"map.overlay.drop": {
1146+
"defaultMessage": "Drop a .geojson file to show it as a reference layer"
1147+
},
1148+
"map.overlay.dropActive": {
1149+
"defaultMessage": "Drop the file to add the overlay"
1150+
},
1151+
"map.overlay.empty": {
1152+
"defaultMessage": "No reference layers added"
1153+
},
1154+
"map.overlay.hidden": {
1155+
"defaultMessage": "Hidden"
1156+
},
1157+
"map.overlay.invalid": {
1158+
"defaultMessage": "Could not read “{name}”. Not valid GeoJSON."
1159+
},
1160+
"map.overlay.remove": {
1161+
"defaultMessage": "Remove overlay"
1162+
},
1163+
"map.overlay.title": {
1164+
"defaultMessage": "Reference layers"
1165+
},
1166+
"map.overlay.tooLarge": {
1167+
"defaultMessage": "“{name}” is {size}. Large files may slow down the editor."
1168+
},
1169+
"map.overlay.toggle": {
1170+
"defaultMessage": "Toggle overlay visibility"
1171+
},
11211172
"map.saved.activeError": {
11221173
"defaultMessage": "Could not update active map. Please try again."
11231174
},
@@ -1151,6 +1202,9 @@
11511202
"map.saved.noProjectLink": {
11521203
"defaultMessage": "Go to Home"
11531204
},
1205+
"map.saved.originProject": {
1206+
"defaultMessage": "Origin project"
1207+
},
11541208
"map.saved.removeActive": {
11551209
"defaultMessage": "Remove active"
11561210
},
@@ -1196,6 +1250,12 @@
11961250
"map.saved.untitledProject": {
11971251
"defaultMessage": "Untitled Project"
11981252
},
1253+
"map.scope.allProjects": {
1254+
"defaultMessage": "All projects"
1255+
},
1256+
"map.scope.thisProject": {
1257+
"defaultMessage": "This project"
1258+
},
11991259
"map.stylePicker.customMode": {
12001260
"defaultMessage": "Custom URL"
12011261
},

src/i18n/messages/es.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,13 @@
330330
"map.download.starting": "Iniciando descarga…",
331331
"map.download.storageWarning": "Espacio de almacenamiento insuficiente. {available} disponibles, {estimated} estimados.",
332332
"map.download.tryAnyway": "Intentar de todos modos",
333+
"map.history.downloaded": "Descargado",
334+
"map.history.empty": "Aún no hay descargas",
335+
"map.history.retry": "Reintentar",
336+
"map.history.size": "Tamaño: {size}",
337+
"map.history.statusError": "Fallido",
338+
"map.history.statusReady": "Listo",
339+
"map.history.title": "Historial de descargas",
333340
"map.nameDialog.cancel": "Cancelar",
334341
"map.nameDialog.description": "Guarde esta configuración de mapa como borrador. Luego puede descargarlo como un archivo .smp.",
335342
"map.nameDialog.nameLabel": "Nombre del mapa",
@@ -338,6 +345,16 @@
338345
"map.nameDialog.saveDraft": "Guardar borrador",
339346
"map.nameDialog.saveError": "No se pudo guardar el mapa. Inténtelo de nuevo.",
340347
"map.nameDialog.title": "Guardar mapa",
348+
"map.overlay.clear": "Limpiar todas las capas",
349+
"map.overlay.drop": "Suelte un archivo .geojson para mostrarlo como capa de referencia",
350+
"map.overlay.dropActive": "Suelte el archivo para añadir la capa",
351+
"map.overlay.empty": "No hay capas de referencia añadidas",
352+
"map.overlay.hidden": "Oculto",
353+
"map.overlay.invalid": "No se pudo leer “{name}”. GeoJSON no válido.",
354+
"map.overlay.remove": "Quitar capa",
355+
"map.overlay.title": "Capas de referencia",
356+
"map.overlay.tooLarge": "“{name}” es {size}. Los archivos grandes pueden ralentizar el editor.",
357+
"map.overlay.toggle": "Alternar visibilidad de la capa",
341358
"map.saved.activeError": "No se pudo actualizar el mapa activo. Inténtelo de nuevo.",
342359
"map.saved.canvasAria": "Lienzo de autoría de mapa",
343360
"map.saved.closeSettings": "Cerrar configuración del mapa",
@@ -349,6 +366,7 @@
349366
"map.saved.empty": "Aún no hay mapas guardados",
350367
"map.saved.noProject": "Seleccione un proyecto desde Inicio para crear mapas",
351368
"map.saved.noProjectLink": "Ir a Inicio",
369+
"map.saved.originProject": "Proyecto de origen",
352370
"map.saved.removeActive": "Quitar activo",
353371
"map.saved.rename": "Renombrar",
354372
"map.saved.renameDialog.description": "Ingrese un nuevo nombre para este mapa guardado.",
@@ -364,6 +382,8 @@
364382
"map.saved.statusReady": "Listo",
365383
"map.saved.title": "Mapas guardados",
366384
"map.saved.untitledProject": "Proyecto sin título",
385+
"map.scope.allProjects": "Todos los proyectos",
386+
"map.scope.thisProject": "Este proyecto",
367387
"map.stylePicker.customMode": "URL personalizada",
368388
"map.stylePicker.customUrlHostnameWarning": "Este proveedor de teselas puede no estar disponible para descargas offline. Solo los proveedores compatibles funcionan con la generación de paquetes de mapas.",
369389
"map.stylePicker.customUrlLabel": "URL personalizada",

src/i18n/messages/pt.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,13 @@
330330
"map.download.starting": "Iniciando download…",
331331
"map.download.storageWarning": "Espaço de armazenamento insuficiente. {available} disponível, {estimated} estimado.",
332332
"map.download.tryAnyway": "Tentar mesmo assim",
333+
"map.history.downloaded": "Baixado",
334+
"map.history.empty": "Ainda não há downloads",
335+
"map.history.retry": "Tentar novamente",
336+
"map.history.size": "Tamanho: {size}",
337+
"map.history.statusError": "Falhou",
338+
"map.history.statusReady": "Pronto",
339+
"map.history.title": "Histórico de downloads",
333340
"map.nameDialog.cancel": "Cancelar",
334341
"map.nameDialog.description": "Salve esta configuração de mapa como rascunho. Depois você pode baixá-lo como um arquivo .smp.",
335342
"map.nameDialog.nameLabel": "Nome do mapa",
@@ -338,6 +345,16 @@
338345
"map.nameDialog.saveDraft": "Salvar rascunho",
339346
"map.nameDialog.saveError": "Não foi possível salvar o mapa. Tente novamente.",
340347
"map.nameDialog.title": "Salvar mapa",
348+
"map.overlay.clear": "Limpar todas as camadas",
349+
"map.overlay.drop": "Solte um arquivo .geojson para mostrá-lo como camada de referência",
350+
"map.overlay.dropActive": "Solte o arquivo para adicionar a camada",
351+
"map.overlay.empty": "Nenhuma camada de referência adicionada",
352+
"map.overlay.hidden": "Oculto",
353+
"map.overlay.invalid": "Não foi possível ler “{name}”. GeoJSON inválido.",
354+
"map.overlay.remove": "Remover camada",
355+
"map.overlay.title": "Camadas de referência",
356+
"map.overlay.tooLarge": "“{name}” tem {size}. Arquivos grandes podem lentear o editor.",
357+
"map.overlay.toggle": "Alternar visibilidade da camada",
341358
"map.saved.activeError": "Não foi possível atualizar o mapa ativo. Tente novamente.",
342359
"map.saved.canvasAria": "Tela de autoria de mapa",
343360
"map.saved.closeSettings": "Fechar configurações do mapa",
@@ -349,6 +366,7 @@
349366
"map.saved.empty": "Ainda não há mapas salvos",
350367
"map.saved.noProject": "Selecione um projeto no Início para criar mapas",
351368
"map.saved.noProjectLink": "Ir para o Início",
369+
"map.saved.originProject": "Projeto de origem",
352370
"map.saved.removeActive": "Remover ativo",
353371
"map.saved.rename": "Renomear",
354372
"map.saved.renameDialog.description": "Insira um novo nome para este mapa salvo.",
@@ -364,6 +382,8 @@
364382
"map.saved.statusReady": "Pronto",
365383
"map.saved.title": "Mapas salvos",
366384
"map.saved.untitledProject": "Projeto sem título",
385+
"map.scope.allProjects": "Todos os projetos",
386+
"map.scope.thisProject": "Este projeto",
367387
"map.stylePicker.customMode": "URL personalizada",
368388
"map.stylePicker.customUrlHostnameWarning": "Este provedor de mosaicos pode não estar disponível para downloads offline. Somente provedores compatíveis funcionam com a geração de pacotes de mapas.",
369389
"map.stylePicker.customUrlLabel": "URL personalizada",

0 commit comments

Comments
 (0)