feat:use api-sync layer to sync local updates state with db

This commit is contained in:
2025-12-01 13:25:27 +01:00
parent f8acec9a79
commit b060f53589
6 changed files with 179 additions and 110 deletions

View File

@@ -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 } }));

View File

@@ -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);

View File

@@ -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?.();
}
</script>
<section class="finds-feed">
@@ -89,8 +79,6 @@
isLiked={find.isLiked}
{currentUserId}
onExplore={handleFindExplore}
onDeleted={handleFindDeleted}
onUpdated={handleFindUpdated}
onEdit={() => onEdit?.(find)}
/>
{/each}

View File

@@ -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<void> {
// Optimistically update the find state
const currentState = this.getEntityState<FindState>('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<void> {
// 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<FindState[]> {
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