Compare commits
No commits in common. "9161e0cf6825599d45e57cafae94de98aca43480" and "51aff8cc8747d31b7755ea639493f640562858b0" have entirely different histories.
9161e0cf68
...
51aff8cc87
18 changed files with 152 additions and 441 deletions
60
.env.example
60
.env.example
|
|
@ -42,63 +42,3 @@ VITE_MARKET_NADDR=naddr1qqjxgdp4vv6rydej943n2dny956rwwf4943xzwfc95ekyd3evenrsvrr
|
||||||
# VITE_LIGHTNING_ENABLED=true
|
# VITE_LIGHTNING_ENABLED=true
|
||||||
# OBSOLETE: Not used in codebase - config.market.defaultCurrency is never consumed
|
# OBSOLETE: Not used in codebase - config.market.defaultCurrency is never consumed
|
||||||
# VITE_MARKET_DEFAULT_CURRENCY=sat
|
# 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`.
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
|
||||||
|
|
@ -11,159 +11,115 @@ http {
|
||||||
real_ip_header X-Forwarded-For;
|
real_ip_header X-Forwarded-For;
|
||||||
real_ip_recursive on;
|
real_ip_recursive on;
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# Reusable location blocks
|
||||||
# PATH-MODE deployment (recommended)
|
# JS / CSS / image MIME and caching
|
||||||
#
|
map $sent_http_content_type $cache_static {
|
||||||
# demo.<domain>.<com>/ — minimal AIO chakra hub
|
default "off";
|
||||||
# demo.<domain>.<com>/activities/ — Sortir / activities standalone
|
~image/ "6M";
|
||||||
# demo.<domain>.<com>/market/ — marketplace standalone
|
}
|
||||||
# demo.<domain>.<com>/wallet/ — wallet standalone
|
|
||||||
# demo.<domain>.<com>/chat/ — chat standalone
|
# ───────────────────────────────────────────────────────────────
|
||||||
# demo.<domain>.<com>/forum/ — forum standalone
|
# AIO hub — minimal app at app.<domain>
|
||||||
# demo.<domain>.<com>/tasks/ — tasks standalone
|
# Serves only the chakra icon hub + base infra (profile, relays).
|
||||||
# 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.
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
|
||||||
server {
|
server {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
server_name demo.<domain>.<com>;
|
server_name app.<domain>.<com>;
|
||||||
|
|
||||||
# Hub at the root
|
|
||||||
root /var/www/aio/dist;
|
root /var/www/aio/dist;
|
||||||
index index.html;
|
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 / { try_files $uri $uri/ /index.html; }
|
||||||
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 ~* \.js$ { types { application/javascript js; } default_type application/javascript; }
|
location ~* \.js$ { types { application/javascript js; } default_type application/javascript; }
|
||||||
location ~* \.css$ { types { text/css css; } default_type text/css; }
|
location ~* \.css$ { types { text/css css; } default_type text/css; }
|
||||||
location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; }
|
location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; }
|
||||||
}
|
}
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────
|
||||||
# Optional subdomain shortcuts → canonical path
|
# Standalone module PWAs — one server block per subdomain
|
||||||
#
|
# ───────────────────────────────────────────────────────────────
|
||||||
# If you want pretty subdomain URLs that funnel into the path-mode
|
|
||||||
# canonical, add 301 redirects per app. Example:
|
# Marketplace — Muladhara
|
||||||
#
|
|
||||||
# events.demo.<domain>.<com> → demo.<domain>.<com>/activities/
|
|
||||||
# market.demo.<domain>.<com> → demo.<domain>.<com>/market/
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
|
||||||
server {
|
server {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
server_name events.demo.<domain>.<com>;
|
server_name market.<domain>.<com>;
|
||||||
return 301 https://demo.<domain>.<com>/activities/$request_uri;
|
root /var/www/aio/dist-market;
|
||||||
}
|
index market.html;
|
||||||
server {
|
location / { try_files $uri $uri/ /market.html; }
|
||||||
listen 8080;
|
location ~* \.js$ { types { application/javascript js; } default_type application/javascript; }
|
||||||
server_name market.demo.<domain>.<com>;
|
location ~* \.css$ { types { text/css css; } default_type text/css; }
|
||||||
return 301 https://demo.<domain>.<com>/market/$request_uri;
|
location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; }
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# Activities — Swadhisthana
|
||||||
# SUBDOMAIN-MODE deployment (alternative — pure subdomains, no /path/)
|
server {
|
||||||
#
|
listen 8080;
|
||||||
# If you'd rather give each standalone its own subdomain and skip the
|
server_name sortir.<domain>.<com>;
|
||||||
# path-mode entirely:
|
root /var/www/aio/dist-activities;
|
||||||
#
|
index activities.html;
|
||||||
# server { server_name app.<domain>; root /var/www/aio/dist; ... }
|
location / { try_files $uri $uri/ /activities.html; }
|
||||||
# server { server_name market.<domain>; root /var/www/aio/dist-market; ... }
|
location ~* \.js$ { types { application/javascript js; } default_type application/javascript; }
|
||||||
# server { server_name sortir.<domain>; root /var/www/aio/dist-activities; ... }
|
location ~* \.css$ { types { text/css css; } default_type text/css; }
|
||||||
# server { server_name wallet.<domain>; root /var/www/aio/dist-wallet; ... }
|
location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; }
|
||||||
# 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; ... }
|
# Wallet — Manipura
|
||||||
# server { server_name castle.<domain>; root /var/www/aio/dist-castle; ... }
|
server {
|
||||||
#
|
listen 8080;
|
||||||
# Each block uses `location / { try_files $uri $uri/ /<name>.html; }`.
|
server_name wallet.<domain>.<com>;
|
||||||
# In subdomain mode, build each standalone WITHOUT VITE_BASE_PATH (the
|
root /var/www/aio/dist-wallet;
|
||||||
# default `/` is correct), and set VITE_HUB_<NAME>_URL to the subdomain
|
index wallet.html;
|
||||||
# in the hub's env (e.g. VITE_HUB_MARKET_URL=https://market.<domain>).
|
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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,15 +168,6 @@ const messages: LocaleMessages = {
|
||||||
notAvailable: 'Income submission is not yet available. This feature is coming soon.',
|
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: {
|
dateTimeFormats: {
|
||||||
short: {
|
short: {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|
|
||||||
|
|
@ -168,15 +168,6 @@ const messages: LocaleMessages = {
|
||||||
notAvailable: 'El registro de ingresos a\u00fan no est\u00e1 disponible. Esta funci\u00f3n llegar\u00e1 pronto.',
|
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: {
|
dateTimeFormats: {
|
||||||
short: {
|
short: {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|
|
||||||
|
|
@ -168,15 +168,6 @@ const messages: LocaleMessages = {
|
||||||
notAvailable: 'La saisie de revenus n\u2019est pas encore disponible. Cette fonctionnalit\u00e9 arrive bient\u00f4t.',
|
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: {
|
dateTimeFormats: {
|
||||||
short: {
|
short: {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|
|
||||||
|
|
@ -144,16 +144,6 @@ export interface LocaleMessages {
|
||||||
notAvailable: string
|
notAvailable: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Market module
|
|
||||||
market?: {
|
|
||||||
auth: {
|
|
||||||
loginPrompt: string
|
|
||||||
logIn: string
|
|
||||||
logInToCheckout: string
|
|
||||||
nostrKeyRequired: string
|
|
||||||
nostrKeyDescription: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add date/time formats
|
// Add date/time formats
|
||||||
dateTimeFormats: {
|
dateTimeFormats: {
|
||||||
short: {
|
short: {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ import LoginDialog from '@/components/auth/LoginDialog.vue'
|
||||||
import { useTheme } from '@/components/theme-provider'
|
import { useTheme } from '@/components/theme-provider'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import {
|
import { Button } from '@/components/ui/button'
|
||||||
Store, ShoppingCart, Package, LogIn, User as UserIcon,
|
import { LogIn } from 'lucide-vue-next'
|
||||||
} from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -18,47 +17,8 @@ const { isAuthenticated } = useAuth()
|
||||||
|
|
||||||
const showLoginDialog = ref(false)
|
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')
|
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() {
|
async function handleLoginSuccess() {
|
||||||
showLoginDialog.value = false
|
showLoginDialog.value = false
|
||||||
toast.success('Welcome!')
|
toast.success('Welcome!')
|
||||||
|
|
@ -70,33 +30,15 @@ async function handleLoginSuccess() {
|
||||||
<div class="relative flex min-h-screen flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
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 />
|
<router-view />
|
||||||
</main>
|
</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>
|
</div>
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
|
||||||
|
|
@ -64,15 +64,17 @@ export function useMarket() {
|
||||||
return 'disconnected'
|
return 'disconnected'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Load market from naddr (or empty for public browse mode)
|
// Load market from naddr
|
||||||
const loadMarket = async (naddr: string) => {
|
const loadMarket = async (naddr: string) => {
|
||||||
return await marketOperation.execute(async () => {
|
return await marketOperation.execute(async () => {
|
||||||
// Parse naddr (when given) to get market identifier + pubkey.
|
// Parse naddr to get market data
|
||||||
// Empty naddr + unauth user → public browse mode (no pubkey filter).
|
|
||||||
const parts = naddr ? naddr.split(':') : []
|
|
||||||
const marketData = {
|
const marketData = {
|
||||||
identifier: parts[2] || 'default',
|
identifier: naddr.split(':')[2] || 'default',
|
||||||
pubkey: parts[1] || authService.user.value?.pubkey || ''
|
pubkey: naddr.split(':')[1] || authService.user.value?.pubkey || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!marketData.pubkey) {
|
||||||
|
throw new Error('No pubkey available for market')
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadMarketData(marketData)
|
await loadMarketData(marketData)
|
||||||
|
|
@ -85,29 +87,7 @@ export function useMarket() {
|
||||||
// Load market data from Nostr events
|
// Load market data from Nostr events
|
||||||
const loadMarketData = async (marketData: any) => {
|
const loadMarketData = async (marketData: any) => {
|
||||||
try {
|
try {
|
||||||
console.log('🛒 Loading market data for:', { identifier: marketData.identifier, pubkey: marketData.pubkey?.slice(0, 8) || '(public browse)' })
|
console.log('🛒 Loading market data for:', { identifier: marketData.identifier, pubkey: marketData.pubkey?.slice(0, 8) })
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we can query events (relays are connected)
|
// Check if we can query events (relays are connected)
|
||||||
if (!isConnected.value) {
|
if (!isConnected.value) {
|
||||||
|
|
@ -168,11 +148,7 @@ export function useMarket() {
|
||||||
relays: config.nostr.relays,
|
relays: config.nostr.relays,
|
||||||
selected: true,
|
selected: true,
|
||||||
opts: {
|
opts: {
|
||||||
// Logged-in user has no published market event yet — show their
|
name: `${import.meta.env.VITE_APP_NAME} Market`,
|
||||||
// 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',
|
|
||||||
description: 'A communal market to sell your goods',
|
description: 'A communal market to sell your goods',
|
||||||
merchants: [],
|
merchants: [],
|
||||||
ui: {}
|
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 () => {
|
const loadStalls = async () => {
|
||||||
try {
|
try {
|
||||||
// Get the active market to filter by its merchants
|
// Get the active market to filter by its merchants
|
||||||
|
|
@ -212,20 +188,19 @@ export function useMarket() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const browseAll = (activeMarket as any).browseAll === true
|
|
||||||
const merchants = [...(activeMarket.opts.merchants || [])]
|
const merchants = [...(activeMarket.opts.merchants || [])]
|
||||||
|
|
||||||
if (!browseAll && merchants.length === 0) {
|
if (merchants.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build filter: in browse-all mode no authors filter; otherwise scope to merchants.
|
// Fetch stall events from market merchants only
|
||||||
const stallFilter: any = { kinds: [MARKET_EVENT_KINDS.STALL] }
|
const events = await relayHub.queryEvents([
|
||||||
if (!browseAll && merchants.length > 0) {
|
{
|
||||||
stallFilter.authors = merchants
|
kinds: [MARKET_EVENT_KINDS.STALL],
|
||||||
|
authors: merchants
|
||||||
}
|
}
|
||||||
|
])
|
||||||
const events = await relayHub.queryEvents([stallFilter])
|
|
||||||
|
|
||||||
console.log('🛒 Found', events.length, 'stall events for', merchants.length, '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 () => {
|
const loadProducts = async () => {
|
||||||
try {
|
try {
|
||||||
const activeMarket = marketStore.activeMarket
|
const activeMarket = marketStore.activeMarket
|
||||||
|
|
@ -278,19 +253,18 @@ export function useMarket() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const browseAll = (activeMarket as any).browseAll === true
|
|
||||||
const merchants = [...(activeMarket.opts.merchants || [])]
|
const merchants = [...(activeMarket.opts.merchants || [])]
|
||||||
|
if (merchants.length === 0) {
|
||||||
if (!browseAll && merchants.length === 0) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const productFilter: any = { kinds: [MARKET_EVENT_KINDS.PRODUCT] }
|
// Fetch product events from market merchants
|
||||||
if (!browseAll && merchants.length > 0) {
|
const events = await relayHub.queryEvents([
|
||||||
productFilter.authors = merchants
|
{
|
||||||
|
kinds: [MARKET_EVENT_KINDS.PRODUCT],
|
||||||
|
authors: merchants
|
||||||
}
|
}
|
||||||
|
])
|
||||||
const events = await relayHub.queryEvents([productFilter])
|
|
||||||
|
|
||||||
console.log('🛒 Found', events.length, 'product events for', merchants.length, 'merchants')
|
console.log('🛒 Found', events.length, 'product events for', merchants.length, 'merchants')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,6 @@
|
||||||
<!-- Place Order Button -->
|
<!-- Place Order Button -->
|
||||||
<div class="pt-4 border-t border-border">
|
<div class="pt-4 border-t border-border">
|
||||||
<Button
|
<Button
|
||||||
v-if="auth.isAuthenticated.value"
|
|
||||||
@click="placeOrder"
|
@click="placeOrder"
|
||||||
:disabled="isPlacingOrder || !canPlaceOrder"
|
:disabled="isPlacingOrder || !canPlaceOrder"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
|
|
@ -250,21 +249,9 @@
|
||||||
<span v-if="isPlacingOrder" class="animate-spin mr-2">⚡</span>
|
<span v-if="isPlacingOrder" class="animate-spin mr-2">⚡</span>
|
||||||
Place Order - {{ formatPrice(orderTotal, checkoutCart.currency) }}
|
Place Order - {{ formatPrice(orderTotal, checkoutCart.currency) }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<p v-if="!canPlaceOrder" class="text-xs text-destructive mt-2 text-center">
|
||||||
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">
|
|
||||||
{{ orderValidationMessage }}
|
{{ orderValidationMessage }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="!auth.isAuthenticated.value" class="text-xs text-muted-foreground mt-2 text-center">
|
|
||||||
{{ t('market.auth.loginPrompt') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -275,9 +262,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
import { useMarketStore } from '@/modules/market/stores/market'
|
import { useMarketStore } from '@/modules/market/stores/market'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import { auth } from '@/composables/useAuthService'
|
import { auth } from '@/composables/useAuthService'
|
||||||
|
|
@ -307,8 +292,6 @@ import {
|
||||||
|
|
||||||
const { thumbnail } = useImageOptimizer()
|
const { thumbnail } = useImageOptimizer()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const { t } = useI18n()
|
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
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
|
// Try to get pubkey from main auth first, fallback to auth service
|
||||||
const userPubkey = auth.currentUser.value?.pubkey || authService.user.value?.pubkey
|
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) {
|
if (!auth.isAuthenticated.value) {
|
||||||
toast.info(t('market.auth.loginPrompt'), {
|
throw new Error('You must be logged in to place an order')
|
||||||
action: {
|
|
||||||
label: t('market.auth.logIn'),
|
|
||||||
onClick: () => router.push('/login'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
isPlacingOrder.value = false
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userPubkey) {
|
if (!userPubkey) {
|
||||||
toast.info(t('market.auth.nostrKeyRequired'), {
|
throw new Error('Nostr identity required: Please configure your Nostr public key in your profile settings to place orders.')
|
||||||
description: t('market.auth.nostrKeyDescription'),
|
|
||||||
})
|
|
||||||
isPlacingOrder.value = false
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the order using the market store's order placement functionality
|
// Create the order using the market store's order placement functionality
|
||||||
|
|
|
||||||
|
|
@ -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: '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: '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: '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 },
|
{ 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
|
// Crown at top, root at bottom
|
||||||
|
|
@ -57,7 +57,7 @@ const token = computed(() => localStorage.getItem('lnbits_access_token') || '')
|
||||||
|
|
||||||
function hubLink(m: Module): string | null {
|
function hubLink(m: Module): string | null {
|
||||||
if (!m.envKey) return 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
|
if (m.authRequired && !isAuthenticated.value) return null
|
||||||
const url = import.meta.env[m.envKey] as string | undefined
|
const url = import.meta.env[m.envKey] as string | undefined
|
||||||
if (!url) return null
|
if (!url) return null
|
||||||
|
|
@ -68,24 +68,6 @@ function hubLink(m: Module): string | null {
|
||||||
return url
|
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)
|
const showProfile = ref(false)
|
||||||
|
|
||||||
function notImplemented() {
|
function notImplemented() {
|
||||||
|
|
@ -107,25 +89,32 @@ function notImplemented() {
|
||||||
rgba(185, 28, 28, 0.10) 100%);
|
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 -->
|
<!-- 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">
|
<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">
|
<div class="grid grid-cols-2 gap-2 flex-1 min-h-0">
|
||||||
<component
|
<component
|
||||||
v-for="m in orderedModules"
|
v-for="m in orderedModules"
|
||||||
:key="m.label"
|
:key="m.label"
|
||||||
:is="hubLink(m) ? 'a' : (isAuthGated(m) ? 'button' : 'div')"
|
:is="hubLink(m) ? 'a' : 'div'"
|
||||||
:href="hubLink(m) || undefined"
|
: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="relative flex flex-col items-center justify-center gap-1 rounded-xl border backdrop-blur-sm transition-all p-2 min-h-0"
|
||||||
:class="[
|
:class="[
|
||||||
hubLink(m)
|
hubLink(m)
|
||||||
? 'bg-card/60 hover:bg-card/40 border-white/10 hover:border-white/25 cursor-pointer'
|
? '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})` }" />
|
<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">
|
<div class="text-center leading-tight">
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,6 @@ function activitiesHtmlPlugin(): Plugin {
|
||||||
*/
|
*/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: process.env.VITE_BASE_PATH || '/',
|
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: {
|
server: {
|
||||||
port: 5181,
|
port: 5181,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,6 @@ function castleHtmlPlugin(): Plugin {
|
||||||
*/
|
*/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: process.env.VITE_BASE_PATH || '/',
|
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: {
|
server: {
|
||||||
port: 5180,
|
port: 5180,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|
@ -107,13 +105,10 @@ export default defineConfig(({ mode }) => ({
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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)),
|
'@': 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: {
|
build: {
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,6 @@ function chatHtmlPlugin(): Plugin {
|
||||||
*/
|
*/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: process.env.VITE_BASE_PATH || '/',
|
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: {
|
server: {
|
||||||
port: 5183,
|
port: 5183,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|
@ -100,9 +98,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@/app.config': fileURLToPath(new URL('./src/chat-app/app.config.ts', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
|
|
||||||
cacheDir: 'node_modules/.vite-hub',
|
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,6 @@ function forumHtmlPlugin(): Plugin {
|
||||||
*/
|
*/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: process.env.VITE_BASE_PATH || '/',
|
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: {
|
server: {
|
||||||
port: 5184,
|
port: 5184,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|
@ -100,9 +98,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@/app.config': fileURLToPath(new URL('./src/forum-app/app.config.ts', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,6 @@ function marketHtmlPlugin(): Plugin {
|
||||||
*/
|
*/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: process.env.VITE_BASE_PATH || '/',
|
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: {
|
server: {
|
||||||
port: 5185,
|
port: 5185,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|
@ -100,9 +98,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@/app.config': fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,6 @@ function tasksHtmlPlugin(): Plugin {
|
||||||
*/
|
*/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: process.env.VITE_BASE_PATH || '/',
|
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: {
|
server: {
|
||||||
port: 5186,
|
port: 5186,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|
@ -100,9 +98,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@/app.config': fileURLToPath(new URL('./src/tasks-app/app.config.ts', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,6 @@ function walletHtmlPlugin(): Plugin {
|
||||||
*/
|
*/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
base: process.env.VITE_BASE_PATH || '/',
|
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: {
|
server: {
|
||||||
port: 5182,
|
port: 5182,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|
@ -106,9 +104,8 @@ export default defineConfig(({ mode }) => ({
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
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)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@/app.config': fileURLToPath(new URL('./src/wallet-app/app.config.ts', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue