> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theroyalglow.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Favourite Services

> Customers can heart services to mark them as favourites, which appear first in the booking dialog.

# Favourite Services

Customers can "heart" services to mark them as favourites. Favourited services appear first in the booking dialog (Step 3) on subsequent bookings, reducing friction for repeat customers.

<Info>
  In plain terms: a customer taps a heart on the services they book most often,
  and those services jump to a "Your Favourites" section at the top next time —
  so a regular can re-book their usual two or three services in seconds.
</Info>

## Problem

Repeat customers at Royal Glow typically book the same 2–4 services every visit. Currently, they must scroll through all services each time. This adds unnecessary friction to the booking flow.

## Solution

Add a heart icon on every service card. Tapping toggles the favourite state. On subsequent bookings, favourited services are shown in a "Your Favourites" section above the regular service list in Step 3 of the booking dialog.

## Database Schema

```sql theme={null}
CREATE TABLE favourite_service (
  user_id       TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
  service_id    TEXT NOT NULL REFERENCES service(id) ON DELETE CASCADE,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),

  PRIMARY KEY (user_id, service_id)
);

CREATE INDEX idx_favourite_service_user ON favourite_service(user_id);
```

**Design decisions:**

* Composite primary key prevents duplicate favourites
* CASCADE delete: if user or service is deleted, favourites are cleaned up
* `created_at` allows future sorting by "most recently favourited"
* No limit on favourites — with 20–40 services, no practical reason to limit

The columns at a glance:

| Name         | Type          | Required | Default | Description                                                                       |
| ------------ | ------------- | -------- | ------- | --------------------------------------------------------------------------------- |
| `user_id`    | `text`        | Yes      |         | FK to user(id), ON DELETE CASCADE. Part of the composite primary key.             |
| `service_id` | `text`        | Yes      |         | FK to service(id), ON DELETE CASCADE. Part of the composite primary key.          |
| `created_at` | `timestamptz` | Yes      | `now()` | When the favourite was added — enables future "most recently favourited" sorting. |

## API Endpoints

<Tabs>
  <Tab title="GET">
    `GET /api/favourites` — list the signed-in user's favourites.

    * **Auth:** Required (customer+)
    * **Response:** `{ success: true, data: { favourites: string[] } }` (array of service IDs)
  </Tab>

  <Tab title="POST">
    `POST /api/favourites` — add a favourite.

    * **Auth:** Required (customer+)
    * **Body:** `{ serviceId: string }`
    * **Response:** `{ success: true }`
    * **Conflict handling:** if already favourited, returns success silently (idempotent)
  </Tab>

  <Tab title="DELETE">
    `DELETE /api/favourites` — remove a favourite.

    * **Auth:** Required (customer+)
    * **Body:** `{ serviceId: string }`
    * **Response:** `{ success: true }`
    * **Not-found handling:** if not favourited, returns success silently (idempotent)
  </Tab>
</Tabs>

## UI Implementation

### Heart Icon Component

```tsx theme={null}
'use client'
import { useOptimistic } from 'react'

export function FavouriteHeart({ serviceId, isFavourited }) {
  const [optimistic, setOptimistic] = useOptimistic(isFavourited)

  const toggle = async () => {
    setOptimistic(!optimistic)  // Instant UI update

    if (optimistic) {
      await fetch('/api/favourites', { method: 'DELETE', body: JSON.stringify({ serviceId }) })
    } else {
      await fetch('/api/favourites', { method: 'POST', body: JSON.stringify({ serviceId }) })
    }
  }

  return (
    <button
      type="button"
      onClick={toggle}
      aria-label={optimistic ? 'Remove from favourites' : 'Add to favourites'}
      aria-pressed={optimistic}
    >
      {optimistic ? '❤️' : '♡'}
    </button>
  )
}
```

### Booking Dialog Step 3 — Service Ordering

```tsx theme={null}
function sortServices(services: Service[], favouriteIds: string[]) {
  const favouriteSet = new Set(favouriteIds)
  const favourites = services.filter(s => favouriteSet.has(s.id))
  const rest = services.filter(s => !favouriteSet.has(s.id))
  return { favourites, rest }
}

// Render:
// {favourites.length > 0 && (
//   <section>
//     <h3>Your Favourites</h3>
//     {favourites.map(s => <ServiceCard key={s.id} service={s} favourited />)}
//   </section>
// )}
// <section>
//   <h3>All Services</h3>
//   {rest.map(s => <ServiceCard key={s.id} service={s} />)}
// </section>
```

## Data Flow

```
Page load (/services or booking dialog):
  ├── GET /api/services (public, KV cached)
  └── GET /api/favourites (auth required)
      → Client merges: marks hearts, splits "Your Favourites" + "All Services"

Toggle favourite:
  ├── Optimistic UI update (heart fills/empties instantly)
  ├── POST or DELETE /api/favourites (background)
  └── On error: revert optimistic state + show toast
```

## Edge Cases

<AccordionGroup>
  <Accordion title="User not signed in">
    The heart icon is hidden — favourites require auth.
  </Accordion>

  <Accordion title="Service deactivated after being favourited">
    CASCADE delete removes it; inactive services are also filtered out client-side.
  </Accordion>

  <Accordion title="User has 0 favourites">
    The "Your Favourites" section is not shown.
  </Accordion>

  <Accordion title="Network error on toggle">
    The optimistic state is reverted and an error toast is shown.
  </Accordion>

  <Accordion title="Same service favourited from /services and the booking dialog">
    Same API, same DB row — no conflict.
  </Accordion>
</AccordionGroup>

## Accessibility

* `aria-label="Add to favourites"` / `"Remove from favourites"` on the heart button
* `aria-pressed="true/false"` for toggle state
* Keyboard: Enter/Space toggles
* Visible focus ring on tab navigation
* Colour is not the only indicator (filled vs outline shape)

## Analytics Events

| Event                  | When                   | Properties                             |
| ---------------------- | ---------------------- | -------------------------------------- |
| `service_favourited`   | User taps empty heart  | `serviceId`, `serviceName`, `category` |
| `service_unfavourited` | User taps filled heart | `serviceId`, `serviceName`             |

## Where It Appears

| Location              | Behaviour                                      |
| --------------------- | ---------------------------------------------- |
| Booking Dialog Step 3 | "Your Favourites" section above "All Services" |
| `/services` page      | Heart icon on each service card (Phase 2)      |
| `/profile` page       | "My Favourite Services" list (Phase 2)         |

## Implementation Priority

<Tabs>
  <Tab title="Phase 1 (Launch)">
    * DB table + migration
    * 3 API endpoints (GET, POST, DELETE)
    * Heart icon on booking dialog Step 3
    * "Your Favourites" section ordering in booking
  </Tab>

  <Tab title="Phase 2 (Post-launch)">
    * Heart icon on `/services` page
    * "My Favourites" section on `/profile`
    * Analytics events
  </Tab>
</Tabs>


## Related topics

- [Pages & Routes](/content/docs/pages-and-routes.md)
- [Analytics](/content/docs/analytics.md)
- [Features](/content/docs/features/index.md)
- [Services & Availability](/content/docs/api-reference/services.md)
- [Memberships](/content/docs/api-reference/memberships.md)
