Your Ultimate Guide To The Favorites Page

by Alex Johnson 42 views

Are you ready to dive deep into creating an amazing user experience for your app's favorite places? Well, buckle up, because we're about to embark on a detailed journey to build the FavoritesPage! This isn't just any page; it's a crucial part of any app that lets users save and revisit their most cherished locations. We're talking about a feature-rich, user-friendly, and robust experience that covers everything from initial loading to offline synchronization. So, whether you're a seasoned developer or just starting, this guide will break down the FavoritesPage from its core requirements to the nitty-gritty implementation details. Let's get started on making this page a standout feature in your application!

Understanding the Core of the Favorites Page

The FavoritesPage is designed to be the go-to spot for users to manage all the places they've marked as special. Think of it as a personalized scrapbook for locations. In Sprint S006, this Presentation Layer feature is assigned a P1 priority, highlighting its importance. It resides at the /tabs/favorites route and is accessible via Tab 3, ensuring it's front and center for your users. To access this protected area, a user must be authenticated, hence the AuthGuard. The initial estimation for this task is 10 hours, which gives us a good benchmark for the complexity involved. The core description of this page is to provide a list of a user's favorite places, enhanced with temporal grouping, category filtering, sorting options, and a delightful swipe-to-delete functionality with an undo option. Furthermore, we're including a toggle for switching between list and map views, an engaging empty state with a clear call-to-action, and seamless synchronization with the server, all while maintaining robust offline support. This comprehensive approach ensures that users have a smooth and powerful experience managing their favorites, no matter their connection status or preferences.

Visualizing the Favorites Page: An ASCII Mockup

To truly grasp the user interface of our FavoritesPage, let's visualize it using a simple ASCII representation. This helps us map out the layout and components before we even write a line of code. At the very top, we have the header displaying the title "Favoritos" and icons for accessing filters ([≑]) and switching to the map view ([πŸ—ΊοΈ]).

Below the header, a prominent search bar with the placeholder "Buscar en favoritos..." invites users to quickly find specific locations. Following this, we see a horizontally scrollable row of category filters, starting with "Todas" and including "CafΓ©", "Restaurant", and "MΓ‘s", allowing users to narrow down their list.

An important feature for organization is the sorting option, presented as a dropdown "Ordenar: Recientes β–Ό", which defaults to displaying the most recently added favorites first. This allows users to choose how they want to view their saved places.

The main content area is structured into temporal groups. We start with "RECIENTES (3)", listing recently added places, followed by "ESTE MES (5)" and "ANTERIORES (12)". Each favorite item within these groups is displayed as a card. These cards show a thumbnail image, the place's name, its rating, price range, distance, and crucially, how long ago it was added, like "Agregado hace 2 horas". A trash icon (πŸ—‘οΈ) is visible on the right of each card, hinting at the swipe-to-delete functionality.

In the scenario where a user has no favorites yet, we transition to an Empty State. This is visually represented by a large heart icon (❀️), a clear message "Aún no tienes favoritos", and a supportive description. A prominent button, "Explorar lugares", serves as a clear call-to-action, guiding users back to discover new places.

This ASCII mockup provides a solid blueprint for the FavoritesPage, ensuring all key elements are considered for an intuitive and effective user experience.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Favoritos              [≑][πŸ—ΊοΈ]       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ πŸ” Buscar en favoritos...            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ [Todas] [CafΓ©] [Restaurant] [MΓ‘s]    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Ordenar: Recientes β–Ό                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RECIENTES (3)                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚ β”‚ [Img] CafΓ© Central             β”‚ πŸ—‘β”‚
β”‚ β”‚ ⭐ 4.5 (89) β€’ $ β€’ 1.2 km     β”‚   β”‚
β”‚ β”‚ Agregado hace 2 horas          β”‚   β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚ β”‚ [Img] Park & Chill             β”‚ πŸ—‘β”‚
β”‚ β”‚ ⭐ 4.2 (45) β€’ $ β€’ 0.8 km      β”‚   β”‚
β”‚ β”‚ Agregado ayer                  β”‚   β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ESTE MES (5)                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚ β”‚ [Img] Restaurant Gourmet       β”‚ πŸ—‘β”‚
β”‚ β”‚ ⭐ 4.8 (234) β€’ $$ β€’ 3.5 km   β”‚   β”‚
β”‚ β”‚ Agregado hace 1 semana         β”‚   β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚ ...                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ANTERIORES (12)                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚ β”‚ [Img] Coffee Shop              β”‚ πŸ—‘β”‚
β”‚ β”‚ ⭐ 4.6 (123) β€’ $ β€’ 2.1 km    β”‚   β”‚
β”‚ β”‚ Agregado hace 2 meses          β”‚   β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚ ...                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Empty State:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                      β”‚
β”‚          ❀️ (icono grande)           β”‚
β”‚                                      β”‚
β”‚   AΓΊn no tienes favoritos            β”‚
β”‚                                      β”‚
β”‚   Guarda tus lugares favoritos       β”‚
β”‚   para encontrarlos fΓ‘cilmente       β”‚
β”‚                                      β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚   β”‚ Explorar lugares     β”‚           β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Navigating Through Different States

A key aspect of building a robust application is handling various states gracefully. The FavoritesPage is no exception, and it needs to manage transitions between loading, success, empty, and even error conditions. Let's explore each of these states to ensure a seamless user experience.

Loading State (Initial)

