Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bitelist

Description / Background

The core purpose of BiteList is to democratize food influence. We believe that a recommendation from a trusted friend or a local “regular” is often more impactful than a paid celebrity endorsement. BiteList allows every Hungry Hub user to become a Micro-influencer.

Currently, users discover restaurants on Hungry Hub, but there is no native way to organize favorites into themes (e.g., “Date Night,” “Best Buffets”). When users want to share recommendations with friends, they must send individual links, which is fragmented and fails to reward the “tastemaker” for driving a sale.

Objectives

  • Allow user and guest to share the restaurant to others via social media, messaging apps, or email.
  • Shareable Short Links use enerate unique alphanumeric hash for list + referrer hhub.ly/[hash]
  • Allow user to earn rewards when their shared links lead to bookings.
  • Allow users to organize favorite restaurants into custom themed lists
  • Enable sharing of curated lists as Group Landing Pages
  • Implement Share & Earn referral program to reward users for recommendations
  • Transform users into micro-influencers through personalized curation
  • Allow user to create Discoverable list and Unlisted list:
    • Discoverable — may be featured on homepage; anyone with link can view
    • Unlisted — only people with link can view
  • Allow user to generate bitelist cover image using AI
  • Allow user to generate bitelist description using AI

Scope

 Web and App: Phase I:

  • Share & Earn functionality
  • Email notification Phase II:
  • Personal curation tool for saving restaurants
  • Custom folder organization (e.g., “Date Night,” “Best Buffets”)
  • Group Landing Page generation from lists
  • Referral tracking and rewards Out of Scope: -

AI Logic and Prompts

AI Cover Image Generation

  • Trigger: “Generate Cover Image by AI” from 3-dot menu
  • Pre-condition: Logged in + BiteList name ≥ 3 chars
  • Flow:
    • Check user daily image quota (limit: 5/day, resets 00:00 ICT)
    • 3-second debounce on button
    • Fetch top 5 highest-rated restaurants (Name, Description, Tags, Price From, Rating, Award)
    • Pass BiteList Name + structured metadata to AI image system prompt
    • Generate 4:1 image; auto-crop center for 1:1 card
    • Save to HH CDN, apply as cover, success toast
  • Edge cases: empty list → pass only name; quota exceeded → disable + tooltip; timeout → revert + error toast

AI Description Generation

  • Trigger: ✨ “Auto-Write” icon inside BiteList Description field
  • Pre-condition: Logged in + name ≥ 3 chars
  • Flow:
    • Check daily text quota (limit: 5/day, resets 00:00 ICT)
    • 3-second debounce; “typing” shimmer in text area
    • Backend fetches top 5 restaurant profiles (Name, Tags, Price From, Rating, Award)
    • LLM (Gemini Flash recommended) populates Description field; user can edit before save
  • Edge cases: name < 3 chars → icon greyed; empty list → base on name only; quota exceeded → tooltip

Prompts & Guardrails

Image System Prompt:

You are an expert food photographer. Generate a highly appetizing, modern, and realistic cover image for a restaurant collection titled: “{BiteList_Name}”. Use the following detailed restaurant profiles to inspire the visual aesthetic, lighting, and food items depicted: [Restaurant 1: {Name}, Description: {Description}, Tags: {Tags}, Award: {Award}]… Do NOT include any typography, text, words, or logos. Aspect ratio: 4:1. Reject profanity, explicit content, copyrighted brand names. Strictly photographic — no cartoons/3D.

Text System Prompt:

Write a catchy, brief description for a restaurant collection named “{BiteList_Name}”. Use the following restaurant profiles to weave a narrative: [Restaurant 1: {Name}, Tags: {Tags}, Price starts at: {Price_From}, Rating: {Rating}, Award: {Award}]… Tone: enthusiastic foodie. Include 1–2 relevant food emojis. Reject profanity, explicit content, copyrighted brand names.

Empty list fallback: omit restaurant brackets; generate purely from {BiteList_Name}.

Location

Share & Earn Button

  • Group Landing Page
  • Restaurant Page

How to find Bitelist

