Skip to main content
Open Tarteel features a distributed favorites system powered by GunDB, allowing you to save preferred reciters and see real-time popularity statistics across all users.

Adding Favorites

Mark reciters as favorites for quick access:
  1. Find a reciter you enjoy
  2. Click the star icon on their card or in the player
  3. The star fills in to indicate favorite status
  4. Your favorite is saved locally and synced to the network
Favorites are indicated with a filled yellow star, while non-favorites show an outlined gray star.

Removing Favorites

To unfavorite a reciter:
  1. Click the filled star icon
  2. The star returns to outlined state
  3. The favorite is removed locally and the network is updated
// From use-favorites.ts:24-33
const toggleFavorite = useCallback(
  (favId: string) => {
    const isFav = favoriteReciters.includes(favId);
    setFavoriteReciters((previous) =>
      isFav ? previous.filter((id) => id !== favId) : [...previous, favId]
    );
    syncFavorite(favId, !isFav);
  },
  [favoriteReciters, setFavoriteReciters]
);

GunDB Synchronization

Open Tarteel uses GunDB for decentralized data synchronization:

Local Storage

Favorites saved to browser localStorage

Peer Network

Synced across GunDB distributed network

Real-time Updates

Live favorite counts from all users

No Account Required

Works without registration or login

How GunDB Works

Data is distributed across multiple peers rather than stored on a central server. This makes the system resilient and fast.

Favorite Counts

See how many users have favorited each reciter:
// From favorite-rank.ts:13-27
export function fetchFavoriteCounts(): Promise<Record<string, number>> {
  return new Promise((resolve) => {
    const counts: Record<string, number> = {};
    let timeout = setTimeout(() => resolve({ ...counts }), 1000);

    favoriteCountsNode.map().once((count, key) => {
      if (!key || typeof count !== 'number') return;
      counts[key] = count;
      
      clearTimeout(timeout);
      timeout = setTimeout(() => resolve({ ...counts }), 300);
    });
  });
}
Favorite counts are fetched from the GunDB network with a 1-second timeout to ensure the app remains responsive.

Live Updates

Favorite counts update in real-time as users interact:
// From favorite-rank.ts:32-46
export function subscribeToFavoriteCounts(
  callback: (counts: Record<string, number>) => void
): () => void {
  const counts: Record<string, number> = {};

  const onUpdate = (count: number, key: string) => {
    if (!key || typeof count !== 'number') return;
    counts[key] = count;
    callback({ ...counts });
  };

  favoriteCountsNode.map().on(onUpdate);
  return () => favoriteCountsNode.map().off();
}

Real-time Features

  • Instant Updates - See favorite counts change as users interact
  • Network Sync - Updates propagate through GunDB peer network
  • No Polling - Uses WebSocket-like subscriptions for efficiency
  • Automatic Cleanup - Subscriptions are properly disposed when components unmount

Filtering by Favorites

Quickly access your favorited reciters:
  1. Open the reciter selector
  2. Toggle “Show only favorites”
  3. The list filters to show only your favorited reciters
  4. Toggle off to see all reciters again
The favorites filter state is maintained in the hook and can be toggled on/off without losing your place in the list.

Favorite Storage

Favorites are stored using Jotai atoms with persistence:
// From atom.ts:5-8
export const favoriteRecitersAtom = createAtomWithStorage<string[]>(
  'favorite-reciter',
  []
);

Storage Features

Browser Storage

Saved to localStorage under key ‘favorite-reciter’

Array Format

Stored as array of reciter IDs

Persistent

Survives browser restarts and tab closes

Reactive

Updates trigger re-renders automatically

Generating Favorite IDs

Each favorite is identified by a unique ID:
// From reciter-selector.tsx:51-52
const favId = selectedReciter ? generateFavId(selectedReciter) : null;
const isFavorite = favId ? favoriteReciters.includes(favId) : false;
The ID is generated from:
  • Reciter ID
  • Mushaf name
  • Riwaya type
This ensures each unique reciter/mushaf combination is tracked separately.

Network Synchronization

When you favorite a reciter:
// From favorite-rank.ts:51-59
export function syncFavorite(id: string, isFavorited: boolean): void {
  const ref = favoriteCountsNode.get(id);
  ref.once((currentCount: number = 0) => {
    const updated = isFavorited
      ? currentCount + 1
      : Math.max(0, currentCount - 1);
    ref.put(updated);
  });
}

Sync Process

  1. Read Current Count - Fetch the current favorite count from GunDB
  2. Calculate Update - Increment if favoriting, decrement if unfavoriting
  3. Write to Network - Push the updated count to all peers
  4. Propagate - GunDB distributes the change to all connected clients
Favorite counts are incremented/decremented but not verified against unique users. The count represents total favorite actions, not unique users.

Favorites Hook

The useFavorites hook provides complete favorites functionality:
// From use-favorites.ts:11-42
export function useFavorites() {
  const [favoriteReciters, setFavoriteReciters] = useAtom(favoriteRecitersAtom);
  const [favoriteCounts, setFavoriteCounts] = useState<Record<string, number>>({});
  const [showOnlyFavorites, setShowOnlyFavorites] = useState(false);

  useEffect(() => {
    fetchFavoriteCounts().then(setFavoriteCounts);
    const unsubscribe = subscribeToFavoriteCounts(setFavoriteCounts);
    return () => unsubscribe();
  }, []);

  const toggleFavorite = useCallback(/* ... */);

  return {
    favoriteReciters,
    favoriteCounts,
    toggleFavorite,
    showOnlyFavorites,
    setShowOnlyFavorites,
  };
}

Hook Features

  • Manages local favorite list
  • Tracks global favorite counts
  • Controls filter toggle state

Visual Indicators

Favorite state is clearly indicated:

In Reciter Cards

  • Outlined star - Not favorited, gray color
  • Filled star - Favorited, yellow/amber color
  • Amber tint - Card background tints amber when favorited
  • Count badge - Shows total favorite count from all users

In Player

  • Star icon appears in the reciter selector bar
  • Clicking toggles favorite status
  • Color indicates current state (gray/yellow)
Favorite indicators are accessible with proper ARIA labels for screen readers (reciter-card.tsx:74).

Data Privacy

The favorites system respects your privacy:
  • No personal information is stored
  • Only reciter IDs are synced to the network
  • Local favorites remain in your browser
  • No account or email required
  • Anonymous participation in global counts
Clear browser data will remove your local favorites. They cannot be recovered as they’re stored locally, not on a server.

Troubleshooting

Favorites not syncing?
  • Check your internet connection
  • Verify GunDB peers are accessible
  • Try refreshing the page
  • Check browser console for errors
Lost favorites after clearing data?
  • Favorites are stored in localStorage
  • Clearing browser data removes them permanently
  • Re-favorite your preferred reciters
Counts seem incorrect?
  • Counts represent total actions, not unique users
  • Network may take a moment to sync
  • Refresh to fetch latest counts

Build docs developers (and LLMs) love