From b060f535895519cf476acbe7000a3bfeffe8f374 Mon Sep 17 00:00:00 2001 From: Zias van Nes Date: Mon, 1 Dec 2025 13:25:27 +0100 Subject: [PATCH] feat:use api-sync layer to sync local updates state with db --- src/lib/components/finds/EditFindModal.svelte | 39 ++---- src/lib/components/finds/FindCard.svelte | 10 +- src/lib/components/finds/FindsList.svelte | 12 -- src/lib/stores/api-sync.ts | 122 ++++++++++++++++++ src/routes/+page.svelte | 97 ++++++-------- src/routes/finds/[findId]/+page.svelte | 9 +- 6 files changed, 179 insertions(+), 110 deletions(-) diff --git a/src/lib/components/finds/EditFindModal.svelte b/src/lib/components/finds/EditFindModal.svelte index f272cfc..7bf4b16 100644 --- a/src/lib/components/finds/EditFindModal.svelte +++ b/src/lib/components/finds/EditFindModal.svelte @@ -4,6 +4,7 @@ import { Button } from '$lib/components/button'; import POISearch from '../map/POISearch.svelte'; import type { PlaceResult } from '$lib/utils/places'; + import { apiSync } from '$lib/stores/api-sync'; interface FindData { id: string; @@ -157,28 +158,18 @@ ...uploadedMedia ]; - const response = await fetch(`/api/finds/${find.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - title: title.trim(), - description: description.trim() || null, - latitude: lat, - longitude: lng, - locationName: locationName.trim() || null, - category, - isPublic, - media: allMedia, - mediaToDelete - }) + await apiSync.updateFind(find.id, { + title: title.trim(), + description: description.trim() || null, + latitude: lat, + longitude: lng, + locationName: locationName.trim() || null, + category, + isPublic, + media: allMedia, + mediaToDelete }); - if (!response.ok) { - throw new Error('Failed to update find'); - } - onFindUpdated(new CustomEvent('findUpdated', { detail: { reload: true } })); onClose(); } catch (error) { @@ -198,13 +189,7 @@ isSubmitting = true; try { - const response = await fetch(`/api/finds/${find.id}`, { - method: 'DELETE' - }); - - if (!response.ok) { - throw new Error('Failed to delete find'); - } + await apiSync.deleteFind(find.id); if (onFindDeleted) { onFindDeleted(new CustomEvent('findDeleted', { detail: { findId: find.id } })); diff --git a/src/lib/components/finds/FindCard.svelte b/src/lib/components/finds/FindCard.svelte index 98714c7..da22f97 100644 --- a/src/lib/components/finds/FindCard.svelte +++ b/src/lib/components/finds/FindCard.svelte @@ -13,6 +13,7 @@ import ProfilePicture from '../profile/ProfilePicture.svelte'; import CommentsList from './CommentsList.svelte'; import { Ellipsis, MessageCircle, Share, Edit, Trash2 } from '@lucide/svelte'; + import { apiSync } from '$lib/stores/api-sync'; interface FindCardProps { id: string; @@ -120,14 +121,7 @@ isDeleting = true; try { - const response = await fetch(`/api/finds/${id}`, { - method: 'DELETE' - }); - - if (!response.ok) { - throw new Error('Failed to delete find'); - } - + await apiSync.deleteFind(id); onDeleted?.(); } catch (error) { console.error('Error deleting find:', error); diff --git a/src/lib/components/finds/FindsList.svelte b/src/lib/components/finds/FindsList.svelte index f11d2a9..8f392a3 100644 --- a/src/lib/components/finds/FindsList.svelte +++ b/src/lib/components/finds/FindsList.svelte @@ -30,7 +30,6 @@ finds: Find[]; onFindExplore?: (id: string) => void; currentUserId?: string; - onFindsChanged?: () => void; onEdit?: (find: Find) => void; title?: string; showEmpty?: boolean; @@ -42,7 +41,6 @@ finds, onFindExplore, currentUserId, - onFindsChanged, onEdit, title = 'Finds', showEmpty = true, @@ -53,14 +51,6 @@ function handleFindExplore(id: string) { onFindExplore?.(id); } - - function handleFindDeleted() { - onFindsChanged?.(); - } - - function handleFindUpdated() { - onFindsChanged?.(); - }
@@ -89,8 +79,6 @@ isLiked={find.isLiked} {currentUserId} onExplore={handleFindExplore} - onDeleted={handleFindDeleted} - onUpdated={handleFindUpdated} onEdit={() => onEdit?.(find)} /> {/each} diff --git a/src/lib/stores/api-sync.ts b/src/lib/stores/api-sync.ts index f875e4a..5e15efd 100644 --- a/src/lib/stores/api-sync.ts +++ b/src/lib/stores/api-sync.ts @@ -408,6 +408,23 @@ class APISync { 'Content-Type': 'application/json' } }); + } else if (entityType === 'find' && op === 'update') { + // Handle find update + response = await fetch(`/api/finds/${entityId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + } else if (entityType === 'find' && op === 'delete') { + // Handle find deletion + response = await fetch(`/api/finds/${entityId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }); } else if (entityType === 'comment' && op === 'create') { // Handle comment creation response = await fetch(`/api/finds/${entityId}/comments`, { @@ -439,6 +456,14 @@ class APISync { // Update entity state with successful result if (entityType === 'find' && action === 'like') { this.updateFindLikeState(entityId, result.isLiked, result.likeCount); + } else if (entityType === 'find' && op === 'update') { + // Reload the find data to get the updated state + // For now, just clear loading state - the parent component handles refresh + // TODO: Ideally, we'd merge the update data into the existing state + this.setEntityLoading(entityType, entityId, false); + } else if (entityType === 'find' && op === 'delete') { + // Find already removed optimistically, just clear loading state + this.setEntityLoading(entityType, entityId, false); } else if (entityType === 'comment' && op === 'create') { this.addCommentToState(result.data.findId, result.data); } else if (entityType === 'comment' && op === 'delete') { @@ -721,6 +746,103 @@ class APISync { this.subscriptions.delete(key); } } + + /** + * Remove find from state after successful deletion + */ + private removeFindFromState(findId: string): void { + const store = this.getEntityStore('find'); + + store.update(($entities) => { + const newEntities = new Map($entities); + newEntities.delete(findId); + return newEntities; + }); + + // Also clean up associated comments + const commentStore = this.getEntityStore('comment'); + commentStore.update(($entities) => { + const newEntities = new Map($entities); + newEntities.delete(findId); + return newEntities; + }); + } + + /** + * Update a find + */ + async updateFind( + findId: string, + data: { + title?: string; + description?: string | null; + latitude?: number; + longitude?: number; + locationName?: string | null; + category?: string; + isPublic?: boolean; + media?: Array<{ type: string; url: string; thumbnailUrl?: string }>; + mediaToDelete?: string[]; + } + ): Promise { + // Optimistically update the find state + const currentState = this.getEntityState('find', findId); + if (currentState) { + const updatedFind: FindState = { + ...currentState, + ...(data.title !== undefined && { title: data.title }), + ...(data.description !== undefined && { description: data.description || undefined }), + ...(data.latitude !== undefined && { latitude: data.latitude.toString() }), + ...(data.longitude !== undefined && { longitude: data.longitude.toString() }), + ...(data.locationName !== undefined && { locationName: data.locationName || undefined }), + ...(data.category !== undefined && { category: data.category }), + ...(data.isPublic !== undefined && { isPublic: data.isPublic }), + ...(data.media !== undefined && { + media: data.media.map((m, index) => ({ + id: (m as any).id || `temp-${index}`, + findId: findId, + type: m.type, + url: m.url, + thumbnailUrl: m.thumbnailUrl || null, + orderIndex: index + })) + }) + }; + this.setEntityState('find', findId, updatedFind, false); + } + + // Queue the operation + await this.queueOperation('find', findId, 'update', undefined, data); + } + + /** + * Delete a find + */ + async deleteFind(findId: string): Promise { + // Optimistically remove find from state + this.removeFindFromState(findId); + + // Queue the operation + await this.queueOperation('find', findId, 'delete', undefined, {}); + } + + /** + * Subscribe to all finds as an array + */ + subscribeAllFinds(): Readable { + const store = this.getEntityStore('find'); + + return derived(store, ($entities) => { + const finds: FindState[] = []; + $entities.forEach((entity) => { + if (entity.data) { + finds.push(entity.data as FindState); + } + }); + // Sort by creation date, newest first + return finds.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + }); + } } // Create singleton instance diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f7c93de..b03662b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -102,8 +102,12 @@ let currentFilter = $state('all'); let isSidebarVisible = $state(true); + // Subscribe to all finds from api-sync + const allFindsStore = apiSync.subscribeAllFinds(); + let allFindsFromSync = $state([]); + // Initialize API sync with server data on mount - onMount(async () => { + onMount(() => { if (browser && data.finds && data.finds.length > 0) { const findStates: FindState[] = data.finds.map((serverFind: ServerFind) => ({ id: serverFind.id, @@ -129,34 +133,43 @@ } }); - // All finds - convert server format to component format + // Subscribe to find updates using $effect + $effect(() => { + const unsubscribe = allFindsStore.subscribe((finds) => { + allFindsFromSync = finds; + }); + + return unsubscribe; + }); + + // All finds - convert FindState to MapFind format let allFinds = $derived( - (data.finds || ([] as ServerFind[])).map((serverFind: ServerFind) => ({ - ...serverFind, - createdAt: new Date(serverFind.createdAt), // Convert string to Date + allFindsFromSync.map((findState: FindState) => ({ + id: findState.id, + title: findState.title, + description: findState.description, + latitude: findState.latitude, + longitude: findState.longitude, + locationName: findState.locationName, + category: findState.category, + isPublic: findState.isPublic ? 1 : 0, + createdAt: findState.createdAt, + userId: findState.userId, user: { - id: serverFind.userId, - username: serverFind.username, - profilePictureUrl: serverFind.profilePictureUrl + id: findState.userId, + username: findState.username, + profilePictureUrl: findState.profilePictureUrl || undefined }, - likeCount: serverFind.likeCount, - isLiked: serverFind.isLikedByUser, - isFromFriend: serverFind.isFromFriend, - media: serverFind.media?.map( - (m: { - id: string; - type: string; - url: string; - thumbnailUrl: string | null; - orderIndex: number | null; - }) => ({ - id: m.id, - type: m.type, - url: m.url, - thumbnailUrl: m.thumbnailUrl || m.url, - orderIndex: m.orderIndex - }) - ) + likeCount: findState.likeCount, + isLiked: findState.isLikedByUser, + isFromFriend: findState.isFromFriend, + media: findState.media?.map((m) => ({ + id: m.id, + type: m.type, + url: m.url, + thumbnailUrl: m.thumbnailUrl || m.url, + orderIndex: m.orderIndex + })) })) as MapFind[] ); @@ -267,44 +280,17 @@ function handleFindUpdated() { closeEditModal(); - handleFindsChanged(); + // api-sync handles the update, no manual reload needed } function handleFindDeleted() { closeEditModal(); - handleFindsChanged(); + // api-sync handles the deletion, no manual reload needed } function toggleSidebar() { isSidebarVisible = !isSidebarVisible; } - - async function handleFindsChanged() { - // Reload finds data after a find is updated or deleted - if (browser) { - try { - const params = new SvelteURLSearchParams(); - if ($coordinates) { - params.set('lat', $coordinates.latitude.toString()); - params.set('lng', $coordinates.longitude.toString()); - } - if (data.user) { - params.set('includePrivate', 'true'); - if (currentFilter === 'friends') { - params.set('includeFriends', 'true'); - } - } - - const response = await fetch(`/api/finds?${params}`); - if (response.ok) { - const updatedFinds = await response.json(); - data.finds = updatedFinds; - } - } catch (error) { - console.error('Error reloading finds:', error); - } - } - } @@ -415,7 +401,6 @@ }))} onFindExplore={handleFindExplore} currentUserId={data.user?.id} - onFindsChanged={handleFindsChanged} onEdit={(find) => { const mapFind = finds.find((f) => f.id === find.id); if (mapFind) openEditModal(mapFind); diff --git a/src/routes/finds/[findId]/+page.svelte b/src/routes/finds/[findId]/+page.svelte index 8917b44..b163c7a 100644 --- a/src/routes/finds/[findId]/+page.svelte +++ b/src/routes/finds/[findId]/+page.svelte @@ -9,6 +9,7 @@ import { Button } from '$lib/components/button'; import { Edit, Trash2 } from 'lucide-svelte'; import type { PageData } from './$types'; + import { apiSync } from '$lib/stores/api-sync'; let { data }: { data: PageData } = $props(); @@ -98,13 +99,7 @@ isDeleting = true; try { - const response = await fetch(`/api/finds/${data.find.id}`, { - method: 'DELETE' - }); - - if (!response.ok) { - throw new Error('Failed to delete find'); - } + await apiSync.deleteFind(data.find.id); // Redirect to home page after successful deletion goto('/');