When the user first navigates to the FavoritesPage, there's a brief period while the data is being fetched from the server. During this time, we don't want the user to see a blank screen. Instead, we'll present a Loading State. This typically involves showing skeleton placeholders that mimic the structure of the actual content. We'll display a skeleton for the search bar, followed by skeletons for the category filters, and then a series of skeleton cards, likely grouped similarly to the final content (e.g., 6 location cards grouped together). Importantly, interactive elements like swipe-to-delete won't be enabled until the data is fully loaded.

Success State (With Data)

Once the data successfully loads, the Success State is activated. This is where the user sees their actual list of favorite locations. The favorites will be displayed, grouped by the temporal logic we'll discuss later (Recientes, Esta semana, etc.). All the interactive features will now be fully functional: swipe-to-delete is enabled, filters and sorting options are active, and the pull-to-refresh gesture is ready to update the list. This state represents the ideal, fully functional view of the favorites list.

Empty State (No Favorites)

What happens when a user hasn't favorited any places yet? This is where the Empty State comes into play. Instead of a blank list, we present a visually appealing and informative screen. This state will feature a large, prominent icon, perhaps a heart (❀️). Accompanying this will be a clear, encouraging message like "Aún no tienes favoritos" (You don't have favorites yet). A secondary, more descriptive text will explain how to add favorites: "Guarda tus lugares favoritos para encontrarlos fÑcilmente" (Save your favorite places to find them easily). Crucially, there will be a Call-to-Action (CTA) button, such as "Explorar lugares", which directs the user to the HomeLobbyPage or a similar discovery page. In this state, the search bar, filters, and sorting options will be hidden to simplify the interface and focus on the core message.

Filtered Empty State

Sometimes, users might apply filters or search queries that result in no matching favorites. In this case, we need a specific Filtered Empty State. This state will be similar to the general empty state but with a tailored message, such as "No hay favoritos en esta categorΓ­a" (No favorites in this category). It should also offer a clear option to "Limpiar filtros" (Clear filters) to help users discover any existing favorites. The search bar and filters will remain visible in this state, allowing users to adjust their criteria.

Deleting State

When a user performs the swipe-to-delete action, the Deleting State is momentarily activated. As the card slides to reveal the delete button, the UI visually indicates the action. Upon deletion, a toast message appears, saying "Eliminado" (Deleted), with an "Undo" button that remains visible for 5 seconds. During this time, the card animates out of view, and the group counters are updated. If the user doesn't tap "Undo", the deletion becomes permanent.

Syncing State

To keep the user informed about data synchronization, we'll use a Syncing State. A small indicator, perhaps in the header, will appear while the app is syncing with the server. Optionally, a subtle toast message like "Sincronizando..." (Syncing...) might appear. Importantly, this state should not block user interaction with the page.

Error State

If there's an issue fetching the favorites data, the Error State will be triggered. This will typically manifest as a toast message indicating the error. A "Reintentar" (Retry) button should be provided to allow the user to attempt the data load again. If cached data exists, it should be displayed while the user retries, providing a fallback mechanism.

Offline State

When the user loses internet connectivity, the Offline State is crucial. A small banner at the top of the screen will clearly indicate "Sin conexiΓ³n" (No connection). The app will display the locally stored data (from IndexedDB). While offline, users can still perform actions like deleting favorites, which will be queued for synchronization once the connection is restored.

By carefully designing and implementing these states, we ensure that the FavoritesPage remains intuitive, informative, and functional under all circumstances.

Core Features for an Engaging Favorites Experience

To make the FavoritesPage truly shine, we're implementing a suite of powerful features designed to enhance user interaction and organization. Each feature plays a vital role in creating a seamless and enjoyable experience for managing saved locations.

1. Header

The header is the primary navigation point for the FavoritesPage. It will prominently display the title "Favoritos". For enhanced functionality, it includes a button for advanced filters (which will likely open a modal) and a toggle button to switch between the list view and a map view. An optional, subtle indicator for synchronization status can also be included here, providing at-a-glance information about data updates.

2. Searchbar

To quickly find specific places within a potentially long list of favorites, a search bar is essential. It will feature the placeholder text "Buscar en favoritos...". To optimize performance and prevent excessive API calls, a debounce of 300ms will be implemented, meaning the search will only trigger after the user has paused typing for 300 milliseconds. The search functionality will filter favorites by their names. A clear search icon will be present, and a button to clear the search text will appear when there is input. This search bar will be hidden when the page is in its empty state to maintain a clean UI.

3. Filters by Category

Users often categorize their saved places. This feature provides horizontally scrollable chips for category filtering. The "Todas" chip will serve as the default, displaying all favorites. Other category chips will be dynamically generated based on the user's actual favorites, ensuring only relevant categories are shown. Each category chip will display the category name along with a count of favorites within that category (e.g., "CafΓ© (3)"). Categories will only be displayed if they contain at least one favorite. Active filters will be visually highlighted.

4. Sorting

To cater to different user preferences, a robust sorting mechanism is implemented. Users can choose from a dropdown or segment control the following options: Recientes (default, by date added descending), MΓ‘s antiguos (by date added ascending), Mejor rating (by rating descending), CercanΓ­a (by distance ascending, requiring user location), and AlfabΓ©tico (by name ascending). Importantly, the user's sorting preference will be persisted using localStorage, so their chosen order is remembered across sessions.

5. Temporal Grouping

