merge: market standalone
This commit is contained in:
commit
c22b5de8bc
8 changed files with 392 additions and 1 deletions
19
market.html
Normal file
19
market.html
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
||||||
|
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||||
|
<title>Market — Nostr</title>
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Market">
|
||||||
|
<meta name="description" content="Decentralized marketplace on Nostr with Lightning payments">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/market-app/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -21,6 +21,9 @@
|
||||||
"dev:chat": "vite --host --config vite.chat.config.ts",
|
"dev:chat": "vite --host --config vite.chat.config.ts",
|
||||||
"build:chat": "vue-tsc -b && vite build --config vite.chat.config.ts",
|
"build:chat": "vue-tsc -b && vite build --config vite.chat.config.ts",
|
||||||
"preview:chat": "vite preview --host --config vite.chat.config.ts",
|
"preview:chat": "vite preview --host --config vite.chat.config.ts",
|
||||||
|
"dev:market": "vite --host --config vite.market.config.ts",
|
||||||
|
"build:market": "vue-tsc -b && vite build --config vite.market.config.ts",
|
||||||
|
"preview:market": "vite preview --host --config vite.market.config.ts",
|
||||||
"electron:dev": "concurrently \"vite --host\" \"electron-forge start\"",
|
"electron:dev": "concurrently \"vite --host\" \"electron-forge start\"",
|
||||||
"electron:build": "vue-tsc -b && vite build && electron-builder",
|
"electron:build": "vue-tsc -b && vite build && electron-builder",
|
||||||
"electron:package": "electron-builder",
|
"electron:package": "electron-builder",
|
||||||
|
|
|
||||||
47
src/market-app/App.vue
Normal file
47
src/market-app/App.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
|
import LoginDialog from '@/components/auth/LoginDialog.vue'
|
||||||
|
import { useTheme } from '@/components/theme-provider'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { LogIn } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useTheme()
|
||||||
|
const { isAuthenticated } = useAuth()
|
||||||
|
|
||||||
|
const showLoginDialog = ref(false)
|
||||||
|
|
||||||
|
const isLoginPage = computed(() => route.path === '/login')
|
||||||
|
|
||||||
|
async function handleLoginSuccess() {
|
||||||
|
showLoginDialog.value = false
|
||||||
|
toast.success('Welcome!')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
|
<div class="relative flex min-h-screen flex-col"
|
||||||
|
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
53
src/market-app/app.config.ts
Normal file
53
src/market-app/app.config.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import type { AppConfig } from '@/core/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone Market app configuration.
|
||||||
|
* Only enables base + market modules.
|
||||||
|
*/
|
||||||
|
export const appConfig: AppConfig = {
|
||||||
|
modules: {
|
||||||
|
base: {
|
||||||
|
name: 'base',
|
||||||
|
enabled: true,
|
||||||
|
lazy: false,
|
||||||
|
config: {
|
||||||
|
nostr: {
|
||||||
|
relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]')
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
sessionTimeout: 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
pwa: {
|
||||||
|
autoPrompt: true
|
||||||
|
},
|
||||||
|
imageUpload: {
|
||||||
|
baseUrl: import.meta.env.VITE_PICTRS_BASE_URL || 'https://img.mydomain.com',
|
||||||
|
maxSizeMB: 10,
|
||||||
|
acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
market: {
|
||||||
|
name: 'market',
|
||||||
|
enabled: true,
|
||||||
|
lazy: false,
|
||||||
|
config: {
|
||||||
|
defaultCurrency: 'sats',
|
||||||
|
paymentTimeout: 300000,
|
||||||
|
maxOrderHistory: 50,
|
||||||
|
apiConfig: {
|
||||||
|
baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
features: {
|
||||||
|
pwa: true,
|
||||||
|
pushNotifications: true,
|
||||||
|
electronApp: false,
|
||||||
|
developmentMode: import.meta.env.DEV
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default appConfig
|
||||||
137
src/market-app/app.ts
Normal file
137
src/market-app/app.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { pluginManager } from '@/core/plugin-manager'
|
||||||
|
import { eventBus } from '@/core/event-bus'
|
||||||
|
import { container } from '@/core/di-container'
|
||||||
|
|
||||||
|
import appConfig from './app.config'
|
||||||
|
import baseModule from '@/modules/base'
|
||||||
|
import marketModule from '@/modules/market'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
import '@/assets/index.css'
|
||||||
|
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||||
|
|
||||||
|
function acceptTokenFromUrl() {
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
const token = params.get('token')
|
||||||
|
if (token) {
|
||||||
|
localStorage.setItem('lnbits_access_token', token)
|
||||||
|
params.delete('token')
|
||||||
|
const clean = params.toString()
|
||||||
|
const newUrl = window.location.pathname + (clean ? `?${clean}` : '') + window.location.hash
|
||||||
|
window.history.replaceState({}, '', newUrl)
|
||||||
|
console.log('[Market] Auth token accepted from URL')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAppInstance() {
|
||||||
|
console.log('Starting Market app...')
|
||||||
|
|
||||||
|
acceptTokenFromUrl()
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
const moduleRoutes = [
|
||||||
|
...baseModule.routes || [],
|
||||||
|
...marketModule.routes || [],
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/market'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: import.meta.env.VITE_DEMO_MODE === 'true'
|
||||||
|
? () => import('@/pages/LoginDemo.vue')
|
||||||
|
: () => import('@/pages/Login.vue'),
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
|
...moduleRoutes,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
const defaultLocale = import.meta.env.VITE_DEFAULT_LOCALE as AvailableLocale | undefined
|
||||||
|
if (defaultLocale && !localStorage.getItem('user-locale')) {
|
||||||
|
await changeLocale(defaultLocale)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginManager.init(app, router)
|
||||||
|
|
||||||
|
const moduleRegistrations = []
|
||||||
|
|
||||||
|
if (appConfig.modules.base.enabled) {
|
||||||
|
moduleRegistrations.push(
|
||||||
|
pluginManager.register(baseModule, appConfig.modules.base)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConfig.modules.market?.enabled) {
|
||||||
|
moduleRegistrations.push(
|
||||||
|
pluginManager.register(marketModule, appConfig.modules.market)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(moduleRegistrations)
|
||||||
|
await pluginManager.installAll()
|
||||||
|
|
||||||
|
const { auth } = await import('@/composables/useAuthService')
|
||||||
|
await auth.initialize()
|
||||||
|
|
||||||
|
router.beforeEach(async (to, _from, next) => {
|
||||||
|
const requiresAuth = to.meta.requiresAuth === true
|
||||||
|
|
||||||
|
if (requiresAuth && !auth.isAuthenticated.value) {
|
||||||
|
next('/login')
|
||||||
|
} else if (to.path === '/login' && auth.isAuthenticated.value) {
|
||||||
|
next('/')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.config.errorHandler = (err, _vm, info) => {
|
||||||
|
console.error('Global error:', err, info)
|
||||||
|
eventBus.emit('app:error', { error: err, info }, 'app')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConfig.features.developmentMode) {
|
||||||
|
;(window as any).__pluginManager = pluginManager
|
||||||
|
;(window as any).__eventBus = eventBus
|
||||||
|
;(window as any).__container = container
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Market app initialized')
|
||||||
|
return { app, router }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startApp() {
|
||||||
|
try {
|
||||||
|
const { app } = await createAppInstance()
|
||||||
|
app.mount('#app')
|
||||||
|
console.log('Market app started!')
|
||||||
|
eventBus.emit('app:started', {}, 'app')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start Market app:', error)
|
||||||
|
document.getElementById('app')!.innerHTML = `
|
||||||
|
<div style="padding: 20px; text-align: center; color: red;">
|
||||||
|
<h1>Failed to Start</h1>
|
||||||
|
<p>${error instanceof Error ? error.message : 'Unknown error'}</p>
|
||||||
|
<p>Please refresh the page.</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/market-app/main.ts
Normal file
17
src/market-app/main.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { startApp } from './app'
|
||||||
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
import 'vue-sonner/style.css'
|
||||||
|
|
||||||
|
const intervalMS = 60 * 60 * 1000
|
||||||
|
registerSW({
|
||||||
|
onRegistered(r) {
|
||||||
|
r && setInterval(() => {
|
||||||
|
r.update()
|
||||||
|
}, intervalMS)
|
||||||
|
},
|
||||||
|
onOfflineReady() {
|
||||||
|
console.log('Market app ready to work offline')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
startApp()
|
||||||
|
|
@ -25,7 +25,7 @@ export default defineConfig(({ mode }) => ({
|
||||||
'**/*.{js,css,html,ico,png,svg}'
|
'**/*.{js,css,html,ico,png,svg}'
|
||||||
],
|
],
|
||||||
// Don't intercept standalone app paths — they have their own service workers
|
// Don't intercept standalone app paths — they have their own service workers
|
||||||
navigateFallbackDenylist: [/^\/sortir\//, /^\/castle\//, /^\/wallet\//, /^\/chat\//],
|
navigateFallbackDenylist: [/^\/sortir\//, /^\/castle\//, /^\/wallet\//, /^\/chat\//, /^\/market\//, /^\/cart\//, /^\/checkout\//],
|
||||||
},
|
},
|
||||||
includeAssets: [
|
includeAssets: [
|
||||||
'favicon.ico',
|
'favicon.ico',
|
||||||
|
|
|
||||||
115
vite.market.config.ts
Normal file
115
vite.market.config.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { defineConfig, type Plugin } from 'vite'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
|
||||||
|
function marketHtmlPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'market-html-rewrite',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use((req, _res, next) => {
|
||||||
|
if (
|
||||||
|
req.url &&
|
||||||
|
!req.url.startsWith('/@') &&
|
||||||
|
!req.url.startsWith('/src/') &&
|
||||||
|
!req.url.startsWith('/node_modules/') &&
|
||||||
|
!req.url.includes('.')
|
||||||
|
) {
|
||||||
|
req.url = '/market.html'
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vite config for the standalone Market app.
|
||||||
|
*
|
||||||
|
* Set VITE_BASE_PATH to deploy under a path prefix:
|
||||||
|
* VITE_BASE_PATH=/market/ → app.${domain}/market/ (shared auth)
|
||||||
|
* (default: /) → market.${domain} (standalone subdomain)
|
||||||
|
*/
|
||||||
|
export default defineConfig(({ mode }) => ({
|
||||||
|
base: process.env.VITE_BASE_PATH || '/',
|
||||||
|
plugins: [
|
||||||
|
marketHtmlPlugin(),
|
||||||
|
vue(),
|
||||||
|
tailwindcss(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
devOptions: { enabled: true },
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
|
||||||
|
navigateFallback: 'market.html',
|
||||||
|
navigateFallbackAllowlist: [
|
||||||
|
new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
includeAssets: [
|
||||||
|
'favicon.ico',
|
||||||
|
'apple-touch-icon.png',
|
||||||
|
'mask-icon.svg',
|
||||||
|
'icon-192.png',
|
||||||
|
'icon-512.png',
|
||||||
|
'icon-maskable-192.png',
|
||||||
|
'icon-maskable-512.png',
|
||||||
|
],
|
||||||
|
manifest: {
|
||||||
|
name: 'Market — Nostr',
|
||||||
|
short_name: 'Market',
|
||||||
|
description: 'Decentralized marketplace on Nostr with Lightning payments',
|
||||||
|
theme_color: '#dc2626',
|
||||||
|
background_color: '#ffffff',
|
||||||
|
display: 'standalone',
|
||||||
|
orientation: 'portrait-primary',
|
||||||
|
start_url: process.env.VITE_BASE_PATH || '/',
|
||||||
|
scope: process.env.VITE_BASE_PATH || '/',
|
||||||
|
id: 'market-app',
|
||||||
|
categories: ['shopping', 'business'],
|
||||||
|
lang: 'en',
|
||||||
|
icons: [
|
||||||
|
{ src: 'icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
||||||
|
{ src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
||||||
|
{ src: 'icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' },
|
||||||
|
{ src: 'icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ViteImageOptimizer({
|
||||||
|
jpg: { quality: 80 },
|
||||||
|
png: { quality: 80 },
|
||||||
|
webp: { lossless: true },
|
||||||
|
}),
|
||||||
|
mode === 'analyze' &&
|
||||||
|
visualizer({
|
||||||
|
open: true,
|
||||||
|
filename: 'dist-market/stats.html',
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'@/app.config': fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist-market',
|
||||||
|
rollupOptions: {
|
||||||
|
input: 'market.html',
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||||
|
'ui-vendor': ['radix-vue', '@vueuse/core'],
|
||||||
|
'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
|
},
|
||||||
|
}))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue