diff --git a/src/components/Card/LocationCard/index.tsx b/src/components/Card/LocationCard/index.tsx index 97b984a..11f4499 100755 --- a/src/components/Card/LocationCard/index.tsx +++ b/src/components/Card/LocationCard/index.tsx @@ -1,6 +1,7 @@ import { JSXInternal } from "node_modules/preact/src/jsx"; import { LocationInfo } from "../../../domains"; import { cn } from "../../../utils/common"; +import FallbackImage from "../../../../src/components/Img/FallbackImage"; interface ComponentProps { onCardClick: (id: number) => void, @@ -37,15 +38,11 @@ const LocationCard = (props: ComponentProps) => {
props.onCardClick(props.data.id)}>
- {props.data.name} { - e.currentTarget.src = 'https://pub-6b637ea51b64436dbf0514bc956972d1.r2.dev/public/upload/misty-forest-black-white.webp'; - e.currentTarget.onerror = null; - }} + style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
diff --git a/src/components/Img/FallbackImage.tsx b/src/components/Img/FallbackImage.tsx index 268f8ff..9d8cc65 100644 --- a/src/components/Img/FallbackImage.tsx +++ b/src/components/Img/FallbackImage.tsx @@ -15,7 +15,7 @@ const FallbackImage = ({ thumbnail, locationType, style, alt }: FallbackImagePro style={style} alt={alt} onError={(e) => { - if (locationType === 'restaurant') { + if (locationType === 'restaurant' || locationType === 'culinary') { e.currentTarget.src = restaurantThumbnailSrc; } else { e.currentTarget.src = fallbackThumbnailSrc; diff --git a/src/constants/api.ts b/src/constants/api.ts index e713591..c819110 100755 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -19,12 +19,15 @@ const POST_NEWS_EVENTS_URI = GET_NEWS_EVENTS_URI; const GET_LIST_LOCATIONS_URI = `${BASE_URL}/locations`; const GET_SEARCH_LOCATIONS_URI = `${BASE_URL}/locations/search`; const GET_LIST_TOP_LOCATIONS = `${BASE_URL}/locations/top-ratings`; +const GET_LIST_TRENDING_LOCATIONS_URI = `${BASE_URL}/locations/trending`; const GET_LIST_RECENT_LOCATIONS_RATING_URI = `${BASE_URL}/locations/recent`; const GET_LOCATION_URI = `${BASE_URL}/location`; const GET_LOCATION_TAGS_URI = `${BASE_URL}/location/tags`; const POST_CREATE_LOCATION = `${BASE_URL}/location`; +const POST_LOCATION_VISIT_URI = `${BASE_URL}/location`; const GET_IMAGES_BY_LOCATION_URI = `${BASE_URL}/images/location`; +const GET_MENU_ITEMS_URI = `${BASE_URL}/menu-items`; const POST_REVIEW_LOCATION_URI = `${BASE_URL}/review/location`; const POST_REVIEW_IMAGES_URI = `${BASE_URL}/review/location/images`; @@ -43,6 +46,7 @@ export { GET_CURRENT_USER_STATS, GET_LIST_RECENT_LOCATIONS_RATING_URI, GET_LIST_TOP_LOCATIONS, + GET_LIST_TRENDING_LOCATIONS_URI, GET_LIST_LOCATIONS_URI, GET_LOCATION_URI, GET_SEARCH_LOCATIONS_URI, @@ -54,5 +58,7 @@ export { POST_REVIEW_LOCATION_URI, POST_REVIEW_IMAGES_URI, POST_CREATE_LOCATION, + POST_LOCATION_VISIT_URI, POST_NEWS_EVENTS_URI, -} \ No newline at end of file + GET_MENU_ITEMS_URI, +} diff --git a/src/pages/AddLocation/BusinessHoursModal.tsx b/src/pages/AddLocation/BusinessHoursModal.tsx new file mode 100644 index 0000000..a34d997 --- /dev/null +++ b/src/pages/AddLocation/BusinessHoursModal.tsx @@ -0,0 +1,166 @@ +import { useState } from "preact/compat"; +import { BusinessHourEntry, DayOfWeek } from "./types"; + +const DAYS: DayOfWeek[] = [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', +]; + +const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/; + +interface BusinessHoursModalProps { + selected: Array; + onSave: (entries: Array) => void; + onClose: () => void; +} + +function buildInitialState(selected: Array): Record { + const byDay = new Map(selected.map(e => [e.day, e])); + const init = {} as Record; + for (const day of DAYS) { + const existing = byDay.get(day); + init[day] = existing + ? { ...existing } + : { day, open: '09:00', close: '21:00', closed: false }; + } + return init; +} + +export default function BusinessHoursModal({ selected, onSave, onClose }: BusinessHoursModalProps) { + const [entries, setEntries] = useState>( + () => buildInitialState(selected) + ); + const [error, setError] = useState(''); + + function toggleClosed(day: DayOfWeek) { + setEntries(prev => ({ + ...prev, + [day]: { ...prev[day], closed: !prev[day].closed }, + })); + setError(''); + } + + function setTime(day: DayOfWeek, field: 'open' | 'close', value: string) { + setEntries(prev => ({ + ...prev, + [day]: { ...prev[day], [field]: value }, + })); + setError(''); + } + + function applyToAll(day: DayOfWeek) { + const src = entries[day]; + setEntries(prev => { + const next = { ...prev }; + for (const d of DAYS) { + next[d] = { ...src, day: d }; + } + return next; + }); + setError(''); + } + + function handleSave() { + // Validate every non-closed day. + for (const day of DAYS) { + const e = entries[day]; + if (e.closed) continue; + if (!TIME_RE.test(e.open)) { + setError(`${day}: invalid open time "${e.open}" (expected HH:MM, 24-hour)`); + return; + } + if (!TIME_RE.test(e.close)) { + setError(`${day}: invalid close time "${e.close}" (expected HH:MM, 24-hour)`); + return; + } + } + + // Emit canonical entries: drop open/close on closed days. + const out: Array = DAYS.map(d => { + const e = entries[d]; + return e.closed + ? { day: d, open: '', close: '', closed: true } + : { day: d, open: e.open, close: e.close, closed: false }; + }); + onSave(out); + } + + const setCount = DAYS.filter(d => !entries[d].closed).length; + + return ( +
+
e.stopPropagation()}> +
+ Configure Business Hours + +
+ +
+

+ Use 24-hour format (HH:MM). Toggle "Closed" for days the location is shut. +

+ + {DAYS.map(day => { + const e = entries[day]; + return ( +
+ {day} + + + + setTime(day, 'open', (ev.target as HTMLInputElement).value)} + /> + + setTime(day, 'close', (ev.target as HTMLInputElement).value)} + /> + + +
+ ); + })} + + {error && ( +

{error}

+ )} +
+ +
+ {setCount} day{setCount === 1 ? '' : 's'} open +
+ + +
+
+
+
+ ); +} diff --git a/src/pages/AddLocation/index.tsx b/src/pages/AddLocation/index.tsx index 399fce1..6694174 100755 --- a/src/pages/AddLocation/index.tsx +++ b/src/pages/AddLocation/index.tsx @@ -14,6 +14,7 @@ import DefaultLoadingAnimation from "../../components/LoadingAnimation/Default"; import FallbackImage from "../../../src/components/Img/FallbackImage"; import AmenitiesModal from "./AmenitiesModal"; import MenuItemsModal from "./MenuItemsModal"; +import BusinessHoursModal from "./BusinessHoursModal"; function AddLocation() { const [recentLocations, setRecentLocations] = useState>() @@ -29,9 +30,11 @@ function AddLocation() { thumbnails: [], amenities: [], restaurant_menu: [], + business_hours: [], }) const [showAmenitiesModal, setShowAmenitiesModal] = useState(false) const [showMenuModal, setShowMenuModal] = useState(false) + const [showBusinessHoursModal, setShowBusinessHoursModal] = useState(false) const [submitLoading, setSubmitLoading ] = useState(false) const user = useSelector((state: UserRootState) => state.auth) @@ -69,7 +72,7 @@ function AddLocation() { function onChangeLocationType(e: ChangeEvent) { const value = (e.target as HTMLSelectElement).value as LocationType - setForm({ ...form, location_type: value, amenities: [], restaurant_menu: [] }) + setForm({ ...form, location_type: value, amenities: [], restaurant_menu: [], business_hours: [] }) } async function onSubmitForm(e: TargetedEvent) { @@ -101,6 +104,9 @@ function AddLocation() { if (form.restaurant_menu.length > 0) { formData.append("restaurant_menu", JSON.stringify(form.restaurant_menu)) } + if (form.business_hours.length > 0) { + formData.append("business_hours", JSON.stringify(form.business_hours)) + } for(let i = 0; i < tempThumbnailArr.length; i++) { formData.append("thumbnail", tempThumbnailArr[i]) @@ -239,6 +245,22 @@ function AddLocation() { )}
)} +
+ + {form.business_hours.length > 0 && ( +
+ {form.business_hours.map(h => ( + + {h.day}: {h.closed ? 'Closed' : `${h.open}–${h.close}`} + + ))} +
+ )} +
Kota / Kabupaten * (regency mustn't be empty) setShowMenuModal(false)} /> )} + {showBusinessHoursModal && ( + { setForm({ ...form, business_hours }); setShowBusinessHoursModal(false); }} + onClose={() => setShowBusinessHoursModal(false)} + /> + )} ) } diff --git a/src/pages/AddLocation/types.ts b/src/pages/AddLocation/types.ts index 514c70b..769bfe4 100755 --- a/src/pages/AddLocation/types.ts +++ b/src/pages/AddLocation/types.ts @@ -13,6 +13,22 @@ export interface MenuItem { description: string, } +export type DayOfWeek = + | 'Sunday' + | 'Monday' + | 'Tuesday' + | 'Wednesday' + | 'Thursday' + | 'Friday' + | 'Saturday'; + +export interface BusinessHourEntry { + day: DayOfWeek, + open: string, // "HH:MM" 24h + close: string, // "HH:MM" 24h + closed: boolean, +} + export interface Form { name: string, address: string, @@ -22,4 +38,5 @@ export interface Form { thumbnails: Array, amenities: Array>, restaurant_menu: Array, + business_hours: Array, } diff --git a/src/pages/BestLocations/MenuPopup.tsx b/src/pages/BestLocations/MenuPopup.tsx new file mode 100644 index 0000000..90b4f1c --- /dev/null +++ b/src/pages/BestLocations/MenuPopup.tsx @@ -0,0 +1,133 @@ +import { useEffect, useState } from "preact/hooks"; +import { getMenuItemsService } from "../../services/locations"; + +export interface MenuItemRow { + id: number; + location_id: number; + name: string; + price: number; + category: string; + description: string; + is_available: boolean; + submitted_by: number; + avg_score: number | null; +} + +const CATEGORY_LABELS: Record = { + appetizer: 'Appetizer', + main_course: 'Main Course', + dessert: 'Dessert', + beverages: 'Beverages', + snack: 'Snack', +}; + +function formatPrice(price: number): string { + return 'Rp' + price.toLocaleString('id-ID'); +} + +function getRatingColor(rating: number): string { + if (rating >= 70) return '#3ba55d'; + if (rating >= 40) return '#faa61a'; + return '#ed4245'; +} + +interface Props { + locationId: number; + locationName: string; + onClose: () => void; +} + +export default function MenuPopup({ locationId, locationName, onClose }: Props) { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [activeCategory, setActiveCategory] = useState('all'); + const [search, setSearch] = useState(''); + + useEffect(() => { + getMenuItemsService(locationId).then(res => { + if (res.data) setItems(res.data); + setLoading(false); + }); + }, [locationId]); + + const categories = ['all', ...Array.from(new Set(items.map(i => i.category).filter(Boolean)))]; + + const filtered = items.filter(item => { + const matchCat = activeCategory === 'all' || item.category === activeCategory; + const matchSearch = item.name.toLowerCase().includes(search.toLowerCase()); + return matchCat && matchSearch; + }); + + // split into two columns + const mid = Math.ceil(filtered.length / 2); + const col1 = filtered.slice(0, mid); + const col2 = filtered.slice(mid); + + return ( +
+
e.stopPropagation()}> + +
+
+

{locationName} Menu

+
+
+ setSearch((e.target as HTMLInputElement).value)} + /> + +
+
+ +
+ {categories.map(cat => ( + + ))} +
+ +
+ {loading ? ( +

Loading...

+ ) : filtered.length === 0 ? ( +

No items found.

+ ) : ( +
+ {[col1, col2].map((col, ci) => ( +
+
+ + Rating +
+ {col.map(item => ( +
+ {item.name} + {formatPrice(item.price)} + {item.avg_score != null ? ( + + {Math.round(item.avg_score)} + + ) : ( + + )} +
+ ))} +
+ ))} +
+ )} +
+ +
+
+ ); +} diff --git a/src/pages/BestLocations/index.tsx b/src/pages/BestLocations/index.tsx index 17bfcae..96655e2 100755 --- a/src/pages/BestLocations/index.tsx +++ b/src/pages/BestLocations/index.tsx @@ -5,6 +5,8 @@ import { DefaultSeparator } from "../../components"; import './style.css'; import { UserStar, User, Map, HandPlatter, ClockCheck } from 'lucide-react'; import { Link } from "react-router-dom"; +import FallbackImage from "../../../src/components/Img/FallbackImage"; +import MenuPopup from "./MenuPopup"; interface TopLocation { row_number: number, @@ -57,6 +59,7 @@ function getRatingColorClass(score: number): string { function BestLocation() { const [page, _setPage] = useState(1); const [topLocations, setTopLocations] = useState>([]) + const [menuPopup, setMenuPopup] = useState<{ id: number; name: string } | null>(null) const [pageState, setPageState] = useState({ filterScoreType: 'all', filterScoreTypeidx: 1, @@ -151,14 +154,11 @@ const [page, _setPage] = useState(1);
- { - e.currentTarget.src = x.location_type === 'culinary' ? 'https://pub-6b637ea51b64436dbf0514bc956972d1.r2.dev/restaorunta.webp' : 'https://pub-6b637ea51b64436dbf0514bc956972d1.r2.dev/public/upload/misty-forest-black-white.webp'; - e.currentTarget.onerror = null; - }} + {/*
@@ -172,7 +172,13 @@ const [page, _setPage] = useState(1);
{x.location_type === 'culinary' && - Menu + } Open
@@ -229,6 +235,14 @@ const [page, _setPage] = useState(1);
+ + {menuPopup && ( + setMenuPopup(null)} + /> + )} ) diff --git a/src/pages/BestLocations/style.css b/src/pages/BestLocations/style.css index 7f3d2b0..fd06926 100755 --- a/src/pages/BestLocations/style.css +++ b/src/pages/BestLocations/style.css @@ -60,4 +60,206 @@ a .selected-reviewer-filter:hover{ position: sticky; align-self: flex-start; top: 30px; +} + +/* Menu Popup */ +.menu-popup-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; +} + +.menu-popup-modal { + background: #1e2124; + border-radius: 12px; + width: 100%; + max-width: 860px; + max-height: 85vh; + display: flex; + flex-direction: column; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6); + overflow: hidden; +} + +.menu-popup-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 20px 24px 14px; + gap: 12px; + flex-wrap: wrap; +} + +.menu-popup-title { + font-size: 22px; + font-weight: 800; + letter-spacing: -0.3px; +} + +.menu-popup-header-actions { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; +} + +.menu-popup-search { + background: #2f3136; + border: none; + border-radius: 20px; + padding: 6px 14px; + color: white; + font-size: 13px; + width: 190px; + max-width: 40vw; + outline: none; +} + +.menu-popup-search::placeholder { + opacity: 0.45; +} + +.menu-popup-close { + background: none; + border: none; + color: white; + font-size: 16px; + cursor: pointer; + opacity: 0.5; + padding: 0 2px; + line-height: 1; +} + +.menu-popup-close:hover { + opacity: 1; +} + +.menu-popup-tabs { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding: 0 24px 12px; + border-bottom: 1px solid #2f3136; +} + +.menu-popup-tab { + background: none; + border: none; + color: white; + font-size: 14px; + padding: 6px 18px; + border-radius: 20px; + cursor: pointer; + opacity: 0.55; + transition: background 0.15s, opacity 0.15s; +} + +.menu-popup-tab:hover { + opacity: 0.85; + background: #2f3136; +} + +.menu-popup-tab--active { + background: white; + color: #111; + opacity: 1; + font-weight: 600; +} + +.menu-popup-tab--active:hover { + background: white; + opacity: 1; +} + +.menu-popup-body { + overflow-y: auto; + flex: 1; + padding: 0 24px 20px; +} + +.menu-popup-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0 32px; + padding-top: 8px; +} + +@media (max-width: 560px) { + .menu-popup-grid { + grid-template-columns: 1fr; + } + + .menu-popup-title { + font-size: 18px; + } + + .menu-popup-header { + padding: 16px 16px 10px; + } + + .menu-popup-tabs { + padding: 0 16px 10px; + } + + .menu-popup-body { + padding: 0 16px 16px; + } + + .menu-popup-search { + width: 130px; + } +} + +.menu-popup-col {} + +.menu-popup-col-header { + display: flex; + align-items: center; + padding: 8px 0 4px; + border-bottom: 1px solid #2f3136; + margin-bottom: 2px; +} + +.menu-popup-col-rating-label { + font-size: 11px; + opacity: 0.45; + width: 44px; + text-align: right; +} + +.menu-popup-item { + display: flex; + align-items: baseline; + padding: 9px 0; + border-bottom: 1px solid #2a2d31; + gap: 10px; +} + +.menu-popup-item:last-child { + border-bottom: none; +} + +.menu-popup-item-name { + flex: 1; + font-size: 14px; + line-height: 1.35; +} + +.menu-popup-item-price { + font-size: 13px; + white-space: nowrap; + opacity: 0.85; +} + +.menu-popup-item-rating { + font-size: 13px; + font-weight: 700; + width: 28px; + text-align: right; + flex-shrink: 0; } \ No newline at end of file diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 3412488..f77099f 100755 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,3 +1,4 @@ +import FallbackImage from '../../components/Img/FallbackImage'; import { LocationCard, SeparatorWithAnchor } from '../../components'; import popular from '../../datas/popular.json'; import './style.css'; @@ -127,21 +128,17 @@ function Home() { {popular.data.map((x) => (
- { - e.currentTarget.src = 'https://pub-6b637ea51b64436dbf0514bc956972d1.r2.dev/public/upload/misty-forest-black-white.webp'; - e.currentTarget.onerror = null; - }} - /> +

{x.name}

{x.location}

1.2k users visit this month

-
))} @@ -152,7 +149,13 @@ function Home() { {topCriticsLocations.map((x) => (
- {x.name} + {/* + /> */}

{x.name}

{x.regency_name}

@@ -183,7 +186,13 @@ function Home() { {topUsersLocations.map((x) => (
- {x.name} + {/* + /> */}

{x.name}

{x.regency_name}

diff --git a/src/pages/LocationDetail/index.tsx b/src/pages/LocationDetail/index.tsx index cdd076b..e0ae219 100755 --- a/src/pages/LocationDetail/index.tsx +++ b/src/pages/LocationDetail/index.tsx @@ -12,6 +12,7 @@ import { } from './types'; import { handleApiError, useAutosizeTextArea } from '../../utils'; import { getCurrentUserLocationReviewService, getImagesByLocationService, getLocationService, postReviewLocation, postReviewImages } from "../../services"; +import { recordLocationVisitService } from "../../services/locations"; import { DefaultSeparator, SeparatorWithAnchor, CustomInterweave, SpinnerLoading, ReviewCard, ReviewCardFull } from '../../components'; import RatingsCard from '../../components/Card/RatingsCard'; import { useSelector } from 'react-redux'; @@ -112,6 +113,11 @@ function LocationDetail() { getImage(val.detail.thumbnail) } }) + // Fire-and-forget visit hit. Only fires once per (browser tab × location); + // the backend additionally dedupes by client IP for 30 minutes via Redis, + // so refreshes / multiple tabs / bots cannot inflate the trending counts. + // Anything that fails here is silently swallowed inside the service. + recordLocationVisitService(Number(id)) } catch (error) { const err = error as IHttpResponse; if (err.status == 404) { diff --git a/src/pages/LocationDetail/types.ts b/src/pages/LocationDetail/types.ts index af4dffc..abb39da 100755 --- a/src/pages/LocationDetail/types.ts +++ b/src/pages/LocationDetail/types.ts @@ -7,6 +7,7 @@ export interface ILocationDetail { address: String, regency_name: String, province_name: String, + location_type: String, region_name: String, google_maps_link: String, thumbnail: string | null, @@ -27,6 +28,7 @@ export function emptyLocationDetail(): ILocationDetail { province_name: '', regency_name: '', region_name: '', + location_type: '', submitted_by: 0, critic_score: 0, critic_count: 0, diff --git a/src/services/locations.ts b/src/services/locations.ts index 2ad3cbf..701ede6 100755 --- a/src/services/locations.ts +++ b/src/services/locations.ts @@ -4,10 +4,13 @@ import { GET_LIST_LOCATIONS_URI, GET_LIST_RECENT_LOCATIONS_RATING_URI, GET_LIST_TOP_LOCATIONS, + GET_LIST_TRENDING_LOCATIONS_URI, GET_LOCATION_TAGS_URI, GET_LOCATION_URI, GET_SEARCH_LOCATIONS_URI, - POST_CREATE_LOCATION + POST_CREATE_LOCATION, + POST_LOCATION_VISIT_URI, + GET_MENU_ITEMS_URI, } from "../constants/api"; import { client } from "./config"; @@ -170,6 +173,7 @@ async function getListTopLocationsService(params: GetListLocationsArg) { async function getLocationService(params: GetLocationArg) { try { const data = await fetchLocation(params) + console.log(data) return { data, error: null } } catch (error) { throw error @@ -211,6 +215,77 @@ async function getSearchLocationService(arg: GetSearchLocations): Promise { + const response = await client({ method: 'GET', url: `${GET_MENU_ITEMS_URI}?location_id=${locationId}` }) + return response.data +} + +interface GetTrendingLocationsArg extends GetRequestPagination { + /** week | month | 3month | semester | year */ + window: string +} + +const fetchTrendingLocations = async ({ window, page, page_size }: GetTrendingLocationsArg) => { + const url = `${GET_LIST_TRENDING_LOCATIONS_URI}?window=${encodeURIComponent(window)}&page=${page}&page_size=${page_size}` + const response = await client({ method: 'GET', url }) + return response.data +} + +export const useTrendingLocations = ( + params: GetTrendingLocationsArg, + options?: Omit, 'queryKey' | 'queryFn'>, +) => { + return useQuery({ + queryKey: ['locations', 'trending', params], + queryFn: () => fetchTrendingLocations(params), + enabled: !!params.window, + ...options, + }) +} + +export const getTrendingLocationsService = async (params: GetTrendingLocationsArg) => { + try { + const data = await fetchTrendingLocations(params) + return { data, error: null } + } catch (error: any) { + return { data: null, error, status: error?.status } + } +} + +/** + * Records a page-visit hit for a location. The backend already de-duplicates + * by client IP (one count per IP per location per 30 minutes via Redis), so + * this is intentionally fire-and-forget. We additionally suppress repeats + * within the same browser tab/session so React re-renders, StrictMode double + * effects, or back/forward nav don't even hit the wire. + */ +const visitedThisSession = new Set() + +export const recordLocationVisitService = async (locationId: number): Promise => { + if (!Number.isFinite(locationId) || locationId <= 0) return + if (visitedThisSession.has(locationId)) return + visitedThisSession.add(locationId) + try { + await client({ + method: 'POST', + url: `${POST_LOCATION_VISIT_URI}/${locationId}/visit`, + }) + } catch { + // Silent: visit counting must never disrupt the page render. The backend + // also responds 204 on dedup, so the only errors here are network/server. + visitedThisSession.delete(locationId) // allow a future retry on real failure + } +} + +export const getMenuItemsService = async (locationId: number) => { + try { + const data = await fetchMenuItems(locationId) + return { data, error: null } + } catch (error: any) { + return { data: null, error, status: error.status } + } +} + export { getListLocationsService, getSearchLocationService,