diff --git a/.env.example b/.env.example index 1643766..c92c023 100644 --- a/.env.example +++ b/.env.example @@ -42,63 +42,3 @@ VITE_MARKET_NADDR=naddr1qqjxgdp4vv6rydej943n2dny956rwwf4943xzwfc95ekyd3evenrsvrr # VITE_LIGHTNING_ENABLED=true # OBSOLETE: Not used in codebase - config.market.defaultCurrency is never consumed # VITE_MARKET_DEFAULT_CURRENCY=sat - -# ─────────────────────────────────────────────────────────────────────── -# Hub → standalone navigation URLs -# -# Each chakra tile in the hub builds an from these env vars and -# (when authenticated) appends ?token= so the destination -# auto-logs in via acceptTokenFromUrl(). -# -# Trailing slash matters under path-mode deployment: -# ✓ https://demo.example.com/market/ asset URLs resolve correctly -# ✗ https://demo.example.com/market relies on nginx 301 to add the -# slash; brittle, extra round trip. -# -# In LOCAL DEV with `npm run dev:all` use the per-app dev ports (defined -# in the vite configs): -# VITE_HUB_ACTIVITIES_URL=http://localhost:5181 -# VITE_HUB_CASTLE_URL=http://localhost:5180 -# VITE_HUB_WALLET_URL=http://localhost:5182 -# VITE_HUB_CHAT_URL=http://localhost:5183 -# VITE_HUB_FORUM_URL=http://localhost:5184 -# VITE_HUB_MARKET_URL=http://localhost:5185 -# VITE_HUB_TASKS_URL=http://localhost:5186 -# -# In PATH-MODE production (recommended for demo) — note the trailing slash: -# VITE_HUB_ACTIVITIES_URL=https://demo.example.com/activities/ -# VITE_HUB_CASTLE_URL=https://demo.example.com/castle/ -# VITE_HUB_WALLET_URL=https://demo.example.com/wallet/ -# VITE_HUB_CHAT_URL=https://demo.example.com/chat/ -# VITE_HUB_FORUM_URL=https://demo.example.com/forum/ -# VITE_HUB_MARKET_URL=https://demo.example.com/market/ -# VITE_HUB_TASKS_URL=https://demo.example.com/tasks/ -# -# In SUBDOMAIN-MODE production: -# VITE_HUB_ACTIVITIES_URL=https://sortir.example.com -# VITE_HUB_CASTLE_URL=https://castle.example.com -# ...etc -# ─────────────────────────────────────────────────────────────────────── -VITE_HUB_ACTIVITIES_URL= -VITE_HUB_CASTLE_URL= -VITE_HUB_WALLET_URL= -VITE_HUB_CHAT_URL= -VITE_HUB_FORUM_URL= -VITE_HUB_MARKET_URL= -VITE_HUB_TASKS_URL= - -# ─────────────────────────────────────────────────────────────────────── -# VITE_BASE_PATH — build-time only, NOT per .env -# -# Each standalone vite config (vite..config.ts) reads VITE_BASE_PATH -# at build time. For path-mode deployment, set it as a shell variable when -# you build, NOT in this .env file (which is read at runtime by the -# bundle): -# -# VITE_BASE_PATH=/market/ npm run build:market -# VITE_BASE_PATH=/wallet/ npm run build:wallet -# ... -# -# The default '/' (no override) is what you want for subdomain-mode and -# for `npm run dev:all`. -# ─────────────────────────────────────────────────────────────────────── diff --git a/nginx.conf.example b/nginx.conf.example index 662b4c9..05cf1f6 100644 --- a/nginx.conf.example +++ b/nginx.conf.example @@ -11,159 +11,115 @@ http { real_ip_header X-Forwarded-For; real_ip_recursive on; - # ─────────────────────────────────────────────────────────────────────── - # PATH-MODE deployment (recommended) - # - # demo../ — minimal AIO chakra hub - # demo../activities/ — Sortir / activities standalone - # demo../market/ — marketplace standalone - # demo../wallet/ — wallet standalone - # demo../chat/ — chat standalone - # demo../forum/ — forum standalone - # demo../tasks/ — tasks standalone - # demo../castle/ — castle (accounting) standalone - # - # Each standalone is built with VITE_BASE_PATH=// so its asset URLs - # are prefixed correctly. The hub's chakra tiles point at the canonical - # trailing-slash path (VITE_HUB__URL=https://demo.//). - # - # Per-app no-trailing-slash → with-slash 301 redirects exist for users - # who hand-type the URL or follow a stripped-slash link. - # - # All static assets (JS / CSS / images / SVGs) are MIME-typed and image - # types get a 6-month cache-control. - # ─────────────────────────────────────────────────────────────────────── + # Reusable location blocks + # JS / CSS / image MIME and caching + map $sent_http_content_type $cache_static { + default "off"; + ~image/ "6M"; + } + + # ─────────────────────────────────────────────────────────────── + # AIO hub — minimal app at app. + # Serves only the chakra icon hub + base infra (profile, relays). + # ─────────────────────────────────────────────────────────────── server { listen 8080; - server_name demo..; + server_name app..; - # Hub at the root root /var/www/aio/dist; index index.html; - location = / { try_files $uri /index.html; } - location / { - # Default: serve from hub bundle if no // prefix matched. - try_files $uri $uri/ /index.html; - } - # ── Activities (Sortir) ────────────────────────────────────────── - location = /activities { return 301 /activities/$is_args$args; } - location /activities/ { - alias /var/www/aio/dist-activities/; - try_files $uri $uri/ /activities.html; - } - - # ── Market ─────────────────────────────────────────────────────── - location = /market { return 301 /market/$is_args$args; } - location /market/ { - alias /var/www/aio/dist-market/; - try_files $uri $uri/ /market.html; - } - - # ── Wallet ─────────────────────────────────────────────────────── - location = /wallet { return 301 /wallet/$is_args$args; } - location /wallet/ { - alias /var/www/aio/dist-wallet/; - try_files $uri $uri/ /wallet.html; - } - - # ── Chat ───────────────────────────────────────────────────────── - location = /chat { return 301 /chat/$is_args$args; } - location /chat/ { - alias /var/www/aio/dist-chat/; - try_files $uri $uri/ /chat.html; - } - - # ── Forum ──────────────────────────────────────────────────────── - location = /forum { return 301 /forum/$is_args$args; } - location /forum/ { - alias /var/www/aio/dist-forum/; - try_files $uri $uri/ /forum.html; - } - - # ── Tasks ──────────────────────────────────────────────────────── - location = /tasks { return 301 /tasks/$is_args$args; } - location /tasks/ { - alias /var/www/aio/dist-tasks/; - try_files $uri $uri/ /tasks.html; - } - - # ── Castle (accounting) ────────────────────────────────────────── - location = /castle { return 301 /castle/$is_args$args; } - location /castle/ { - alias /var/www/aio/dist-castle/; - try_files $uri $uri/ /castle.html; - } - - # ── Static asset MIME / cache (applies to all bundles) ─────────── + location / { try_files $uri $uri/ /index.html; } location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } location ~* \.css$ { types { text/css css; } default_type text/css; } location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } } - # ─────────────────────────────────────────────────────────────────────── - # Optional subdomain shortcuts → canonical path - # - # If you want pretty subdomain URLs that funnel into the path-mode - # canonical, add 301 redirects per app. Example: - # - # events.demo.. → demo../activities/ - # market.demo.. → demo../market/ - # ─────────────────────────────────────────────────────────────────────── + # ─────────────────────────────────────────────────────────────── + # Standalone module PWAs — one server block per subdomain + # ─────────────────────────────────────────────────────────────── + + # Marketplace — Muladhara server { listen 8080; - server_name events.demo..; - return 301 https://demo../activities/$request_uri; - } - server { - listen 8080; - server_name market.demo..; - return 301 https://demo../market/$request_uri; - } - server { - listen 8080; - server_name wallet.demo..; - return 301 https://demo../wallet/$request_uri; - } - server { - listen 8080; - server_name chat.demo..; - return 301 https://demo../chat/$request_uri; - } - server { - listen 8080; - server_name forum.demo..; - return 301 https://demo../forum/$request_uri; - } - server { - listen 8080; - server_name tasks.demo..; - return 301 https://demo../tasks/$request_uri; - } - server { - listen 8080; - server_name castle.demo..; - return 301 https://demo../castle/$request_uri; + server_name market..; + root /var/www/aio/dist-market; + index market.html; + location / { try_files $uri $uri/ /market.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } } - # ─────────────────────────────────────────────────────────────────────── - # SUBDOMAIN-MODE deployment (alternative — pure subdomains, no /path/) - # - # If you'd rather give each standalone its own subdomain and skip the - # path-mode entirely: - # - # server { server_name app.; root /var/www/aio/dist; ... } - # server { server_name market.; root /var/www/aio/dist-market; ... } - # server { server_name sortir.; root /var/www/aio/dist-activities; ... } - # server { server_name wallet.; root /var/www/aio/dist-wallet; ... } - # server { server_name chat.; root /var/www/aio/dist-chat; ... } - # server { server_name forum.; root /var/www/aio/dist-forum; ... } - # server { server_name tasks.; root /var/www/aio/dist-tasks; ... } - # server { server_name castle.; root /var/www/aio/dist-castle; ... } - # - # Each block uses `location / { try_files $uri $uri/ /.html; }`. - # In subdomain mode, build each standalone WITHOUT VITE_BASE_PATH (the - # default `/` is correct), and set VITE_HUB__URL to the subdomain - # in the hub's env (e.g. VITE_HUB_MARKET_URL=https://market.). - # ─────────────────────────────────────────────────────────────────────── + # Activities — Swadhisthana + server { + listen 8080; + server_name sortir..; + root /var/www/aio/dist-activities; + index activities.html; + location / { try_files $uri $uri/ /activities.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Wallet — Manipura + server { + listen 8080; + server_name wallet..; + root /var/www/aio/dist-wallet; + index wallet.html; + location / { try_files $uri $uri/ /wallet.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Chat — Anahata + server { + listen 8080; + server_name chat..; + root /var/www/aio/dist-chat; + index chat.html; + location / { try_files $uri $uri/ /chat.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Forum — Vishuddha + server { + listen 8080; + server_name forum..; + root /var/www/aio/dist-forum; + index forum.html; + location / { try_files $uri $uri/ /forum.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Tasks — Ajna + server { + listen 8080; + server_name tasks..; + root /var/www/aio/dist-tasks; + index tasks.html; + location / { try_files $uri $uri/ /tasks.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } + + # Castle — Sahasrara (accounting) + server { + listen 8080; + server_name castle..; + root /var/www/aio/dist-castle; + index castle.html; + location / { try_files $uri $uri/ /castle.html; } + location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } + location ~* \.css$ { types { text/css css; } default_type text/css; } + location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 3e1a597..9b77759 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -168,15 +168,6 @@ const messages: LocaleMessages = { notAvailable: 'Income submission is not yet available. This feature is coming soon.', }, }, - market: { - auth: { - loginPrompt: 'Log in to place your order', - logIn: 'Log in', - logInToCheckout: 'Log in to checkout', - nostrKeyRequired: 'A Nostr identity is required', - nostrKeyDescription: 'Configure your Nostr public key in Profile settings to place orders.', - }, - }, dateTimeFormats: { short: { year: 'numeric', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 53a7d40..82f0816 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -168,15 +168,6 @@ const messages: LocaleMessages = { notAvailable: 'El registro de ingresos a\u00fan no est\u00e1 disponible. Esta funci\u00f3n llegar\u00e1 pronto.', }, }, - market: { - auth: { - loginPrompt: 'Inicia sesi\u00f3n para realizar tu pedido', - logIn: 'Iniciar sesi\u00f3n', - logInToCheckout: 'Iniciar sesi\u00f3n para finalizar compra', - nostrKeyRequired: 'Se requiere una identidad Nostr', - nostrKeyDescription: 'Configura tu clave p\u00fablica Nostr en los ajustes del perfil para realizar pedidos.', - }, - }, dateTimeFormats: { short: { year: 'numeric', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 5f11684..b4b4c08 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -168,15 +168,6 @@ const messages: LocaleMessages = { notAvailable: 'La saisie de revenus n\u2019est pas encore disponible. Cette fonctionnalit\u00e9 arrive bient\u00f4t.', }, }, - market: { - auth: { - loginPrompt: 'Connectez-vous pour passer commande', - logIn: 'Se connecter', - logInToCheckout: 'Se connecter pour commander', - nostrKeyRequired: 'Une identit\u00e9 Nostr est requise', - nostrKeyDescription: 'Configurez votre cl\u00e9 publique Nostr dans les param\u00e8tres du profil pour passer commande.', - }, - }, dateTimeFormats: { short: { year: 'numeric', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index d81753a..ddd20c6 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -144,16 +144,6 @@ export interface LocaleMessages { notAvailable: string } } - // Market module - market?: { - auth: { - loginPrompt: string - logIn: string - logInToCheckout: string - nostrKeyRequired: string - nostrKeyDescription: string - } - } // Add date/time formats dateTimeFormats: { short: { diff --git a/src/market-app/App.vue b/src/market-app/App.vue index 9aed606..8c7f8ba 100644 --- a/src/market-app/App.vue +++ b/src/market-app/App.vue @@ -6,9 +6,8 @@ import LoginDialog from '@/components/auth/LoginDialog.vue' import { useTheme } from '@/components/theme-provider' import { toast } from 'vue-sonner' import { useAuth } from '@/composables/useAuthService' -import { - Store, ShoppingCart, Package, LogIn, User as UserIcon, -} from 'lucide-vue-next' +import { Button } from '@/components/ui/button' +import { LogIn } from 'lucide-vue-next' const route = useRoute() const router = useRouter() @@ -18,47 +17,8 @@ const { isAuthenticated } = useAuth() const showLoginDialog = ref(false) -interface Tab { - name: string - icon: any - path?: string - authRequired?: boolean - onClick?: () => void -} - -const bottomTabs = computed(() => [ - { name: 'Browse', icon: Store, path: '/market' }, - { name: 'Cart', icon: ShoppingCart, path: '/cart' }, - { name: 'My Store', icon: Package, path: '/market/dashboard', authRequired: true }, - isAuthenticated.value - ? { name: 'Profile', icon: UserIcon, path: '/profile' } - : { name: 'Log in', icon: LogIn, path: '/login' }, -]) - const isLoginPage = computed(() => route.path === '/login') -function isActiveTab(tab: Tab): boolean { - if (!tab.path) return false - if (tab.path === '/market') { - return route.path === '/market' || route.path.startsWith('/market/stall/') || route.path.startsWith('/market/product/') - } - if (tab.path === '/cart') return route.path === '/cart' || route.path.startsWith('/checkout/') - return route.path.startsWith(tab.path) -} - -function onTabClick(tab: Tab) { - if (tab.authRequired && !isAuthenticated.value) { - toast.info(`${tab.name} requires login`, { - action: { - label: 'Log in', - onClick: () => router.push('/login'), - }, - }) - return - } - if (tab.path) router.push(tab.path) -} - async function handleLoginSuccess() { showLoginDialog.value = false toast.success('Welcome!') @@ -70,33 +30,15 @@ async function handleLoginSuccess() {
-
+
+ +
+ +
- -
diff --git a/src/modules/market/composables/useMarket.ts b/src/modules/market/composables/useMarket.ts index 47ccaa0..cadb14b 100644 --- a/src/modules/market/composables/useMarket.ts +++ b/src/modules/market/composables/useMarket.ts @@ -64,15 +64,17 @@ export function useMarket() { return 'disconnected' }) - // Load market from naddr (or empty for public browse mode) + // Load market from naddr const loadMarket = async (naddr: string) => { return await marketOperation.execute(async () => { - // Parse naddr (when given) to get market identifier + pubkey. - // Empty naddr + unauth user → public browse mode (no pubkey filter). - const parts = naddr ? naddr.split(':') : [] + // Parse naddr to get market data const marketData = { - identifier: parts[2] || 'default', - pubkey: parts[1] || authService.user.value?.pubkey || '' + identifier: naddr.split(':')[2] || 'default', + pubkey: naddr.split(':')[1] || authService.user.value?.pubkey || '' + } + + if (!marketData.pubkey) { + throw new Error('No pubkey available for market') } await loadMarketData(marketData) @@ -85,30 +87,8 @@ export function useMarket() { // Load market data from Nostr events const loadMarketData = async (marketData: any) => { try { - console.log('🛒 Loading market data for:', { identifier: marketData.identifier, pubkey: marketData.pubkey?.slice(0, 8) || '(public browse)' }) - - // Public browse mode: no curated naddr and no logged-in user. - // Skip the kind 30019 query and use a "Discover" placeholder market; - // loadStalls/loadProducts treat browseAll=true as "no authors filter". - if (!marketData.pubkey) { - const market = { - d: marketData.identifier, - pubkey: '', - relays: config.nostr.relays, - selected: true, - browseAll: true, - opts: { - name: 'Discover', - description: 'Public stalls and products from your relays', - merchants: [], - ui: {} - } - } - marketStore.addMarket(market) - marketStore.setActiveMarket(market) - return - } - + console.log('🛒 Loading market data for:', { identifier: marketData.identifier, pubkey: marketData.pubkey?.slice(0, 8) }) + // Check if we can query events (relays are connected) if (!isConnected.value) { console.log('🛒 Not connected to relays, creating default market') @@ -125,12 +105,12 @@ export function useMarket() { ui: {} } } - + marketStore.addMarket(market) marketStore.setActiveMarket(market) return } - + // Load market data from Nostr events // Fetch market configuration event const events = await relayHub.queryEvents([ @@ -168,11 +148,7 @@ export function useMarket() { relays: config.nostr.relays, selected: true, opts: { - // Logged-in user has no published market event yet — show their - // namespace as "My Market". Avoids leaking VITE_APP_NAME (which - // is the brand of whichever standalone app is bundled, e.g. - // "Sortir" for activities) into the market label. - name: 'My Market', + name: `${import.meta.env.VITE_APP_NAME} Market`, description: 'A communal market to sell your goods', merchants: [], ui: {} @@ -203,7 +179,7 @@ export function useMarket() { } } - // Load stalls from market merchants (or all stalls in public browse mode) + // Load stalls from market merchants const loadStalls = async () => { try { // Get the active market to filter by its merchants @@ -212,20 +188,19 @@ export function useMarket() { return } - const browseAll = (activeMarket as any).browseAll === true - const merchants = [...(activeMarket.opts.merchants || [])] + const merchants = [...(activeMarket.opts.merchants || [])] + + if (merchants.length === 0) { + return + } - if (!browseAll && merchants.length === 0) { - return - } - - // Build filter: in browse-all mode no authors filter; otherwise scope to merchants. - const stallFilter: any = { kinds: [MARKET_EVENT_KINDS.STALL] } - if (!browseAll && merchants.length > 0) { - stallFilter.authors = merchants - } - - const events = await relayHub.queryEvents([stallFilter]) + // Fetch stall events from market merchants only + const events = await relayHub.queryEvents([ + { + kinds: [MARKET_EVENT_KINDS.STALL], + authors: merchants + } + ]) console.log('🛒 Found', events.length, 'stall events for', merchants.length, 'merchants') @@ -270,7 +245,7 @@ export function useMarket() { } } - // Load products from market stalls (or all products in public browse mode) + // Load products from market stalls const loadProducts = async () => { try { const activeMarket = marketStore.activeMarket @@ -278,19 +253,18 @@ export function useMarket() { return } - const browseAll = (activeMarket as any).browseAll === true const merchants = [...(activeMarket.opts.merchants || [])] + if (merchants.length === 0) { + return + } - if (!browseAll && merchants.length === 0) { - return - } - - const productFilter: any = { kinds: [MARKET_EVENT_KINDS.PRODUCT] } - if (!browseAll && merchants.length > 0) { - productFilter.authors = merchants - } - - const events = await relayHub.queryEvents([productFilter]) + // Fetch product events from market merchants + const events = await relayHub.queryEvents([ + { + kinds: [MARKET_EVENT_KINDS.PRODUCT], + authors: merchants + } + ]) console.log('🛒 Found', events.length, 'product events for', merchants.length, 'merchants') diff --git a/src/modules/market/views/CheckoutPage.vue b/src/modules/market/views/CheckoutPage.vue index 997ad39..9e87ab7 100644 --- a/src/modules/market/views/CheckoutPage.vue +++ b/src/modules/market/views/CheckoutPage.vue @@ -241,7 +241,6 @@
- -

+

{{ orderValidationMessage }}

-

- {{ t('market.auth.loginPrompt') }} -

@@ -275,9 +262,7 @@