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

- Go to your profile page → Bitelist
Sequence Diagram / Flow
ERD
Backend Implementation
- Create
PointsEarnedMailerto 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
PointsEarnedMailerJobto 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 headersapplication_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 hookupdate.rb— enqueue referral points email jobsecond_half_step.rb— enqueue membership signup points emailcreate.rb— delay review reward job with TTL storage (supports correct timing before email enqueue)- Update
GiveReservationRewardWorkerto 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_typeto improve lookup/filter performance after introducing these types. reward.rb— add booking/reviewreward_typeoptions20260121000000_add_reward_type_index_to_rewards.rb— addreward_typeindexschema.rb— updated schema snapshot- Update email provider configuration to enable SES
configuration_setto support event tracking. 3_email_provider_config.rb— enable SESconfiguration_setfor 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 cancelsecond_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_idin 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 templatepoints_earned_notification.html.erb(+341); localized via new i18n keys inen.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
SesTrackingmodule (ses_tracking.rb, +52) to inject SES email headers; include it inApplicationMailerso any mailer can opt into tracking. - Enable SES
configuration_setin3_email_provider_config.rbto 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_typeenum to includebookingandreview; add20260121000000_add_reward_type_index_to_rewards.rbindex for lookup performance (schema.rbsnapshot 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 inrewards.rb(default description,country). - PR #7659 — feat: CU-86d1fzbfn — share and earn
- Add
ReservationReferrerRewardworker (reservation_referrer_reward.rb, +124) — dedicated component that grants the referrer a points reward when a referred user completes a booking; queuesPointsEarnedMailerJobon creation. - Extend
Rewardmodel (reward.rb, +26/-21) — expand thereward_typeenum (booking / review / referrer reward) and add alock!mechanism for safe concurrent reward creation. - Permit
referrer_idinreservations_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 thereferrer_idattribute, and migration20260113074653_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 bycancel_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 newReservationReferrerRewardflow. - 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_typeindex migration, andschema.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.vueto open and render the new Share & Earn modal from the restaurant page toolbar. - Update
GroupLandingToolbar.vueto use the same Share & Earn modal in group contexts. - Update
GroupLandingBody.vueto embed/enable the modal on the group landing page content area. - Update
GroupLandingPage.astroto pass required landing props needed by the toolbar/modal wiring. - Update
ShareRestaurant.vueto replace the old share modal implementation with the new Share & Earn Points modal. - Update
redirection.tsto include ashareAndEarnLinkbuilder function (used to generate the correct referral link). - Add
referrer.tsimplementing ashareAndEarnReferrerutility to manage how referrer data is stored/read (e.g., cookie/local persistence strategy). - Update
usePartner.tsto parse the URL query and initialize the Share & Earn referrer value early in the session. - Update
RestaurantDetailEntry.vueandGroupLandingEntry.vueto ensure the relevant “user initiate” event/listener runs on mount (so referrer capture/initialization happens reliably). - Update
CheckOutPage.vueto addreferrerIdinto the booking payload. - Update
checkOutEvents.tsto handle thereferrerfield in setup data. - Update
createBooking.tsto includereferrerIdin the create-booking request payload. - Update booking schema typing in
createBooking.tsto include the new field. - Update
booking.tsto addreferrerIdto the booking store state. - Update
BookingConfirmationContent.vueto remove/clear the Share & Earn referrer once booking is confirmed (prevents referrer leaking into future unrelated bookings). - Update
profile.tsto remove the referrer cookie when the referral code matches certain conditions (prevents stale referral state). - Update
index.tsto addSHARE_AND_EARN_REFERRERconstant (likely a key name for persistence). - Update multiple
profile.jsonlocale files to include new Share & Earn translation keys. - Add
bitelist-share-earn-points.mddocumenting the Share & Earn Points feature (large doc addition). - Swaps the Share & Earn modal’s “Learn More” link to a language-aware URL and threads
langthrough the consumer components and Astro pages. - Replace the generic
HOW_TO_MAINTAIN_HUNGRY_POINT_LINKwith a language-based selection in the template:hrefand thelearnMore()handler. - Add a new required
lang: stringprop to thePropsinterface; switch theWHAT_IS_SHARE_AND_EARN_TH/WHAT_IS_SHARE_AND_EARN_ENconstant 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
langprop into modal consumers (Enhancement, +1 line each) - pass
:lang="props.lang"intoShareAndEarnPointsModal; also pass:lang="lang"toGroupLandingToolbar. - declare new
lang: { type: String, required: true }prop, pass:lang="lang"to the modal, drop the no-longer-usedlangimport from~/stores/nanostores/config, and switch thegroupLandingPageURL builder to useprops.lang(so the share URL is consistent with the prop). - pass
:lang="lang.get()"intoShareEarnPointsModal. - declare
Props { lang: string }, acceptlangas a prop, drop thelangimport from~/stores/nanostores/config, and switch therestaurantPageURL builder to useprops.langfor the share link. - pass
lang={lang}intoGroupLandingBody. - pass
lang={lang}intoRestaurantPageDesktop. - add
lang: stringto the AstroPropstype, destructure it, and forward it intoRestaurantHeaderDesktop.addlang: stringtoProps, destructure it, and passlang={lang}to<ShareRestaurant />.fromrestaurantDetailStoreand introduces a sharedFavouriteButtoncomponent. - redirect cookie format changed from a bare destination pathname to
source:destination; the loop-protection check (hasRedirectLoopProtection) now takes bothcurrentPathnameanddestinationPathnameand 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 thesource:destinationcookie write. -
bitelistStore.favouriteCountassignment now prefersresult.restaurantIds?.lengthwhen present, then falls back toresult.totalRestaurants ?? 0(the previous assignment wasresult.totalRestaurants, which was sometimes0/missing and broke the badge). defaultFavouriteBiteListgetter now readsuseUserStore(appStore).favouriteRestaurants.ids.lengthfirst, then falls back tothis.favouriteCount,gives a more reliablesavedCountfor the default favourite card.- add a
resetBodyScrollLock()helper that clears the iOS body-scroll-lock inline styles (position,top,left,right,width,overflowon bothdocument.bodyanddocument.documentElement, plus theoverflow-hiddenclass) and restores the saved scroll position viawindow.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 inCreateBiteListBottomSheet.vue. Root cause:body-scroll-lock-upgradeusesposition: fixedvia arequestAnimationFramecallback, so itsbodyStylesnapshot can be unset whenenableBodyScrollruns. - add an
onBeforeUnmountcleanup that callshistoryRemoveState("bottom_sheet", { encode: false })andtoggleBodyScroll(true)if the sheet is still open at unmount, so a navigating-away cleanup doesn’t leave the page frozen. defaultFavouriteBiteListgetter now excludesDEFAULT_FAVOURITE_IDfrom the bitelist ID counts (it was being double-counted as a real BiteList) and adds favourite membership as a separate total increment, sototalRestaurantsInBiteListreflects the real total across the user’s custom BiteLists + favourites.- Add-to-BiteList modal refactor
- Switch the four heavy children (
AddToBiteListBottomSheet,AddToBiteListModal,CreateBiteListModalWrapper,ConfirmModal) todefineAsyncComponentwith anonErrorhook that toggles off the full-page loader — reduces initial bundle and recovers gracefully from chunk-load failures. - Drop the
useRestaurantDetailStoreimport/usage entirely; threadrestaurantIdas a requiredProps.restaurantId: string | number | nulland useprops.restaurantIdinhandleBitelistCreated(the new BiteList is created with the restaurant pre-added, the map is seeded with the restaurant, and thesavedCountis 1 only ifprops.restaurantIdis set). - Gate the modal render behind a new
isAddToBiteListLoadedref; show the full-page loader while the async chunk is being fetched (handled bytoggleFullPageLoader(true)on open,falseonuseComponentMounted("addToBiteListRef", ...)). - New shared favourite primitive
- Slot-based component that wraps the
useToggleFavouritecomposable and exposes{ isFavourite, isUserSignedIn, tap }to its slot. - Three contexts:
default(plain toggle),bitelist-owner(carriesbitelistIdso removing a restaurant from the BiteList also removes it from the favourite set),bitelist-non-owner(no side effects on the BiteList). - Emits
toggled/deletedevents. Replaces the oldToggleFavRestaurantimport inBitelistRestaurantCard. - Replaces
ToggleFavRestaurantwith the newFavouriteButton; renders two slot branches (non-owner and owner) bound to the same toggle handler. - Simplifies the
on-favourite-clickedemit payload to no longer carryrestaurantId(the consumer can readprops.id). - Adds
bitelistId?: string | numbertoPropsand forwards it to the owner-contextFavouriteButton. - 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). NewbiteListLinkcomputed usesbitelistPage(lang, String(props.biteList.slug || ""))fromredirection.ts; returnsundefinedin hybrid mode so the JS click path stays the same on app.handleClick(event)now callsevent.preventDefault()when not in edit mode so the click + nav don’t double-fire. bitelistPage()now setscityId=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. UsesnormalizeSearchParams(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.vueandBitelistSettingsMenu.vue. - Added
EditBiteListModal.vueto 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.vueto orchestrate save/remove flows; opensAddToBiteListModal.vue(desktop) orAddToBiteListBottomSheet.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.vueas a new desktop variant of the toggle. - Updated
ToggleFavRestaurantMobile.vueto integrate with the BiteList flow. - Updated
ProfileDesktop.vueandProfileMobile.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.tsto register new BiteList routes. - Updated
serverRedirection.tsto support BiteList slugs in server-side redirects. - Updated
redirection.tswith BiteList URL builder helpers. - Added
bitelist.tsPinia 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.tswith a BiteList search type. - Extended
myFavoriteFilter.tsto support BiteList-based filtering. - Updated
group_landing.json,restaurant.json, andbooking.jsonacross 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
0becausetotalRestaurantsfrom the API was not always populated. - Updated
FavouriteDesktop.vueandFavouriteMobile.vue— count assignment now prefersresult.restaurantIds?.length, falling back toresult.totalRestaurants, then0. - Updated
savedCountgetter inbitelist.tsstore — now readsfavoriteIdsfromuseUserStore(appStore), computesfavoriteIdsCountfrom its length, and uses that before falling back tothis.favouriteCount. - Simplified
.husky/pre-pushto callnpm run typecheckinstead of inline env assignment. - Updated
package.jsontypecheckscript to usecross-envfor cross-platformNODE_OPTIONScompatibility.
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
| Method | Path | URL | Description | Payload |
|---|---|---|---|---|
| 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_cover | Generate AI cover image | { bite_list_name, restaurant_ids: string[] (max 5) } | |
| POST | /api/v4/bitelists/:id/ai_description | Generate AI description | { bite_list_name, restaurant_ids: string[] (max 5) } | |
| GET | /s/{hash} | Short link redirect → full URL w/ UTMs | hash param, 301 | |
| PATCH | /api/v4/bitelists/:id/visibility | Update visibility (discoverable / unlisted) | { visibility: "discoverable" | "unlisted" } | |
| POST | /api/v4/bitelists/:id/cover_upload | Upload dual-cropped cover | { banner_blob: base64, thumbnail_blob: base64 } |
New Query
-
DB Schema / Database Migration
- Add
referrer_idto reservation metadata / tracking - Add
visibilityenum/flag onbiteliststable (values:discoverable,unlisted) — defaultdiscoverable - Add
ai_cover_urlandai_descriptionfields tobitelists - New
ai_generation_quotastable:user_id,date,image_count,text_count(daily reset) - New
bitelist_short_linkstable:hash(unique),bite_list_id,referrer_id,created_at - Add
noindexmeta tag logic driven byvisibilityflag
Improvement:
| Feature Name | Date | What Changed | Description |
|---|---|---|---|
| Bitelist V1 — Share & Earn + Lists | Jan–Apr 2026 | Share/Earn referral, points email, BiteList hub, cover/upload, short-link landing pages | See Backend / Hybrid / Frontend Implementation sections above |
| Bitelist V2 — UX Improvements | Apr 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 |