diff --git a/src/modules/restaurant/components/CategoryNav.vue b/src/modules/restaurant/components/CategoryNav.vue new file mode 100644 index 0000000..21b1dff --- /dev/null +++ b/src/modules/restaurant/components/CategoryNav.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/modules/restaurant/components/MenuItemCard.vue b/src/modules/restaurant/components/MenuItemCard.vue new file mode 100644 index 0000000..4f00537 --- /dev/null +++ b/src/modules/restaurant/components/MenuItemCard.vue @@ -0,0 +1,149 @@ + + + diff --git a/src/modules/restaurant/components/MenuTree.vue b/src/modules/restaurant/components/MenuTree.vue new file mode 100644 index 0000000..d53885a --- /dev/null +++ b/src/modules/restaurant/components/MenuTree.vue @@ -0,0 +1,82 @@ + + + diff --git a/src/modules/restaurant/components/ModifierSelector.vue b/src/modules/restaurant/components/ModifierSelector.vue new file mode 100644 index 0000000..b556b3d --- /dev/null +++ b/src/modules/restaurant/components/ModifierSelector.vue @@ -0,0 +1,221 @@ + + + diff --git a/src/modules/restaurant/components/RestaurantHeader.vue b/src/modules/restaurant/components/RestaurantHeader.vue new file mode 100644 index 0000000..8a4af4f --- /dev/null +++ b/src/modules/restaurant/components/RestaurantHeader.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/modules/restaurant/composables/useMenu.ts b/src/modules/restaurant/composables/useMenu.ts new file mode 100644 index 0000000..b35e178 --- /dev/null +++ b/src/modules/restaurant/composables/useMenu.ts @@ -0,0 +1,97 @@ +/** + * useMenu — fetches a restaurant's menu via REST. + * + * v1: REST-only via RestaurantAPI.getMenu(). The Nostr live-overlay + * merge lands in commit 8 (subscribes to kind-30402 listings for the + * restaurant's pubkey and patches `items` reactively). + * + * Usage: + * const { restaurant, tree, items, isLoading, error, refresh } + * = useMenu(slugOrId) + * + * Pass a slug or an id — the composable picks the right endpoint + * based on the format. Slugs are kebab-case strings; ids are + * urlsafe-short-hash from the extension (alphanumeric, ~22 chars). + */ + +import { ref, computed, onScopeDispose, watch, type Ref } from 'vue' +import { injectService, SERVICE_TOKENS } from '@/core/di-container' +import type { RestaurantAPI } from '../services/RestaurantAPI' +import type { + EnrichedMenuItem, + MenuNode, + Restaurant, +} from '../types/restaurant' + +// Heuristic: ids from urlsafe_short_hash are 22-char base64url. Slugs +// allow dashes and are typically shorter. Anything containing a dash +// or shorter than 20 chars is treated as a slug. +function looksLikeId(value: string): boolean { + return !value.includes('-') && value.length >= 20 && /^[A-Za-z0-9_-]+$/.test(value) +} + +export interface UseMenuReturn { + restaurant: Ref + tree: Ref + items: Ref + isLoading: Ref + error: Ref + refresh: () => Promise +} + +export function useMenu(slugOrId: Ref | string): UseMenuReturn { + const api = injectService(SERVICE_TOKENS.RESTAURANT_API) + + const restaurant = ref(null) + const tree = ref([]) + const items = ref([]) + const isLoading = ref(false) + const error = ref(null) + + let abortController: AbortController | null = null + + const target = computed(() => + typeof slugOrId === 'string' ? slugOrId : slugOrId.value + ) + + async function load(value: string): Promise { + if (!value) return + abortController?.abort() + abortController = new AbortController() + const my = abortController + + isLoading.value = true + error.value = null + try { + const r = looksLikeId(value) + ? await api.getRestaurantById(value) + : await api.getRestaurantBySlug(value) + if (my.signal.aborted) return + + const menu = await api.getMenu(r.id) + if (my.signal.aborted) return + + restaurant.value = menu.restaurant + tree.value = menu.tree + items.value = menu.items + } catch (err) { + if (my.signal.aborted) return + error.value = err instanceof Error ? err : new Error(String(err)) + } finally { + if (!my.signal.aborted) isLoading.value = false + } + } + + async function refresh(): Promise { + if (target.value) await load(target.value) + } + + // React to slug/id changes (Vue Router param updates). + watch(target, (value) => load(value), { immediate: true }) + + onScopeDispose(() => { + abortController?.abort() + }) + + return { restaurant, tree, items, isLoading, error, refresh } +} diff --git a/src/modules/restaurant/index.ts b/src/modules/restaurant/index.ts index 864e32c..dbec7da 100644 --- a/src/modules/restaurant/index.ts +++ b/src/modules/restaurant/index.ts @@ -76,6 +76,18 @@ export const restaurantModule: ModulePlugin = { component: () => import('./views/HomePage.vue'), meta: { requiresAuth: false, title: 'Restaurant' }, }, + { + path: '/r/:slug', + name: 'restaurant-menu', + component: () => import('./views/RestaurantPage.vue'), + meta: { requiresAuth: false, title: 'Menu' }, + }, + { + path: '/r/:slug/item/:itemId', + name: 'restaurant-item', + component: () => import('./views/ItemPage.vue'), + meta: { requiresAuth: false, title: 'Item' }, + }, ] as RouteRecordRaw[], } diff --git a/src/modules/restaurant/views/HomePage.vue b/src/modules/restaurant/views/HomePage.vue index d31faf9..5f71516 100644 --- a/src/modules/restaurant/views/HomePage.vue +++ b/src/modules/restaurant/views/HomePage.vue @@ -1,32 +1,85 @@ diff --git a/src/modules/restaurant/views/ItemPage.vue b/src/modules/restaurant/views/ItemPage.vue new file mode 100644 index 0000000..9d6e271 --- /dev/null +++ b/src/modules/restaurant/views/ItemPage.vue @@ -0,0 +1,236 @@ + + +