How to find Bitelist On Group Landing Page

  •   *   Open [https://web.hungryhub.com/](https://web.hungryhub.com/)
    
    • Pick the Group Landing Page / Restaurant you want to share
    • Find the button
    • Copy the link or you can share directly to your social media by clicking the button How to find Bitelist On Profile Page
  •   *   Open [https://web.hungryhub.com/](https://web.hungryhub.com/)
    
    • Go to your profile page → Bitelist

Sequence Diagram / Flow

ERD

Backend Implementation

  • Create PointsEarnedMailer to send a dedicated “points earned” notification email.
  • Add a full HTML email template for the points-earned notification.
  • Ensure email content is localized via i18n keys (see i18n section below).
  • Create PointsEarnedMailerJob to send the email asynchronously.
  • Add idempotency protection (unique locking) to reduce duplicate deliveries when events are retried.
  • points_earned_mailer_job.rb — new job for “Points Earned” email delivery
  • Implement helpers/modules to attach SES tracking headers to emails.
  • Update the base mailer so tracking behavior is consistently available to mailers that need it.
  • ses_tracking.rb — new module(s) that inject SES headers
  • application_mailer.rb — include SES tracking/header helpers
  • This PR makes the system send the points-earned email when points are granted from multiple sources:
  • Enqueue “Points Earned” email job after referral reward is granted.
  • Add logging/error handling around enqueueing.
  • Add a hook to enqueue the email job for promo-code related rewards.
  • Adjust review reward creation timing (see delayed job + TTL below) and enqueue email once reward is created.
  • Enqueue points-earned email on membership sign-up completion.
  • user_referral_reward.rb — enqueue referral reward email job (with error logging)
  • reservation.rb — add promo code reward email hook
  • update.rb — enqueue referral points email job
  • second_half_step.rb — enqueue membership signup points email
  • create.rb — delay review reward job with TTL storage (supports correct timing before email enqueue)
  • Update GiveReservationRewardWorker to separate booking rewards and review rewards rather than treating them as a single combined flow.
  • Ensure reward records are created with clearer types and that email enqueueing aligns with the reward that was actually granted.
  • give_reservation_reward_worker.rb — split booking/review reward logic and align email enqueueing
  • Expand reward classification by adding explicit reward types for:
    • booking
    • review
  • Add a database index on reward_type to improve lookup/filter performance after introducing these types.
  • reward.rb — add booking/review reward_type options
  • 20260121000000_add_reward_type_index_to_rewards.rb — add reward_type index
  • schema.rb — updated schema snapshot
  • Update email provider configuration to enable SES configuration_set to support event tracking.
  • 3_email_provider_config.rb — enable SES configuration_set for tracking
  • Add a new Lambda handler to forward/transform SES events into ClickHouse for analytics.
  • Ensure reservation cancellation cancels all reservation rewards (not just a subset).
  • Cancel delayed reward jobs appropriately in the relevant step flow.
  • cancel_reservation_service.rb — cancel all reservation rewards on cancel
  • second_step.rb — cancel delayed reward job on second step
  • Fix a query used to detect cancelled redemptions (small correctness fix).
  • reward_points.rb — fix cancel redemption detection query
  • Add translation keys for the points-earned email in:
    • English
    • Thai
    • Chinese
  • Refactor the reservation reward worker so “booking reward” and “review reward” are handled as distinct reward paths (instead of coupled logic).
  • Ensures each reward type can be created/validated/emailed independently.
  • Reward creation happens in async processing (worker), triggered by reservation lifecycle events.
  • Add a dedicated component for “reservation referrer reward” so a referrer can earn points when a referred user completes a booking.
  • Allow the system to store/propagate a referrer_id in reservation tracking metadata so downstream reward calculation can reliably identify the referrer.
  • Extend reservation cancellation logic so referrer rewards tied to a reservation can be cancelled/reversed appropriately when the reservation is cancelled.
  • New mailer dedicated to “points earned” notifications.
  • Intended to be triggered whenever a reward is created (booking/review/referral/membership, etc., depending on hooks added).
  • Add configuration to attach SES tracking headers for these emails (for deliverability/analytics observability).
  • Expand/adjust reward type enum usage (to support newly introduced reward types like referrer reward and to clarify existing ones).
  • Update reservation “arrived” / “paid” services to integrate with reward + email enqueueing (ensuring points-earned notifications fire at the correct lifecycle stage).
  • Add PointsEarnedMailer (points_earned_mailer.rb, +129) and its full HTML template points_earned_notification.html.erb (+341); localized via new i18n keys in en.yml / th.yml / cn.yml.
  • Add PointsEarnedMailerJob (points_earned_mailer_job.rb, +42) — async delivery with idempotency protection via unique locking, and error logging on failure.
  • Add SesTracking module (ses_tracking.rb, +52) to inject SES email headers; include it in ApplicationMailer so any mailer can opt into tracking.
  • Enable SES configuration_set in 3_email_provider_config.rb to support event tracking.
  • Add a new Lambda (index.js, +144) that forwards/transforms SES events into ClickHouse for analytics.
  • Split GiveReservationRewardWorker (give_reservation_reward_worker.rb, +205/-84) into two distinct reward paths — booking reward and review reward — each created/validated/emailed independently.
  • Expand Reward.reward_type enum to include booking and review; add 20260121000000_add_reward_type_index_to_rewards.rb index for lookup performance (schema.rb snapshot updated).
  • Hook email enqueueing into the reward creation points:
  • user_referral_reward.rb — enqueue referral reward email job with error logging.
  • reservation.rb — add promo code reward email hook.
  • update.rb — enqueue referral points email job.
  • second_half_step.rb — enqueue membership signup points email.
  • create.rb (review flow) — delay review reward job with TTL storage so email is enqueued at the correct time.
  • Cancel-on-cancel semantics:
  • cancel_reservation_service.rb — cancel all reservation rewards (not just a subset).
  • second_step.rb — cancel the delayed review-reward job on the second step.
  • Bug fix: reward_points.rb — fix the cancel-redemption detection query.
  • Tests: points_earned_mailer_spec.rb (+237), points_earned_mailer_job_spec.rb (+138), give_reservation_reward_worker_spec.rb (+123), plus factory updates in rewards.rb (default description, country).
  • PR #7659 — feat: CU-86d1fzbfn — share and earn
  • Add ReservationReferrerReward worker (reservation_referrer_reward.rb, +124) — dedicated component that grants the referrer a points reward when a referred user completes a booking; queues PointsEarnedMailerJob on creation.
  • Extend Reward model (reward.rb, +26/-21) — expand the reward_type enum (booking / review / referrer reward) and add a lock! mechanism for safe concurrent reward creation.
  • Permit referrer_id in reservations_controller#tracking_params (reservations_controller.rb, +40) so the Share & Earn referral is propagated into reservation metadata at create time.
  • Persist referrer on the reservation tracking model — reservation_tracking.rb (+12) adds the referrer_id attribute, and migration 20260113074653_add_referrer_id_to_reservation_metadata.rb (+18) backfills the column. reservation_trackings.rb (+13) wires the model. tmp_reservation.rb (+3/-1) threads the value through to the booking creation step.
  • Cancel referrer rewards on reservation cancel — cancel_reservation_service.rb (+46/-1) reverses/cancels the referrer reward tied to a cancelled reservation; covered by cancel_reservation_service_spec.rb (+103).
  • Expose referrer context on the public read path — group_landing_page_serializer.rb (+4) surfaces the referrer info needed by the share landing.
  • Refactor reward_points.rb (+21/-8) — split dynamic-points and base-points calculation; cleaner computation of the referrer reward amount.
  • Tighten GiveReservationRewardWorker (+216/-84) — further separation of booking vs. review reward records and email enqueueing, integrated with the new ReservationReferrerReward flow.
  • Wire reward + email enqueueing into reservation lifecycle services:
  • mark_reservation_arrived_service.rb (+6) — fire referrer/reward chain on arrival.
  • mark_reservation_as_paid_service.rb (+5) — fire referrer/reward chain on payment.
  • reservation.rb (+8) — additional callback wiring.
  • Carry forward from #7647: idempotent PointsEarnedMailerJob (+47), PointsEarnedMailer (+158), user_referral_reward.rb (+28/-7) sending the email in the worker, second_half_step.rb (+27/-6) enqueueing membership signup reward email, SES tracking + config, i18n (en/th/cn), Lambda, reward_type index migration, and schema.rb (+3) updates.
  • Tests: reward_spec.rb (+260 — uniqueness + journal), points_earned_mailer_spec.rb (+240), reservation_referrer_reward_spec.rb (+231), points_earned_mailer_job_spec.rb (+161), give_reservation_reward_worker_spec.rb (+123), cancel_reservation_service_spec.rb (+103), reservations_controller_spec.rb (+73 — referrer_id tracking).

[

github.com

https://github.com/hungryhub-team/hh-server/pull/7647

](https://github.com/hungryhub-team/hh-server/pull/7647)

[

github.com

https://github.com/hungryhub-team/hh-server/pull/7659

](https://github.com/hungryhub-team/hh-server/pull/7659)

Hybrid Implementation

  • Provides the Share & Earn UI (copy link, share to social, and native share behavior).
  • Centralizes the “share action” handling into a single reusable component used across multiple pages.
  • Update RestaurantToolbar.vue to open and render the new Share & Earn modal from the restaurant page toolbar.
  • Update GroupLandingToolbar.vue to use the same Share & Earn modal in group contexts.
  • Update GroupLandingBody.vue to embed/enable the modal on the group landing page content area.
  • Update GroupLandingPage.astro to pass required landing props needed by the toolbar/modal wiring.
  • Update ShareRestaurant.vue to replace the old share modal implementation with the new Share & Earn Points modal.
  • Update redirection.ts to include a shareAndEarnLink builder function (used to generate the correct referral link).
  • Add referrer.ts implementing a shareAndEarnReferrer utility to manage how referrer data is stored/read (e.g., cookie/local persistence strategy).
  • Update usePartner.ts to parse the URL query and initialize the Share & Earn referrer value early in the session.
  • Update RestaurantDetailEntry.vue and GroupLandingEntry.vue to ensure the relevant “user initiate” event/listener runs on mount (so referrer capture/initialization happens reliably).
  • Update CheckOutPage.vue to add referrerId into the booking payload.
  • Update checkOutEvents.ts to handle the referrer field in setup data.
  • Update createBooking.ts to include referrerId in the create-booking request payload.
  • Update booking schema typing in createBooking.ts to include the new field.
  • Update booking.ts to add referrerId to the booking store state.
  • Update BookingConfirmationContent.vue to remove/clear the Share & Earn referrer once booking is confirmed (prevents referrer leaking into future unrelated bookings).
  • Update profile.ts to remove the referrer cookie when the referral code matches certain conditions (prevents stale referral state).
  • Update index.ts to add SHARE_AND_EARN_REFERRER constant (likely a key name for persistence).
  • Update multiple profile.json locale files to include new Share & Earn translation keys.
  • Add bitelist-share-earn-points.md documenting the Share & Earn Points feature (large doc addition).
  • Swaps the Share & Earn modal’s “Learn More” link to a language-aware URL and threads lang through the consumer components and Astro pages.
  • Replace the generic HOW_TO_MAINTAIN_HUNGRY_POINT_LINK with a language-based selection in the template :href and the learnMore() handler.
  • Add a new required lang: string prop to the Props interface; switch the WHAT_IS_SHARE_AND_EARN_TH / WHAT_IS_SHARE_AND_EARN_EN constant imports in place of the old link.
  • Add WHAT_IS_SHARE_AND_EARN_TH —  https://blog.hungryhub.com/hungry-hub-share-and-earn.
  • Add WHAT_IS_SHARE_AND_EARN_EN —  https://blog.hungryhub.com/en/hungry-hub-share-earn.
  • Thread lang prop into modal consumers (Enhancement, +1 line each)
  • pass :lang="props.lang" into ShareAndEarnPointsModal; also pass :lang="lang" to GroupLandingToolbar.
  • declare new lang: { type: String, required: true } prop, pass :lang="lang" to the modal, drop the no-longer-used lang import from ~/stores/nanostores/config, and switch the groupLandingPage URL builder to use props.lang (so the share URL is consistent with the prop).
  • pass :lang="lang.get()" into ShareEarnPointsModal.
  • declare Props { lang: string }, accept lang as a prop, drop the lang import from ~/stores/nanostores/config, and switch the restaurantPage URL builder to use props.lang for the share link.
  • pass lang={lang} into GroupLandingBody.
  • pass lang={lang} into RestaurantPageDesktop.
  • add lang: string to the Astro Props type, destructure it, and forward it into RestaurantHeaderDesktop.add lang: string to Props, destructure it, and pass lang={lang} to <ShareRestaurant />.from restaurantDetailStore and introduces a shared FavouriteButton component.
  • redirect cookie format changed from a bare destination pathname to source:destination; the loop-protection check (hasRedirectLoopProtection) now takes both currentPathname and destinationPathname and only blocks a real A→B→A reverse path (preserves valid re-navigations from A to B again). The cookie is now set before the pathname is mutated, so the recorded source is the pre-redirect URL.
  • updated to cover the A→B→A loop case; mocked cookie now uses "/en/mobile:/en/web" format; expectations updated for the new blocked-redirect behavior and the source:destination cookie write.
  •  bitelistStore.favouriteCount assignment now prefers result.restaurantIds?.length when present, then falls back to result.totalRestaurants ?? 0 (the previous assignment was result.totalRestaurants, which was sometimes 0/missing and broke the badge).
  • defaultFavouriteBiteList getter now reads useUserStore(appStore).favouriteRestaurants.ids.length first, then falls back to this.favouriteCount, gives a more reliable savedCount for the default favourite card.
  • add a resetBodyScrollLock() helper that clears the iOS body-scroll-lock inline styles (position, top, left, right, width, overflow on both document.body and document.documentElement, plus the overflow-hidden class) and restores the saved scroll position via window.scrollTo(0, savedScrollY). Wired into all 5 close paths : closeBottomSheet, handleCreateNew, handleAddToBiteLists, handleRemoveFromAll, handleRemoveSelected — to cover the parent-closes-after-save flow as well as the explicit close. Mirrors the existing manual fix in CreateBiteListBottomSheet.vue. Root cause: body-scroll-lock-upgrade uses position: fixed via a requestAnimationFrame callback, so its bodyStyle snapshot can be unset when enableBodyScroll runs.
  • add an onBeforeUnmount cleanup that calls historyRemoveState("bottom_sheet", { encode: false }) and toggleBodyScroll(true) if the sheet is still open at unmount, so a navigating-away cleanup doesn’t leave the page frozen.
  • defaultFavouriteBiteList getter now excludes DEFAULT_FAVOURITE_ID from the bitelist ID counts (it was being double-counted as a real BiteList) and adds favourite membership as a separate total increment, so totalRestaurantsInBiteList reflects the real total across the user’s custom BiteLists + favourites.
  • Add-to-BiteList modal refactor
  • Switch the four heavy children (AddToBiteListBottomSheet, AddToBiteListModal, CreateBiteListModalWrapper, ConfirmModal) to defineAsyncComponent with an onError hook that toggles off the full-page loader — reduces initial bundle and recovers gracefully from chunk-load failures.
  • Drop the useRestaurantDetailStore import/usage entirely; thread restaurantId as a required Props.restaurantId: string | number | null and use props.restaurantId in handleBitelistCreated (the new BiteList is created with the restaurant pre-added, the map is seeded with the restaurant, and the savedCount is 1 only if props.restaurantId is set).
  • Gate the modal render behind a new isAddToBiteListLoaded ref; show the full-page loader while the async chunk is being fetched (handled by toggleFullPageLoader(true) on open, false on useComponentMounted("addToBiteListRef", ...)).
  • New shared favourite primitive
  • Slot-based component that wraps the useToggleFavourite composable and exposes { isFavourite, isUserSignedIn, tap } to its slot.
  • Three contexts: default (plain toggle), bitelist-owner (carries bitelistId so removing a restaurant from the BiteList also removes it from the favourite set), bitelist-non-owner (no side effects on the BiteList).
  • Emits toggled / deleted events. Replaces the old ToggleFavRestaurant import in BitelistRestaurantCard.
  • Replaces ToggleFavRestaurant with the new FavouriteButton; renders two slot branches (non-owner and owner) bound to the same toggle handler.
  • Simplifies the on-favourite-clicked emit payload to no longer carry restaurantId (the consumer can read props.id).
  • Adds bitelistId?: string | number to Props and forwards it to the owner-context FavouriteButton.
  • Profile BiteList Card is now a real <a href> link, not a click handler. The image block and the name <span> are wrapped in <a :href="biteListLink"> (with the existing @click="handleClick" retained for analytics), restoring standard browser behaviors (right-click → open in new tab, hover URL preview, SEO crawlable links). New biteListLink computed uses bitelistPage(lang, String(props.biteList.slug || "")) from redirection.ts; returns undefined in hybrid mode so the JS click path stays the same on app. handleClick(event) now calls event.preventDefault() when not in edit mode so the click + nav don’t double-fire.
  • bitelistPage() now sets cityId=CITY_NAME_ANYWHERE.toLowerCase() on the landing URL’s query string. Fixes the location filter that incorrectly restricted restaurants to “Bangkok” when the “Anywhere” option was selected for restaurants using THB/MYR/SGD currencies. Uses normalizeSearchParams(filteredQueryParams()) so the new param is merged with existing query params instead of replacing them.

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2488

](https://github.com/hungryhub-team/hh-pegasus/pull/2488)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2570

](https://github.com/hungryhub-team/hh-pegasus/pull/2570)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2687

](https://github.com/hungryhub-team/hh-pegasus/pull/2687)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2686

](https://github.com/hungryhub-team/hh-pegasus/pull/2686)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2692

](https://github.com/hungryhub-team/hh-pegasus/pull/2692)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2877

](https://github.com/hungryhub-team/hh-pegasus/pull/2877)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2884

](https://github.com/hungryhub-team/hh-pegasus/pull/2884)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2892

](https://github.com/hungryhub-team/hh-pegasus/pull/2892)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2693

](https://github.com/hungryhub-team/hh-pegasus/pull/2693)

[

github.com

https://github.com/hungryhub-team/hh-pegasus/pull/2961

](https://github.com/hungryhub-team/hh-pegasus/pull/2961)

Frontend Implementation

  • Replaced the old Favourites page with a BiteList hub — users now see a grid of their BiteLists instead of a flat list of restaurants.
  • Added Create New BiteList flow: name input + cover image upload via a modal (desktop) or bottom-sheet (mobile).
  • Added BiteList landing page showing the list’s cover, title, toolbar actions, and its restaurants in a paginated / infinite-scroll layout.
  • Added editable cover image on the landing page with an edit menu (upload / remove).
  • Added BitelistToolbar with Share and Settings actions visible only to the list owner.
  • Added Share & Earn modal (ShareEarnPointsModal.vue) — shows the user’s unique referral link, social share buttons, and points-earn explanation.
  • Added generic share modal (ShareModal.vue) for sharing a BiteList publicly (copy link, social).
  • Added BiteList settings flow (edit name/cover, delete) via BitelistSettingsWrapper.vue and BitelistSettingsMenu.vue.
  • Added EditBiteListModal.vue to rename or update the cover of an existing BiteList.
  • Added Add Restaurant modal (AddRestaurantModal.vue) — search-driven, supports batch-adding multiple restaurants to a BiteList; uses visit history for suggestions.
  • Added AddToBiteListModalWrapper.vue to orchestrate save/remove flows; opens AddToBiteListModal.vue (desktop) or AddToBiteListBottomSheet.vue (mobile) depending on whether the restaurant is already saved.
  • Added AddToBiteListFooter.vue — sticky footer inside the add modal showing selection count and confirm CTA.
  • Added BitelistRestaurantCard.vue — individual restaurant card inside a BiteList (image, name, deal info, remove action).
  • Added NoRestaurant.vue — empty state when a BiteList has no restaurants, with a CTA to add.
  • Added BitelistLandingCustomerReview — customer review section on the BiteList landing page.
  • Updated ToggleFavRestaurant.vue — the heart/bookmark toggle now opens the AddToBiteList modal instead of directly toggling a favourite.
  • Added ToggleFavRestaurantDesktop.vue as a new desktop variant of the toggle.
  • Updated ToggleFavRestaurantMobile.vue to integrate with the BiteList flow.
  • Updated ProfileDesktop.vue and ProfileMobile.vue — navigation links now point to the BiteList hub instead of the old Favourites page.
  • Added new Astro pages: BitelistLanding.astro, BitelistLandingPage.astro, BitelistLandingRestaurant.astro, BitelistLandingSearchDesktop.astro, BitelistLandingSearchMobile.astro, [client]-hybrid.astro, [client].astro.
  • Updated router.ts to register new BiteList routes.
  • Updated serverRedirection.ts to support BiteList slugs in server-side redirects.
  • Updated redirection.ts with BiteList URL builder helpers.
  • Added bitelist.ts Pinia store — manages all user BiteLists, active BiteList state, restaurant selection for batch add/remove, savedCount, per-list restaurant counts, and pagination state.
  • Added API service files: getMyBitelists.ts, getBitelistBySlug.ts, getBitelistItems.ts, getBitelistRestaurants.ts, getUserRestaurantHistories.ts, createBitelist.ts, updateBitelist.ts, deleteBitelist.ts, addToBitelist.ts, addRestaurantToBitelist.ts, addRestaurantsBitelist.ts, deleteRestaurantBitelist.ts, uploadFile.ts, fetchCustomerReviews.ts.
  • Added BiteList.ts — TypeScript type definitions for BiteList data structures.
  • Extended SearchTypes.ts with a BiteList search type.
  • Extended myFavoriteFilter.ts to support BiteList-based filtering.
  • Updated group_landing.json, restaurant.json, and booking.json across all supported locales (EN, TH, ZH, and others) with new BiteList and Share & Earn translation keys.
  • Fixed an incorrect restaurant count on the default Favourite BiteList card — it was showing 0 because totalRestaurants from the API was not always populated.
  • Updated FavouriteDesktop.vue and FavouriteMobile.vue — count assignment now prefers result.restaurantIds?.length, falling back to result.totalRestaurants, then 0.
  • Updated savedCount getter in bitelist.ts store — now reads favoriteIds from useUserStore(appStore), computes favoriteIdsCount from its length, and uses that before falling back to this.favouriteCount.
  • Simplified .husky/pre-push to call npm run typecheck instead of inline env assignment.
  • Updated package.json typecheck script to use cross-env for cross-platform NODE_OPTIONS compatibility.

https://github.com/hungryhub-team/hh-pegasus/pull/2486 https://github.com/hungryhub-team/hh-pegasus/pull/2687

PRD & Task

V1 PRD Link: Bitelist PRD V2 PRD Link: Bitelist V2 — UX Improvements Tasks: Bitelist Tasks

Design

V1 — Profile Page - Bitelist Design V2 — Bitelist V2 Design

API Blueprint

MethodPathURLDescriptionPayload
 POST /api/v4/reservations Create a booking. Extended with Share & Earn referral support { …, referrerId: string | null }

optional referrer user ID captured from the share link; passed when a booking is made through a referred session.
POST/api/v4/bitelists/:id/ai_coverGenerate AI cover image{ bite_list_name, restaurant_ids: string[] (max 5) }
POST/api/v4/bitelists/:id/ai_descriptionGenerate AI description{ bite_list_name, restaurant_ids: string[] (max 5) }
GET/s/{hash}Short link redirect → full URL w/ UTMshash param, 301
PATCH/api/v4/bitelists/:id/visibilityUpdate visibility (discoverable / unlisted){ visibility: "discoverable" | "unlisted" }
POST/api/v4/bitelists/:id/cover_uploadUpload dual-cropped cover{ banner_blob: base64, thumbnail_blob: base64 }

New Query

-

DB Schema / Database Migration

  • Add referrer_id to reservation metadata / tracking
  • Add visibility enum/flag on bitelists table (values: discoverable, unlisted) — default discoverable
  • Add ai_cover_url and ai_description fields to bitelists
  • New ai_generation_quotas table: user_id, date, image_count, text_count (daily reset)
  • New bitelist_short_links table: hash (unique), bite_list_id, referrer_id, created_at
  • Add noindex meta tag logic driven by visibility flag

Improvement:

Feature NameDateWhat ChangedDescription
Bitelist V1 — Share & Earn + ListsJan–Apr 2026Share/Earn referral, points email, BiteList hub, cover/upload, short-link landing pagesSee Backend / Hybrid / Frontend Implementation sections above
Bitelist V2 — UX ImprovementsApr 9, 2026 (Sprint 70)AI cover + description generator, sequential dual-crop image upload, short-link shortener (hhub.ly), Discoverable/Unlisted visibility revamp, AI quotas (5/day, reset 00:00 ICT)See “Bitelist V2 — UX Improvements” section above