This feature organizes favorites into logical time-based groups to make the list more digestible: Recientes (less than 24 hours old), Esta semana (1-7 days ago), Este mes (8-30 days ago), and Anteriores (older than 30 days). Each group will have a clear header displaying the group title and the count of items within it (e.g., "RECIENTES (3)"). Only groups that contain items will be displayed. Optionally, these groups could be collapsible, allowing users to expand or collapse them using a toggle arrow.

6. Location Cards

Each favorite item will be presented using a reusable LocationCardComponent. This component will display a thumbnail image (ideally around 80x80px), the name of the place, its rating, number of reviews, price range, and distance from the user's current location. It will also show the relative date the place was added, such as "Agregado hace 2 horas". A key interaction here is the swipe-left gesture to reveal a delete option.

7. Swipe to Delete

Leveraging Ionic's ion-item-sliding component, users can swipe left on a location card to reveal a delete button (typically red with a trash icon). When activated, the card will animate out of view. A toast message "Eliminado de favoritos" will appear, accompanied by an "Undo" button that stays visible for 5 seconds. This feature will also incorporate haptic feedback for a more tactile confirmation of the action. The group counters will be updated immediately to reflect the deletion.

8. Undo Delete

Complementing the swipe-to-delete feature, the undo functionality provides a safety net. After a favorite is deleted, a toast message appears with an "Deshacer" (Undo) button, active for 5 seconds. If the user taps "Deshacer", the item is restored to its list position, and a "Restaurado" (Restored) toast is shown. If the 5-second window passes without user interaction, the deletion is then finalized on the server.

9. Pull to Refresh

To ensure the favorites list is always up-to-date, a pull-to-refresh mechanism using ion-refresher will be implemented at the top of the list. This action will trigger a synchronization with the server and reload the entire favorites list, providing users with the latest information.

10. Toggle View List/Map

Users have different preferences for viewing their saved places. A toggle button in the header allows switching between the detailed list view (with temporal grouping) and a map view. The map view will utilize a library like Leaflet or Google Maps, displaying markers for each favorite location. Tapping a marker will reveal a bottom sheet with a preview card of the location, offering options to "Ver detalle" (View details) or "Eliminar de favoritos" (Remove from favorites).

11. Empty State

As detailed previously, the Empty State is a crucial UX element. It will feature a prominent icon, a clear message "AΓΊn no tienes favoritos", a helpful description, and a CTA button "Explorar lugares" to guide users. This state hides auxiliary UI elements like the search bar and filters to maintain focus.

12. Sync Offline/Online

Robustness is key, especially concerning data integrity. When users add or delete favorites while offline, these changes will be saved locally (e.g., in IndexedDB) and marked for synchronization. Upon regaining an internet connection, the app will automatically sync these pending changes with the server, possibly accompanied by a "Sincronizado" (Synchronized) toast. A visual indicator will alert the user if there are pending sync operations.

These features collectively ensure that the FavoritesPage is not just a list, but a dynamic, user-centric hub for managing personal places.

Mastering Temporal Grouping in Favorites

Organizing a list of favorites efficiently is paramount for user satisfaction, and temporal grouping is a powerful technique to achieve this. The FavoritesPage leverages this by segmenting saved locations based on when they were added. This approach helps users quickly find recently added places or browse through older discoveries. Let's delve into the logic behind these groupings, ensuring a clear and intuitive structure.

The Logic of Grouping

Our temporal grouping strategy is based on distinct time intervals relative to the current moment. The system defines four primary groups: Recientes, Esta semana, Este mes, and Anteriores. Each group is dynamically populated with favorites that fall within its specified time frame. The groupFavoritesByTime function is central to this process. It takes an array of Favorite objects and returns an array of FavoriteGroup objects, where each group has a title, an array of items, and a count.

Here's a breakdown of the logic:

  • Recientes: This group includes favorites added within the last 24 hours. The differenceInHours function from a library like date-fns is used to calculate this. This is perfect for users who want to see places they've just discovered or revisited.
  • Esta semana: Favorites added between 1 and 7 days ago fall into this category. differenceInDays helps us accurately place items within this weekly bracket.
  • Este mes: This group captures favorites added between 8 and 30 days ago, again utilizing differenceInDays for precise calculation.
  • Anteriores: Any favorites older than 30 days are categorized as "Anteriores".

Only groups that contain at least one favorite item are displayed on the UI. This ensures that the list remains uncluttered and only relevant sections are shown. The group headers will clearly indicate the group title and the number of items it contains, such as "RECIENTES (3)". This provides immediate context and helps users navigate their saved locations more effectively. The implementation uses a Map to efficiently group items before converting them into the final array of FavoriteGroup objects. This structured approach to temporal grouping makes the FavoritesPage highly organized and user-friendly.

interface FavoriteGroup {
  title: string;
  items: Location[];
  count: number;
}

groupFavoritesByTime(favorites: Favorite[]): FavoriteGroup[] {
  const now = new Date();
  const groups: FavoriteGroup[] = [];
  
  // Recientes (< 24h)
  const recent = favorites.filter(f => 
    differenceInHours(now, f.addedAt) < 24
  );
  
  // Esta semana (1-7 dΓ­as)
  const thisWeek = favorites.filter(f => 
    differenceInDays(now, f.addedAt) >= 1 &&
    differenceInDays(now, f.addedAt) <= 7
  );
  
  // Este mes (8-30 dΓ­as)
  const thisMonth = favorites.filter(f => 
    differenceInDays(now, f.addedAt) > 7 &&
    differenceInDays(now, f.addedAt) <= 30
  );
  
  // Anteriores (> 30 dΓ­as)
  const older = favorites.filter(f => 
    differenceInDays(now, f.addedAt) > 30
  );
  
  if (recent.length) groups.push({ title: 'RECIENTES', items: recent, count: recent.length });
  if (thisWeek.length) groups.push({ title: 'ESTA SEMANA', items: thisWeek, count: thisWeek.length });
  if (thisMonth.length) groups.push({ title: 'ESTE MES', items: thisMonth, count: thisMonth.length });
  if (older.length) groups.push({ title: 'ANTERIORES', items: older, count: older.length });
  
  return groups;
}

