Quick fixes to lang plus mmap realtime

This commit is contained in:
AlacrisDevs
2026-02-08 23:30:09 +02:00
parent f2384bceb8
commit ce80dc6d75
5 changed files with 438 additions and 242 deletions

View File

@@ -250,3 +250,50 @@ export async function deleteMapShape(supabase: SupabaseClient, shapeId: string):
if (error) throw error;
}
// ── Realtime ──
export interface RealtimeMapPayload<T> {
event: 'INSERT' | 'UPDATE' | 'DELETE';
new: T;
old: Partial<T>;
}
export function subscribeToMapLayers(
supabase: SupabaseClient,
layerIds: string[],
onPinChange: (payload: RealtimeMapPayload<MapPin>) => void,
onShapeChange: (payload: RealtimeMapPayload<MapShape>) => void,
) {
const layerIdSet = new Set(layerIds);
const channel = supabase.channel(`map:${layerIds.join(',')}`);
channel
.on('postgres_changes', { event: '*', schema: 'public', table: 'map_pins' },
(payload) => {
const pin = (payload.new ?? payload.old) as Partial<MapPin>;
const lid = pin.layer_id ?? (payload.old as Partial<MapPin>)?.layer_id;
if (lid && !layerIdSet.has(lid)) return;
onPinChange({
event: payload.eventType as 'INSERT' | 'UPDATE' | 'DELETE',
new: payload.new as MapPin,
old: payload.old as Partial<MapPin>,
});
}
)
.on('postgres_changes', { event: '*', schema: 'public', table: 'map_shapes' },
(payload) => {
const shape = (payload.new ?? payload.old) as Partial<MapShape>;
const lid = shape.layer_id ?? (payload.old as Partial<MapShape>)?.layer_id;
if (lid && !layerIdSet.has(lid)) return;
onShapeChange({
event: payload.eventType as 'INSERT' | 'UPDATE' | 'DELETE',
new: payload.new as MapShape,
old: payload.old as Partial<MapShape>,
});
}
)
.subscribe();
return channel;
}

View File

