diff --git a/src/core/base/BaseModulePlugin.ts b/src/core/base/BaseModulePlugin.ts new file mode 100644 index 0000000..797daee --- /dev/null +++ b/src/core/base/BaseModulePlugin.ts @@ -0,0 +1,259 @@ +import type { App, Component } from 'vue' +import type { ModulePlugin, ServiceToken } from '@/core/types' +import type { RouteRecordRaw } from 'vue-router' +import { container } from '@/core/di-container' +import { eventBus } from '@/core/event-bus' + +/** + * Module service registration configuration + */ +export interface ServiceRegistration { + token: ServiceToken + service: any + initialize?: { + waitForDependencies?: boolean + maxRetries?: number + } +} + +/** + * Module event listener configuration + */ +export interface EventListener { + event: string + handler: (event: any) => void + description?: string +} + +/** + * Module configuration for BaseModulePlugin + */ +export interface BaseModuleConfig { + /** Module name */ + name: string + + /** Module version */ + version: string + + /** Required dependencies */ + dependencies?: string[] + + /** Services to register in DI container */ + services?: ServiceRegistration[] + + /** Components to register globally */ + components?: Record + + /** Routes provided by this module */ + routes?: RouteRecordRaw[] + + /** Event listeners to set up */ + eventListeners?: EventListener[] + + /** Custom installation logic (called after standard setup) */ + onInstall?: (app: App, options?: any) => Promise | void + + /** Custom uninstallation logic (called before standard cleanup) */ + onUninstall?: () => Promise | void + + /** Re-exports from this module */ + exports?: { + types?: Record + composables?: Record + } +} + +/** + * Base class for module plugins that provides common registration patterns + * Eliminates boilerplate code and ensures consistency across modules + */ +export abstract class BaseModulePlugin implements ModulePlugin { + public readonly name: string + public readonly version: string + public readonly dependencies?: string[] + public readonly routes?: RouteRecordRaw[] + public readonly components?: Record + public readonly services?: Record + public readonly composables?: Record + public readonly config?: Record + + protected moduleConfig: BaseModuleConfig + protected installedServices: ServiceToken[] = [] + + constructor(config: BaseModuleConfig) { + this.moduleConfig = config + this.config = config as any // ModulePlugin config compatibility + this.name = config.name + this.version = config.version + this.dependencies = config.dependencies + this.routes = config.routes + this.components = config.components + this.composables = config.exports?.composables + } + + /** + * Standard module installation with common patterns + */ + async install(app: App, options?: any): Promise { + this.logInstallStart() + + try { + // 1. Register global components + await this.registerComponents(app) + + // 2. Register and initialize services + await this.registerServices() + + // 3. Set up event listeners + this.setupEventListeners() + + // 4. Call custom installation logic + if (this.moduleConfig.onInstall) { + await this.moduleConfig.onInstall(app, options) + } + + this.logInstallComplete() + + } catch (error) { + console.error(`❌ Failed to install ${this.name} module:`, error) + throw error + } + } + + /** + * Standard module uninstallation with cleanup + */ + async uninstall(): Promise { + this.logUninstallStart() + + try { + // 1. Call custom uninstallation logic + if (this.moduleConfig.onUninstall) { + await this.moduleConfig.onUninstall() + } + + // 2. Clean up registered services + this.cleanupServices() + + this.logUninstallComplete() + + } catch (error) { + console.error(`❌ Failed to uninstall ${this.name} module:`, error) + } + } + + /** + * Register components globally with the Vue app + */ + protected async registerComponents(app: App): Promise { + if (!this.moduleConfig.components) return + + for (const [name, component] of Object.entries(this.moduleConfig.components)) { + app.component(name, component) + } + } + + /** + * Register and initialize services in the DI container + */ + protected async registerServices(): Promise { + if (!this.moduleConfig.services) return + + for (const serviceConfig of this.moduleConfig.services) { + // Register service in DI container + container.provide(serviceConfig.token, serviceConfig.service) + this.installedServices.push(serviceConfig.token) + + // Initialize service if it has an initialize method and config + if (serviceConfig.initialize && serviceConfig.service.initialize) { + try { + await serviceConfig.service.initialize(serviceConfig.initialize) + } catch (error) { + console.warn(`⚠️ ${this.name} service initialization deferred:`, error) + // Service will auto-initialize when dependencies are available + } + } + } + } + + /** + * Set up event listeners for inter-module communication + */ + protected setupEventListeners(): void { + // Set up standard auth logout listener for all modules + eventBus.on('auth:logout', () => { + this.handleAuthLogout() + }) + + // Set up custom event listeners + if (this.moduleConfig.eventListeners) { + for (const listener of this.moduleConfig.eventListeners) { + eventBus.on(listener.event, listener.handler) + } + } + } + + /** + * Handle authentication logout (override in subclasses if needed) + */ + protected handleAuthLogout(): void { + console.log(`${this.name} module: user logged out`) + // Default behavior - can be overridden by individual modules + } + + /** + * Clean up registered services + */ + protected cleanupServices(): void { + for (const token of this.installedServices) { + container.remove(token) + } + this.installedServices = [] + } + + /** + * Logging helpers with consistent emoji patterns + */ + protected logInstallStart(): void { + const emoji = this.getModuleEmoji() + console.log(`${emoji} Installing ${this.name} module...`) + } + + protected logInstallComplete(): void { + console.log(`✅ ${this.name} module installed successfully`) + } + + protected logUninstallStart(): void { + console.log(`🗑️ Uninstalling ${this.name} module...`) + } + + protected logUninstallComplete(): void { + console.log(`✅ ${this.name} module uninstalled`) + } + + /** + * Get emoji for this module type (override in subclasses) + */ + protected getModuleEmoji(): string { + const emojiMap: Record = { + 'chat': '💬', + 'market': '🛒', + 'events': '🎫', + 'nostr-feed': '📰', + 'base': '🔧' + } + return emojiMap[this.name] || '🔌' + } +} + +/** + * Helper function to create a module plugin from configuration + * Eliminates the need for boilerplate class definitions + */ +export function createModulePlugin(config: BaseModuleConfig): ModulePlugin { + return new (class extends BaseModulePlugin { + constructor() { + super(config) + } + })() +} \ No newline at end of file diff --git a/src/modules/events/index.ts b/src/modules/events/index.ts index 56fa69c..8e5b725 100644 --- a/src/modules/events/index.ts +++ b/src/modules/events/index.ts @@ -1,10 +1,4 @@ -import type { App } from 'vue' -import type { ModulePlugin } from '@/core/types' -import type { RouteRecordRaw } from 'vue-router' -import { container } from '@/core/di-container' -import { eventBus } from '@/core/event-bus' - -// Import components and services +import { createModulePlugin } from '@/core/base/BaseModulePlugin' import PurchaseTicketDialog from './components/PurchaseTicketDialog.vue' import { EventsApiService, type EventsApiConfig } from './services/events-api' import { useEvents, EVENTS_API_TOKEN } from './composables/useEvents' @@ -19,41 +13,15 @@ export interface EventsModuleConfig { * Events Module Plugin * Provides event management and ticket purchasing functionality */ -export const eventsModule: ModulePlugin = { +export const eventsModule = createModulePlugin({ name: 'events', version: '1.0.0', dependencies: ['base'], - - async install(app: App, options?: { config?: EventsModuleConfig }) { - console.log('🎫 Installing events module...') - - const config = options?.config - if (!config) { - throw new Error('Events module requires configuration') - } - - // Create and register events API service - const eventsApiService = new EventsApiService(config.apiConfig) - container.provide(EVENTS_API_TOKEN, eventsApiService) - - // Register global components - app.component('PurchaseTicketDialog', PurchaseTicketDialog) - - // Set up event listeners for integration with other modules - setupEventListeners() - - console.log('✅ Events module installed successfully') + + components: { + PurchaseTicketDialog }, - - async uninstall() { - console.log('🗑️ Uninstalling events module...') - - // Clean up services - container.remove(EVENTS_API_TOKEN) - - console.log('✅ Events module uninstalled') - }, - + routes: [ { path: '/events', @@ -73,40 +41,55 @@ export const eventsModule: ModulePlugin = { requiresAuth: true } } - ] as RouteRecordRaw[], + ], + + eventListeners: [ + { + event: 'payment:completed', + handler: (event) => { + console.log('Events module: payment completed', event.data) + // Could refresh events or ticket status here + }, + description: 'Handle payment completion to refresh ticket status' + }, + { + event: 'events:ticket-purchased', + handler: (event) => { + console.log('Ticket purchased:', event.data) + // Other modules can listen to this event + }, + description: 'Emit ticket purchase events for other modules' + } + ], + + onInstall: async (_app, options) => { + const config = options?.config as EventsModuleConfig | undefined + if (!config) { + throw new Error('Events module requires configuration') + } - components: { - PurchaseTicketDialog + // Create and register events API service manually since it needs config + const eventsApiService = new EventsApiService(config.apiConfig) + const { container } = await import('@/core/di-container') + container.provide(EVENTS_API_TOKEN, eventsApiService) }, - - composables: { - useEvents + + onUninstall: async () => { + const { container } = await import('@/core/di-container') + container.remove(EVENTS_API_TOKEN) }, - - services: { - eventsApi: EVENTS_API_TOKEN + + exports: { + composables: { + useEvents + } } -} +}) -// Set up event listeners for integration with other modules -function setupEventListeners() { - // Listen for auth events - eventBus.on('auth:logout', () => { - // Clear any cached event data if needed - console.log('Events module: user logged out, clearing cache') - }) - - // Listen for payment events from other modules - eventBus.on('payment:completed', (event) => { - console.log('Events module: payment completed', event.data) - // Could refresh events or ticket status here - }) - - // Emit events for other modules - eventBus.on('events:ticket-purchased', (event) => { - console.log('Ticket purchased:', event.data) - // Other modules can listen to this event - }) +// Override auth logout handler for events-specific cleanup +;(eventsModule as any).handleAuthLogout = () => { + console.log('Events module: user logged out, clearing cache') + // Clear any cached event data if needed } export default eventsModule diff --git a/src/modules/nostr-feed/index.ts b/src/modules/nostr-feed/index.ts index 95f8958..d4f3b39 100644 --- a/src/modules/nostr-feed/index.ts +++ b/src/modules/nostr-feed/index.ts @@ -1,6 +1,4 @@ -import type { App } from 'vue' -import type { ModulePlugin } from '@/core/types' -import type { RouteRecordRaw } from 'vue-router' +import { createModulePlugin } from '@/core/base/BaseModulePlugin' import NostrFeed from './components/NostrFeed.vue' import { useFeed } from './composables/useFeed' @@ -8,40 +6,22 @@ import { useFeed } from './composables/useFeed' * Nostr Feed Module Plugin * Provides social feed functionality with admin announcements support */ -export const nostrFeedModule: ModulePlugin = { +export const nostrFeedModule = createModulePlugin({ name: 'nostr-feed', version: '1.0.0', dependencies: ['base'], - - async install(app: App, _options?: any) { - console.log('📰 Installing nostr-feed module...') - - // Register global components - app.component('NostrFeed', NostrFeed) - - // Module-specific initialization - console.log('✅ Nostr-feed module installed successfully') - }, - - async uninstall() { - console.log('🗑️ Uninstalling nostr-feed module...') - // Cleanup if needed - console.log('✅ Nostr-feed module uninstalled') - }, - - // Routes - currently none, but feed could have its own page - routes: [] as RouteRecordRaw[], - + components: { NostrFeed }, - - composables: { - useFeed - }, - - // Services that other modules can use - services: {} -} + + routes: [], + + exports: { + composables: { + useFeed + } + } +}) export default nostrFeedModule \ No newline at end of file