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

View File

@@ -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<FindState[]>([]);
// 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);
}
}
}
</script>
<svelte:head>
@@ -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);

View File

@@ -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('/');