Structuring the Project and Defining Acceptance Criteria

To ensure a well-organized and maintainable codebase for the FavoritesPage, we'll follow a structured approach, defining the necessary files and outlining clear acceptance criteria. This systematic process helps in development, testing, and future enhancements.

Files to Create

We'll organize the project files within the src/app/presentation/pages/favorites/ directory for the page itself and related components. Domain models will reside in src/app/domain/models/. This separation adheres to clean architecture principles, keeping presentation logic distinct from domain logic.

src/app/
β”œβ”€β”€ presentation/
β”‚   └── pages/
β”‚       └── favorites/
β”‚           β”œβ”€β”€ favorites.page.ts
β”‚           β”œβ”€β”€ favorites.page.html
β”‚           β”œβ”€β”€ favorites.page.scss
β”‚           β”œβ”€β”€ favorites.page.spec.ts
β”‚           └── components/
β”‚               β”œβ”€β”€ favorite-group/
β”‚               β”‚   β”œβ”€β”€ favorite-group.component.ts
β”‚               β”‚   β”œβ”€β”€ favorite-group.component.html
β”‚               β”‚   β”œβ”€β”€ favorite-group.component.scss
β”‚               β”‚   └── favorite-group.component.spec.ts
β”‚               β”œβ”€β”€ favorite-map-view/
β”‚               β”‚   β”œβ”€β”€ favorite-map-view.component.ts
β”‚               β”‚   β”œβ”€β”€ favorite-map-view.component.html
β”‚               β”‚   β”œβ”€β”€ favorite-map-view.component.scss
β”‚               β”‚   └── favorite-map-view.component.spec.ts
β”‚               β”œβ”€β”€ favorite-preview-sheet/
β”‚               β”‚   β”œβ”€β”€ favorite-preview-sheet.component.ts
β”‚               β”‚   β”œβ”€β”€ favorite-preview-sheet.component.html
β”‚               β”‚   β”œβ”€β”€ favorite-preview-sheet.component.scss
β”‚               β”‚   └── favorite-preview-sheet.component.spec.ts
β”‚               └── empty-favorites-state/
β”‚                   β”œβ”€β”€ empty-favorites-state.component.ts
β”‚                   β”œβ”€β”€ empty-favorites-state.component.html
β”‚                   β”œβ”€β”€ empty-favorites-state.component.scss
β”‚                   └── empty-favorites-state.component.spec.ts
β”œβ”€β”€ domain/
β”‚   └── models/
β”‚       └── favorite.model.ts
β”‚       └── favorite-group.model.ts

Acceptance Criteria

To ensure the FavoritesPage meets all functional and user experience requirements, we've defined the following acceptance criteria, categorized for clarity:

Functionality

  • [ ] Initial Load: The page must load the user's favorites upon initialization.
  • [ ] Temporal Grouping: Favorites must be grouped by time (Recientes, Esta semana, Este mes, Anteriores).
  • [ ] Group Visibility: Only groups with items should be displayed.
  • [ ] Group Counters: Group headers must show the correct item count.
  • [ ] Search: The search bar must filter favorites by name with a debounce mechanism.
  • [ ] Category Filters: Category filters must function correctly.
  • [ ] Category Display: Only categories with at least one favorite should be shown.
  • [ ] Sorting: The list must update according to the selected sort option.
  • [ ] Sort Persistence: The chosen sort order must be saved in localStorage.
  • [ ] Swipe to Delete: Swiping left must reveal the delete button.
  • [ ] Delete Toast: Deleting an item must show a toast with an "Undo" option for 5 seconds.
  • [ ] Undo Functionality: The "Undo" button must restore the deleted item.
  • [ ] Definitive Delete: After 5 seconds, the item must be permanently deleted from the server.
  • [ ] Pull to Refresh: Pull-to-refresh must sync with the server.
  • [ ] View Toggle: The list/map view toggle must switch the view correctly.
  • [ ] Card Navigation: Tapping a favorite card must navigate to its detail page.
  • [ ] Empty State CTA: The empty state must display an "Explorar lugares" CTA.
  • [ ] CTA Navigation: The CTA must navigate to the HomeLobbyPage.

List View

  • [ ] Temporal Display: Favorites must be displayed grouped temporally.
  • [ ] Group Headers: Groups must have titles and item counters.
  • [ ] Relative Date: Cards must show the relative "added ago" date.
  • [ ] Smooth Scrolling: The list must scroll smoothly without lag.
  • [ ] Group Updates: Groups must update correctly when items are deleted.

Map View

  • [ ] Map Display: The map must show markers for all favorite locations.
  • [ ] Marker Preview: Tapping a marker must display a preview card.
  • [ ] Preview Actions: The preview card must have "View details" and "Remove from favorites" buttons.
  • [ ] Map Interactivity: The map must be interactive (zoom, pan).

