diff --git a/src/components/auth/LoginDialog.vue b/src/components/auth/LoginDialog.vue index 485f1f8..5e9ade4 100644 --- a/src/components/auth/LoginDialog.vue +++ b/src/components/auth/LoginDialog.vue @@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { User } from 'lucide-vue-next' import { auth } from '@/composables/useAuth' -import { toast } from 'vue-sonner' +import { useToast } from '@/core/composables/useToast' interface Props { isOpen: boolean @@ -20,6 +20,7 @@ interface Emits { } const router = useRouter() +const toast = useToast() defineProps() const emit = defineEmits() @@ -63,14 +64,14 @@ async function handleLogin() { password: loginForm.value.password }) - toast.success('Login successful!') + toast.auth.loginSuccess() emit('success') handleClose() // Redirect to home page after successful login router.push('/') } catch (err) { error.value = err instanceof Error ? err.message : 'Login failed' - toast.error('Login failed. Please check your credentials.') + toast.auth.loginError() } finally { isLoading.value = false } @@ -90,14 +91,14 @@ async function handleRegister() { password_repeat: registerForm.value.password_repeat }) - toast.success('Registration successful!') + toast.auth.registrationSuccess() emit('success') handleClose() // Redirect to home page after successful registration router.push('/') } catch (err) { error.value = err instanceof Error ? err.message : 'Registration failed' - toast.error('Registration failed. Please try again.') + toast.auth.registrationError() } finally { isLoading.value = false } diff --git a/src/core/base/BaseService.ts b/src/core/base/BaseService.ts index e6a9dd9..f08ac44 100644 --- a/src/core/base/BaseService.ts +++ b/src/core/base/BaseService.ts @@ -46,6 +46,7 @@ export abstract class BaseService { protected authService: any = null protected visibilityService: any = null protected storageService: any = null + protected toastService: any = null // Service state public readonly isInitialized: Ref = ref(false) @@ -136,6 +137,7 @@ export abstract class BaseService { this.authService = tryInjectService(SERVICE_TOKENS.AUTH_SERVICE) this.visibilityService = tryInjectService(SERVICE_TOKENS.VISIBILITY_SERVICE) this.storageService = tryInjectService(SERVICE_TOKENS.STORAGE_SERVICE) + this.toastService = tryInjectService(SERVICE_TOKENS.TOAST_SERVICE) // Check if all required dependencies are available const missingDeps = this.getMissingDependencies() @@ -186,6 +188,9 @@ export abstract class BaseService { if (deps.includes('StorageService') && !this.storageService) { missing.push('StorageService') } + if (deps.includes('ToastService') && !this.toastService) { + missing.push('ToastService') + } return missing } @@ -271,6 +276,7 @@ export abstract class BaseService { this.authService = null this.visibilityService = null this.storageService = null + this.toastService = null console.log(`♻️ ${this.metadata.name} disposed`) diff --git a/src/core/composables/useToast.ts b/src/core/composables/useToast.ts new file mode 100644 index 0000000..4ef9169 --- /dev/null +++ b/src/core/composables/useToast.ts @@ -0,0 +1,66 @@ +import { injectService, SERVICE_TOKENS } from '@/core/di-container' +import type { ToastService } from '@/core/services/ToastService' + +/** + * Composable for accessing centralized toast notifications + * Provides consistent toast patterns across the application + */ +export function useToast() { + const toastService = injectService(SERVICE_TOKENS.TOAST_SERVICE) as ToastService + + if (!toastService) { + console.error('ToastService not available - make sure base module is installed') + // Return a fallback that does nothing to prevent runtime errors + return { + success: () => {}, + error: () => {}, + info: () => {}, + warning: () => {}, + loading: () => '', + dismiss: () => {}, + auth: { + loginSuccess: () => {}, + loginError: () => {}, + logoutSuccess: () => {}, + registrationSuccess: () => {}, + registrationError: () => {} + }, + payment: { + processing: () => '', + success: () => {}, + failed: () => {}, + copied: () => {}, + copyFailed: () => {} + }, + clipboard: { + copied: () => {}, + copyFailed: () => {} + }, + operation: { + success: () => {}, + failed: () => {}, + loading: () => '' + }, + asyncOperation: async (operation: Promise) => operation + } + } + + return { + // Basic toast methods + success: toastService.success.bind(toastService), + error: toastService.error.bind(toastService), + info: toastService.info.bind(toastService), + warning: toastService.warning.bind(toastService), + loading: toastService.loading.bind(toastService), + dismiss: toastService.dismiss.bind(toastService), + + // Context-specific methods + auth: toastService.auth, + payment: toastService.payment, + clipboard: toastService.clipboard, + operation: toastService.operation, + + // Advanced method + asyncOperation: toastService.asyncOperation.bind(toastService) + } +} \ No newline at end of file diff --git a/src/core/di-container.ts b/src/core/di-container.ts index 9035af7..21e5f4f 100644 --- a/src/core/di-container.ts +++ b/src/core/di-container.ts @@ -122,6 +122,9 @@ export const SERVICE_TOKENS = { // Storage services STORAGE_SERVICE: Symbol('storageService'), + // Toast services + TOAST_SERVICE: Symbol('toastService'), + // Market services MARKET_STORE: Symbol('marketStore'), PAYMENT_MONITOR: Symbol('paymentMonitor'), diff --git a/src/core/services/ToastService.ts b/src/core/services/ToastService.ts new file mode 100644 index 0000000..d126543 --- /dev/null +++ b/src/core/services/ToastService.ts @@ -0,0 +1,303 @@ +import { BaseService } from '@/core/base/BaseService' +import { toast } from 'vue-sonner' + +// Define our own ToastOptions interface based on vue-sonner's common options +interface ToastOptions { + duration?: number + position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center' + richColors?: boolean + closeButton?: boolean + description?: string + action?: { + label: string + onClick: () => void + } + cancel?: { + label: string + onClick?: () => void + } + id?: string + dismissible?: boolean + onDismiss?: () => void + onAutoClose?: () => void +} + +/** + * Toast notification configuration + */ +interface ToastConfig { + duration: number + position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center' + richColors: boolean + closeButton: boolean +} + +/** + * Predefined toast action types with consistent styling + */ +export enum ToastActionType { + SUCCESS = 'success', + ERROR = 'error', + WARNING = 'warning', + INFO = 'info', + LOADING = 'loading' +} + +/** + * Context-specific toast message templates + */ +interface ToastContext { + auth: { + loginSuccess: string + loginError: string + logoutSuccess: string + registrationSuccess: string + registrationError: string + } + payment: { + processing: string + success: string + failed: string + copied: string + copyFailed: string + } + clipboard: { + copied: string + copyFailed: string + } + general: { + operationSuccess: string + operationFailed: string + loading: string + } +} + +/** + * Centralized Toast Service providing standardized notifications + * Eliminates duplicate import patterns and ensures consistent styling + */ +export class ToastService extends BaseService { + // Service metadata + protected readonly metadata = { + name: 'ToastService', + version: '1.0.0', + dependencies: [] // No dependencies + } + + private config: ToastConfig = { + duration: 4000, + position: 'top-right', + richColors: true, + closeButton: false + } + + private context: ToastContext = { + auth: { + loginSuccess: 'Login successful!', + loginError: 'Login failed. Please check your credentials.', + logoutSuccess: 'Logged out successfully', + registrationSuccess: 'Registration successful!', + registrationError: 'Registration failed. Please try again.' + }, + payment: { + processing: 'Processing payment...', + success: 'Payment successful!', + failed: 'Payment failed', + copied: 'Payment request copied to clipboard', + copyFailed: 'Failed to copy payment request' + }, + clipboard: { + copied: 'Copied to clipboard!', + copyFailed: 'Failed to copy to clipboard' + }, + general: { + operationSuccess: 'Operation completed successfully', + operationFailed: 'Operation failed', + loading: 'Loading...' + } + } + + /** + * Service initialization + */ + protected async onInitialize(): Promise { + this.debug('ToastService initialized') + } + + /** + * Show success toast + */ + success(message: string, options?: Partial): void { + toast.success(message, { + duration: this.config.duration, + ...options + }) + } + + /** + * Show error toast + */ + error(message: string, options?: Partial): void { + toast.error(message, { + duration: this.config.duration, + ...options + }) + } + + /** + * Show info toast + */ + info(message: string, options?: Partial): void { + toast.info(message, { + duration: this.config.duration, + ...options + }) + } + + /** + * Show warning toast + */ + warning(message: string, options?: Partial): void { + toast.warning(message, { + duration: this.config.duration, + ...options + }) + } + + /** + * Show loading toast + */ + loading(message: string, options?: Partial): string | number { + return toast.loading(message, { + duration: Infinity, // Loading toasts don't auto-dismiss + ...options + }) + } + + /** + * Dismiss a specific toast + */ + dismiss(toastId?: string | number): void { + toast.dismiss(toastId) + } + + /** + * Context-specific toast methods + */ + + // Authentication toasts + auth = { + loginSuccess: (options?: Partial) => + this.success(this.context.auth.loginSuccess, options), + + loginError: (error?: string, options?: Partial) => + this.error(error || this.context.auth.loginError, options), + + logoutSuccess: (options?: Partial) => + this.success(this.context.auth.logoutSuccess, options), + + registrationSuccess: (options?: Partial) => + this.success(this.context.auth.registrationSuccess, options), + + registrationError: (error?: string, options?: Partial) => + this.error(error || this.context.auth.registrationError, options) + } + + // Payment toasts + payment = { + processing: (options?: Partial) => + this.loading(this.context.payment.processing, options), + + success: (message?: string, options?: Partial) => + this.success(message || this.context.payment.success, options), + + failed: (error?: string, options?: Partial) => + this.error(error || this.context.payment.failed, options), + + copied: (options?: Partial) => + this.success(this.context.payment.copied, options), + + copyFailed: (options?: Partial) => + this.error(this.context.payment.copyFailed, options) + } + + // Clipboard toasts + clipboard = { + copied: (item?: string, options?: Partial) => + this.success(item ? `${item} copied to clipboard!` : this.context.clipboard.copied, options), + + copyFailed: (item?: string, options?: Partial) => + this.error(item ? `Failed to copy ${item}` : this.context.clipboard.copyFailed, options) + } + + // General operation toasts + operation = { + success: (message?: string, options?: Partial) => + this.success(message || this.context.general.operationSuccess, options), + + failed: (error?: string, options?: Partial) => + this.error(error || this.context.general.operationFailed, options), + + loading: (message?: string, options?: Partial) => + this.loading(message || this.context.general.loading, options) + } + + /** + * Create a toast for async operations with auto-updating states + */ + async asyncOperation( + operation: Promise, + messages: { + loading?: string + success?: string | ((result: T) => string) + error?: string | ((error: Error) => string) + }, + options?: Partial + ): Promise { + const loadingToast = this.loading(messages.loading || 'Processing...', options) + + try { + const result = await operation + this.dismiss(loadingToast) + + const successMessage = typeof messages.success === 'function' + ? messages.success(result) + : messages.success || 'Operation successful!' + + this.success(successMessage, options) + return result + + } catch (error) { + this.dismiss(loadingToast) + + const errorMessage = typeof messages.error === 'function' + ? messages.error(error as Error) + : messages.error || (error as Error).message || 'Operation failed' + + this.error(errorMessage, options) + throw error + } + } + + /** + * Update toast configuration + */ + updateConfig(newConfig: Partial): void { + this.config = { ...this.config, ...newConfig } + } + + /** + * Update context messages + */ + updateContext(newContext: Partial): void { + this.context = { + auth: { ...this.context.auth, ...newContext.auth }, + payment: { ...this.context.payment, ...newContext.payment }, + clipboard: { ...this.context.clipboard, ...newContext.clipboard }, + general: { ...this.context.general, ...newContext.general } + } + } +} + +// Export singleton instance +export const toastService = new ToastService() \ No newline at end of file diff --git a/src/modules/base/index.ts b/src/modules/base/index.ts index a102f4d..28ea856 100644 --- a/src/modules/base/index.ts +++ b/src/modules/base/index.ts @@ -13,6 +13,7 @@ import { pwaService } from './pwa/pwa-service' import { paymentService } from '@/core/services/PaymentService' import { visibilityService } from '@/core/services/VisibilityService' import { storageService } from '@/core/services/StorageService' +import { toastService } from '@/core/services/ToastService' /** * Base Module Plugin @@ -40,6 +41,9 @@ export const baseModule: ModulePlugin = { // Register storage service container.provide(SERVICE_TOKENS.STORAGE_SERVICE, storageService) + // Register toast service + container.provide(SERVICE_TOKENS.TOAST_SERVICE, toastService) + // Register PWA service container.provide('pwaService', pwaService) @@ -62,6 +66,10 @@ export const baseModule: ModulePlugin = { waitForDependencies: true, // StorageService depends on AuthService maxRetries: 3 }) + await toastService.initialize({ + waitForDependencies: false, // ToastService has no dependencies + maxRetries: 1 + }) console.log('✅ Base module installed successfully') }, @@ -75,6 +83,7 @@ export const baseModule: ModulePlugin = { await paymentService.dispose() await visibilityService.dispose() await storageService.dispose() + await toastService.dispose() console.log('✅ Base module uninstalled') }, @@ -85,6 +94,7 @@ export const baseModule: ModulePlugin = { paymentService, visibilityService, storageService, + toastService, pwaService },