@@ -6,11 +6,13 @@
import * as m from "$lib/paraglide/messages";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
import type { RealtimeChannel } from "@supabase/supabase-js";
import {
type MapLayerWithPins,
type MapLayer,
type MapPin as MapPinType,
type MapShape,
type RealtimeMapPayload,
createMapLayer,
updateMapLayer,
deleteMapLayer,
@@ -20,6 +22,7 @@
createMapShape,
updateMapShape,
deleteMapShape,
subscribeToMapLayers,
} from "$lib/api/map";
let L: any;
@@ -51,6 +54,10 @@
let leafletShapes = new Map<string, any>();
let leafletBoundsRects = new Map<string, any>();
// Realtime
let realtimeChannel: RealtimeChannel | null = null;
const optimisticIds = new Set<string>();
// svelte-ignore state_referenced_locally
let layers = $state<MapLayerWithPins[]>(initialLayers);
let activeLayerIdx = $state(0);
@@ -415,6 +422,7 @@
(s) => s.id === shapeId,
);
if (shape) {
optimisticIds.add(shapeId);
try {
await updateMapShape(supabase, shapeId, {
vertices: shape.vertices,
@@ -461,6 +469,7 @@
vertices,
sort_order: activeLayer.shapes?.length ?? 0,
});
optimisticIds.add(shape.id);
layers = layers.map((l, i) =>
i === activeLayerIdx
? { ...l, shapes: [...(l.shapes ?? []), shape] }
@@ -485,6 +494,7 @@
vertices: verts,
sort_order: activeLayer.shapes?.length ?? 0,
});
optimisticIds.add(shape.id);
layers = layers.map((l, i) =>
i === activeLayerIdx
? { ...l, shapes: [...(l.shapes ?? []), shape] }
@@ -643,6 +653,7 @@
);
showPinModal = false;
syncAllObjects(layers[activeLayerIdx]);
optimisticIds.add(editingPin.id);
try {
await updateMapPin(supabase, editingPin.id, updated);
} catch {
@@ -658,6 +669,7 @@
lng: pendingLatLng.lng,
sort_order: activeLayer.pins.length,
});
optimisticIds.add(pin.id);
layers = layers.map((l, i) =>
i === activeLayerIdx ? { ...l, pins: [...l.pins, pin] } : l,
);
@@ -687,6 +699,7 @@
);
if (selectedObjectId === pinId) deselectAll();
syncAllObjects(layers[activeLayerIdx]);
optimisticIds.add(pinId);
try {
await deleteMapPin(supabase, pinId);
} catch {
@@ -720,6 +733,7 @@
);
showShapeModal = false;
syncAllObjects(layers[activeLayerIdx]);
optimisticIds.add(editingShape.id);
try {
await updateMapShape(supabase, editingShape.id, updated);
} catch {
@@ -740,6 +754,7 @@
);
if (selectedObjectId === shapeId) deselectAll();
syncAllObjects(layers[activeLayerIdx]);
optimisticIds.add(shapeId);
try {
await deleteMapShape(supabase, shapeId);
} catch {
@@ -774,6 +789,7 @@
rotation: original.rotation,
sort_order: activeLayer.shapes?.length ?? 0,
});
optimisticIds.add(shape.id);
layers = layers.map((l, i) =>
i === activeLayerIdx
? { ...l, shapes: [...(l.shapes ?? []), shape] }
@@ -815,6 +831,7 @@
layers = [...layers, withData];
activeLayerIdx = layers.length - 1;
showLayerOnMap(withData);
setupRealtime();
} catch {
toasts.error("Failed to create layer");
}
@@ -827,6 +844,7 @@
if (activeLayerIdx >= layers.length)
activeLayerIdx = Math.max(0, layers.length - 1);
if (layers.length > 0) showLayerOnMap(layers[activeLayerIdx]);
setupRealtime();
try {
await deleteMapLayer(supabase, layerId);
} catch {
@@ -979,6 +997,120 @@
}
}
// ── Realtime handlers ──
function handlePinRealtime(payload: RealtimeMapPayload<MapPinType>) {
const id = payload.new?.id ?? payload.old?.id;
if (!id) return;
if (optimisticIds.has(id)) {
optimisticIds.delete(id);
return;
}
const layerId = payload.new?.layer_id ?? payload.old?.layer_id;
if (!layerId) return;
if (payload.event === "INSERT") {
layers = layers.map((l) =>
l.id === layerId
? {
...l,
pins: [
...l.pins.filter((p) => p.id !== id),
payload.new,
],
}
: l,
);
} else if (payload.event === "UPDATE") {
layers = layers.map((l) =>
l.id === layerId
? {
...l,
pins: l.pins.map((p) =>
p.id === id ? payload.new : p,
),
}
: l,
);
} else if (payload.event === "DELETE") {
layers = layers.map((l) =>
l.id === layerId
? { ...l, pins: l.pins.filter((p) => p.id !== id) }
: l,
);
if (selectedObjectId === id) deselectAll();
}
const layerIdx = layers.findIndex((l) => l.id === layerId);
if (layerIdx === activeLayerIdx) syncAllObjects(layers[layerIdx]);
}
function handleShapeRealtime(payload: RealtimeMapPayload<MapShape>) {
const id = payload.new?.id ?? payload.old?.id;
if (!id) return;
if (optimisticIds.has(id)) {
optimisticIds.delete(id);
return;
}
const layerId = payload.new?.layer_id ?? payload.old?.layer_id;
if (!layerId) return;
if (payload.event === "INSERT") {
layers = layers.map((l) =>
l.id === layerId
? {
...l,
shapes: [
...(l.shapes ?? []).filter((s) => s.id !== id),
payload.new,
],
}
: l,
);
} else if (payload.event === "UPDATE") {
layers = layers.map((l) =>
l.id === layerId
? {
...l,
shapes: (l.shapes ?? []).map((s) =>
s.id === id ? payload.new : s,
),
}
: l,
);
} else if (payload.event === "DELETE") {
layers = layers.map((l) =>
l.id === layerId
? {
...l,
shapes: (l.shapes ?? []).filter((s) => s.id !== id),
}
: l,
);
if (selectedObjectId === id) deselectAll();
}
const layerIdx = layers.findIndex((l) => l.id === layerId);
if (layerIdx === activeLayerIdx) syncAllObjects(layers[layerIdx]);
}
function setupRealtime() {
if (realtimeChannel) {
supabase.removeChannel(realtimeChannel);
realtimeChannel = null;
}
const layerIds = layers.map((l) => l.id).filter(Boolean);
if (layerIds.length === 0) return;
realtimeChannel = subscribeToMapLayers(
supabase,
layerIds,
handlePinRealtime,
handleShapeRealtime,
);
}
// ── Lifecycle ──
onMount(async () => {
@@ -1003,9 +1135,15 @@
} else {
showLayerOnMap(layers[activeLayerIdx]);
}
setupRealtime();
});
onDestroy(() => {
if (realtimeChannel) {
supabase.removeChannel(realtimeChannel);
realtimeChannel = null;
}
if (map) {
map.remove();
map = null;

View File

@@ -59,6 +59,7 @@
let newEventStartDate = $state("");
let newEventEndDate = $state("");
let newEventVenue = $state("");
let newEventVenueAddress = $state("");
let newEventColor = $state(data.org.default_event_color || "#00A3E0");
let creating = $state(false);
@@ -111,6 +112,7 @@
start_date: newEventStartDate || null,
end_date: newEventEndDate || null,
venue_name: newEventVenue.trim() || null,
venue_address: newEventVenueAddress.trim() || null,
color: newEventColor,
created_by: userId,
})
@@ -154,6 +156,7 @@
newEventStartDate = "";
newEventEndDate = "";
newEventVenue = "";
newEventVenueAddress = "";
newEventColor = data.org.default_event_color || "#00A3E0";
}
@@ -331,12 +334,19 @@
</div>
<!-- Venue -->
<Input
variant="compact"
label={m.events_form_venue()}
bind:value={newEventVenue}
placeholder={m.events_form_venue_placeholder()}
/>
<div class="flex flex-col gap-2">
<Input
variant="compact"
label={m.events_form_venue()}
bind:value={newEventVenue}
placeholder={m.events_form_venue_placeholder()}
/>
<Input
variant="compact"
bind:value={newEventVenueAddress}
placeholder={m.events_form_venue_address_placeholder()}
/>
</div>
<!-- Color -->
<div class="flex flex-col gap-1.5">

View File

@@ -1272,13 +1272,14 @@
</div>
{:else}
<div
class="grid {layoutConfig.cols} gap-4 {currentLayout === 'grid'
? 'auto-rows-[calc(50vh-5rem)]'
: 'h-full'}"
class="grid {layoutConfig.cols} gap-4"
style={currentLayout === "grid"
? "grid-auto-rows: minmax(320px, calc(50vh - 5rem))"
: ""}
>
{#each dashboard.panels as panel (panel.id)}
<div
class="bg-surface/50 rounded-2xl border border-light/5 flex flex-col overflow-hidden {currentLayout ===
class="bg-surface/50 rounded-2xl border border-light/5 flex flex-col overflow-hidden min-h-[320px] {currentLayout ===
'single'
? 'col-span-full'
: ''}"