/**
* @fileoverview Main Application Class for Rantii
* @author retoor <retoor@molodetz.nl>
* @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 };