From 00eddc918925438c64c79ec48a7981340eaf076d Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 20 Apr 2026 07:40:26 +0200 Subject: [PATCH] Add standalone Sortir activities app (sortir.ariege.io) Second Vite entry point for deploying the activities module as an independent PWA at sortir.ariege.io. Includes its own App.vue with bottom navigation bar (p'a semana style: Feed, Calendar, Map, Favorites, Settings), stripped-down app config (base + activities only), French PWA manifest, and SPA fallback plugin for dev server. New routes for calendar, map, and favorites views (placeholder). Settings page with theme toggle, language switcher (FR/EN), and auth. Build: npm run build:activities -> dist-activities/ Dev: npm run dev:activities -> localhost:5173 Co-Authored-By: Claude Opus 4.6 (1M context) --- activities.html | 19 +++ package.json | 3 + src/activities-app/App.vue | 82 +++++++++++ src/activities-app/app.config.ts | 55 ++++++++ src/activities-app/app.ts | 132 ++++++++++++++++++ src/activities-app/main.ts | 18 +++ src/activities-app/views/SettingsPage.vue | 89 ++++++++++++ src/modules/activities/index.ts | 27 ++++ .../views/ActivitiesCalendarPage.vue | 13 ++ .../views/ActivitiesFavoritesPage.vue | 14 ++ .../activities/views/ActivitiesMapPage.vue | 13 ++ vite.activities.config.ts | 113 +++++++++++++++ 12 files changed, 578 insertions(+) create mode 100644 activities.html create mode 100644 src/activities-app/App.vue create mode 100644 src/activities-app/app.config.ts create mode 100644 src/activities-app/app.ts create mode 100644 src/activities-app/main.ts create mode 100644 src/activities-app/views/SettingsPage.vue create mode 100644 src/modules/activities/views/ActivitiesCalendarPage.vue create mode 100644 src/modules/activities/views/ActivitiesFavoritesPage.vue create mode 100644 src/modules/activities/views/ActivitiesMapPage.vue create mode 100644 vite.activities.config.ts diff --git a/activities.html b/activities.html new file mode 100644 index 0000000..d227e52 --- /dev/null +++ b/activities.html @@ -0,0 +1,19 @@ + + + + + + + + + + + Sortir — Activités + + + + +
+ + + diff --git a/package.json b/package.json index 304528e..ef5de0b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "build": "vue-tsc -b && vite build", "preview": "vite preview --host", "analyze": "vite build --mode analyze", + "dev:activities": "vite --host --config vite.activities.config.ts", + "build:activities": "vue-tsc -b && vite build --config vite.activities.config.ts", + "preview:activities": "vite preview --host --config vite.activities.config.ts", "electron:dev": "concurrently \"vite --host\" \"electron-forge start\"", "electron:build": "vue-tsc -b && vite build && electron-builder", "electron:package": "electron-builder", diff --git a/src/activities-app/App.vue b/src/activities-app/App.vue new file mode 100644 index 0000000..4aaa827 --- /dev/null +++ b/src/activities-app/App.vue @@ -0,0 +1,82 @@ + + + diff --git a/src/activities-app/app.config.ts b/src/activities-app/app.config.ts new file mode 100644 index 0000000..30cbb22 --- /dev/null +++ b/src/activities-app/app.config.ts @@ -0,0 +1,55 @@ +import type { AppConfig } from '@/core/types' + +/** + * Standalone activities app configuration. + * Only enables base + activities 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'] + } + } + }, + activities: { + name: 'activities', + enabled: true, + lazy: false, + config: { + apiConfig: { + baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000', + apiKey: import.meta.env.VITE_API_KEY || '' + }, + defaultMapCenter: { lat: 42.9667, lng: 1.6000 }, // Ariège, France + maxTicketsPerUser: 10, + enableMap: true, + enablePrivateEvents: false + } + }, + }, + + features: { + pwa: true, + pushNotifications: true, + electronApp: false, + developmentMode: import.meta.env.DEV + } +} + +export default appConfig diff --git a/src/activities-app/app.ts b/src/activities-app/app.ts new file mode 100644 index 0000000..e997b9b --- /dev/null +++ b/src/activities-app/app.ts @@ -0,0 +1,132 @@ +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 activitiesModule from '@/modules/activities' + +import App from './App.vue' + +import '@/assets/index.css' +import { i18n } from '@/i18n' + +/** + * Initialize the standalone activities app + */ +export async function createAppInstance() { + console.log('🚀 Starting Sortir — Activities App...') + + const app = createApp(App) + + // Collect routes from enabled modules only + const moduleRoutes = [ + ...baseModule.routes || [], + ...activitiesModule.routes || [], + ].filter(Boolean) + + const router = createRouter({ + history: createWebHistory(), + routes: [ + // Activities page is the home page in standalone mode + { + path: '/', + redirect: '/activities' + }, + { + path: '/login', + name: 'login', + component: () => import('@/pages/Login.vue'), + meta: { requiresAuth: false } + }, + ...moduleRoutes, + // App-specific routes + { + path: '/settings', + name: 'settings', + component: () => import('./views/SettingsPage.vue'), + meta: { requiresAuth: false } + }, + ] + }) + + const pinia = createPinia() + + app.use(router) + app.use(pinia) + app.use(i18n) + + // Initialize plugin manager + pluginManager.init(app, router) + + // Register modules + const moduleRegistrations = [] + + if (appConfig.modules.base.enabled) { + moduleRegistrations.push( + pluginManager.register(baseModule, appConfig.modules.base) + ) + } + + if (appConfig.modules.activities?.enabled) { + moduleRegistrations.push( + pluginManager.register(activitiesModule, appConfig.modules.activities) + ) + } + + await Promise.all(moduleRegistrations) + await pluginManager.installAll() + + // Initialize auth + const { auth } = await import('@/composables/useAuthService') + await auth.initialize() + + // Auth guard — only redirect for routes that explicitly require auth + 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() + } + }) + + // Global error handling + 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('✅ Sortir app initialized') + return { app, router } +} + +export async function startApp() { + try { + const { app } = await createAppInstance() + app.mount('#app') + console.log('🎉 Sortir app started!') + eventBus.emit('app:started', {}, 'app') + } catch (error) { + console.error('💥 Failed to start Sortir app:', error) + document.getElementById('app')!.innerHTML = ` +
+

Failed to Start

+

${error instanceof Error ? error.message : 'Unknown error'}

+

Please refresh the page.

+
+ ` + } +} diff --git a/src/activities-app/main.ts b/src/activities-app/main.ts new file mode 100644 index 0000000..c9c8429 --- /dev/null +++ b/src/activities-app/main.ts @@ -0,0 +1,18 @@ +import { startApp } from './app' +import { registerSW } from 'virtual:pwa-register' +import 'vue-sonner/style.css' + +// PWA service worker with periodic updates +const intervalMS = 60 * 60 * 1000 // 1 hour +registerSW({ + onRegistered(r) { + r && setInterval(() => { + r.update() + }, intervalMS) + }, + onOfflineReady() { + console.log('Sortir app ready to work offline') + } +}) + +startApp() diff --git a/src/activities-app/views/SettingsPage.vue b/src/activities-app/views/SettingsPage.vue new file mode 100644 index 0000000..007264c --- /dev/null +++ b/src/activities-app/views/SettingsPage.vue @@ -0,0 +1,89 @@ + + + diff --git a/src/modules/activities/index.ts b/src/modules/activities/index.ts index 42ec29d..2816a86 100644 --- a/src/modules/activities/index.ts +++ b/src/modules/activities/index.ts @@ -32,6 +32,33 @@ export const activitiesModule = createModulePlugin({ requiresAuth: false, }, }, + { + path: '/activities/calendar', + name: 'activities-calendar', + component: () => import('./views/ActivitiesCalendarPage.vue'), + meta: { + title: 'Calendar', + requiresAuth: false, + }, + }, + { + path: '/activities/map', + name: 'activities-map', + component: () => import('./views/ActivitiesMapPage.vue'), + meta: { + title: 'Map', + requiresAuth: false, + }, + }, + { + path: '/activities/favorites', + name: 'activities-favorites', + component: () => import('./views/ActivitiesFavoritesPage.vue'), + meta: { + title: 'Favorites', + requiresAuth: true, + }, + }, { path: '/activities/:id', name: 'activity-detail', diff --git a/src/modules/activities/views/ActivitiesCalendarPage.vue b/src/modules/activities/views/ActivitiesCalendarPage.vue new file mode 100644 index 0000000..e09a719 --- /dev/null +++ b/src/modules/activities/views/ActivitiesCalendarPage.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/modules/activities/views/ActivitiesFavoritesPage.vue b/src/modules/activities/views/ActivitiesFavoritesPage.vue new file mode 100644 index 0000000..6c5dee5 --- /dev/null +++ b/src/modules/activities/views/ActivitiesFavoritesPage.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/modules/activities/views/ActivitiesMapPage.vue b/src/modules/activities/views/ActivitiesMapPage.vue new file mode 100644 index 0000000..85e053a --- /dev/null +++ b/src/modules/activities/views/ActivitiesMapPage.vue @@ -0,0 +1,13 @@ + + + diff --git a/vite.activities.config.ts b/vite.activities.config.ts new file mode 100644 index 0000000..59329b3 --- /dev/null +++ b/vite.activities.config.ts @@ -0,0 +1,113 @@ +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' + +/** + * Plugin to rewrite dev server requests to activities.html + * (SPA fallback for the standalone activities app entry point) + */ +function activitiesHtmlPlugin(): Plugin { + return { + name: 'activities-html-rewrite', + configureServer(server) { + server.middlewares.use((req, _res, next) => { + // Rewrite all non-asset requests to activities.html + if ( + req.url && + !req.url.startsWith('/@') && + !req.url.startsWith('/src/') && + !req.url.startsWith('/node_modules/') && + !req.url.includes('.') // skip files with extensions + ) { + req.url = '/activities.html' + } + next() + }) + }, + } +} + +/** + * Vite config for the standalone Sortir activities app. + * Deployed to sortir.ariege.io + */ +export default defineConfig(({ mode }) => ({ + plugins: [ + activitiesHtmlPlugin(), + vue(), + tailwindcss(), + VitePWA({ + registerType: 'autoUpdate', + devOptions: { + enabled: true, + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg}'], + }, + 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: 'Sortir — Activités & Événements', + short_name: 'Sortir', + description: 'Découvrez les activités et événements près de chez vous', + theme_color: '#1f2937', + background_color: '#ffffff', + display: 'standalone', + orientation: 'portrait-primary', + start_url: '/', + scope: '/', + id: 'sortir-activities', + categories: ['social', 'entertainment', 'lifestyle'], + lang: 'fr', + 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-activities/stats.html', + gzipSize: true, + brotliSize: true, + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + build: { + outDir: 'dist-activities', + rollupOptions: { + input: 'activities.html', + output: { + manualChunks: { + 'vue-vendor': ['vue', 'vue-router', 'pinia'], + 'ui-vendor': ['radix-vue', '@vueuse/core'], + 'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'], + }, + }, + }, + chunkSizeWarningLimit: 1000, + }, +}))