Swipe to Delete

  • [ ] Swipe Fluidity: The swipe gesture must be fluid and responsive.
  • [ ] Delete Button Clarity: The delete button must be clearly visible and intuitive.
  • [ ] Deletion Animation: The animation for item removal must be smooth.
  • [ ] Toast Visibility: The "Deleted" toast must appear immediately.
  • [ ] Undo Button Functionality: The "Undo" button must be visible and functional for 5 seconds.
  • [ ] Haptic Feedback: Haptic feedback should be felt upon deletion.

Offline/Sync

  • [ ] Offline Operations: Users must be able to add/delete favorites while offline.
  • [ ] Local Storage: Changes must be saved locally (e.g., IndexedDB).
  • [ ] Sync Indicator: An indicator must show pending sync operations.
  • [ ] Automatic Sync: Sync must occur automatically when back online.
  • [ ] Sync Confirmation: A "Synchronized" toast must appear upon successful sync.
  • [ ] Offline Banner: An "No connection" banner must be displayed when offline.

UI/UX

  • [ ] Header Clarity: The header must be clear and functional.
  • [ ] Searchbar Debounce: The search bar must use debounce to avoid excessive calls.
  • [ ] Filter Indication: Active filters must be visually indicated.
  • [ ] Skeleton Loading: Skeleton loading must display 6 grouped cards.
  • [ ] Empty State Clarity: The empty state must be clear and motivating.
  • [ ] Filtered Empty State Help: The filtered empty state must offer a way to clear filters.
  • [ ] Error Handling: Errors must be shown with a toast and a "Retry" option.
  • [ ] Cached Data: Cached data must be maintained if loading fails.

States

  • [ ] Initial Loading: Skeleton loading must be shown on initial load.
  • [ ] Empty State: The empty state must be displayed if no favorites exist.
  • [ ] Filtered Empty State: The filtered empty state must appear if filters yield no results.
  • [ ] Offline Banner: The offline banner must be shown if there's no connection.
  • [ ] Sync Indicator: An indicator must be shown for pending sync.
  • [ ] Error Toast: An error toast must appear if loading fails.
  • [ ] Cached Data Fallback: Cached data must be shown if loading fails.

Performance

  • [ ] Debounced Search: Search with debounce must prevent excessive calls.
  • [ ] Lazy Image Loading: Card images must use lazy loading.
  • [ ] Smooth Scrolling: Scrolling must be at 60fps.
  • [ ] Grouping Performance: Temporal grouping must not cause lag.
  • [ ] Swipe Fluidity: Swipe-to-delete must be fluid.

Responsive

  • [ ] Small Screens: Must function correctly on iPhone SE (320px).
  • [ ] Larger Screens: Must function correctly on iPad (768px+).
  • [ ] Filter Adaptation: Filters must adapt to screen width.
  • [ ] Map Responsiveness: The map view must be responsive.

Testing Strategy: TDD for Robustness

To guarantee the quality and reliability of the FavoritesPage, we'll adopt a Test-Driven Development (TDD) approach. This involves writing tests before writing the actual code, ensuring that each piece of functionality is verified from the outset. This methodology not only helps catch bugs early but also leads to cleaner, more modular code.

Unit Tests (favorites.page.spec.ts)

These tests focus on verifying the logic within the FavoritesPage component itself. They ensure that the page correctly handles data loading, state management, user interactions, and interactions with domain services.

describe('FavoritesPage', () => {
  it('should load user favorites on init', () => {}); // Verifies that favorites are fetched on page load.
  it('should group favorites by time', () => {}); // Tests the logic for temporal grouping.
  it('should show only non-empty groups', () => {}); // Ensures that empty groups are not rendered.
  it('should show group headers with counters', () => {}); // Checks if group headers display correct item counts.
  it('should show skeleton while loading', () => {}); // Verifies the loading state displays skeleton UI.
  it('should show empty state if no favorites', () => {}); // Tests the display of the empty state when no favorites exist.
  it('should show filtered empty state if no results', () => {}); // Checks the specific empty state for filtered results.
  it('should filter by search query with debounce', () => {}); // Verifies search filtering with the correct debounce delay.
  it('should filter by category', () => {}); // Tests category filtering functionality.
  it('should show only categories with items', () => {}); // Ensures only categories with favorites are listed.
  it('should sort by selected option', () => {}); // Verifies that sorting by different options works correctly.
  it('should persist sort in localStorage', () => {}); // Checks if the sort preference is saved and restored.
  it('should delete favorite on swipe', () => {}); // Tests the swipe-to-delete action.
  it('should show undo toast for 5 seconds', () => {}); // Verifies the appearance and duration of the undo toast.
  it('should restore favorite on undo', () => {}); // Tests the functionality of the "Undo" button.
  it('should delete definitively after timeout', () => {}); // Checks that the item is deleted permanently after the undo timeout.
  it('should sync with server on refresh', () => {}); // Verifies that pull-to-refresh triggers data sync.
  it('should toggle between list and map view', () => {}); // Tests the functionality of the list/map view toggle.
  it('should navigate to detail on card tap', () => {}); // Checks navigation to the location detail page.
  it('should navigate to explore on CTA', () => {}); // Verifies navigation to the explore page from the empty state CTA.
  it('should save changes offline', () => {}); // Tests that offline changes are correctly saved.
  it('should sync when back online', () => {}); // Verifies that pending changes are synced upon reconnection.
  it('should show sync indicator if pending', () => {}); // Checks that the sync indicator is displayed when there are pending changes.
});

Component Tests

These tests focus on individual UI components used within the FavoritesPage, ensuring they function correctly in isolation.

