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) <noreply@anthropic.com>
This commit is contained in:
parent
eebc1865c9
commit
00eddc9189
12 changed files with 578 additions and 0 deletions
19
activities.html
Normal file
19
activities.html
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<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>Sortir — Activités</title>
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Sortir">
|
||||||
|
<meta name="description" content="Découvrez les activités et événements près de chez vous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/activities-app/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview --host",
|
"preview": "vite preview --host",
|
||||||
"analyze": "vite build --mode analyze",
|
"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: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",
|
||||||
|
|
|
||||||
82
src/activities-app/App.vue
Normal file
82
src/activities-app/App.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<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 {
|
||||||
|
CalendarDays, Map, Heart, Settings, Search,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useTheme()
|
||||||
|
|
||||||
|
const showLoginDialog = ref(false)
|
||||||
|
|
||||||
|
// Bottom navigation tabs (p'a semana style)
|
||||||
|
const bottomTabs = [
|
||||||
|
{ name: 'Feed', icon: Search, path: '/activities' },
|
||||||
|
{ name: 'Calendar', icon: CalendarDays, path: '/activities/calendar' },
|
||||||
|
{ name: 'Map', icon: Map, path: '/activities/map' },
|
||||||
|
{ name: 'Favorites', icon: Heart, path: '/activities/favorites' },
|
||||||
|
{ name: 'Settings', icon: Settings, path: '/settings' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const isLoginPage = computed(() => route.path === '/login')
|
||||||
|
|
||||||
|
function isActiveTab(path: string): boolean {
|
||||||
|
if (path === '/activities') {
|
||||||
|
return route.path === '/activities' || route.path.startsWith('/activities/') &&
|
||||||
|
!route.path.startsWith('/activities/calendar') &&
|
||||||
|
!route.path.startsWith('/activities/map') &&
|
||||||
|
!route.path.startsWith('/activities/favorites')
|
||||||
|
}
|
||||||
|
return route.path.startsWith(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)">
|
||||||
|
|
||||||
|
<!-- Main content (with bottom padding for nav bar) -->
|
||||||
|
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Bottom navigation bar (p'a semana style) -->
|
||||||
|
<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.path"
|
||||||
|
class="flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors"
|
||||||
|
:class="isActiveTab(tab.path)
|
||||||
|
? 'text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'"
|
||||||
|
@click="router.push(tab.path)"
|
||||||
|
>
|
||||||
|
<component :is="tab.icon" class="w-5 h-5" />
|
||||||
|
<span class="text-[10px] font-medium">{{ tab.name }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
<LoginDialog v-model:is-open="showLoginDialog" @success="handleLoginSuccess" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
55
src/activities-app/app.config.ts
Normal file
55
src/activities-app/app.config.ts
Normal file
|
|
@ -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
|
||||||
132
src/activities-app/app.ts
Normal file
132
src/activities-app/app.ts
Normal file
|
|
@ -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 = `
|
||||||
|
<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>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/activities-app/main.ts
Normal file
18
src/activities-app/main.ts
Normal file
|
|
@ -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()
|
||||||
89
src/activities-app/views/SettingsPage.vue
Normal file
89
src/activities-app/views/SettingsPage.vue
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { useTheme } from '@/components/theme-provider'
|
||||||
|
import { changeLocale, type AvailableLocale } from '@/i18n'
|
||||||
|
import { auth } from '@/composables/useAuthService'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Sun, Moon, LogIn, LogOut } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
const isAuthenticated = computed(() => auth.isAuthenticated.value)
|
||||||
|
const userPubkey = computed(() => auth.currentUser.value?.pubkey)
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(theme.value === 'dark' ? 'light' : 'dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLanguage(lang: AvailableLocale) {
|
||||||
|
changeLocale(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await auth.logout()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto px-4 py-6 max-w-lg">
|
||||||
|
<h1 class="text-2xl font-bold text-foreground mb-6">Settings</h1>
|
||||||
|
|
||||||
|
<!-- Account -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">Account</h2>
|
||||||
|
<div v-if="isAuthenticated" class="bg-muted/50 rounded-lg p-4 space-y-3">
|
||||||
|
<p class="text-sm text-foreground font-mono truncate">
|
||||||
|
{{ userPubkey }}
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" size="sm" class="w-full gap-2" @click="handleLogout">
|
||||||
|
<LogOut class="w-4 h-4" />
|
||||||
|
Log out
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="bg-muted/50 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-muted-foreground mb-3">
|
||||||
|
Log in to bookmark activities, RSVP, and purchase tickets.
|
||||||
|
</p>
|
||||||
|
<Button size="sm" class="w-full gap-2" @click="$router.push('/login')">
|
||||||
|
<LogIn class="w-4 h-4" />
|
||||||
|
Log in
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator class="my-6" />
|
||||||
|
|
||||||
|
<!-- Appearance -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">Appearance</h2>
|
||||||
|
<div class="flex items-center justify-between bg-muted/50 rounded-lg p-4">
|
||||||
|
<span class="text-sm text-foreground">Theme</span>
|
||||||
|
<Button variant="outline" size="icon" class="h-8 w-8" @click="toggleTheme">
|
||||||
|
<Sun v-if="theme === 'dark'" class="w-4 h-4" />
|
||||||
|
<Moon v-else class="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator class="my-6" />
|
||||||
|
|
||||||
|
<!-- Language -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">Language</h2>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
v-for="lang in (['fr', 'en'] as const)"
|
||||||
|
:key="lang"
|
||||||
|
:variant="locale === lang ? 'default' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
@click="setLanguage(lang)"
|
||||||
|
>
|
||||||
|
{{ lang === 'fr' ? 'Français' : 'English' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -32,6 +32,33 @@ export const activitiesModule = createModulePlugin({
|
||||||
requiresAuth: false,
|
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',
|
path: '/activities/:id',
|
||||||
name: 'activity-detail',
|
name: 'activity-detail',
|
||||||
|
|
|
||||||
13
src/modules/activities/views/ActivitiesCalendarPage.vue
Normal file
13
src/modules/activities/views/ActivitiesCalendarPage.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CalendarDays } from 'lucide-vue-next'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto px-4 py-6">
|
||||||
|
<h1 class="text-2xl font-bold text-foreground mb-4">Calendar</h1>
|
||||||
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
|
<CalendarDays class="w-16 h-16 text-muted-foreground/30 mb-4" />
|
||||||
|
<p class="text-muted-foreground">Calendar view coming in Phase 5</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
14
src/modules/activities/views/ActivitiesFavoritesPage.vue
Normal file
14
src/modules/activities/views/ActivitiesFavoritesPage.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Heart } from 'lucide-vue-next'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto px-4 py-6">
|
||||||
|
<h1 class="text-2xl font-bold text-foreground mb-4">Favorites</h1>
|
||||||
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
|
<Heart class="w-16 h-16 text-muted-foreground/30 mb-4" />
|
||||||
|
<p class="text-muted-foreground">Bookmark your favorite activities</p>
|
||||||
|
<p class="text-sm text-muted-foreground/70 mt-1">Coming in Phase 4</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
13
src/modules/activities/views/ActivitiesMapPage.vue
Normal file
13
src/modules/activities/views/ActivitiesMapPage.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Map } from 'lucide-vue-next'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto px-4 py-6">
|
||||||
|
<h1 class="text-2xl font-bold text-foreground mb-4">Map</h1>
|
||||||
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
|
<Map class="w-16 h-16 text-muted-foreground/30 mb-4" />
|
||||||
|
<p class="text-muted-foreground">Map view coming in Phase 5</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
113
vite.activities.config.ts
Normal file
113
vite.activities.config.ts
Normal file
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue