Initial commit.

This commit is contained in:
2025-12-04 20:29:35 +01:00
commit ae789a5b40
87 changed files with 9798 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
/**
* @fileoverview Authentication Service for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Handles user authentication and session management
* @keywords auth, authentication, login, session, token
*/
class AuthService {
constructor(apiClient, storageService) {
this.api = apiClient;
this.storage = storageService;
this.currentUser = null;
this.onAuthChange = null;
}
async init() {
const authData = this.storage.getAuth();
if (authData && authData.tokenId && authData.tokenKey) {
this.api.setAuth(authData.tokenId, authData.tokenKey, authData.userId);
this.currentUser = {
id: authData.userId,
username: authData.username
};
return true;
}
return false;
}
async login(username, password, remember = true) {
const result = await this.api.login(username, password);
if (result.success) {
const authData = {
tokenId: result.authToken.id,
tokenKey: result.authToken.key,
userId: result.authToken.user_id,
username: username,
expireTime: result.authToken.expire_time
};
if (remember) {
this.storage.setAuth(authData);
}
this.currentUser = {
id: authData.userId,
username: username
};
this.notifyAuthChange();
return { success: true, user: this.currentUser };
}
return { success: false, error: result.error };
}
logout() {
this.api.clearAuth();
this.storage.clearAuth();
this.currentUser = null;
this.notifyAuthChange();
}
isLoggedIn() {
return this.api.isAuthenticated() && this.currentUser !== null;
}
getUser() {
return this.currentUser;
}
getUserId() {
return this.currentUser?.id || null;
}
getUsername() {
return this.currentUser?.username || null;
}
setAuthChangeCallback(callback) {
this.onAuthChange = callback;
}
notifyAuthChange() {
if (this.onAuthChange) {
this.onAuthChange(this.isLoggedIn(), this.currentUser);
}
window.dispatchEvent(new CustomEvent('rantii:auth-change', {
detail: {
isLoggedIn: this.isLoggedIn(),
user: this.currentUser
}
}));
}
requireAuth() {
if (!this.isLoggedIn()) {
window.dispatchEvent(new CustomEvent('rantii:require-auth'));
return false;
}
return true;
}
}
export { AuthService };
+177
View File
@@ -0,0 +1,177 @@
/**
* @fileoverview URL Router Service for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Handles URL routing and navigation state management
* @keywords router, navigation, url, history, routing
*/
class Router {
constructor() {
this.routes = new Map();
this.currentRoute = null;
this.currentParams = {};
this.onRouteChange = null;
this.basePath = this.detectBasePath();
}
detectBasePath() {
const path = window.location.pathname;
const htmlIndex = path.lastIndexOf('.html');
if (htmlIndex !== -1) {
return path.substring(0, path.lastIndexOf('/') + 1);
}
return path.endsWith('/') ? path : path + '/';
}
init() {
window.addEventListener('popstate', () => this.handleRoute());
this.handleRoute();
}
register(name, handler) {
this.routes.set(name, handler);
}
getParams() {
const params = new URLSearchParams(window.location.search);
const result = {};
for (const [key, value] of params) {
result[key] = value;
}
return result;
}
handleRoute() {
const params = this.getParams();
this.currentParams = params;
let routeName = 'home';
if (params.rant) {
routeName = 'rant';
} else if (params.user) {
routeName = 'user';
} else if (params.search !== undefined) {
routeName = 'search';
} else if (params.notifications !== undefined) {
routeName = 'notifications';
} else if (params.settings !== undefined) {
routeName = 'settings';
} else if (params.login !== undefined) {
routeName = 'login';
} else if (params.weekly !== undefined) {
routeName = 'weekly';
} else if (params.collabs !== undefined) {
routeName = 'collabs';
} else if (params.stories !== undefined) {
routeName = 'stories';
}
this.currentRoute = routeName;
const handler = this.routes.get(routeName);
if (handler) {
handler(params);
}
if (this.onRouteChange) {
this.onRouteChange(routeName, params);
}
window.dispatchEvent(new CustomEvent('rantii:route-change', {
detail: { route: routeName, params }
}));
}
navigate(routeName, params = {}) {
const url = this.buildUrl(params);
window.history.pushState({ route: routeName, params }, '', url);
this.handleRoute();
}
replace(routeName, params = {}) {
const url = this.buildUrl(params);
window.history.replaceState({ route: routeName, params }, '', url);
this.handleRoute();
}
buildUrl(params = {}) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
searchParams.set(key, value);
} else if (value === '') {
searchParams.set(key, '');
}
});
const queryString = searchParams.toString();
const currentPath = window.location.pathname;
return queryString ? `${currentPath}?${queryString}` : currentPath;
}
goHome() {
this.navigate('home', {});
}
goToRant(rantId, commentId = null) {
const params = { rant: rantId };
if (commentId) {
params.comment = commentId;
}
this.navigate('rant', params);
}
goToUser(username) {
this.navigate('user', { user: username });
}
goToSearch(term = '') {
this.navigate('search', { search: term });
}
goToNotifications() {
this.navigate('notifications', { notifications: '' });
}
goToSettings() {
this.navigate('settings', { settings: '' });
}
goToLogin() {
this.navigate('login', { login: '' });
}
goToWeekly() {
this.navigate('weekly', { weekly: '' });
}
goToCollabs() {
this.navigate('collabs', { collabs: '' });
}
goToStories() {
this.navigate('stories', { stories: '' });
}
back() {
window.history.back();
}
forward() {
window.history.forward();
}
getCurrentRoute() {
return this.currentRoute;
}
getCurrentParams() {
return this.currentParams;
}
setRouteChangeCallback(callback) {
this.onRouteChange = callback;
}
isCurrentRoute(routeName) {
return this.currentRoute === routeName;
}
}
export { Router };
+155
View File
@@ -0,0 +1,155 @@
/**
* @fileoverview Local Storage Service for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Manages persistent storage of user data and settings
* @keywords storage, localStorage, persistence, settings
*/
const STORAGE_PREFIX = 'rantii_';
class StorageService {
constructor() {
this.prefix = STORAGE_PREFIX;
}
key(name) {
return `${this.prefix}${name}`;
}
set(name, value) {
try {
const serialized = JSON.stringify(value);
localStorage.setItem(this.key(name), serialized);
return true;
} catch (error) {
return false;
}
}
get(name, defaultValue = null) {
try {
const item = localStorage.getItem(this.key(name));
if (item === null) {
return defaultValue;
}
return JSON.parse(item);
} catch (error) {
return defaultValue;
}
}
remove(name) {
try {
localStorage.removeItem(this.key(name));
return true;
} catch (error) {
return false;
}
}
clear() {
try {
const keys = Object.keys(localStorage).filter(k => k.startsWith(this.prefix));
keys.forEach(key => localStorage.removeItem(key));
return true;
} catch (error) {
return false;
}
}
has(name) {
return localStorage.getItem(this.key(name)) !== null;
}
getAuth() {
return this.get('auth', null);
}
setAuth(authData) {
return this.set('auth', authData);
}
clearAuth() {
return this.remove('auth');
}
getTheme() {
return this.get('theme', 'dark');
}
setTheme(theme) {
return this.set('theme', theme);
}
getSettings() {
return this.get('settings', {
theme: 'dark',
fontSize: 'medium',
notifications: true
});
}
setSettings(settings) {
return this.set('settings', settings);
}
updateSettings(partial) {
const current = this.getSettings();
return this.setSettings({ ...current, ...partial });
}
getRecentSearches() {
return this.get('recent_searches', []);
}
addRecentSearch(term) {
const searches = this.getRecentSearches();
const filtered = searches.filter(s => s !== term);
filtered.unshift(term);
return this.set('recent_searches', filtered.slice(0, 10));
}
getDraftRant() {
return this.get('draft_rant', '');
}
setDraftRant(text) {
return this.set('draft_rant', text);
}
clearDraftRant() {
return this.remove('draft_rant');
}
getDraftComment(rantId) {
return this.get(`draft_comment_${rantId}`, '');
}
setDraftComment(rantId, text) {
return this.set(`draft_comment_${rantId}`, text);
}
clearDraftComment(rantId) {
return this.remove(`draft_comment_${rantId}`);
}
getCachedProfile(userId) {
return this.get(`profile_cache_${userId}`, null);
}
setCachedProfile(userId, profile) {
return this.set(`profile_cache_${userId}`, {
data: profile,
timestamp: Date.now()
});
}
isCacheValid(cacheEntry, maxAge = 300000) {
if (!cacheEntry || !cacheEntry.timestamp) {
return false;
}
return Date.now() - cacheEntry.timestamp < maxAge;
}
}
export { StorageService, STORAGE_PREFIX };
+156
View File
@@ -0,0 +1,156 @@
/**
* @fileoverview Theme Service for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Manages application themes and visual appearance
* @keywords theme, dark mode, light mode, appearance, styling
*/
const THEMES = {
dark: {
name: 'Dark',
class: 'theme-dark'
},
light: {
name: 'Light',
class: 'theme-light'
},
black: {
name: 'Black',
class: 'theme-black'
},
white: {
name: 'White',
class: 'theme-white'
},
ocean: {
name: 'Ocean',
class: 'theme-ocean'
},
forest: {
name: 'Forest',
class: 'theme-forest'
},
sunset: {
name: 'Sunset',
class: 'theme-sunset'
}
};
class ThemeService {
constructor(storageService) {
this.storage = storageService;
this.currentTheme = 'dark';
this.onThemeChange = null;
}
init() {
const savedTheme = this.storage.getTheme();
if (savedTheme && THEMES[savedTheme]) {
this.applyTheme(savedTheme);
} else {
this.applyTheme(this.detectPreferredTheme());
}
this.listenToSystemPreference();
}
detectPreferredTheme() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
listenToSystemPreference() {
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (!this.storage.has('theme')) {
this.applyTheme(e.matches ? 'dark' : 'light');
}
});
}
}
applyTheme(themeName) {
const theme = THEMES[themeName];
if (!theme) {
return false;
}
Object.values(THEMES).forEach(t => {
document.documentElement.classList.remove(t.class);
document.body.classList.remove(t.class);
});
document.documentElement.classList.add(theme.class);
document.body.classList.add(theme.class);
this.currentTheme = themeName;
this.storage.setTheme(themeName);
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
const colors = {
dark: '#1a1a2e',
light: '#f5f5f7',
black: '#000000',
white: '#ffffff',
ocean: '#0a1929',
forest: '#0d1f0d',
sunset: '#1f1410'
};
metaThemeColor.setAttribute('content', colors[themeName] || '#1a1a2e');
}
if (this.onThemeChange) {
this.onThemeChange(themeName, theme);
}
window.dispatchEvent(new CustomEvent('rantii:theme-change', {
detail: { theme: themeName, themeData: theme }
}));
return true;
}
setTheme(themeName) {
return this.applyTheme(themeName);
}
getTheme() {
return this.currentTheme;
}
getThemeData() {
return THEMES[this.currentTheme];
}
getAvailableThemes() {
return Object.entries(THEMES).map(([key, value]) => ({
id: key,
name: value.name
}));
}
toggle() {
const currentIndex = Object.keys(THEMES).indexOf(this.currentTheme);
const nextIndex = (currentIndex + 1) % Object.keys(THEMES).length;
const nextTheme = Object.keys(THEMES)[nextIndex];
return this.applyTheme(nextTheme);
}
toggleDarkLight() {
if (this.currentTheme === 'dark' || this.currentTheme === 'black') {
return this.applyTheme('light');
}
return this.applyTheme('dark');
}
setThemeChangeCallback(callback) {
this.onThemeChange = callback;
}
isDark() {
return ['dark', 'black', 'ocean', 'forest', 'sunset'].includes(this.currentTheme);
}
}
export { ThemeService, THEMES };