feat(webapp): restaurant bundle skeleton
Standalone customer-facing bundle for the LNbits 'restaurant' extension, modeled on the market bundle. v1 ships single-venue (URL-driven via /r/:slug) with REST-only order placement; festival aggregator and NIP-17 transport are tracked as aiolabs/restaurant#8 and #9 respectively. Skeleton this commit lands: vite.restaurant.config.ts — port 5186, dist-restaurant/, green theme color, PWA manifest, alias @/app.config -> restaurant-app/. restaurant.html — entry; title 'Restaurant — Order'. src/restaurant-app/ main.ts — startApp + PWA SW registration. app.ts — module registration glue (baseModule + restaurantModule). app.config.ts — modules.restaurant config block. Reserves a features:{} slot for tier-gated UI (aiolabs/restaurant#2). App.vue — AppShell with Browse / Cart / Orders bottom-nav tabs. src/modules/restaurant/ index.ts — ModulePlugin shell with the future- roadmap context inlined as top-of-file comment (#1..#9). views/HomePage.vue — placeholder; commit 4 replaces it with real discovery + redirect. src/core/di-container.ts — RESTAURANT_API + RESTAURANT_NOSTR_SYNC tokens reserved (consumers land in 3 / 8). package.json — dev:restaurant, build:restaurant, preview:restaurant scripts and append to dev:all + build:demo. Verified: - vue-tsc -b passes (whole webapp, all bundles). - vite build --config vite.restaurant.config.ts builds clean against VITE_LNBITS_BASE_URL=http://localhost:5001 VITE_RESTAURANT_DEFAULT_SLUG=big-jays-bustaurant. - vite dev server boots on :5186 and serves the entry. Companion branch: extension repo aiolabs/restaurant on branch feat/restaurant-by-slug already provides GET /restaurants/by-slug/{slug} that the webapp will consume in commit 3.
This commit is contained in:
parent
7bc92e21b8
commit
41fbad3d90
10 changed files with 526 additions and 2 deletions
|
|
@ -30,8 +30,11 @@
|
||||||
"dev:forum": "vite --host --config vite.forum.config.ts",
|
"dev:forum": "vite --host --config vite.forum.config.ts",
|
||||||
"build:forum": "vue-tsc -b && vite build --config vite.forum.config.ts",
|
"build:forum": "vue-tsc -b && vite build --config vite.forum.config.ts",
|
||||||
"preview:forum": "vite preview --host --config vite.forum.config.ts",
|
"preview:forum": "vite preview --host --config vite.forum.config.ts",
|
||||||
"dev:all": "concurrently -n hub,libra,sortir,wallet,chat,forum,market,tasks -c blue,magenta,cyan,yellow,green,blue,red,gray \"npm:dev\" \"npm:dev:libra\" \"npm:dev:activities\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\"",
|
"dev:restaurant": "vite --host --config vite.restaurant.config.ts",
|
||||||
"build:demo": "npm run build && VITE_BASE_PATH=/sortir/ npm run build:activities && VITE_BASE_PATH=/libra/ npm run build:libra && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks",
|
"build:restaurant": "vue-tsc -b && vite build --config vite.restaurant.config.ts",
|
||||||
|
"preview:restaurant": "vite preview --host --config vite.restaurant.config.ts",
|
||||||
|
"dev:all": "concurrently -n hub,libra,sortir,wallet,chat,forum,market,tasks,restaurant -c blue,magenta,cyan,yellow,green,blue,red,gray,green \"npm:dev\" \"npm:dev:libra\" \"npm:dev:activities\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\" \"npm:dev:restaurant\"",
|
||||||
|
"build:demo": "npm run build && VITE_BASE_PATH=/sortir/ npm run build:activities && VITE_BASE_PATH=/libra/ npm run build:libra && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks && VITE_BASE_PATH=/restaurant/ npm run build:restaurant",
|
||||||
"electron:dev": "concurrently \"vite --host\" \"electron-forge start\"",
|
"electron:dev": "concurrently \"vite --host\" \"electron-forge start\"",
|
||||||
"electron:build": "vue-tsc -b && vite build && electron-builder",
|
"electron:build": "vue-tsc -b && vite build && electron-builder",
|
||||||
"electron:package": "electron-builder",
|
"electron:package": "electron-builder",
|
||||||
|
|
|
||||||
20
restaurant.html
Normal file
20
restaurant.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
||||||
|
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||||
|
<title>Restaurant — Order</title>
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Restaurant">
|
||||||
|
<meta name="description" content="Order from your local Nostr-native restaurant with Lightning payments">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/restaurant-app/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -173,6 +173,10 @@ export const SERVICE_TOKENS = {
|
||||||
|
|
||||||
// Expenses services
|
// Expenses services
|
||||||
EXPENSES_API: Symbol('expensesAPI'),
|
EXPENSES_API: Symbol('expensesAPI'),
|
||||||
|
|
||||||
|
// Restaurant services
|
||||||
|
RESTAURANT_API: Symbol('restaurantAPI'),
|
||||||
|
RESTAURANT_NOSTR_SYNC: Symbol('restaurantNostrSync'),
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
// Type-safe injection helpers
|
// Type-safe injection helpers
|
||||||
|
|
|
||||||
70
src/modules/restaurant/index.ts
Normal file
70
src/modules/restaurant/index.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import type { App } from 'vue'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import type { ModulePlugin } from '@/core/types'
|
||||||
|
|
||||||
|
// v1 skeleton — REST-only, single-venue, URL-driven (/r/:slug).
|
||||||
|
//
|
||||||
|
// Feature-roadmap context (do NOT build in v1; see issues on
|
||||||
|
// aiolabs/restaurant):
|
||||||
|
// #1 PDF menu, #2 tier modes, #3 inventory, #4 kitchen workflow,
|
||||||
|
// #5 loyalty, #6 cost-of-goods, #7 deployment/monetization,
|
||||||
|
// #8 festival/aggregator (NIP-51), #9 NIP-17 order transport.
|
||||||
|
//
|
||||||
|
// Future-compatibility scaffolding baked in even at v1:
|
||||||
|
// • Cart store keys by restaurant_id (multi-restaurant ready
|
||||||
|
// for #8 without a refactor).
|
||||||
|
// • OrderStatus is an open string type (#4 may add states).
|
||||||
|
// • MenuItem.extra carries forward-compatible metadata for
|
||||||
|
// inventory (#3), cost-of-goods (#6), loyalty (#5),
|
||||||
|
// mode-gated badges (#2).
|
||||||
|
// • Module config has a `features: Record<string, boolean>`
|
||||||
|
// slot reserved for tier gating (#2).
|
||||||
|
// • useCheckout builds CreateOrder through a single
|
||||||
|
// buildCreateOrder() helper so loyalty (#5) can inject
|
||||||
|
// loyalty fields without rewriting the flow.
|
||||||
|
|
||||||
|
export interface RestaurantModuleConfig {
|
||||||
|
apiBaseUrl: string
|
||||||
|
defaultSlug: string
|
||||||
|
orderPollMs: number
|
||||||
|
currencyDisplay: 'sats' | 'msat'
|
||||||
|
features: Record<string, boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaurant Module Plugin (v1 skeleton).
|
||||||
|
*
|
||||||
|
* The real surface — types/RestaurantAPI/views/cart/checkout/Nostr —
|
||||||
|
* lands across commits 3–8. This file is the lifecycle anchor and
|
||||||
|
* the route table.
|
||||||
|
*/
|
||||||
|
export const restaurantModule: ModulePlugin = {
|
||||||
|
name: 'restaurant',
|
||||||
|
version: '0.1.0',
|
||||||
|
dependencies: ['base'],
|
||||||
|
|
||||||
|
async install(_app: App, options?: { config?: RestaurantModuleConfig }) {
|
||||||
|
console.log('🍴 Installing restaurant module…')
|
||||||
|
|
||||||
|
if (!options?.config) {
|
||||||
|
throw new Error('Restaurant module requires configuration')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services (RestaurantAPI, RestaurantNostrSync) are wired in
|
||||||
|
// commits 3 and 8 respectively. v1 skeleton only registers
|
||||||
|
// the route table.
|
||||||
|
|
||||||
|
console.log('✅ Restaurant module installed')
|
||||||
|
},
|
||||||
|
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'restaurant-home',
|
||||||
|
component: () => import('./views/HomePage.vue'),
|
||||||
|
meta: { requiresAuth: false, title: 'Restaurant' },
|
||||||
|
},
|
||||||
|
] as RouteRecordRaw[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restaurantModule
|
||||||
32
src/modules/restaurant/views/HomePage.vue
Normal file
32
src/modules/restaurant/views/HomePage.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
// v1 skeleton placeholder. Real implementation lands in commit 4:
|
||||||
|
// - Redirect to `/r/${defaultSlug}` when one is configured.
|
||||||
|
// - Otherwise show a slug input + "recent venues" list pulled
|
||||||
|
// from STORAGE_SERVICE['restaurant.recentRestaurants.v1'].
|
||||||
|
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import appConfig from '@/app.config'
|
||||||
|
|
||||||
|
const defaultSlug = computed<string>(
|
||||||
|
() =>
|
||||||
|
(appConfig.modules.restaurant?.config as { defaultSlug?: string })
|
||||||
|
?.defaultSlug || ''
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="mx-auto max-w-md p-6 text-center text-foreground">
|
||||||
|
<h1 class="text-2xl font-semibold">Restaurant — Order</h1>
|
||||||
|
<p class="mt-4 text-sm text-muted-foreground">
|
||||||
|
v1 skeleton. Real browse / cart / checkout land in commits 4–8.
|
||||||
|
</p>
|
||||||
|
<p v-if="defaultSlug" class="mt-6 text-sm">
|
||||||
|
Default venue:
|
||||||
|
<code class="font-mono text-primary">{{ defaultSlug }}</code>
|
||||||
|
</p>
|
||||||
|
<p v-else class="mt-6 text-xs text-muted-foreground">
|
||||||
|
Set <code class="font-mono">VITE_RESTAURANT_DEFAULT_SLUG</code> to
|
||||||
|
auto-redirect.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
45
src/restaurant-app/App.vue
Normal file
45
src/restaurant-app/App.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { Utensils, ShoppingCart, ReceiptText } from 'lucide-vue-next'
|
||||||
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
|
import type { BottomTab } from '@/components/layout/BottomNav.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// The cart store hooks in during commit 5; v1 skeleton renders the
|
||||||
|
// badge as null. Bottom-nav lives in AppShell and works fine with
|
||||||
|
// zero badge values.
|
||||||
|
const cartItemCount = computed<number | null>(() => null)
|
||||||
|
|
||||||
|
const tabs = computed<BottomTab[]>(() => [
|
||||||
|
{ name: 'Browse', icon: Utensils, path: '/' },
|
||||||
|
{
|
||||||
|
name: 'Cart',
|
||||||
|
icon: ShoppingCart,
|
||||||
|
path: '/cart',
|
||||||
|
badge: cartItemCount.value,
|
||||||
|
},
|
||||||
|
{ name: 'Orders', icon: ReceiptText, path: '/orders' },
|
||||||
|
])
|
||||||
|
|
||||||
|
function isActive(path: string): boolean {
|
||||||
|
if (path === '/') {
|
||||||
|
return (
|
||||||
|
route.path === '/' ||
|
||||||
|
route.path.startsWith('/r/')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (path === '/cart') {
|
||||||
|
return route.path === '/cart' || route.path === '/checkout'
|
||||||
|
}
|
||||||
|
if (path === '/orders') {
|
||||||
|
return route.path === '/orders' || route.path.startsWith('/orders/')
|
||||||
|
}
|
||||||
|
return route.path.startsWith(path)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AppShell :tabs="tabs" :is-active="isActive" />
|
||||||
|
</template>
|
||||||
73
src/restaurant-app/app.config.ts
Normal file
73
src/restaurant-app/app.config.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import type { AppConfig } from '@/core/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone Restaurant app configuration.
|
||||||
|
*
|
||||||
|
* Customer-facing surface for the LNbits "restaurant" extension. v1
|
||||||
|
* is single-venue (URL-driven via `/r/:slug`); the bundle ships
|
||||||
|
* REST-only order placement. Festival/aggregator (NIP-51) and
|
||||||
|
* NIP-17 transport are deferred — see issues #8 / #9 on
|
||||||
|
* aiolabs/restaurant.
|
||||||
|
*/
|
||||||
|
export const appConfig: AppConfig = {
|
||||||
|
modules: {
|
||||||
|
base: {
|
||||||
|
name: 'base',
|
||||||
|
enabled: true,
|
||||||
|
lazy: false,
|
||||||
|
config: {
|
||||||
|
nostr: {
|
||||||
|
relays: JSON.parse(
|
||||||
|
import.meta.env.VITE_NOSTR_RELAYS ||
|
||||||
|
'["wss://relay.damus.io", "wss://nos.lol"]'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
sessionTimeout: 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
pwa: {
|
||||||
|
autoPrompt: true,
|
||||||
|
},
|
||||||
|
imageUpload: {
|
||||||
|
baseUrl:
|
||||||
|
import.meta.env.VITE_PICTRS_BASE_URL ||
|
||||||
|
'https://img.mydomain.com',
|
||||||
|
maxSizeMB: 10,
|
||||||
|
acceptedTypes: [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/webp',
|
||||||
|
'image/avif',
|
||||||
|
'image/gif',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
restaurant: {
|
||||||
|
name: 'restaurant',
|
||||||
|
enabled: true,
|
||||||
|
lazy: false,
|
||||||
|
config: {
|
||||||
|
apiBaseUrl:
|
||||||
|
import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000',
|
||||||
|
defaultSlug: import.meta.env.VITE_RESTAURANT_DEFAULT_SLUG || '',
|
||||||
|
orderPollMs: 5000,
|
||||||
|
currencyDisplay: 'sats' as const,
|
||||||
|
// Reserved for tier-gated UI (aiolabs/restaurant#2:
|
||||||
|
// bar/bistro/full operator modes). Future contributors set
|
||||||
|
// e.g. { inventoryPanel: true, loyaltyRewards: true } to
|
||||||
|
// unlock surfaces gated on the operator's tier. Empty in v1.
|
||||||
|
features: {} as Record<string, boolean>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
features: {
|
||||||
|
pwa: true,
|
||||||
|
pushNotifications: true,
|
||||||
|
electronApp: false,
|
||||||
|
developmentMode: import.meta.env.DEV,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default appConfig
|
||||||
125
src/restaurant-app/app.ts
Normal file
125
src/restaurant-app/app.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { pluginManager } from '@/core/plugin-manager'
|
||||||
|
import { eventBus } from '@/core/event-bus'
|
||||||
|
import { container } from '@/core/di-container'
|
||||||
|
|
||||||
|
import appConfig from './app.config'
|
||||||
|
import baseModule from '@/modules/base'
|
||||||
|
import restaurantModule from '@/modules/restaurant'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
import '@/assets/index.css'
|
||||||
|
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||||
|
import {
|
||||||
|
installLenientAuthGuard,
|
||||||
|
markAuthReady,
|
||||||
|
catchAllRoute,
|
||||||
|
} from '@/lib/router-helpers'
|
||||||
|
import { acceptTokenFromUrl } from '@/lib/url-token'
|
||||||
|
|
||||||
|
export async function createAppInstance() {
|
||||||
|
console.log('Starting Restaurant app...')
|
||||||
|
|
||||||
|
acceptTokenFromUrl('Restaurant')
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
const moduleRoutes = [
|
||||||
|
...(baseModule.routes || []),
|
||||||
|
...(restaurantModule.routes || []),
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
// The restaurant module owns `/` (HomePage handles redirect to
|
||||||
|
// the configured default slug or shows a discovery prompt); no
|
||||||
|
// top-level redirect like market needs.
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component:
|
||||||
|
import.meta.env.VITE_DEMO_MODE === 'true'
|
||||||
|
? () => import('@/pages/LoginDemo.vue')
|
||||||
|
: () => import('@/pages/Login.vue'),
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
},
|
||||||
|
...moduleRoutes,
|
||||||
|
catchAllRoute,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
installLenientAuthGuard(router)
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
const defaultLocale = import.meta.env
|
||||||
|
.VITE_DEFAULT_LOCALE as AvailableLocale | undefined
|
||||||
|
if (defaultLocale && !localStorage.getItem('user-locale')) {
|
||||||
|
await changeLocale(defaultLocale)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginManager.init(app, router)
|
||||||
|
|
||||||
|
const moduleRegistrations = []
|
||||||
|
|
||||||
|
if (appConfig.modules.base.enabled) {
|
||||||
|
moduleRegistrations.push(
|
||||||
|
pluginManager.register(baseModule, appConfig.modules.base)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConfig.modules.restaurant?.enabled) {
|
||||||
|
moduleRegistrations.push(
|
||||||
|
pluginManager.register(restaurantModule, appConfig.modules.restaurant)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(moduleRegistrations)
|
||||||
|
await pluginManager.installAll()
|
||||||
|
|
||||||
|
// Dynamic import: useAuthService depends on services registered by
|
||||||
|
// pluginManager.installAll() (LNbits API).
|
||||||
|
const { auth } = await import('@/composables/useAuthService')
|
||||||
|
await auth.initialize()
|
||||||
|
markAuthReady(auth)
|
||||||
|
|
||||||
|
app.config.errorHandler = (err, _vm, info) => {
|
||||||
|
console.error('Global error:', err, info)
|
||||||
|
eventBus.emit('app:error', { error: err, info }, 'app')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConfig.features.developmentMode) {
|
||||||
|
;(window as any).__pluginManager = pluginManager
|
||||||
|
;(window as any).__eventBus = eventBus
|
||||||
|
;(window as any).__container = container
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Restaurant app initialized')
|
||||||
|
return { app, router }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startApp() {
|
||||||
|
try {
|
||||||
|
const { app } = await createAppInstance()
|
||||||
|
app.mount('#app')
|
||||||
|
console.log('Restaurant app started!')
|
||||||
|
eventBus.emit('app:started', {}, 'app')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start Restaurant app:', error)
|
||||||
|
document.getElementById('app')!.innerHTML = `
|
||||||
|
<div style="padding: 20px; text-align: center; color: red;">
|
||||||
|
<h1>Failed to Start</h1>
|
||||||
|
<p>${error instanceof Error ? error.message : 'Unknown error'}</p>
|
||||||
|
<p>Please refresh the page.</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/restaurant-app/main.ts
Normal file
20
src/restaurant-app/main.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { startApp } from './app'
|
||||||
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||||
|
import 'vue-sonner/style.css'
|
||||||
|
|
||||||
|
cleanupStaleDevServiceWorkers()
|
||||||
|
|
||||||
|
const intervalMS = 60 * 60 * 1000
|
||||||
|
registerSW({
|
||||||
|
onRegistered(r) {
|
||||||
|
r && setInterval(() => {
|
||||||
|
r.update()
|
||||||
|
}, intervalMS)
|
||||||
|
},
|
||||||
|
onOfflineReady() {
|
||||||
|
console.log('Restaurant app ready to work offline')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
startApp()
|
||||||
132
vite.restaurant.config.ts
Normal file
132
vite.restaurant.config.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { defineConfig, type Plugin } from 'vite'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
|
||||||
|
function restaurantHtmlPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'restaurant-html-rewrite',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use((req, _res, next) => {
|
||||||
|
// Strip query before checking for an extension — JWTs (e.g. ?token=...)
|
||||||
|
// contain dots and would otherwise get mistaken for an asset request.
|
||||||
|
const path = req.url ? req.url.split('?')[0] : ''
|
||||||
|
if (
|
||||||
|
req.url &&
|
||||||
|
!req.url.startsWith('/@') &&
|
||||||
|
!req.url.startsWith('/src/') &&
|
||||||
|
!req.url.startsWith('/node_modules/') &&
|
||||||
|
!path.includes('.')
|
||||||
|
) {
|
||||||
|
req.url = '/restaurant.html'
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vite config for the standalone Restaurant customer app.
|
||||||
|
*
|
||||||
|
* Set VITE_BASE_PATH to deploy under a path prefix:
|
||||||
|
* VITE_BASE_PATH=/restaurant/ → app.${domain}/restaurant/ (shared auth)
|
||||||
|
* (default: /) → restaurant.${domain} (standalone subdomain)
|
||||||
|
*
|
||||||
|
* The companion server is the LNbits "restaurant" extension at
|
||||||
|
* ~/dev/shared/extensions/restaurant. v1 ships single-venue (URL-driven
|
||||||
|
* via /r/:slug); festival/aggregator and NIP-17 transport are tracked
|
||||||
|
* in repo issues #8 and #9 on aiolabs/restaurant.
|
||||||
|
*/
|
||||||
|
export default defineConfig(({ mode }) => ({
|
||||||
|
base: process.env.VITE_BASE_PATH || '/',
|
||||||
|
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
|
||||||
|
cacheDir: 'node_modules/.vite-restaurant',
|
||||||
|
server: {
|
||||||
|
port: 5186,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
restaurantHtmlPlugin(),
|
||||||
|
vue(),
|
||||||
|
tailwindcss(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
devOptions: { enabled: false },
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
|
||||||
|
navigateFallback: 'restaurant.html',
|
||||||
|
navigateFallbackAllowlist: [
|
||||||
|
new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
includeAssets: [
|
||||||
|
'favicon.ico',
|
||||||
|
'apple-touch-icon.png',
|
||||||
|
'mask-icon.svg',
|
||||||
|
'icon-192.png',
|
||||||
|
'icon-512.png',
|
||||||
|
'icon-maskable-192.png',
|
||||||
|
'icon-maskable-512.png',
|
||||||
|
],
|
||||||
|
manifest: {
|
||||||
|
name: 'Restaurant — Order',
|
||||||
|
short_name: 'Restaurant',
|
||||||
|
description: 'Order from your local Nostr-native restaurant with Lightning payments',
|
||||||
|
// Green to differentiate from market red. PDF tile is purple
|
||||||
|
// (see ~/dev/shared/extensions/restaurant/static/image/restaurant.png).
|
||||||
|
theme_color: '#16a34a',
|
||||||
|
background_color: '#ffffff',
|
||||||
|
display: 'standalone',
|
||||||
|
orientation: 'portrait-primary',
|
||||||
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
scope: process.env.VITE_BASE_PATH || '/',
|
||||||
|
id: 'restaurant-app',
|
||||||
|
categories: ['food', 'shopping'],
|
||||||
|
lang: 'en',
|
||||||
|
icons: [
|
||||||
|
{ src: 'icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
||||||
|
{ src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
||||||
|
{ src: 'icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' },
|
||||||
|
{ src: 'icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ViteImageOptimizer({
|
||||||
|
jpg: { quality: 80 },
|
||||||
|
png: { quality: 80 },
|
||||||
|
webp: { lossless: true },
|
||||||
|
}),
|
||||||
|
mode === 'analyze' &&
|
||||||
|
visualizer({
|
||||||
|
open: true,
|
||||||
|
filename: 'dist-restaurant/stats.html',
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// ORDER MATTERS — @/app.config must precede @ (first-match-wins).
|
||||||
|
'@/app.config': fileURLToPath(new URL('./src/restaurant-app/app.config.ts', import.meta.url)),
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist-restaurant',
|
||||||
|
rollupOptions: {
|
||||||
|
input: 'restaurant.html',
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||||
|
'ui-vendor': ['radix-vue', '@vueuse/core'],
|
||||||
|
'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
|
},
|
||||||
|
}))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue