Initial commit.
This commit is contained in:
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user