describe('FavoriteGroupComponent', () => {
  it('should render group header with counter', () => {}); // Tests rendering of the group header and its counter.
  it('should render group items', () => {}); // Verifies that the items within the group are displayed.
  it('should emit delete event on swipe', () => {}); // Checks if the component emits a delete event when swiped.
  it('should collapse/expand group (optional)', () => {}); // Tests the optional collapse/expand functionality.
});

describe('FavoriteMapViewComponent', () => {
  it('should render map with markers', () => {}); // Verifies the map is rendered with correct markers.
  it('should show preview on marker tap', () => {}); // Tests that a preview card appears when a marker is tapped.
  it('should emit navigate event', () => {}); // Checks if a navigation event is emitted.
  it('should emit delete event', () => {}); // Verifies that a delete event is emitted from the map view.
});

describe('FavoritePreviewSheetComponent', () => {
  it('should display location info', () => {}); // Tests if the component correctly displays location details.
  it('should emit navigate event', () => {}); // Checks if navigation events are emitted.
  it('should emit delete event', () => {}); // Verifies that delete events are emitted.
});

describe('EmptyFavoritesStateComponent', () => {
  it('should display empty message', () => {}); // Tests the display of the main empty message.
  it('should emit explore event on CTA', () => {}); // Verifies that an explore event is emitted when the CTA is clicked.
  it('should show different message if filtered', () => {}); // Checks if the component displays the correct message for a filtered empty state.
});

Integration Tests (E2E)

End-to-end tests simulate real user scenarios across the entire application flow, ensuring all components and features work together harmoniously.

describe('FavoritesPage E2E', () => {
  it('should load and display favorites list', () => {}); // Simulates loading and viewing the favorites list.
  it('should group favorites by time correctly', () => {}); // Verifies temporal grouping in a user-flow scenario.
  it('should search and filter favorites', () => {}); // Tests searching and filtering from a user perspective.
  it('should filter by category', () => {}); // Simulates filtering by category.
  it('should sort favorites', () => {}); // Tests sorting functionality as a user would interact.
  it('should delete favorite with swipe', () => {}); // Simulates the entire swipe-to-delete process.
  it('should undo delete within 5 seconds', () => {}); // Tests the undo functionality within the given time frame.
  it('should sync with server on refresh', () => {}); // Simulates refreshing the list to sync data.
  it('should toggle between list and map view', () => {}); // Tests switching between list and map views.
  it('should navigate to location detail', () => {}); // Simulates navigating to a location's detail page.
  it('should navigate to explore on empty CTA', () => {}); // Verifies navigation to the explore page from the empty state.
  it('should work offline and sync later', () => {}); // Tests the complete offline experience and subsequent sync.
});

Dependencies and Architecture

To build the FavoritesPage effectively, we rely on a well-defined set of dependencies and adhere to a clean architecture pattern. This ensures modularity, testability, and maintainability throughout the development process.

Use Cases (Domain Layer)

These represent the core business logic of our application and are independent of any specific UI or data implementation.

  • GetUserFavoritesUseCase: Fetches the list of favorites for a given user.
  • RemoveFavoriteUseCase: Handles the logic for removing a favorite.
  • SyncFavoritesUseCase: Manages the synchronization of favorites between the local storage and the server, crucial for offline support.
  • GetUserLocationUseCase: Retrieves the user's current geographical location, necessary for sorting favorites by proximity.

Shared Components

Leveraging reusable UI components streamlines development and ensures consistency across the application.

  • LocationCardComponent: Displays individual location details in a card format.
  • SkeletonLocationCardComponent: Provides a placeholder UI during data loading.
  • StarRatingComponent: Renders star ratings for locations.
  • ErrorStateComponent: Displays error messages and retry options.
  • LoadingSpinnerComponent: Shows a visual indicator for loading processes.

Services

These services encapsulate specific functionalities and can be injected where needed.

  • GeolocationService: Provides access to the device's geolocation capabilities.
  • ToastService: Manages the display of temporary notification messages (toasts).
  • HapticService: Offers feedback through device vibrations, particularly useful for actions like deletion.
  • OfflineSyncService: Handles the intricacies of storing and synchronizing data when the device is offline.

Guards

  • AuthGuard: Ensures that users must be authenticated before accessing the FavoritesPage, protecting sensitive user data.

Ionic Components

Ionic Framework provides a rich set of UI components that form the building blocks of our application.

  • ion-content, ion-header, ion-toolbar, ion-searchbar, ion-button, ion-icon, ion-chip, ion-list, ion-item, ion-item-sliding, ion-item-options, ion-item-option, ion-refresher, ion-refresher-content, ion-skeleton-text, ion-segment, ion-segment-button.

Capacitor Plugins

Capacitor allows us to bridge native device features with our web application.

  • @capacitor/haptics: For implementing haptic feedback.
  • @capacitor/network: To detect the device's online/offline status.

External Libraries

  • date-fns: A modern JavaScript utility library for manipulating dates and times, crucial for temporal grouping.
  • Leaflet or Google Maps: For rendering the map view in the FavoritesPage.

Clean Architecture Breakdown

Presentation Layer (FavoritesPage)

  • UI State Management: Handles loading, error, success, and empty states.
  • Use Case Interaction: Calls GetUserFavoritesUseCase to fetch data.
  • Data Manipulation: Groups favorites by time, filters by search/category, and sorts them.
  • Action Handling: Triggers RemoveFavoriteUseCase for deletions (with undo logic).
  • Synchronization: Initiates SyncFavoritesUseCase on pull-to-refresh and manages offline/online sync.
  • View Toggling: Manages the switch between list and map views.
  • Navigation: Navigates to LocationDetailPage or HomeLobbyPage as needed.

