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.
}
}
|