Quick fixes to lang plus mmap realtime
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'
|
||||
: ''}"
|
||||
|
||||
Reference in New Issue
Block a user