Domain Layer (Use Cases)

  • GetUserFavoritesUseCase: Takes userId and returns an Observable<Favorite[]>. Interacts with FavoriteRepository.getUserFavorites(userId).
  • RemoveFavoriteUseCase: Takes favoriteId and returns an Observable<void>. Interacts with FavoriteRepository.remove(favoriteId).
  • SyncFavoritesUseCase: Takes pendingChanges: Change[] and returns an Observable<void>. Interacts with FavoriteRepository.sync(pendingChanges).
  • GetUserLocationUseCase: Takes no input and returns an Observable<{ lat: number, lng: number }>. Interacts with GeolocationService.getCurrentPosition().

Data Layer

  • FavoriteRepositoryImpl: Implements the FavoriteRepository interface, handling data operations by interacting with both a remote source (e.g., Supabase) and a local database (IndexedDB) for offline support.
  • OfflineSyncService: Manages a queue of pending changes (adds, deletes) that occur offline, ensuring they are processed correctly when connectivity is restored.

This layered approach ensures that the presentation layer is kept clean and focused on UI, while the domain layer contains the core business logic, and the data layer handles data persistence and retrieval, including offline capabilities.

Implementation Notes and Optimizations

Developing a feature-rich and performant application like the FavoritesPage requires careful attention to implementation details and strategic optimizations. Here are some key considerations and code snippets to guide the development process, focusing on crucial features like undo delete, offline synchronization, and temporal grouping.

Undo Delete Logic

Implementing the undo-delete functionality requires careful state management to provide a seamless user experience. The core idea is to optimistically remove the item from the UI, add it to a temporary list of pending deletions, and then provide a short window for the user to undo the action. If the user doesn't undo, the deletion is finalized on the server.

pendingDeletes = new Map<string, Favorite>(); // Stores favorites marked for deletion but not yet confirmed.

async removeFavorite(favorite: Favorite): Promise<void> {
  // Optimistic UI update: Remove item immediately from the displayed list.
  this.favorites = this.favorites.filter(f => f.id !== favorite.id);
  this.pendingDeletes.set(favorite.id, favorite); // Mark for deletion.
  
  // Show toast with an undo option.
  const toast = await this.toastService.show({
    message: 'Eliminado de favoritos',
    duration: 5000, // Visible for 5 seconds.
    buttons: [{
      text: 'Deshacer',
      handler: () => this.undoDelete(favorite.id) // Call undo function on button press.
    }]
  });
  
  // Wait for the toast to be dismissed (either by timeout or user action).
  await toast.onDidDismiss();
  
  // If the favorite is still in pendingDeletes map after the toast dismisses (meaning undo was not triggered),
  // proceed with permanent deletion from the server.
  if (this.pendingDeletes.has(favorite.id)) {
    await this.removeFavoriteUseCase.execute(favorite.id); // Execute the use case to delete from backend.
    this.pendingDeletes.delete(favorite.id); // Remove from pending list.
  }
}

undoDelete(favoriteId: string): void {
  const favorite = this.pendingDeletes.get(favoriteId);
  if (favorite) {
    this.favorites.push(favorite); // Add the favorite back to the list.
    this.favorites = this.sortFavorites(this.favorites); // Re-sort the list to place it correctly.
    this.pendingDeletes.delete(favoriteId); // Remove from pending deletions.
    this.toastService.show({ message: 'Restaurado', duration: 2000 }); // Inform the user.
  }
}

Offline Sync

Offline support is critical for a reliable user experience. This involves storing changes locally when offline and synchronizing them with the server once connectivity is restored. A dedicated service (OfflineSyncService) can manage the queue of pending changes.

async syncFavorites(): Promise<void> {
  const pendingChanges = await this.offlineSyncService.getPendingChanges(); // Fetch pending changes from local storage.
  
  if (pendingChanges.length > 0) {
    // If there are changes, send them to the server using the sync use case.
    await this.syncFavoritesUseCase.execute(pendingChanges);
    await this.offlineSyncService.clearPendingChanges(); // Clear pending changes after successful sync.
    this.toastService.show({ message: 'Sincronizado', duration: 2000 }); // Notify user.
  }
}

async handleOnline(): Promise<void> {
  this.isOnline = true; // Update online status.
  await this.syncFavorites(); // Attempt to sync pending changes.
}

async handleOffline(): Promise<void> {
  this.isOnline = false; // Update offline status.
  // Optionally, display an offline banner to the user.
}

Temporal Grouping

Efficiently grouping favorites by time is essential for organizing the list. The following function demonstrates how date-fns can be used to calculate time differences and assign favorites to their respective groups.

import { differenceInHours, differenceInDays } from 'date-fns';

groupByTime(favorites: Favorite[]): FavoriteGroup[] {
  const now = new Date();
  const groups: Map<string, Favorite[]> = new Map(); // Use a Map for efficient grouping.
  
  favorites.forEach(fav => {
    const hours = differenceInHours(now, fav.addedAt);
    const days = differenceInDays(now, fav.addedAt);
    
    let groupKey: string;
    if (hours < 24) {
      groupKey = 'RECIENTES'; // Less than 24 hours ago.
    } else if (days <= 7) {
      groupKey = 'ESTA SEMANA'; // Between 1 and 7 days ago.
    } else if (days <= 30) {
      groupKey = 'ESTE MES'; // Between 8 and 30 days ago.
    } else {
      groupKey = 'ANTERIORES'; // Older than 30 days.
    }
    
    // Initialize group if it doesn't exist.
    if (!groups.has(groupKey)) {
      groups.set(groupKey, []);
    }
    groups.get(groupKey)!.push(fav); // Add favorite to the correct group.
  });
  
  // Convert the Map into the desired array format.
  return Array.from(groups.entries()).map(([title, items]) => ({
    title,
    items,
    count: items.length
  }));
}

