Skip to content

Integrating Favorites in Frontend

This guide shows how to integrate the favorites API into your frontend application using JavaScript.

Store favorites data using these standardized keys:

const FAVORITES_KEY = 'idx_favorites';
const AUTH_TOKEN_KEY = 'idx_auth_token';
const AUTH_EMAIL_KEY = 'idx_auth_email';
function getFavorites() {
const stored = localStorage.getItem(FAVORITES_KEY);
return stored ? JSON.parse(stored) : [];
}
function isAuthenticated() {
return !!localStorage.getItem(AUTH_TOKEN_KEY);
}
function saveFavorites(favorites) {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
// Dispatch event for UI updates
window.dispatchEvent(new CustomEvent('favoritesChanged', {
detail: favorites
}));
}

After user authenticates, sync favorites to the server without blocking UI:

async function syncToServer(favorites) {
const token = localStorage.getItem(AUTH_TOKEN_KEY);
if (!token) return; // Not authenticated yet
try {
const response = await fetch('/api/v1/favorites/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ favorites }),
});
if (!response.ok) {
console.error('Favorites sync failed:', response.status);
return;
}
const data = await response.json();
// Server may return additional favorites from other devices
saveFavorites(data.favorites);
} catch (err) {
console.error('Favorites sync error:', err);
}
}
async function toggleFavorite(listingId) {
let favorites = getFavorites();
const index = favorites.indexOf(listingId);
if (index > -1) {
// Remove from favorites
favorites.splice(index, 1);
saveFavorites(favorites);
// Sync deletion to server if authenticated
const token = localStorage.getItem(AUTH_TOKEN_KEY);
if (token) {
await fetch(`/api/v1/favorites/${listingId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
},
});
}
} else {
// Add to favorites
favorites.push(listingId);
saveFavorites(favorites);
// Sync addition to server if authenticated
syncToServer(favorites);
}
}
async function requestMagicLink(email) {
const response = await fetch('/api/v1/favorites/magic-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Session-ID': getSessionId(), // Optional session tracking
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to send magic link');
}
return response.json();
}

Called when user clicks email link:

async function verifyMagicLink(token) {
const response = await fetch(`/api/v1/favorites/verify/${token}`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Magic link verification failed');
}
const data = await response.json();
// Store auth token
localStorage.setItem(AUTH_TOKEN_KEY, data.auth_token);
localStorage.setItem(AUTH_EMAIL_KEY, data.email);
// Merge server favorites with local favorites
const localFavorites = getFavorites();
const mergedFavorites = [...new Set([...localFavorites, ...data.favorites])];
saveFavorites(mergedFavorites);
// Sync merged set back to server
await syncToServer(mergedFavorites);
// Dispatch auth change event
window.dispatchEvent(new CustomEvent('authChanged', {
detail: { authenticated: true, email: data.email }
}));
return data;
}

Listen for favorites changes across your application:

// Listen for favorites changes
window.addEventListener('favoritesChanged', (event) => {
const favorites = event.detail; // Array of listing IDs
updateUI(favorites);
});
// Listen for auth changes
window.addEventListener('authChanged', (event) => {
const { authenticated, email } = event.detail;
if (authenticated) {
showWelcomeMessage(email);
}
});

Create a reusable hook for favorites management:

import { useState, useEffect } from 'react';
function useFavorites() {
const [favorites, setFavorites] = useState<string[]>([]);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
// Load initial state
setFavorites(getFavorites());
setIsAuthenticated(!!localStorage.getItem(AUTH_TOKEN_KEY));
// Listen for changes
const handleFavoritesChange = (e: CustomEvent) => {
setFavorites(e.detail);
};
const handleAuthChange = (e: CustomEvent) => {
setIsAuthenticated(e.detail.authenticated);
};
window.addEventListener('favoritesChanged', handleFavoritesChange);
window.addEventListener('authChanged', handleAuthChange);
return () => {
window.removeEventListener('favoritesChanged', handleFavoritesChange);
window.removeEventListener('authChanged', handleAuthChange);
};
}, []);
const toggleFavorite = async (listingId: string) => {
await toggleFavorite(listingId); // Uses global function
};
const isFavorite = (listingId: string) => {
return favorites.includes(listingId);
};
return {
favorites,
isAuthenticated,
toggleFavorite,
isFavorite,
};
}
export default useFavorites;
function PropertyCard({ listingId }) {
const { isFavorite, toggleFavorite } = useFavorites();
return (
<div>
<button onClick={() => toggleFavorite(listingId)}>
{isFavorite(listingId) ? '' : ''}
</button>
</div>
);
}
async function syncToServer(favorites) {
const token = localStorage.getItem(AUTH_TOKEN_KEY);
if (!token) return;
try {
const response = await fetch('/api/v1/favorites/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ favorites }),
});
if (response.status === 401) {
// Token expired - clear auth and prompt re-authentication
localStorage.removeItem(AUTH_TOKEN_KEY);
localStorage.removeItem(AUTH_EMAIL_KEY);
window.dispatchEvent(new CustomEvent('authChanged', {
detail: { authenticated: false }
}));
// Optionally prompt user to re-authenticate
showReauthenticationPrompt();
return;
}
if (!response.ok) {
throw new Error(`Sync failed: ${response.status}`);
}
const data = await response.json();
saveFavorites(data.favorites);
} catch (err) {
console.error('Favorites sync error:', err);
}
}

For analytics and session linking:

function getSessionId() {
let sessionId = sessionStorage.getItem('session_id');
if (!sessionId) {
sessionId = crypto.randomUUID();
sessionStorage.setItem('session_id', sessionId);
}
return sessionId;
}

Pass session ID when requesting magic link to link anonymous browsing to authenticated account.