Compare commits

..

No commits in common. "9161e0cf6825599d45e57cafae94de98aca43480" and "51aff8cc8747d31b7755ea639493f640562858b0" have entirely different histories.

18 changed files with 152 additions and 441 deletions

View file

@ -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 <a href> from these env vars and
# (when authenticated) appends ?token=<lnbits_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.<name>.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`.
# ───────────────────────────────────────────────────────────────────────

View file

@ -11,159 +11,115 @@ http {
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# ───────────────────────────────────────────────────────────────────────
# PATH-MODE deployment (recommended)
#
# demo.<domain>.<com>/ — minimal AIO chakra hub
# demo.<domain>.<com>/activities/ — Sortir / activities standalone
# demo.<domain>.<com>/market/ — marketplace standalone
# demo.<domain>.<com>/wallet/ — wallet standalone
# demo.<domain>.<com>/chat/ — chat standalone
# demo.<domain>.<com>/forum/ — forum standalone
# demo.<domain>.<com>/tasks/ — tasks standalone
# demo.<domain>.<com>/castle/ — castle (accounting) standalone
#
# Each standalone is built with VITE_BASE_PATH=/<name>/ so its asset URLs
# are prefixed correctly. The hub's chakra tiles point at the canonical
# trailing-slash path (VITE_HUB_<NAME>_URL=https://demo.<domain>/<name>/).
#
# 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.<domain>
# Serves only the chakra icon hub + base infra (profile, relays).
# ───────────────────────────────────────────────────────────────
server {
listen 8080;
server_name demo.<domain>.<com>;
server_name app.<domain>.<com>;
# 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 /<app>/ 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.<domain>.<com> → demo.<domain>.<com>/activities/
# market.demo.<domain>.<com> → demo.<domain>.<com>/market/
# ───────────────────────────────────────────────────────────────────────
# ───────────────────────────────────────────────────────────────
# Standalone module PWAs — one server block per subdomain
# ───────────────────────────────────────────────────────────────
# Marketplace — Muladhara
server {
listen 8080;
server_name events.demo.<domain>.<com>;
return 301 https://demo.<domain>.<com>/activities/$request_uri;
}
server {
listen 8080;
server_name market.demo.<domain>.<com>;
return 301 https://demo.<domain>.<com>/market/$request_uri;
}
server {
listen 8080;
server_name wallet.demo.<domain>.<com>;
return 301 https://demo.<domain>.<com>/wallet/$request_uri;
}
server {
listen 8080;
server_name chat.demo.<domain>.<com>;
return 301 https://demo.<domain>.<com>/chat/$request_uri;
}
server {
listen 8080;
server_name forum.demo.<domain>.<com>;
return 301 https://demo.<domain>.<com>/forum/$request_uri;
}
server {
listen 8080;
server_name tasks.demo.<domain>.<com>;
return 301 https://demo.<domain>.<com>/tasks/$request_uri;
}
server {
listen 8080;
server_name castle.demo.<domain>.<com>;
return 301 https://demo.<domain>.<com>/castle/$request_uri;
server_name market.<domain>.<com>;
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.<domain>; root /var/www/aio/dist; ... }
# server { server_name market.<domain>; root /var/www/aio/dist-market; ... }
# server { server_name sortir.<domain>; root /var/www/aio/dist-activities; ... }
# server { server_name wallet.<domain>; root /var/www/aio/dist-wallet; ... }
# server { server_name chat.<domain>; root /var/www/aio/dist-chat; ... }
# server { server_name forum.<domain>; root /var/www/aio/dist-forum; ... }
# server { server_name tasks.<domain>; root /var/www/aio/dist-tasks; ... }
# server { server_name castle.<domain>; root /var/www/aio/dist-castle; ... }
#
# Each block uses `location / { try_files $uri $uri/ /<name>.html; }`.
# In subdomain mode, build each standalone WITHOUT VITE_BASE_PATH (the
# default `/` is correct), and set VITE_HUB_<NAME>_URL to the subdomain
# in the hub's env (e.g. VITE_HUB_MARKET_URL=https://market.<domain>).
# ───────────────────────────────────────────────────────────────────────
# Activities — Swadhisthana
server {
listen 8080;
server_name sortir.<domain>.<com>;
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.<domain>.<com>;
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.<domain>.<com>;
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.<domain>.<com>;
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.<domain>.<com>;
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.<domain>.<com>;
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; }
}
}

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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: {

View file

@ -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<Tab[]>(() => [
{ 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() {
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
<Button variant="ghost" size="icon" class="h-8 w-8" @click="router.push('/login')">
<LogIn class="w-4 h-4" />
</Button>
</div>
<main class="flex-1">
<router-view />
</main>
<nav
v-if="!isLoginPage"
class="fixed bottom-0 left-0 right-0 z-50 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
style="padding-bottom: env(safe-area-inset-bottom)"
>
<div class="flex items-center justify-around h-14 max-w-lg mx-auto">
<button
v-for="tab in bottomTabs"
:key="tab.name"
class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors"
:class="[
isActiveTab(tab)
? 'text-primary'
: 'text-muted-foreground hover:text-foreground',
tab.authRequired && !isAuthenticated ? 'opacity-50' : '',
]"
@click="onTabClick(tab)"
>
<component :is="tab.icon" class="w-5 h-5" />
<span class="text-[10px] font-medium">{{ tab.name }}</span>
</button>
</div>
</nav>
</div>
<Toaster />

View file

@ -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')

View file

@ -241,7 +241,6 @@
<!-- Place Order Button -->
<div class="pt-4 border-t border-border">
<Button
v-if="auth.isAuthenticated.value"
@click="placeOrder"
:disabled="isPlacingOrder || !canPlaceOrder"
class="w-full"
@ -250,21 +249,9 @@
<span v-if="isPlacingOrder" class="animate-spin mr-2"></span>
Place Order - {{ formatPrice(orderTotal, checkoutCart.currency) }}
</Button>
<Button
v-else
@click="router.push('/login')"
class="w-full"
size="lg"
variant="outline"
>
{{ t('market.auth.logInToCheckout') }}
</Button>
<p v-if="auth.isAuthenticated.value && !canPlaceOrder" class="text-xs text-destructive mt-2 text-center">
<p v-if="!canPlaceOrder" class="text-xs text-destructive mt-2 text-center">
{{ orderValidationMessage }}
</p>
<p v-else-if="!auth.isAuthenticated.value" class="text-xs text-muted-foreground mt-2 text-center">
{{ t('market.auth.loginPrompt') }}
</p>
</div>
</CardContent>
</Card>
@ -275,9 +262,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import { useRoute } from 'vue-router'
import { useMarketStore } from '@/modules/market/stores/market'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { auth } from '@/composables/useAuthService'
@ -307,8 +292,6 @@ import {
const { thumbnail } = useImageOptimizer()
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const marketStore = useMarketStore()
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
@ -451,24 +434,12 @@ const placeOrder = async () => {
// Try to get pubkey from main auth first, fallback to auth service
const userPubkey = auth.currentUser.value?.pubkey || authService.user.value?.pubkey
// Friendly toast instead of throw same pattern as Activities favorites prompt.
if (!auth.isAuthenticated.value) {
toast.info(t('market.auth.loginPrompt'), {
action: {
label: t('market.auth.logIn'),
onClick: () => router.push('/login'),
},
})
isPlacingOrder.value = false
return
throw new Error('You must be logged in to place an order')
}
if (!userPubkey) {
toast.info(t('market.auth.nostrKeyRequired'), {
description: t('market.auth.nostrKeyDescription'),
})
isPlacingOrder.value = false
return
throw new Error('Nostr identity required: Please configure your Nostr public key in your profile settings to place orders.')
}
// Create the order using the market store's order placement functionality

View file

@ -47,7 +47,7 @@ const modules: Module[] = [
{ label: 'Activities', chakra: 'Swadhisthana', icon: CalendarDays, bgClass: '', glow: 'rgba(255,165,0,0.5)', envKey: 'VITE_HUB_ACTIVITIES_URL', status: 'beta' },
{ label: 'Chat', chakra: 'Anahata', icon: MessageCircle, bgClass: '', glow: 'rgba(0,200,80,0.5)', envKey: 'VITE_HUB_CHAT_URL', status: 'alpha', authRequired: true },
{ label: 'Forum', chakra: 'Vishuddha', icon: Newspaper, bgClass: '', glow: 'rgba(60,120,255,0.5)', envKey: 'VITE_HUB_FORUM_URL', status: 'alpha' },
{ label: 'Tasks', chakra: 'Ajna', icon: ListTodo, bgClass: '', glow: 'rgba(99,80,200,0.5)', envKey: 'VITE_HUB_TASKS_URL', status: 'alpha', authRequired: true },
{ label: 'Tasks', chakra: 'Ajna', icon: ListTodo, bgClass: '', glow: 'rgba(99,80,200,0.5)', envKey: 'VITE_HUB_TASKS_URL', status: 'alpha' },
{ label: 'Castle', chakra: 'Sahasrara', icon: Castle, bgClass: '', glow: 'rgba(160,80,220,0.5)', envKey: 'VITE_HUB_CASTLE_URL', status: 'beta', authRequired: true },
]
// Crown at top, root at bottom
@ -57,7 +57,7 @@ const token = computed(() => localStorage.getItem('lnbits_access_token') || '')
function hubLink(m: Module): string | null {
if (!m.envKey) return null
// Auth-only modules (wallet, chat, castle, tasks) are ghosted when not logged in.
// Auth-only modules (wallet, chat, castle) are ghosted when not logged in.
if (m.authRequired && !isAuthenticated.value) return null
const url = import.meta.env[m.envKey] as string | undefined
if (!url) return null
@ -68,24 +68,6 @@ function hubLink(m: Module): string | null {
return url
}
function isAuthGated(m: Module): boolean {
return !!(m.authRequired && !isAuthenticated.value)
}
function onTileClick(m: Module, event: Event) {
// Ghosted auth-required tiles aren't anchors; intercept and toast.
if (isAuthGated(m)) {
event.preventDefault()
toast.info(`${m.label} requires login`, {
action: {
label: 'Log in',
onClick: () => router.push('/login'),
},
})
}
// "Coming soon" tiles (no envKey, no authRequired) silently do nothing.
}
const showProfile = ref(false)
function notImplemented() {
@ -107,25 +89,32 @@ function notImplemented() {
rgba(185, 28, 28, 0.10) 100%);
"
>
<!-- Faint chakra mandala column behind tiles (peek through translucent tiles) -->
<div class="absolute inset-x-0 top-0 bottom-16 flex flex-col justify-around items-center py-4 pointer-events-none">
<img v-for="svg in ['sahasrara.svg','ajna.svg','vishuddha.svg','anahata.svg','manipura.svg','swadhisthana.svg','muladhara.svg']"
:key="svg"
:src="`/chakras/${svg}`"
class="w-32 h-32 sm:w-40 sm:h-40 opacity-90"
/>
</div>
<!-- Main grid -->
<div class="relative w-full max-w-2xl mx-auto px-4 pt-6 pb-2 flex-1 flex flex-col min-h-0">
<h1 class="text-2xl font-light text-center text-foreground/90 mb-3 tracking-wide">aiolabs</h1>
<h1 class="text-2xl font-light text-center text-foreground/90 mb-1 tracking-wide">aiolabs</h1>
<p class="text-xs text-center text-muted-foreground mb-3 italic">from earth to sky</p>
<div class="grid grid-cols-2 gap-2 flex-1 min-h-0">
<component
v-for="m in orderedModules"
:key="m.label"
:is="hubLink(m) ? 'a' : (isAuthGated(m) ? 'button' : 'div')"
:is="hubLink(m) ? 'a' : 'div'"
:href="hubLink(m) || undefined"
class="relative flex flex-col items-center justify-center gap-1 rounded-xl border backdrop-blur-sm transition-all p-2 min-h-0"
:class="[
hubLink(m)
? 'bg-card/60 hover:bg-card/40 border-white/10 hover:border-white/25 cursor-pointer'
: isAuthGated(m)
? 'bg-card/70 hover:bg-card/55 border-white/5 opacity-60 cursor-pointer'
: 'bg-card/70 border-white/5 opacity-60 cursor-not-allowed',
: 'bg-card/70 border-white/5 opacity-60 cursor-not-allowed',
]"
@click="onTileClick(m, $event)"
>
<component :is="m.icon" class="h-7 w-7 text-foreground/90" :style="{ filter: `drop-shadow(0 0 8px ${m.glow})` }" />
<div class="text-center leading-tight">

View file

@ -40,8 +40,6 @@ function activitiesHtmlPlugin(): Plugin {
*/
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-activities',
server: {
port: 5181,
strictPort: true,

View file

@ -40,8 +40,6 @@ function castleHtmlPlugin(): Plugin {
*/
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-castle',
server: {
port: 5180,
strictPort: true,
@ -107,13 +105,10 @@ export default defineConfig(({ mode }) => ({
],
resolve: {
alias: {
// ORDER MATTERS — @rollup/plugin-alias is first-match-wins.
// The more specific @/app.config remap must precede the @ prefix
// alias, otherwise '@/app.config' matches '@' first and resolves
// to ./src/app.config (the hub config). ExpensesAPI etc. import
// from @/app.config and need the per-app config.
'@/app.config': fileURLToPath(new URL('./src/accounting-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
// CRITICAL: Remap @/app.config to the castle app's config
// ExpensesAPI and other modules import from @/app.config directly
'@/app.config': fileURLToPath(new URL('./src/accounting-app/app.config.ts', import.meta.url)),
},
},
build: {

View file

@ -35,8 +35,6 @@ function chatHtmlPlugin(): Plugin {
*/
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-chat',
server: {
port: 5183,
strictPort: true,
@ -100,9 +98,8 @@ export default defineConfig(({ mode }) => ({
],
resolve: {
alias: {
// ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/chat-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@/app.config': fileURLToPath(new URL('./src/chat-app/app.config.ts', import.meta.url)),
},
},
build: {

View file

@ -9,8 +9,6 @@ import { visualizer } from 'rollup-plugin-visualizer'
// https://vite.dev/config/
export default defineConfig(({ mode }) => ({
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
cacheDir: 'node_modules/.vite-hub',
server: {
port: 5173,
strictPort: true,

View file

@ -35,8 +35,6 @@ function forumHtmlPlugin(): Plugin {
*/
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-forum',
server: {
port: 5184,
strictPort: true,
@ -100,9 +98,8 @@ export default defineConfig(({ mode }) => ({
],
resolve: {
alias: {
// ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/forum-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@/app.config': fileURLToPath(new URL('./src/forum-app/app.config.ts', import.meta.url)),
},
},
build: {

View file

@ -35,8 +35,6 @@ function marketHtmlPlugin(): Plugin {
*/
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-market',
server: {
port: 5185,
strictPort: true,
@ -100,9 +98,8 @@ export default defineConfig(({ mode }) => ({
],
resolve: {
alias: {
// ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@/app.config': fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)),
},
},
build: {

View file

@ -35,8 +35,6 @@ function tasksHtmlPlugin(): Plugin {
*/
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-tasks',
server: {
port: 5186,
strictPort: true,
@ -100,9 +98,8 @@ export default defineConfig(({ mode }) => ({
],
resolve: {
alias: {
// ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/tasks-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@/app.config': fileURLToPath(new URL('./src/tasks-app/app.config.ts', import.meta.url)),
},
},
build: {

View file

@ -39,8 +39,6 @@ function walletHtmlPlugin(): Plugin {
*/
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-wallet',
server: {
port: 5182,
strictPort: true,
@ -106,9 +104,8 @@ export default defineConfig(({ mode }) => ({
],
resolve: {
alias: {
// ORDER MATTERS — @/app.config must precede @ (first-match-wins).
'@/app.config': fileURLToPath(new URL('./src/wallet-app/app.config.ts', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@/app.config': fileURLToPath(new URL('./src/wallet-app/app.config.ts', import.meta.url)),
},
},
build: {