All files / src/lib homeowner-local-favorites.ts

92.85% Statements 39/42
84.21% Branches 16/19
100% Functions 12/12
100% Lines 35/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109                                            1x 1x     12x       33x 33x 33x 33x 24x 24x 2x   21x   1x         11x 11x 11x                 429x       13x             6x 6x 7x             12x 205x 2x   10x   1x   10x 10x             2x 2x 3x   2x 1x       1x 1x 1x          
// apps/marketplace/src/lib/homeowner-local-favorites.ts
 
/**
 * Anonymous favorites persist to localStorage until the user signs in, then
 * get merged into their account via POST /api/homeowner/favorites/bulk.
 *
 * Schema is versioned so we can evolve it without corrupting older clients.
 */
 
export type LocalFavoriteEntityType = "pro" | "project" | "room" | "photo";
 
export interface LocalFavorite {
	entityType: LocalFavoriteEntityType;
	entityId: string;
	savedAt: number; // Unix millis
}
 
interface LocalFavoritesStore {
	version: 1;
	items: LocalFavorite[];
}
 
const STORAGE_KEY = "ho:local-favorites";
const MAX_ITEMS = 200; // Soft cap — prevents runaway localStorage if user saves hundreds before signing in.
 
function emptyStore(): LocalFavoritesStore {
	return { version: 1, items: [] };
}
 
function read(): LocalFavoritesStore {
	try {
		Iif (typeof localStorage === "undefined") return emptyStore();
		const raw = localStorage.getItem(STORAGE_KEY);
		if (!raw) return emptyStore();
		const parsed = JSON.parse(raw) as LocalFavoritesStore;
		if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.items)) {
			return emptyStore();
		}
		return parsed;
	} catch {
		return emptyStore();
	}
}
 
function write(store: LocalFavoritesStore): void {
	try {
		Iif (typeof localStorage === "undefined") return;
		localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
	} catch {
		// Quota exceeded or private mode — non-fatal. We silently drop the
		// write. The server is the source of truth once signed in; anonymous
		// saves are best-effort.
	}
}
 
function keyOf(item: { entityType: string; entityId: string }): string {
	return `${item.entityType}:${item.entityId}`;
}
 
export function listLocalFavorites(): LocalFavorite[] {
	return [...read().items];
}
 
export function isLocallyFavorited(
	entityType: LocalFavoriteEntityType,
	entityId: string,
): boolean {
	const store = read();
	const key = keyOf({ entityType, entityId });
	return store.items.some((i) => keyOf(i) === key);
}
 
export function addLocalFavorite(
	entityType: LocalFavoriteEntityType,
	entityId: string,
): void {
	const store = read();
	if (store.items.some((i) => keyOf(i) === keyOf({ entityType, entityId }))) {
		return; // Already present.
	}
	if (store.items.length >= MAX_ITEMS) {
		// Drop the oldest to stay under the cap.
		store.items.shift();
	}
	store.items.push({ entityType, entityId, savedAt: Date.now() });
	write(store);
}
 
export function removeLocalFavorite(
	entityType: LocalFavoriteEntityType,
	entityId: string,
): void {
	const store = read();
	const next = store.items.filter(
		(i) => keyOf(i) !== keyOf({ entityType, entityId }),
	);
	if (next.length === store.items.length) return;
	write({ ...store, items: next });
}
 
export function clearLocalFavorites(): void {
	try {
		Iif (typeof localStorage === "undefined") return;
		localStorage.removeItem(STORAGE_KEY);
	} catch {
		// Non-fatal.
	}
}