// API Base const API_BASE = '/api'; // Utility functions const api = { async get(endpoint) { const response = await fetch(`${API_BASE}${endpoint}`); if (!response.ok) throw new Error(`API error: ${response.status}`); return response.json(); }, async post(endpoint, data) { const response = await fetch(`${API_BASE}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) throw new Error(`API error: ${response.status}`); return response.json(); }, async put(endpoint, data) { const response = await fetch(`${API_BASE}${endpoint}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) throw new Error(`API error: ${response.status}`); return response.json(); }, async delete(endpoint) { const response = await fetch(`${API_BASE}${endpoint}`, { method: 'DELETE', }); if (!response.ok) throw new Error(`API error: ${response.status}`); return response.json(); }, }; // Toast notifications class Toast { static show(message, type = 'info', duration = 3000) { const toast = document.createElement('div'); toast.className = `toast toast--${type}`; toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 24px; right: 24px; background-color: var(--md-sys-color-surface); color: var(--md-sys-color-on-surface); padding: 16px 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 9999; animation: slideIn 0.3s ease; border-left: 4px solid var(--md-sys-color-primary); `; if (type === 'success') { toast.style.borderLeftColor = '#4caf50'; } else if (type === 'error') { toast.style.borderLeftColor = 'var(--md-sys-color-error)'; } document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => toast.remove(), 300); }, duration); } } // Modal dialog class Modal { constructor(title, content) { this.title = title; this.content = content; this.element = null; } open() { const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 9998; animation: fadeIn 0.2s ease; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background-color: var(--md-sys-color-surface); border-radius: 12px; padding: 24px; max-width: 500px; width: 90%; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); animation: slideUp 0.3s ease; `; const titleEl = document.createElement('h2'); titleEl.textContent = this.title; titleEl.style.cssText = 'margin: 0 0 16px 0;'; const contentEl = document.createElement('div'); contentEl.innerHTML = this.content; contentEl.style.cssText = 'margin-bottom: 24px;'; dialog.appendChild(titleEl); dialog.appendChild(contentEl); modal.appendChild(dialog); modal.addEventListener('click', (e) => { if (e.target === modal) this.close(); }); this.element = modal; document.body.appendChild(modal); } close() { if (this.element) { this.element.style.animation = 'fadeOut 0.2s ease'; setTimeout(() => this.element.remove(), 200); } } } // Post editor class PostEditor { constructor(containerId) { this.container = document.getElementById(containerId); this.postId = null; this.init(); } init() { this.container.innerHTML = `

Create New Post

Supports Markdown syntax. Preview available.
`; this.form = document.getElementById('post-form'); this.form.addEventListener('submit', (e) => this.handleSubmit(e)); document.getElementById('preview-btn').addEventListener('click', () => this.showPreview()); } async handleSubmit(e) { e.preventDefault(); const formData = { title: document.getElementById('title').value, content: document.getElementById('content').value, excerpt: document.getElementById('excerpt').value, type: document.getElementById('type').value, category: document.getElementById('category').value, tags: document.getElementById('tags').value.split(',').map(t => t.trim()).filter(t => t), published: document.getElementById('published').checked, }; try { const endpoint = this.postId ? `/posts/${this.postId}` : '/posts'; const method = this.postId ? 'put' : 'post'; const result = await api[method](endpoint, formData); if (result.success) { Toast.show('Post saved successfully!', 'success'); if (!this.postId) { this.postId = result.data.id; } setTimeout(() => window.location.href = '/', 1500); } } catch (error) { Toast.show('Error saving post: ' + error.message, 'error'); } } showPreview() { const content = document.getElementById('content').value; const title = document.getElementById('title').value; const previewHtml = `

${title || 'Untitled'}

${this.markdownToHtml(content)}
`; const modal = new Modal('Preview', previewHtml); modal.open(); } markdownToHtml(markdown) { let html = markdown .replace(/^### (.*?)$/gm, '

$1

') .replace(/^## (.*?)$/gm, '

$1

') .replace(/^# (.*?)$/gm, '

$1

') .replace(/\*\*(.*?)\*\*/gm, '$1') .replace(/\*(.*?)\*/gm, '$1') .replace(/`(.*?)`/gm, '$1') .replace(/\n\n/gm, '

') .replace(/^/gm, '

') .replace(/$/gm, '

'); return html; } } // Posts list class PostsList { constructor(containerId) { this.container = document.getElementById(containerId); this.page = 1; this.loading = false; this.init(); } async init() { this.container.innerHTML = '
Loading posts...
'; await this.loadPosts(); } async loadPosts() { if (this.loading) return; this.loading = true; try { const result = await api.get(`/posts?page=${this.page}`); if (result.success && result.data) { this.render(result.data); } } catch (error) { this.container.innerHTML = `
Error loading posts
`; } this.loading = false; } render(posts) { if (posts.length === 0) { this.container.innerHTML = '
No posts yet.
'; return; } let html = ''; this.container.innerHTML = html; // Pagination if (posts.length >= 20) { const paginationBtn = document.createElement('div'); paginationBtn.style.cssText = 'text-align: center; margin-top: 40px;'; paginationBtn.innerHTML = ` `; this.container.appendChild(paginationBtn); document.getElementById('load-more-btn').addEventListener('click', () => { this.page++; this.loadPosts(); }); } } } // Search functionality class SearchBox { constructor(containerId) { this.container = document.getElementById(containerId); this.init(); } init() { this.input = this.container.querySelector('input[type="search"]'); if (!this.input) return; this.input.addEventListener('keyup', (e) => this.search(e.target.value)); } async search(query) { if (!query || query.length < 2) { this.clearResults(); return; } try { const result = await api.get(`/search?q=${encodeURIComponent(query)}`); this.renderResults(result.data || []); } catch (error) { console.error('Search error:', error); } } renderResults(posts) { let html = '
'; posts.slice(0, 10).forEach(post => { html += `
${post.title}
${new Date(post.created_at).toLocaleDateString()}
`; }); html += '
'; let dropdown = document.getElementById('search-results'); if (dropdown) dropdown.remove(); const resultsDiv = document.createElement('div'); resultsDiv.id = 'search-results'; resultsDiv.innerHTML = html; resultsDiv.style.cssText = 'position: relative;'; this.container.appendChild(resultsDiv); } clearResults() { const dropdown = document.getElementById('search-results'); if (dropdown) dropdown.remove(); } } // Initialize on document ready document.addEventListener('DOMContentLoaded', () => { // Initialize any editors or lists if they exist if (document.getElementById('post-editor')) { new PostEditor('post-editor'); } if (document.getElementById('posts-list')) { new PostsList('posts-list'); } if (document.getElementById('search-box')) { new SearchBox('search-box'); } }); // Add CSS animations const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } `; document.head.appendChild(style);