/** * @fileoverview Main Application Class for Rantii * @author retoor * @description Core application controller and initialization * @keywords app, application, main, controller, core */ import { ApiClient } from './api/client.js'; import { StorageService } from './services/storage.js'; import { AuthService } from './services/auth.js'; import { Router } from './services/router.js'; import { ThemeService } from './services/theme.js'; import { markdownRenderer } from './utils/markdown.js'; import { BaseComponent } from './components/base-component.js'; import { AppHeader } from './components/app-header.js'; import { AppNav } from './components/app-nav.js'; import { LoadingSpinner } from './components/loading-spinner.js'; import { ToastNotification, ToastContainer } from './components/toast-notification.js'; import { UserAvatar } from './components/user-avatar.js'; import { VoteButtons } from './components/vote-buttons.js'; import { ThemeSelector } from './components/theme-selector.js'; import { LoginForm } from './components/login-form.js'; import { ImagePreview, ImageLightbox } from './components/image-preview.js'; import { YoutubeEmbed } from './components/youtube-embed.js'; import { LinkPreview } from './components/link-preview.js'; import { RantContent } from './components/rant-content.js'; import { RantCard } from './components/rant-card.js'; import { RantDetail } from './components/rant-detail.js'; import { RantFeed } from './components/rant-feed.js'; import { CommentItem } from './components/comment-item.js'; import { CommentForm } from './components/comment-form.js'; import { UserProfile } from './components/user-profile.js'; import { NotificationList, NotificationItem } from './components/notification-list.js'; import { PostForm, PostModal } from './components/post-form.js'; import { HomePage } from './pages/home-page.js'; import { RantPage } from './pages/rant-page.js'; import { ProfilePage } from './pages/profile-page.js'; import { SearchPage } from './pages/search-page.js'; import { NotificationsPage } from './pages/notifications-page.js'; import { SettingsPage } from './pages/settings-page.js'; import { LoginPage } from './pages/login-page.js'; import { WeeklyPage } from './pages/weekly-page.js'; import { CollabsPage } from './pages/collabs-page.js'; import { StoriesPage } from './pages/stories-page.js'; class Application { constructor() { this.api = new ApiClient(); this.storage = new StorageService(); this.auth = new AuthService(this.api, this.storage); this.router = new Router(); this.theme = new ThemeService(this.storage); this.toast = null; this.header = null; this.nav = null; this.main = null; this.currentPage = null; this.isNavOpen = false; this.deferredInstallPrompt = null; this.setupInstallPrompt(); } setupInstallPrompt() { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); this.deferredInstallPrompt = e; window.dispatchEvent(new CustomEvent('rantii:install-available')); }); window.addEventListener('appinstalled', () => { this.deferredInstallPrompt = null; window.dispatchEvent(new CustomEvent('rantii:app-installed')); }); } canInstall() { return this.deferredInstallPrompt !== null; } async installApp() { if (!this.deferredInstallPrompt) return false; this.deferredInstallPrompt.prompt(); const { outcome } = await this.deferredInstallPrompt.userChoice; this.deferredInstallPrompt = null; return outcome === 'accepted'; } async init() { await this.auth.init(); this.theme.init(); await markdownRenderer.init(); this.setupToast(); this.setupHeader(); this.setupNav(); this.setupMain(); this.setupRouter(); this.setupEventListeners(); this.router.init(); if ('serviceWorker' in navigator) { this.registerServiceWorker(); } } setupToast() { this.toast = document.createElement('toast-container'); document.body.appendChild(this.toast); } setupHeader() { this.header = document.querySelector('app-header'); if (!this.header) { this.header = document.createElement('app-header'); document.body.insertBefore(this.header, document.body.firstChild); } } setupNav() { this.nav = document.querySelector('app-nav'); if (!this.nav) { this.nav = document.createElement('app-nav'); this.header.after(this.nav); } } setupMain() { this.main = document.querySelector('main'); if (!this.main) { this.main = document.createElement('main'); this.main.id = 'app-main'; this.main.className = 'app-main'; this.nav.after(this.main); } } setupRouter() { this.router.register('home', (params) => this.showPage('home', params)); this.router.register('rant', (params) => this.showPage('rant', params)); this.router.register('user', (params) => this.showPage('user', params)); this.router.register('search', (params) => this.showPage('search', params)); this.router.register('notifications', (params) => this.showPage('notifications', params)); this.router.register('settings', (params) => this.showPage('settings', params)); this.router.register('login', (params) => this.showPage('login', params)); this.router.register('weekly', (params) => this.showPage('weekly', params)); this.router.register('collabs', (params) => this.showPage('collabs', params)); this.router.register('stories', (params) => this.showPage('stories', params)); } setupEventListeners() { window.addEventListener('rantii:auth-change', () => this.onAuthChange()); window.addEventListener('rantii:require-auth', () => this.router.goToLogin()); if (this.header) { this.header.addEventListener('menu-toggle', () => this.toggleNav()); this.header.addEventListener('create-post', () => this.openPostModal()); this.header.addEventListener('user-menu', () => this.toggleUserMenu()); } if (this.nav) { this.nav.addEventListener('nav-click', () => this.closeNav()); } document.addEventListener('click', (e) => { if (this.isNavOpen && !e.target.closest('app-nav') && !e.target.closest('.menu-toggle')) { this.closeNav(); } }); } showPage(routeName, params) { if (this.currentPage) { this.currentPage.remove(); } let page; switch (routeName) { case 'home': page = document.createElement('home-page'); break; case 'rant': page = document.createElement('rant-page'); page.setAttribute('rant-id', params.rant); if (params.comment) { page.setAttribute('comment-id', params.comment); } break; case 'user': page = document.createElement('profile-page'); page.setAttribute('username', params.user); break; case 'search': page = document.createElement('search-page'); if (params.search) { page.setAttribute('query', params.search); } break; case 'notifications': page = document.createElement('notifications-page'); break; case 'settings': page = document.createElement('settings-page'); break; case 'login': page = document.createElement('login-page'); break; case 'weekly': page = document.createElement('weekly-page'); break; case 'collabs': page = document.createElement('collabs-page'); break; case 'stories': page = document.createElement('stories-page'); break; default: page = document.createElement('home-page'); } this.main.appendChild(page); this.currentPage = page; this.nav?.setActive(routeName); this.closeNav(); window.scrollTo(0, 0); } toggleNav() { this.isNavOpen = !this.isNavOpen; if (this.nav) { this.nav.toggleClass('nav-open', this.isNavOpen); } document.body.classList.toggle('nav-open', this.isNavOpen); } openNav() { this.isNavOpen = true; if (this.nav) { this.nav.addClass('nav-open'); } document.body.classList.add('nav-open'); } closeNav() { this.isNavOpen = false; if (this.nav) { this.nav.removeClass('nav-open'); } document.body.classList.remove('nav-open'); } openPostModal() { if (!this.auth.isLoggedIn()) { this.router.goToLogin(); return; } const modal = document.createElement('post-modal'); modal.open(); } toggleUserMenu() { const username = this.auth.getUsername(); if (username) { this.router.goToUser(username); } } onAuthChange() { if (this.header) { this.header.render(); } if (this.nav) { this.nav.render(); } this.updateNotificationBadge(); } async updateNotificationBadge() { if (!this.auth.isLoggedIn()) return; try { const result = await this.api.getNotifications(); if (result?.success) { const count = result.unread?.total || 0; if (this.header) { this.header.setNotificationCount(count); } if (this.nav) { this.nav.setNotificationBadge(count); } } } catch (error) {} } async registerServiceWorker() { try { const basePath = this.getBasePath(); await navigator.serviceWorker.register(`${basePath}sw.js`); } catch (error) {} } getBasePath() { const path = window.location.pathname; const lastSlash = path.lastIndexOf('/'); return path.substring(0, lastSlash + 1); } showToast(message, type = 'info') { if (this.toast) { this.toast.show(message, type); } } success(message) { this.showToast(message, 'success'); } error(message) { this.showToast(message, 'error'); } warning(message) { this.showToast(message, 'warning'); } info(message) { this.showToast(message, 'info'); } } const app = new Application(); document.addEventListener('DOMContentLoaded', () => { app.init(); }); window.app = app; export { Application, app };