Optimizations for Performance

  • ChangeDetectionStrategy.OnPush: Use this strategy in Angular components to optimize change detection, triggering updates only when input properties change or events are fired from the component.
  • trackBy in ngFor: Implement trackBy in your *ngFor loops to help Angular efficiently update lists by tracking items based on a unique identifier, preventing unnecessary DOM re-renders.
  • Lazy Loading Images: Ensure that images within the LocationCardComponent are loaded lazily to improve initial page load times.
  • Debounce in Searchbar: As already mentioned, debouncing the search input prevents excessive API calls.
  • In-Memory Cache: Cache the fetched favorites in memory within the service or page to avoid redundant data fetching.
  • Virtual Scroll: If the list of favorites is expected to grow very large (e.g., hundreds or thousands of items), consider using Ionic's Virtual Scroll (<ion-virtual-scroll>) to render only the items currently visible in the viewport, significantly improving performance.

Accessibility Considerations

  • Searchbar Labels: Ensure the search bar has an accessible label using ARIA attributes (e.g., aria-label).
  • Descriptive Buttons: All buttons should have clear, descriptive text or aria-label attributes for screen readers.
  • Swipe Alternative: Provide a visible delete button (not just swipe-activated) for users who may have difficulty with swipe gestures, especially on desktop or with assistive technologies.
  • Color Contrast: Maintain sufficient color contrast between text and backgrounds to meet WCAG AA standards.
  • Keyboard Navigation: Ensure the entire page is navigable using a keyboard, with focus indicators clearly visible.

Offline Support Strategy

  • Local Storage: Utilize IndexedDB as the primary mechanism for storing favorites and pending changes offline. It's a robust, transactional database suitable for this purpose.
  • Pending Changes Queue: Maintain a queue (e.g., an array of objects in IndexedDB) storing all add/delete operations performed while offline. Each entry should include the operation type, relevant data, and a status (e.g., 'pending', 'synced').
  • Synchronization Logic: When the device comes back online (detected using Capacitor's Network plugin), iterate through the pending changes queue. For each pending operation, attempt to sync it with the server. Upon successful sync, update the status in the queue or remove the item. If a sync fails, retry later or provide an option for manual sync.
  • Offline Banner: Display a clear visual indicator (banner) at the top of the screen whenever the device is offline.

By integrating these implementation notes, optimizations, and accessibility considerations, the FavoritesPage will be a highly performant, user-friendly, and accessible feature.

Design Visuals: Colors, Typography, and Spacing

A well-thought-out visual design is crucial for user engagement. The FavoritesPage will adhere to a consistent design system, employing specific colors, typography, and spacing to create an intuitive and aesthetically pleasing interface. These guidelines ensure brand consistency and enhance usability.

Colors

We'll leverage Ionic's CSS variables for easy theming and consistency across the application. The primary colors will be:

  • Background: --ion-background-color (the default app background).
  • Cards: --ion-card-background (slightly different from the main background for visual separation).
  • Group Headers: --ion-color-medium (a muted color for section titles).
  • Delete Button: --ion-color-danger (standard red for destructive actions).
  • Active Filter: --ion-color-primary (the main brand color to highlight selected filters).
  • Offline Banner: --ion-color-warning (a distinct color, like yellow or orange, to alert users about connection status).

Typography

Clear and readable typography is key for conveying information effectively. The hierarchy will be established as follows:

  • Header Title: 20px, bold font weight.
  • Group Titles: 12px, uppercase, bold, with slightly increased letter-spacing (e.g., 1px) for definition.
  • Location Name: 16px, medium font weight (slightly bolder than regular text).
  • Added Date: 12px, regular font weight, using a muted color (like --ion-color-medium-contrast or similar) to differentiate it.
  • Empty State Title: 18px, bold font weight.
  • Empty State Message: 14px, regular font weight.

Spacing

Consistent spacing creates visual harmony and improves readability. We'll use standard padding and margin values:

  • Card Padding: 12px inner padding for content within each location card.
  • Card Gap: 8px vertical margin between adjacent location cards.
  • Group Header Padding: 16px top/bottom padding and 16px left/right padding for group headers.
  • Group Margin: 16px vertical margin between distinct temporal groups.

Animations

Subtle animations enhance the user experience by providing visual feedback and guiding attention. We'll implement the following:

  • Card Delete Animation: A smooth slide-out to the left over 300ms when an item is deleted.
  • Undo Restore Animation: A gentle fade-in animation over 200ms when an item is restored via the undo feature.
  • Group Collapse Animation: A height transition animation over 300ms when collapsible groups are expanded or collapsed.
  • Skeleton Pulse Animation: A subtle pulsing effect for skeleton loaders, typically animating over 1.5 seconds and repeating indefinitely, to indicate ongoing loading.

Adhering to these design principles will ensure the FavoritesPage is not only functional but also visually appealing and easy to navigate.

References and Further Reading

To deepen your understanding and explore related topics, here are some valuable resources: