You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1304 lines
33 KiB
1304 lines
33 KiB
/**
|
|
* UI Integration Examples for Daily Notification Plugin
|
|
*
|
|
* This file provides comprehensive examples of how to integrate
|
|
* the Daily Notification Plugin UI components into your application.
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
* @created 2025-01-27 12:00:00 UTC
|
|
*/
|
|
|
|
import { DailyNotification } from '@timesafari/daily-notification-plugin';
|
|
import type {
|
|
NotificationSettings,
|
|
ConfigureOptions,
|
|
DualScheduleStatus,
|
|
PermissionStatus,
|
|
BatteryStatus,
|
|
ExactAlarmStatus,
|
|
RollingWindowStats,
|
|
PerformanceMetrics
|
|
} from '@timesafari/daily-notification-plugin';
|
|
|
|
// ============================================================================
|
|
// 1. PERMISSION MANAGEMENT UI
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Permission Request Dialog Component
|
|
* Handles initial permission requests and status display
|
|
*/
|
|
export class PermissionRequestDialog {
|
|
private container: HTMLElement;
|
|
private onAllow: () => Promise<void>;
|
|
private onDeny: () => void;
|
|
private onNever: () => void;
|
|
|
|
constructor(container: HTMLElement, callbacks: {
|
|
onAllow: () => Promise<void>;
|
|
onDeny: () => void;
|
|
onNever: () => void;
|
|
}) {
|
|
this.container = container;
|
|
this.onAllow = callbacks.onAllow;
|
|
this.onDeny = callbacks.onDeny;
|
|
this.onNever = callbacks.onNever;
|
|
}
|
|
|
|
/**
|
|
* Show permission request dialog
|
|
*/
|
|
async show(): Promise<void> {
|
|
const dialog = this.createDialog();
|
|
this.container.appendChild(dialog);
|
|
|
|
// Add event listeners
|
|
dialog.querySelector('#allow-btn')?.addEventListener('click', async () => {
|
|
await this.onAllow();
|
|
this.hide();
|
|
});
|
|
|
|
dialog.querySelector('#deny-btn')?.addEventListener('click', () => {
|
|
this.onDeny();
|
|
this.hide();
|
|
});
|
|
|
|
dialog.querySelector('#never-btn')?.addEventListener('click', () => {
|
|
this.onNever();
|
|
this.hide();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hide permission request dialog
|
|
*/
|
|
hide(): void {
|
|
const dialog = this.container.querySelector('.permission-dialog');
|
|
if (dialog) {
|
|
dialog.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create permission request dialog HTML
|
|
*/
|
|
private createDialog(): HTMLElement {
|
|
const dialog = document.createElement('div');
|
|
dialog.className = 'permission-dialog';
|
|
dialog.innerHTML = `
|
|
<div class="dialog-overlay">
|
|
<div class="dialog-content">
|
|
<h2>Enable Daily Notifications</h2>
|
|
<p>Get notified about new offers, projects, people, and items in your TimeSafari community.</p>
|
|
<ul>
|
|
<li>New offers directed to you</li>
|
|
<li>Changes to your projects</li>
|
|
<li>Updates from favorited people</li>
|
|
<li>New items of interest</li>
|
|
</ul>
|
|
<div class="dialog-actions">
|
|
<button id="allow-btn" class="btn-primary">Allow Notifications</button>
|
|
<button id="deny-btn" class="btn-secondary">Not Now</button>
|
|
<button id="never-btn" class="btn-tertiary">Never Ask Again</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
return dialog;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Permission Status Display Component
|
|
* Shows current permission status and provides actions
|
|
*/
|
|
export class PermissionStatusDisplay {
|
|
private container: HTMLElement;
|
|
|
|
constructor(container: HTMLElement) {
|
|
this.container = container;
|
|
}
|
|
|
|
/**
|
|
* Update permission status display
|
|
*/
|
|
async updateStatus(): Promise<void> {
|
|
try {
|
|
const status = await DailyNotification.checkPermissions();
|
|
this.renderStatus(status);
|
|
} catch (error) {
|
|
console.error('Failed to check permissions:', error);
|
|
this.renderError('Failed to check permission status');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render permission status
|
|
*/
|
|
private renderStatus(status: PermissionStatus): void {
|
|
const statusClass = status.granted ? 'status-granted' : 'status-denied';
|
|
const statusText = status.granted ? 'Granted' : 'Denied';
|
|
|
|
this.container.innerHTML = `
|
|
<div class="permission-status ${statusClass}">
|
|
<div class="status-indicator">
|
|
<span class="status-icon">${status.granted ? '✓' : '✗'}</span>
|
|
<span class="status-text">${statusText}</span>
|
|
</div>
|
|
<div class="permission-details">
|
|
<div class="permission-item">
|
|
<span>Notifications:</span>
|
|
<span class="${status.notifications === 'granted' ? 'granted' : 'denied'}">
|
|
${status.notifications}
|
|
</span>
|
|
</div>
|
|
${status.backgroundRefresh ? `
|
|
<div class="permission-item">
|
|
<span>Background Refresh:</span>
|
|
<span class="${status.backgroundRefresh === 'granted' ? 'granted' : 'denied'}">
|
|
${status.backgroundRefresh}
|
|
</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
<div class="permission-actions">
|
|
<button id="request-permissions" class="btn-primary">Request Permissions</button>
|
|
<button id="open-settings" class="btn-secondary">Open Settings</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
this.container.querySelector('#request-permissions')?.addEventListener('click', async () => {
|
|
try {
|
|
await DailyNotification.requestPermissions();
|
|
await this.updateStatus();
|
|
} catch (error) {
|
|
console.error('Failed to request permissions:', error);
|
|
}
|
|
});
|
|
|
|
this.container.querySelector('#open-settings')?.addEventListener('click', () => {
|
|
// Platform-specific settings opening
|
|
this.openSettings();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render error state
|
|
*/
|
|
private renderError(message: string): void {
|
|
this.container.innerHTML = `
|
|
<div class="permission-status status-error">
|
|
<div class="error-message">${message}</div>
|
|
<button id="retry-permissions" class="btn-primary">Retry</button>
|
|
</div>
|
|
`;
|
|
|
|
this.container.querySelector('#retry-permissions')?.addEventListener('click', () => {
|
|
this.updateStatus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Open platform-specific settings
|
|
*/
|
|
private openSettings(): void {
|
|
// This would be platform-specific implementation
|
|
console.log('Opening settings...');
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 2. CONFIGURATION UI
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Notification Settings Panel Component
|
|
* Handles notification configuration and preferences
|
|
*/
|
|
export class NotificationSettingsPanel {
|
|
private container: HTMLElement;
|
|
private settings: NotificationSettings;
|
|
|
|
constructor(container: HTMLElement) {
|
|
this.container = container;
|
|
this.settings = this.getDefaultSettings();
|
|
}
|
|
|
|
/**
|
|
* Initialize settings panel
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
try {
|
|
// Load current settings
|
|
const status = await DailyNotification.getNotificationStatus();
|
|
this.settings = {
|
|
...this.settings,
|
|
...status
|
|
};
|
|
|
|
this.render();
|
|
this.attachEventListeners();
|
|
} catch (error) {
|
|
console.error('Failed to initialize settings:', error);
|
|
this.renderError('Failed to load settings');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render settings panel
|
|
*/
|
|
private render(): void {
|
|
this.container.innerHTML = `
|
|
<div class="settings-panel">
|
|
<h3>Notification Settings</h3>
|
|
|
|
<div class="setting-group">
|
|
<label class="setting-label">
|
|
<input type="checkbox" id="enable-notifications" ${this.settings.isEnabled ? 'checked' : ''}>
|
|
Enable Daily Notifications
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<label for="notification-time">Notification Time:</label>
|
|
<input type="time" id="notification-time" value="${this.settings.time || '09:00'}">
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<label>Content Types:</label>
|
|
<div class="checkbox-group">
|
|
<label><input type="checkbox" id="content-offers" checked> Offers</label>
|
|
<label><input type="checkbox" id="content-projects" checked> Projects</label>
|
|
<label><input type="checkbox" id="content-people" checked> People</label>
|
|
<label><input type="checkbox" id="content-items" checked> Items</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<label>Notification Preferences:</label>
|
|
<div class="preference-grid">
|
|
<label>
|
|
<input type="checkbox" id="sound-enabled" ${this.settings.sound ? 'checked' : ''}>
|
|
Sound
|
|
</label>
|
|
<label>
|
|
<input type="checkbox" id="vibration-enabled" checked>
|
|
Vibration
|
|
</label>
|
|
<label>
|
|
<select id="priority-level">
|
|
<option value="low" ${this.settings.priority === 'low' ? 'selected' : ''}>Low</option>
|
|
<option value="normal" ${this.settings.priority === 'normal' ? 'selected' : ''}>Normal</option>
|
|
<option value="high" ${this.settings.priority === 'high' ? 'selected' : ''}>High</option>
|
|
</select>
|
|
Priority
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-actions">
|
|
<button id="test-notification" class="btn-secondary">Test Notification</button>
|
|
<button id="save-settings" class="btn-primary">Save Settings</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Attach event listeners
|
|
*/
|
|
private attachEventListeners(): void {
|
|
// Save settings
|
|
this.container.querySelector('#save-settings')?.addEventListener('click', async () => {
|
|
await this.saveSettings();
|
|
});
|
|
|
|
// Test notification
|
|
this.container.querySelector('#test-notification')?.addEventListener('click', async () => {
|
|
await this.sendTestNotification();
|
|
});
|
|
|
|
// Real-time updates
|
|
this.container.querySelectorAll('input, select').forEach(element => {
|
|
element.addEventListener('change', () => {
|
|
this.updateSettingsFromUI();
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update settings from UI
|
|
*/
|
|
private updateSettingsFromUI(): void {
|
|
const enableNotifications = (this.container.querySelector('#enable-notifications') as HTMLInputElement)?.checked;
|
|
const notificationTime = (this.container.querySelector('#notification-time') as HTMLInputElement)?.value;
|
|
const soundEnabled = (this.container.querySelector('#sound-enabled') as HTMLInputElement)?.checked;
|
|
const priorityLevel = (this.container.querySelector('#priority-level') as HTMLSelectElement)?.value;
|
|
|
|
this.settings = {
|
|
...this.settings,
|
|
isEnabled: enableNotifications,
|
|
time: notificationTime,
|
|
sound: soundEnabled,
|
|
priority: priorityLevel
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Save settings
|
|
*/
|
|
private async saveSettings(): Promise<void> {
|
|
try {
|
|
await DailyNotification.updateSettings(this.settings);
|
|
this.showSuccess('Settings saved successfully');
|
|
} catch (error) {
|
|
console.error('Failed to save settings:', error);
|
|
this.showError('Failed to save settings');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send test notification
|
|
*/
|
|
private async sendTestNotification(): Promise<void> {
|
|
try {
|
|
// This would trigger a test notification
|
|
this.showSuccess('Test notification sent');
|
|
} catch (error) {
|
|
console.error('Failed to send test notification:', error);
|
|
this.showError('Failed to send test notification');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get default settings
|
|
*/
|
|
private getDefaultSettings(): NotificationSettings {
|
|
return {
|
|
isEnabled: true,
|
|
time: '09:00',
|
|
sound: true,
|
|
priority: 'normal',
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Show success message
|
|
*/
|
|
private showSuccess(message: string): void {
|
|
// Implementation for success message
|
|
console.log('Success:', message);
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
*/
|
|
private showError(message: string): void {
|
|
// Implementation for error message
|
|
console.error('Error:', message);
|
|
}
|
|
|
|
/**
|
|
* Render error state
|
|
*/
|
|
private renderError(message: string): void {
|
|
this.container.innerHTML = `
|
|
<div class="settings-error">
|
|
<p>${message}</p>
|
|
<button id="retry-settings" class="btn-primary">Retry</button>
|
|
</div>
|
|
`;
|
|
|
|
this.container.querySelector('#retry-settings')?.addEventListener('click', () => {
|
|
this.initialize();
|
|
});
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 3. STATUS MONITORING UI
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Status Dashboard Component
|
|
* Displays real-time notification system status
|
|
*/
|
|
export class StatusDashboard {
|
|
private container: HTMLElement;
|
|
private refreshInterval: number | null = null;
|
|
|
|
constructor(container: HTMLElement) {
|
|
this.container = container;
|
|
}
|
|
|
|
/**
|
|
* Initialize status dashboard
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
await this.updateStatus();
|
|
this.startAutoRefresh();
|
|
}
|
|
|
|
/**
|
|
* Update status display
|
|
*/
|
|
async updateStatus(): Promise<void> {
|
|
try {
|
|
const status = await DailyNotification.getDualScheduleStatus();
|
|
this.renderStatus(status);
|
|
} catch (error) {
|
|
console.error('Failed to get status:', error);
|
|
this.renderError('Failed to load status');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render status display
|
|
*/
|
|
private renderStatus(status: DualScheduleStatus): void {
|
|
const nextRun = status.nextRuns[0] ? new Date(status.nextRuns[0]) : null;
|
|
const nextRunText = nextRun ? this.formatTimeUntil(nextRun) : 'Not scheduled';
|
|
|
|
const lastOutcome = status.lastOutcomes[0] || 'Unknown';
|
|
const lastOutcomeClass = lastOutcome === 'success' ? 'success' : 'error';
|
|
|
|
this.container.innerHTML = `
|
|
<div class="status-dashboard">
|
|
<h3>Notification Status</h3>
|
|
|
|
<div class="status-grid">
|
|
<div class="status-item">
|
|
<div class="status-label">Overall Status</div>
|
|
<div class="status-value ${status.staleArmed ? 'warning' : 'active'}">
|
|
${status.staleArmed ? 'Stale' : 'Active'}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-item">
|
|
<div class="status-label">Next Notification</div>
|
|
<div class="status-value">${nextRunText}</div>
|
|
</div>
|
|
|
|
<div class="status-item">
|
|
<div class="status-label">Last Outcome</div>
|
|
<div class="status-value ${lastOutcomeClass}">${lastOutcome}</div>
|
|
</div>
|
|
|
|
<div class="status-item">
|
|
<div class="status-label">Cache Age</div>
|
|
<div class="status-value">${status.cacheAgeMs ? this.formatDuration(status.cacheAgeMs) : 'No cache'}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="performance-metrics">
|
|
<h4>Performance</h4>
|
|
<div class="metrics-grid">
|
|
<div class="metric-item">
|
|
<span class="metric-label">Success Rate:</span>
|
|
<span class="metric-value">${status.performance.successRate}%</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-label">Error Count:</span>
|
|
<span class="metric-value">${status.performance.errorCount}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="status-actions">
|
|
<button id="refresh-status" class="btn-secondary">Refresh</button>
|
|
<button id="view-details" class="btn-primary">View Details</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
this.container.querySelector('#refresh-status')?.addEventListener('click', () => {
|
|
this.updateStatus();
|
|
});
|
|
|
|
this.container.querySelector('#view-details')?.addEventListener('click', () => {
|
|
this.showDetailedStatus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start auto-refresh
|
|
*/
|
|
private startAutoRefresh(): void {
|
|
this.refreshInterval = window.setInterval(() => {
|
|
this.updateStatus();
|
|
}, 30000); // Refresh every 30 seconds
|
|
}
|
|
|
|
/**
|
|
* Stop auto-refresh
|
|
*/
|
|
stopAutoRefresh(): void {
|
|
if (this.refreshInterval) {
|
|
clearInterval(this.refreshInterval);
|
|
this.refreshInterval = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format time until date
|
|
*/
|
|
private formatTimeUntil(date: Date): string {
|
|
const now = new Date();
|
|
const diff = date.getTime() - now.getTime();
|
|
|
|
if (diff <= 0) return 'Now';
|
|
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes}m`;
|
|
} else {
|
|
return `${minutes}m`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format duration in milliseconds
|
|
*/
|
|
private formatDuration(ms: number): string {
|
|
const seconds = Math.floor(ms / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes % 60}m`;
|
|
} else if (minutes > 0) {
|
|
return `${minutes}m ${seconds % 60}s`;
|
|
} else {
|
|
return `${seconds}s`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show detailed status
|
|
*/
|
|
private showDetailedStatus(): void {
|
|
// Implementation for detailed status view
|
|
console.log('Showing detailed status...');
|
|
}
|
|
|
|
/**
|
|
* Render error state
|
|
*/
|
|
private renderError(message: string): void {
|
|
this.container.innerHTML = `
|
|
<div class="status-error">
|
|
<p>${message}</p>
|
|
<button id="retry-status" class="btn-primary">Retry</button>
|
|
</div>
|
|
`;
|
|
|
|
this.container.querySelector('#retry-status')?.addEventListener('click', () => {
|
|
this.updateStatus();
|
|
});
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 4. PLATFORM-SPECIFIC UI
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Android Battery Optimization Dialog
|
|
* Handles Android-specific battery optimization prompts
|
|
*/
|
|
export class AndroidBatteryOptimizationDialog {
|
|
private container: HTMLElement;
|
|
|
|
constructor(container: HTMLElement) {
|
|
this.container = container;
|
|
}
|
|
|
|
/**
|
|
* Check and show battery optimization dialog if needed
|
|
*/
|
|
async checkAndShow(): Promise<void> {
|
|
try {
|
|
const batteryStatus = await DailyNotification.getBatteryStatus();
|
|
|
|
if (batteryStatus.optimizationEnabled) {
|
|
this.showDialog();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to check battery status:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show battery optimization dialog
|
|
*/
|
|
private showDialog(): void {
|
|
const dialog = document.createElement('div');
|
|
dialog.className = 'battery-optimization-dialog';
|
|
dialog.innerHTML = `
|
|
<div class="dialog-overlay">
|
|
<div class="dialog-content">
|
|
<h2>Battery Optimization</h2>
|
|
<p>Battery optimization may prevent notifications from being delivered reliably.</p>
|
|
<p>For the best experience, please disable battery optimization for this app.</p>
|
|
<div class="dialog-actions">
|
|
<button id="open-battery-settings" class="btn-primary">Open Battery Settings</button>
|
|
<button id="skip-battery-optimization" class="btn-secondary">Continue Anyway</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.container.appendChild(dialog);
|
|
|
|
// Add event listeners
|
|
dialog.querySelector('#open-battery-settings')?.addEventListener('click', async () => {
|
|
try {
|
|
await DailyNotification.requestBatteryOptimizationExemption();
|
|
this.hideDialog();
|
|
} catch (error) {
|
|
console.error('Failed to open battery settings:', error);
|
|
}
|
|
});
|
|
|
|
dialog.querySelector('#skip-battery-optimization')?.addEventListener('click', () => {
|
|
this.hideDialog();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hide battery optimization dialog
|
|
*/
|
|
private hideDialog(): void {
|
|
const dialog = this.container.querySelector('.battery-optimization-dialog');
|
|
if (dialog) {
|
|
dialog.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* iOS Background App Refresh Dialog
|
|
* Handles iOS-specific background app refresh prompts
|
|
*/
|
|
export class iOSBackgroundRefreshDialog {
|
|
private container: HTMLElement;
|
|
|
|
constructor(container: HTMLElement) {
|
|
this.container = container;
|
|
}
|
|
|
|
/**
|
|
* Check and show background refresh dialog if needed
|
|
*/
|
|
async checkAndShow(): Promise<void> {
|
|
try {
|
|
const status = await DailyNotification.checkPermissions();
|
|
|
|
if (status.backgroundRefresh === 'denied') {
|
|
this.showDialog();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to check background refresh status:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show background refresh dialog
|
|
*/
|
|
private showDialog(): void {
|
|
const dialog = document.createElement('div');
|
|
dialog.className = 'background-refresh-dialog';
|
|
dialog.innerHTML = `
|
|
<div class="dialog-overlay">
|
|
<div class="dialog-content">
|
|
<h2>Background App Refresh</h2>
|
|
<p>Background App Refresh enables the app to fetch new content even when it's not actively being used.</p>
|
|
<p>Without this, notifications will only show cached content.</p>
|
|
<div class="dialog-actions">
|
|
<button id="open-settings" class="btn-primary">Open Settings</button>
|
|
<button id="continue-without" class="btn-secondary">Continue Without</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.container.appendChild(dialog);
|
|
|
|
// Add event listeners
|
|
dialog.querySelector('#open-settings')?.addEventListener('click', () => {
|
|
// Open iOS settings
|
|
window.open('app-settings:', '_blank');
|
|
this.hideDialog();
|
|
});
|
|
|
|
dialog.querySelector('#continue-without')?.addEventListener('click', () => {
|
|
this.hideDialog();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hide background refresh dialog
|
|
*/
|
|
private hideDialog(): void {
|
|
const dialog = this.container.querySelector('.background-refresh-dialog');
|
|
if (dialog) {
|
|
dialog.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 5. ERROR HANDLING UI
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Error Display Component
|
|
* Shows errors and provides recovery options
|
|
*/
|
|
export class ErrorDisplay {
|
|
private container: HTMLElement;
|
|
|
|
constructor(container: HTMLElement) {
|
|
this.container = container;
|
|
}
|
|
|
|
/**
|
|
* Show error
|
|
*/
|
|
showError(error: Error, recoveryActions?: {
|
|
onRetry?: () => Promise<void>;
|
|
onReset?: () => Promise<void>;
|
|
onContactSupport?: () => void;
|
|
}): void {
|
|
this.container.innerHTML = `
|
|
<div class="error-display">
|
|
<div class="error-icon">⚠️</div>
|
|
<div class="error-content">
|
|
<h3>Something went wrong</h3>
|
|
<p class="error-message">${error.message}</p>
|
|
<p class="error-code">Error Code: ${error.name}</p>
|
|
</div>
|
|
<div class="error-actions">
|
|
${recoveryActions?.onRetry ? '<button id="retry-action" class="btn-primary">Retry</button>' : ''}
|
|
${recoveryActions?.onReset ? '<button id="reset-action" class="btn-secondary">Reset Configuration</button>' : ''}
|
|
${recoveryActions?.onContactSupport ? '<button id="support-action" class="btn-tertiary">Contact Support</button>' : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
if (recoveryActions?.onRetry) {
|
|
this.container.querySelector('#retry-action')?.addEventListener('click', async () => {
|
|
try {
|
|
await recoveryActions.onRetry!();
|
|
this.hide();
|
|
} catch (retryError) {
|
|
console.error('Retry failed:', retryError);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (recoveryActions?.onReset) {
|
|
this.container.querySelector('#reset-action')?.addEventListener('click', async () => {
|
|
try {
|
|
await recoveryActions.onReset!();
|
|
this.hide();
|
|
} catch (resetError) {
|
|
console.error('Reset failed:', resetError);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (recoveryActions?.onContactSupport) {
|
|
this.container.querySelector('#support-action')?.addEventListener('click', () => {
|
|
recoveryActions.onContactSupport!();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide error display
|
|
*/
|
|
hide(): void {
|
|
this.container.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 6. USAGE EXAMPLES
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Example: Complete UI Integration
|
|
* Shows how to integrate all UI components
|
|
*/
|
|
export class NotificationUIManager {
|
|
private permissionDialog: PermissionRequestDialog;
|
|
private permissionStatus: PermissionStatusDisplay;
|
|
private settingsPanel: NotificationSettingsPanel;
|
|
private statusDashboard: StatusDashboard;
|
|
private errorDisplay: ErrorDisplay;
|
|
private batteryDialog: AndroidBatteryOptimizationDialog;
|
|
private backgroundRefreshDialog: iOSBackgroundRefreshDialog;
|
|
|
|
constructor(container: HTMLElement) {
|
|
// Initialize all UI components
|
|
this.permissionDialog = new PermissionRequestDialog(
|
|
container.querySelector('#permission-dialog-container')!,
|
|
{
|
|
onAllow: this.handlePermissionAllow.bind(this),
|
|
onDeny: this.handlePermissionDeny.bind(this),
|
|
onNever: this.handlePermissionNever.bind(this)
|
|
}
|
|
);
|
|
|
|
this.permissionStatus = new PermissionStatusDisplay(
|
|
container.querySelector('#permission-status-container')!
|
|
);
|
|
|
|
this.settingsPanel = new NotificationSettingsPanel(
|
|
container.querySelector('#settings-container')!
|
|
);
|
|
|
|
this.statusDashboard = new StatusDashboard(
|
|
container.querySelector('#status-container')!
|
|
);
|
|
|
|
this.errorDisplay = new ErrorDisplay(
|
|
container.querySelector('#error-container')!
|
|
);
|
|
|
|
this.batteryDialog = new AndroidBatteryOptimizationDialog(
|
|
container.querySelector('#battery-dialog-container')!
|
|
);
|
|
|
|
this.backgroundRefreshDialog = new iOSBackgroundRefreshDialog(
|
|
container.querySelector('#background-refresh-dialog-container')!
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize the complete UI
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
try {
|
|
// Check permissions first
|
|
const permissionStatus = await DailyNotification.checkPermissions();
|
|
|
|
if (!permissionStatus.granted) {
|
|
await this.permissionDialog.show();
|
|
} else {
|
|
await this.permissionStatus.updateStatus();
|
|
}
|
|
|
|
// Initialize other components
|
|
await this.settingsPanel.initialize();
|
|
await this.statusDashboard.initialize();
|
|
|
|
// Check platform-specific requirements
|
|
await this.batteryDialog.checkAndShow();
|
|
await this.backgroundRefreshDialog.checkAndShow();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to initialize UI:', error);
|
|
this.errorDisplay.showError(error as Error, {
|
|
onRetry: () => this.initialize(),
|
|
onReset: () => this.resetConfiguration(),
|
|
onContactSupport: () => this.contactSupport()
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle permission allow
|
|
*/
|
|
private async handlePermissionAllow(): Promise<void> {
|
|
try {
|
|
await DailyNotification.requestPermissions();
|
|
await this.permissionStatus.updateStatus();
|
|
} catch (error) {
|
|
console.error('Failed to request permissions:', error);
|
|
this.errorDisplay.showError(error as Error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle permission deny
|
|
*/
|
|
private handlePermissionDeny(): void {
|
|
console.log('User denied permissions');
|
|
// Show limited functionality message
|
|
}
|
|
|
|
/**
|
|
* Handle permission never
|
|
*/
|
|
private handlePermissionNever(): void {
|
|
console.log('User chose never ask again');
|
|
// Store preference and show limited functionality message
|
|
}
|
|
|
|
/**
|
|
* Reset configuration
|
|
*/
|
|
private async resetConfiguration(): Promise<void> {
|
|
try {
|
|
await DailyNotification.cancelAllNotifications();
|
|
// Reset to default settings
|
|
await this.settingsPanel.initialize();
|
|
} catch (error) {
|
|
console.error('Failed to reset configuration:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Contact support
|
|
*/
|
|
private contactSupport(): void {
|
|
// Implementation for contacting support
|
|
console.log('Contacting support...');
|
|
}
|
|
|
|
/**
|
|
* Cleanup
|
|
*/
|
|
destroy(): void {
|
|
this.statusDashboard.stopAutoRefresh();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 7. CSS STYLES (for reference)
|
|
// ============================================================================
|
|
|
|
export const notificationUIStyles = `
|
|
/* Permission Dialog Styles */
|
|
.permission-dialog .dialog-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.permission-dialog .dialog-content {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
max-width: 400px;
|
|
margin: 20px;
|
|
}
|
|
|
|
.permission-dialog h2 {
|
|
margin: 0 0 16px 0;
|
|
color: #333;
|
|
}
|
|
|
|
.permission-dialog p {
|
|
margin: 0 0 16px 0;
|
|
color: #666;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.permission-dialog ul {
|
|
margin: 0 0 24px 0;
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.permission-dialog li {
|
|
margin: 8px 0;
|
|
color: #666;
|
|
}
|
|
|
|
.dialog-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.btn-primary, .btn-secondary, .btn-tertiary {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #1976d2;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #1565c0;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #f5f5f5;
|
|
color: #333;
|
|
border: 1px solid #ddd;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #e0e0e0;
|
|
}
|
|
|
|
.btn-tertiary {
|
|
background: transparent;
|
|
color: #666;
|
|
}
|
|
|
|
.btn-tertiary:hover {
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
/* Status Display Styles */
|
|
.permission-status {
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.status-granted {
|
|
background: #e8f5e8;
|
|
border: 1px solid #4caf50;
|
|
}
|
|
|
|
.status-denied {
|
|
background: #ffebee;
|
|
border: 1px solid #f44336;
|
|
}
|
|
|
|
.status-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.status-icon {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.permission-details {
|
|
margin: 12px 0;
|
|
}
|
|
|
|
.permission-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.granted {
|
|
color: #4caf50;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.denied {
|
|
color: #f44336;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Settings Panel Styles */
|
|
.settings-panel {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.settings-panel h3 {
|
|
margin: 0 0 24px 0;
|
|
color: #333;
|
|
}
|
|
|
|
.setting-group {
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.setting-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
}
|
|
|
|
.checkbox-group {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.preference-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
gap: 16px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.setting-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
/* Status Dashboard Styles */
|
|
.status-dashboard {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.status-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.status-item {
|
|
padding: 16px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.status-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.status-value {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.status-value.active {
|
|
color: #4caf50;
|
|
}
|
|
|
|
.status-value.warning {
|
|
color: #ff9800;
|
|
}
|
|
|
|
.status-value.error {
|
|
color: #f44336;
|
|
}
|
|
|
|
.performance-metrics {
|
|
margin: 24px 0;
|
|
padding: 16px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.metric-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.metric-label {
|
|
color: #666;
|
|
}
|
|
|
|
.metric-value {
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
/* Error Display Styles */
|
|
.error-display {
|
|
background: #ffebee;
|
|
border: 1px solid #f44336;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.error-icon {
|
|
font-size: 24px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.error-content h3 {
|
|
margin: 0 0 8px 0;
|
|
color: #d32f2f;
|
|
}
|
|
|
|
.error-message {
|
|
color: #666;
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.error-code {
|
|
font-size: 12px;
|
|
color: #999;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.error-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.status-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.preference-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.checkbox-group {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.metrics-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
`;
|
|
|
|
// ============================================================================
|
|
// 8. EXPORT ALL COMPONENTS
|
|
// ============================================================================
|
|
|
|
export {
|
|
PermissionRequestDialog,
|
|
PermissionStatusDisplay,
|
|
NotificationSettingsPanel,
|
|
StatusDashboard,
|
|
AndroidBatteryOptimizationDialog,
|
|
iOSBackgroundRefreshDialog,
|
|
ErrorDisplay,
|
|
NotificationUIManager
|
|
};
|
|
|