feat:use api-sync layer to sync local updates state with db
This commit is contained in:
@@ -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 } }));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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('/');
|
||||
|
||||
Reference in New Issue
Block a user