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 { Button } from '$lib/components/button';
|
||||||
import POISearch from '../map/POISearch.svelte';
|
import POISearch from '../map/POISearch.svelte';
|
||||||
import type { PlaceResult } from '$lib/utils/places';
|
import type { PlaceResult } from '$lib/utils/places';
|
||||||
|
import { apiSync } from '$lib/stores/api-sync';
|
||||||
|
|
||||||
interface FindData {
|
interface FindData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -157,12 +158,7 @@
|
|||||||
...uploadedMedia
|
...uploadedMedia
|
||||||
];
|
];
|
||||||
|
|
||||||
const response = await fetch(`/api/finds/${find.id}`, {
|
await apiSync.updateFind(find.id, {
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
description: description.trim() || null,
|
description: description.trim() || null,
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
@@ -172,13 +168,8 @@
|
|||||||
isPublic,
|
isPublic,
|
||||||
media: allMedia,
|
media: allMedia,
|
||||||
mediaToDelete
|
mediaToDelete
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to update find');
|
|
||||||
}
|
|
||||||
|
|
||||||
onFindUpdated(new CustomEvent('findUpdated', { detail: { reload: true } }));
|
onFindUpdated(new CustomEvent('findUpdated', { detail: { reload: true } }));
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -198,13 +189,7 @@
|
|||||||
isSubmitting = true;
|
isSubmitting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/finds/${find.id}`, {
|
await apiSync.deleteFind(find.id);
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to delete find');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onFindDeleted) {
|
if (onFindDeleted) {
|
||||||
onFindDeleted(new CustomEvent('findDeleted', { detail: { findId: find.id } }));
|
onFindDeleted(new CustomEvent('findDeleted', { detail: { findId: find.id } }));
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import ProfilePicture from '../profile/ProfilePicture.svelte';
|
import ProfilePicture from '../profile/ProfilePicture.svelte';
|
||||||
import CommentsList from './CommentsList.svelte';
|
import CommentsList from './CommentsList.svelte';
|
||||||
import { Ellipsis, MessageCircle, Share, Edit, Trash2 } from '@lucide/svelte';
|
import { Ellipsis, MessageCircle, Share, Edit, Trash2 } from '@lucide/svelte';
|
||||||
|
import { apiSync } from '$lib/stores/api-sync';
|
||||||
|
|
||||||
interface FindCardProps {
|
interface FindCardProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -120,14 +121,7 @@
|
|||||||
|
|
||||||
isDeleting = true;
|
isDeleting = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/finds/${id}`, {
|
await apiSync.deleteFind(id);
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to delete find');
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleted?.();
|
onDeleted?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting find:', error);
|
console.error('Error deleting find:', error);
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
finds: Find[];
|
finds: Find[];
|
||||||
onFindExplore?: (id: string) => void;
|
onFindExplore?: (id: string) => void;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
onFindsChanged?: () => void;
|
|
||||||
onEdit?: (find: Find) => void;
|
onEdit?: (find: Find) => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
showEmpty?: boolean;
|
showEmpty?: boolean;
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
finds,
|
finds,
|
||||||
onFindExplore,
|
onFindExplore,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
onFindsChanged,
|
|
||||||
onEdit,
|
onEdit,
|
||||||
title = 'Finds',
|
title = 'Finds',
|
||||||
showEmpty = true,
|
showEmpty = true,
|
||||||
@@ -53,14 +51,6 @@
|
|||||||
function handleFindExplore(id: string) {
|
function handleFindExplore(id: string) {
|
||||||
onFindExplore?.(id);
|
onFindExplore?.(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFindDeleted() {
|
|
||||||
onFindsChanged?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFindUpdated() {
|
|
||||||
onFindsChanged?.();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="finds-feed">
|
<section class="finds-feed">
|
||||||
@@ -89,8 +79,6 @@
|
|||||||
isLiked={find.isLiked}
|
isLiked={find.isLiked}
|
||||||
{currentUserId}
|
{currentUserId}
|
||||||
onExplore={handleFindExplore}
|
onExplore={handleFindExplore}
|
||||||
onDeleted={handleFindDeleted}
|
|
||||||
onUpdated={handleFindUpdated}
|
|
||||||
onEdit={() => onEdit?.(find)}
|
onEdit={() => onEdit?.(find)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -408,6 +408,23 @@ class APISync {
|
|||||||
'Content-Type': 'application/json'
|
'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') {
|
} else if (entityType === 'comment' && op === 'create') {
|
||||||
// Handle comment creation
|
// Handle comment creation
|
||||||
response = await fetch(`/api/finds/${entityId}/comments`, {
|
response = await fetch(`/api/finds/${entityId}/comments`, {
|
||||||
@@ -439,6 +456,14 @@ class APISync {
|
|||||||
// Update entity state with successful result
|
// Update entity state with successful result
|
||||||
if (entityType === 'find' && action === 'like') {
|
if (entityType === 'find' && action === 'like') {
|
||||||
this.updateFindLikeState(entityId, result.isLiked, result.likeCount);
|
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') {
|
} else if (entityType === 'comment' && op === 'create') {
|
||||||
this.addCommentToState(result.data.findId, result.data);
|
this.addCommentToState(result.data.findId, result.data);
|
||||||
} else if (entityType === 'comment' && op === 'delete') {
|
} else if (entityType === 'comment' && op === 'delete') {
|
||||||
@@ -721,6 +746,103 @@ class APISync {
|
|||||||
this.subscriptions.delete(key);
|
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
|
// Create singleton instance
|
||||||
|
|||||||
@@ -102,8 +102,12 @@
|
|||||||
let currentFilter = $state('all');
|
let currentFilter = $state('all');
|
||||||
let isSidebarVisible = $state(true);
|
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
|
// Initialize API sync with server data on mount
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
if (browser && data.finds && data.finds.length > 0) {
|
if (browser && data.finds && data.finds.length > 0) {
|
||||||
const findStates: FindState[] = data.finds.map((serverFind: ServerFind) => ({
|
const findStates: FindState[] = data.finds.map((serverFind: ServerFind) => ({
|
||||||
id: serverFind.id,
|
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(
|
let allFinds = $derived(
|
||||||
(data.finds || ([] as ServerFind[])).map((serverFind: ServerFind) => ({
|
allFindsFromSync.map((findState: FindState) => ({
|
||||||
...serverFind,
|
id: findState.id,
|
||||||
createdAt: new Date(serverFind.createdAt), // Convert string to Date
|
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: {
|
user: {
|
||||||
id: serverFind.userId,
|
id: findState.userId,
|
||||||
username: serverFind.username,
|
username: findState.username,
|
||||||
profilePictureUrl: serverFind.profilePictureUrl
|
profilePictureUrl: findState.profilePictureUrl || undefined
|
||||||
},
|
},
|
||||||
likeCount: serverFind.likeCount,
|
likeCount: findState.likeCount,
|
||||||
isLiked: serverFind.isLikedByUser,
|
isLiked: findState.isLikedByUser,
|
||||||
isFromFriend: serverFind.isFromFriend,
|
isFromFriend: findState.isFromFriend,
|
||||||
media: serverFind.media?.map(
|
media: findState.media?.map((m) => ({
|
||||||
(m: {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
url: string;
|
|
||||||
thumbnailUrl: string | null;
|
|
||||||
orderIndex: number | null;
|
|
||||||
}) => ({
|
|
||||||
id: m.id,
|
id: m.id,
|
||||||
type: m.type,
|
type: m.type,
|
||||||
url: m.url,
|
url: m.url,
|
||||||
thumbnailUrl: m.thumbnailUrl || m.url,
|
thumbnailUrl: m.thumbnailUrl || m.url,
|
||||||
orderIndex: m.orderIndex
|
orderIndex: m.orderIndex
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
})) as MapFind[]
|
})) as MapFind[]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -267,44 +280,17 @@
|
|||||||
|
|
||||||
function handleFindUpdated() {
|
function handleFindUpdated() {
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
handleFindsChanged();
|
// api-sync handles the update, no manual reload needed
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFindDeleted() {
|
function handleFindDeleted() {
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
handleFindsChanged();
|
// api-sync handles the deletion, no manual reload needed
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
isSidebarVisible = !isSidebarVisible;
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -415,7 +401,6 @@
|
|||||||
}))}
|
}))}
|
||||||
onFindExplore={handleFindExplore}
|
onFindExplore={handleFindExplore}
|
||||||
currentUserId={data.user?.id}
|
currentUserId={data.user?.id}
|
||||||
onFindsChanged={handleFindsChanged}
|
|
||||||
onEdit={(find) => {
|
onEdit={(find) => {
|
||||||
const mapFind = finds.find((f) => f.id === find.id);
|
const mapFind = finds.find((f) => f.id === find.id);
|
||||||
if (mapFind) openEditModal(mapFind);
|
if (mapFind) openEditModal(mapFind);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { Button } from '$lib/components/button';
|
import { Button } from '$lib/components/button';
|
||||||
import { Edit, Trash2 } from 'lucide-svelte';
|
import { Edit, Trash2 } from 'lucide-svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { apiSync } from '$lib/stores/api-sync';
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
@@ -98,13 +99,7 @@
|
|||||||
|
|
||||||
isDeleting = true;
|
isDeleting = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/finds/${data.find.id}`, {
|
await apiSync.deleteFind(data.find.id);
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to delete find');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to home page after successful deletion
|
// Redirect to home page after successful deletion
|
||||||
goto('/');
|
goto('/');
|
||||||
|
|||||||
Reference in New Issue
Block a user