feat: implement comprehensive blog system
Add dual-source blog system with Editor.js integration: - Blog storage supporting repo-based (JSON) and database sources - Admin panel with rich text editor using Editor.js - Public news page with infinite scroll - Individual blog post viewer page - Categories management in admin - Image upload functionality - 4 SEO blog posts about WordPress with PluginCompass promotion - 3 News blog posts about Plugin Compass - API endpoints for CRUD operations - Security and validation for admin operations Closes blog feature request
This commit is contained in:
428
chat/public/admin-blogs.html
Normal file
428
chat/public/admin-blogs.html
Normal file
@@ -0,0 +1,428 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Blog Management - Admin</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
|
||||
<!-- Editor.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/link@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@latest"></script>
|
||||
|
||||
<style>
|
||||
.editor-wrapper {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 400px;
|
||||
background: var(--bg-1);
|
||||
}
|
||||
.ce-block__content {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.ce-toolbar__content {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.blog-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.blog-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-1);
|
||||
}
|
||||
.blog-item.repo {
|
||||
border-left: 4px solid var(--info);
|
||||
}
|
||||
.blog-item.database {
|
||||
border-left: 4px solid var(--success);
|
||||
}
|
||||
.blog-meta {
|
||||
font-size: 0.85em;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.blog-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.type-news {
|
||||
background: var(--info-bg, #e0f2fe);
|
||||
color: var(--info, #0284c7);
|
||||
}
|
||||
.type-seo {
|
||||
background: var(--warning-bg, #fef3c7);
|
||||
color: var(--warning, #d97706);
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.status-published {
|
||||
background: var(--success-bg, #d1fae5);
|
||||
color: var(--success, #059669);
|
||||
}
|
||||
.status-draft {
|
||||
background: var(--muted-bg, #f3f4f6);
|
||||
color: var(--muted, #6b7280);
|
||||
}
|
||||
.source-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.source-repo {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
.source-database {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
}
|
||||
.category-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.category-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-1);
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
.modal {
|
||||
background: var(--bg-1);
|
||||
border-radius: 12px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
}
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.blog-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
.hidden { display: none !important; }
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.filters select, .filters input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-1);
|
||||
}
|
||||
.tab-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.tab-button {
|
||||
padding: 12px 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab-button.active {
|
||||
color: var(--text);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- PostHog Analytics -->
|
||||
<script src="/posthog.js"></script>
|
||||
</head>
|
||||
<body data-page="blogs">
|
||||
<div class="sidebar-overlay"></div>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="brand-mark">A</div>
|
||||
<div>
|
||||
<div class="brand-title">Admin</div>
|
||||
<div class="brand-sub">Site management</div>
|
||||
</div>
|
||||
<button id="close-sidebar" class="ghost" style="margin-left: auto; display: none;">×</button>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="section-heading">Navigation</div>
|
||||
<a class="ghost" href="/admin/build">Build models</a>
|
||||
<a class="ghost" href="/admin/plan">Plan models</a>
|
||||
<a class="ghost" href="/admin/plans">Plans</a>
|
||||
<a class="ghost" href="/admin/accounts">Accounts</a>
|
||||
<a class="ghost" href="/admin/affiliates">Affiliates</a>
|
||||
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
|
||||
<a class="ghost" href="/admin/tracking">Tracking</a>
|
||||
<a class="ghost" href="/admin/resources">Resources</a>
|
||||
<a class="ghost" href="/admin/external-testing">External Testing</a>
|
||||
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
|
||||
<a class="ghost active" href="/admin/blogs">Blog Management</a>
|
||||
<a class="ghost" href="/admin/login">Login</a>
|
||||
</div>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<div class="admin-shell">
|
||||
<div class="topbar" style="margin-bottom: 12px;">
|
||||
<button id="menu-toggle">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
<div>
|
||||
<div class="pill">Admin</div>
|
||||
<div class="title" style="margin-top: 6px;">Blog Management</div>
|
||||
<div class="crumb">Manage news and SEO blog posts</div>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<button id="refresh-blogs" class="ghost">Refresh</button>
|
||||
<button id="new-post" class="primary">New Post</button>
|
||||
<button id="admin-logout" class="ghost">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-button active" data-tab="posts">Blog Posts</button>
|
||||
<button class="tab-button" data-tab="categories">Categories</button>
|
||||
</div>
|
||||
|
||||
<!-- Posts Tab -->
|
||||
<div id="posts-tab" class="tab-content active">
|
||||
<div class="filters">
|
||||
<select id="filter-type">
|
||||
<option value="">All Types</option>
|
||||
<option value="news">News</option>
|
||||
<option value="seo">SEO</option>
|
||||
</select>
|
||||
<select id="filter-status">
|
||||
<option value="">All Status</option>
|
||||
<option value="published">Published</option>
|
||||
<option value="draft">Draft</option>
|
||||
</select>
|
||||
<select id="filter-source">
|
||||
<option value="">All Sources</option>
|
||||
<option value="database">Database</option>
|
||||
<option value="repo">Repository</option>
|
||||
</select>
|
||||
<input type="text" id="filter-search" placeholder="Search posts..." />
|
||||
</div>
|
||||
|
||||
<div id="blog-list" class="blog-list">
|
||||
<p>Loading posts...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categories Tab -->
|
||||
<div id="categories-tab" class="tab-content">
|
||||
<div class="admin-card" style="margin-bottom: 16px;">
|
||||
<header>
|
||||
<h3>Add New Category</h3>
|
||||
</header>
|
||||
<form id="category-form" class="admin-form">
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Category Name
|
||||
<input type="text" id="category-name" placeholder="e.g., Tutorials" required />
|
||||
</label>
|
||||
<label>
|
||||
Slug
|
||||
<input type="text" id="category-slug" placeholder="e.g., tutorials" required />
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
Description
|
||||
<textarea id="category-description" rows="2" placeholder="Brief description..."></textarea>
|
||||
</label>
|
||||
<div class="admin-actions">
|
||||
<button type="submit" class="primary">Add Category</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<header>
|
||||
<h3>Categories</h3>
|
||||
</header>
|
||||
<div id="category-list" class="category-list">
|
||||
<p>Loading categories...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Blog Post Modal -->
|
||||
<div id="post-modal" class="modal-overlay hidden">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="modal-title">New Post</h2>
|
||||
<button class="modal-close" id="modal-close">×</button>
|
||||
</div>
|
||||
<form id="post-form">
|
||||
<input type="hidden" id="post-id" />
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Title
|
||||
<input type="text" id="post-title" placeholder="Post title" required />
|
||||
</label>
|
||||
<label>
|
||||
Slug
|
||||
<input type="text" id="post-slug" placeholder="url-friendly-slug" required />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Type
|
||||
<select id="post-type" required>
|
||||
<option value="news">News</option>
|
||||
<option value="seo">SEO</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Status
|
||||
<select id="post-status">
|
||||
<option value="draft">Draft</option>
|
||||
<option value="published">Published</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Category
|
||||
<select id="post-category">
|
||||
<option value="">No Category</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Author
|
||||
<input type="text" id="post-author" placeholder="Author name" />
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
Excerpt
|
||||
<textarea id="post-excerpt" rows="2" placeholder="Brief description of the post..."></textarea>
|
||||
</label>
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Meta Title
|
||||
<input type="text" id="post-meta-title" placeholder="SEO title" />
|
||||
</label>
|
||||
<label>
|
||||
Meta Description
|
||||
<input type="text" id="post-meta-description" placeholder="SEO description" />
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
Featured Image URL
|
||||
<input type="text" id="post-featured-image" placeholder="/blogs/images/image.jpg" />
|
||||
</label>
|
||||
<label>
|
||||
Tags (comma separated)
|
||||
<input type="text" id="post-tags" placeholder="wordpress, tutorial, guide" />
|
||||
</label>
|
||||
<label style="margin-top: 16px;">
|
||||
Content
|
||||
<div id="editor" class="editor-wrapper"></div>
|
||||
</label>
|
||||
<div class="admin-actions" style="margin-top: 20px;">
|
||||
<button type="submit" class="primary" id="save-post">Save Post</button>
|
||||
<button type="button" class="ghost" id="cancel-post">Cancel</button>
|
||||
<span id="post-source" class="source-badge" style="margin-left: auto;"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin.js"></script>
|
||||
<script src="/js/blog-admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -45,6 +45,7 @@
|
||||
<a class="ghost" href="/admin/resources">Resources</a>
|
||||
<a class="ghost" href="/admin/external-testing">External Testing</a>
|
||||
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
|
||||
<a class="ghost" href="/admin/blogs">Blog Management</a>
|
||||
<a class="ghost" href="/admin/login">Login</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
454
chat/public/blog.html
Normal file
454
chat/public/blog.html
Normal file
@@ -0,0 +1,454 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Loading blog post...">
|
||||
<title>Blog - Plugin Compass</title>
|
||||
<link rel="icon" type="image/png" href="/assets/Plugin.png">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
(function applyTailwindConfig(cfg, attempts = 0) {
|
||||
try {
|
||||
if (typeof window.tailwind !== 'undefined') {
|
||||
window.tailwind.config = cfg;
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
if (attempts >= 20) return;
|
||||
setTimeout(() => applyTailwindConfig(cfg, attempts + 1), 100);
|
||||
})({
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: { sans: ['Inter', 'sans-serif'] },
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc',
|
||||
400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca',
|
||||
800: '#3730a3', 900: '#312e81'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
|
||||
/* Blog content styles */
|
||||
.blog-content {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75;
|
||||
color: #374151;
|
||||
}
|
||||
.blog-content h2 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #111827;
|
||||
}
|
||||
.blog-content h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #111827;
|
||||
}
|
||||
.blog-content h4 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #111827;
|
||||
}
|
||||
.blog-content p {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.blog-content ul, .blog-content ol {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.blog-content li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.blog-content a {
|
||||
color: #4f46e5;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.blog-content a:hover {
|
||||
color: #4338ca;
|
||||
}
|
||||
.blog-content blockquote {
|
||||
border-left: 4px solid #e5e7eb;
|
||||
padding-left: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
font-style: italic;
|
||||
color: #6b7280;
|
||||
}
|
||||
.blog-content blockquote cite {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-style: normal;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.blog-content pre {
|
||||
background: #f3f4f6;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.blog-content code {
|
||||
background: #f3f4f6;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.blog-content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.blog-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.blog-content figure {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.blog-content figcaption {
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.blog-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.blog-content th, .blog-content td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
.blog-content th {
|
||||
background: #f9fafb;
|
||||
font-weight: 600;
|
||||
}
|
||||
.blog-content .embed {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.blog-content .embed iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
.loading-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .5; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- PostHog Analytics -->
|
||||
<script src="/posthog.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900">
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass" class="h-8 w-8">
|
||||
<span class="font-bold text-xl text-gray-900">Plugin Compass</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<a href="/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
|
||||
<a href="/news" class="text-gray-600 hover:text-gray-900 font-medium">News</a>
|
||||
<a href="/features" class="text-gray-600 hover:text-gray-900 font-medium">Features</a>
|
||||
<a href="/pricing" class="text-gray-600 hover:text-gray-900 font-medium">Pricing</a>
|
||||
<a href="/builder" class="bg-brand-600 text-white px-4 py-2 rounded-lg hover:bg-brand-700 transition">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loading-state" class="min-h-screen flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-spinner fa-spin text-4xl text-brand-600 mb-4"></i>
|
||||
<p class="text-gray-600">Loading article...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div id="error-state" class="min-h-screen flex items-center justify-center hidden">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-exclamation-circle text-6xl text-red-400 mb-4"></i>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Article Not Found</h1>
|
||||
<p class="text-gray-600 mb-6">The blog post you're looking for doesn't exist or has been removed.</p>
|
||||
<a href="/news" class="inline-flex items-center gap-2 text-brand-600 hover:text-brand-700 font-medium">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to News
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article Content -->
|
||||
<article id="article-content" class="hidden">
|
||||
<!-- Hero Section -->
|
||||
<header class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<span id="post-category" class="px-3 py-1 bg-brand-100 text-brand-700 rounded-full text-sm font-medium">Category</span>
|
||||
<span id="post-type" class="text-gray-400 text-sm"></span>
|
||||
</div>
|
||||
<h1 id="post-title" class="text-4xl md:text-5xl font-bold text-gray-900 mb-6 leading-tight"></h1>
|
||||
<div class="flex items-center gap-4 text-gray-600">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center">
|
||||
<i class="fas fa-user text-brand-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p id="post-author" class="font-medium text-gray-900"></p>
|
||||
<p id="post-date" class="text-sm"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Featured Image -->
|
||||
<div id="featured-image-container" class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 -mt-6 mb-12 hidden">
|
||||
<img id="featured-image" src="" alt="" class="w-full h-64 md:h-96 object-cover rounded-xl shadow-lg">
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-16">
|
||||
<div id="post-content" class="blog-content">
|
||||
<!-- Content will be loaded here -->
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div id="post-tags" class="mt-12 pt-8 border-t border-gray-200">
|
||||
<!-- Tags will be loaded here -->
|
||||
</div>
|
||||
|
||||
<!-- Share -->
|
||||
<div class="mt-8 flex items-center gap-4">
|
||||
<span class="text-gray-600 font-medium">Share this article:</span>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="shareTwitter()" class="w-10 h-10 rounded-full bg-blue-400 text-white flex items-center justify-center hover:bg-blue-500 transition">
|
||||
<i class="fab fa-twitter"></i>
|
||||
</button>
|
||||
<button onclick="shareFacebook()" class="w-10 h-10 rounded-full bg-blue-600 text-white flex items-center justify-center hover:bg-blue-700 transition">
|
||||
<i class="fab fa-facebook-f"></i>
|
||||
</button>
|
||||
<button onclick="shareLinkedIn()" class="w-10 h-10 rounded-full bg-blue-700 text-white flex items-center justify-center hover:bg-blue-800 transition">
|
||||
<i class="fab fa-linkedin-in"></i>
|
||||
</button>
|
||||
<button onclick="copyLink()" class="w-10 h-10 rounded-full bg-gray-600 text-white flex items-center justify-center hover:bg-gray-700 transition" title="Copy link">
|
||||
<i class="fas fa-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</article>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 text-gray-300 py-12">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass" class="h-8 w-8">
|
||||
<span class="font-bold text-white text-lg">Plugin Compass</span>
|
||||
</div>
|
||||
<p class="text-sm">Build custom WordPress plugins with AI. Replace expensive subscriptions with tailored solutions.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-4">Product</h4>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/features" class="hover:text-white">Features</a></li>
|
||||
<li><a href="/pricing" class="hover:text-white">Pricing</a></li>
|
||||
<li><a href="/builder" class="hover:text-white">Builder</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-4">Resources</h4>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/news" class="hover:text-white">News</a></li>
|
||||
<li><a href="/docs" class="hover:text-white">Documentation</a></li>
|
||||
<li><a href="/faq" class="hover:text-white">FAQ</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-4">Legal</h4>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/privacy" class="hover:text-white">Privacy Policy</a></li>
|
||||
<li><a href="/terms" class="hover:text-white">Terms of Service</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-sm">
|
||||
<p>© 2026 Plugin Compass. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
let postData = null;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const slug = window.location.pathname.split('/blog/')[1];
|
||||
if (!slug) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
|
||||
await loadPost(slug);
|
||||
});
|
||||
|
||||
// Load post
|
||||
async function loadPost(slug) {
|
||||
try {
|
||||
const response = await fetch(`/api/blogs/${slug}`);
|
||||
|
||||
if (!response.ok) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
postData = data.post;
|
||||
|
||||
if (!postData) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
|
||||
renderPost(postData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load post:', error);
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
// Render post
|
||||
function renderPost(post) {
|
||||
// Update meta tags
|
||||
document.title = `${post.title} - Plugin Compass`;
|
||||
document.querySelector('meta[name="description"]').content = post.excerpt || post.meta_description || '';
|
||||
|
||||
// Update page content
|
||||
document.getElementById('post-title').textContent = post.title;
|
||||
document.getElementById('post-author').textContent = post.author || 'Plugin Compass Team';
|
||||
document.getElementById('post-date').textContent = formatDate(post.published_at);
|
||||
document.getElementById('post-category').textContent = post.category || 'Article';
|
||||
document.getElementById('post-type').textContent = post.type === 'seo' ? 'SEO Article' : 'News';
|
||||
|
||||
// Featured image
|
||||
if (post.featured_image) {
|
||||
document.getElementById('featured-image').src = post.featured_image;
|
||||
document.getElementById('featured-image').alt = post.title;
|
||||
document.getElementById('featured-image-container').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Content
|
||||
document.getElementById('post-content').innerHTML = post.html || '<p>No content available.</p>';
|
||||
|
||||
// Tags
|
||||
if (post.tags && post.tags.length > 0) {
|
||||
document.getElementById('post-tags').innerHTML = `
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${post.tags.map(tag => `
|
||||
<span class="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">${escapeHtml(tag)}</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Show content, hide loading
|
||||
document.getElementById('loading-state').classList.add('hidden');
|
||||
document.getElementById('article-content').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Show error state
|
||||
function showError() {
|
||||
document.getElementById('loading-state').classList.add('hidden');
|
||||
document.getElementById('error-state').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Share functions
|
||||
function shareTwitter() {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
const text = encodeURIComponent(postData?.title || 'Check out this article');
|
||||
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=600,height=400');
|
||||
}
|
||||
|
||||
function shareFacebook() {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400');
|
||||
}
|
||||
|
||||
function shareLinkedIn() {
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
const title = encodeURIComponent(postData?.title || '');
|
||||
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}&title=${title}`, '_blank', 'width=600,height=400');
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||
// Show feedback
|
||||
const btn = event.target.closest('button');
|
||||
const original = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-check"></i>';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = original;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -253,7 +253,7 @@
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-4 mt-4">
|
||||
<a href="/apps" class="inline-flex items-center px-6 py-3 bg-green-500 hover:bg-green-600 text-white rounded-full font-semibold shadow-md">
|
||||
<a href="/apps" class="inline-flex items-center px-6 py-3 bg-green-700 hover:bg-green-600 text-white rounded-full font-semibold shadow-md">
|
||||
Get started <i class="fa-solid fa-arrow-right ml-2"></i>
|
||||
</a>
|
||||
<a href="/features" class="inline-flex items-center px-6 py-3 border border-gray-300 bg-white rounded-full font-medium hover:bg-gray-50">
|
||||
|
||||
547
chat/public/js/blog-admin.js
Normal file
547
chat/public/js/blog-admin.js
Normal file
@@ -0,0 +1,547 @@
|
||||
/**
|
||||
* Blog Admin JavaScript
|
||||
* Handles blog post management with Editor.js integration
|
||||
*/
|
||||
|
||||
let editor = null;
|
||||
let categories = [];
|
||||
let posts = [];
|
||||
let currentEditingPost = null;
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
await loadCategories();
|
||||
await loadPosts();
|
||||
setupEventListeners();
|
||||
setupTabs();
|
||||
}
|
||||
|
||||
// Setup tab switching
|
||||
function setupTabs() {
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
tabButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tabName = btn.dataset.tab;
|
||||
|
||||
// Update buttons
|
||||
tabButtons.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
// Update content
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
// Refresh button
|
||||
document.getElementById('refresh-blogs')?.addEventListener('click', async () => {
|
||||
await loadPosts();
|
||||
showStatus('Posts refreshed', 'success');
|
||||
});
|
||||
|
||||
// New post button
|
||||
document.getElementById('new-post')?.addEventListener('click', () => {
|
||||
openModal();
|
||||
});
|
||||
|
||||
// Modal close
|
||||
document.getElementById('modal-close')?.addEventListener('click', closeModal);
|
||||
document.getElementById('cancel-post')?.addEventListener('click', closeModal);
|
||||
|
||||
// Click outside modal to close
|
||||
document.getElementById('post-modal')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'post-modal') closeModal();
|
||||
});
|
||||
|
||||
// Post form submit
|
||||
document.getElementById('post-form')?.addEventListener('submit', handlePostSubmit);
|
||||
|
||||
// Category form submit
|
||||
document.getElementById('category-form')?.addEventListener('submit', handleCategorySubmit);
|
||||
|
||||
// Generate slug from title
|
||||
document.getElementById('post-title')?.addEventListener('blur', () => {
|
||||
const title = document.getElementById('post-title').value;
|
||||
const slugInput = document.getElementById('post-slug');
|
||||
if (title && !slugInput.value) {
|
||||
slugInput.value = generateSlug(title);
|
||||
}
|
||||
});
|
||||
|
||||
// Filters
|
||||
['filter-type', 'filter-status', 'filter-source'].forEach(id => {
|
||||
document.getElementById(id)?.addEventListener('change', () => {
|
||||
renderPostsList();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('filter-search')?.addEventListener('input', () => {
|
||||
renderPostsList();
|
||||
});
|
||||
|
||||
// Generate category slug
|
||||
document.getElementById('category-name')?.addEventListener('blur', () => {
|
||||
const name = document.getElementById('category-name').value;
|
||||
const slugInput = document.getElementById('category-slug');
|
||||
if (name && !slugInput.value) {
|
||||
slugInput.value = generateSlug(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Generate slug
|
||||
function generateSlug(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.substring(0, 100);
|
||||
}
|
||||
|
||||
// Load categories from API
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const response = await fetch('/api/blogs/categories');
|
||||
const data = await response.json();
|
||||
categories = data.categories || [];
|
||||
populateCategorySelect();
|
||||
renderCategoriesList();
|
||||
} catch (error) {
|
||||
console.error('Failed to load categories:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load posts from API
|
||||
async function loadPosts() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/blogs');
|
||||
const data = await response.json();
|
||||
posts = data.posts || [];
|
||||
renderPostsList();
|
||||
} catch (error) {
|
||||
console.error('Failed to load posts:', error);
|
||||
document.getElementById('blog-list').innerHTML = '<p class="error">Failed to load posts</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Populate category select dropdown
|
||||
function populateCategorySelect() {
|
||||
const select = document.getElementById('post-category');
|
||||
select.innerHTML = '<option value="">No Category</option>';
|
||||
categories.forEach(cat => {
|
||||
const option = document.createElement('option');
|
||||
option.value = cat.slug;
|
||||
option.textContent = cat.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Render posts list
|
||||
function renderPostsList() {
|
||||
const container = document.getElementById('blog-list');
|
||||
const typeFilter = document.getElementById('filter-type')?.value;
|
||||
const statusFilter = document.getElementById('filter-status')?.value;
|
||||
const sourceFilter = document.getElementById('filter-source')?.value;
|
||||
const searchFilter = document.getElementById('filter-search')?.value.toLowerCase();
|
||||
|
||||
let filtered = posts;
|
||||
|
||||
if (typeFilter) filtered = filtered.filter(p => p.type === typeFilter);
|
||||
if (statusFilter) filtered = filtered.filter(p => p.status === statusFilter);
|
||||
if (sourceFilter) filtered = filtered.filter(p => p.source === sourceFilter);
|
||||
if (searchFilter) {
|
||||
filtered = filtered.filter(p =>
|
||||
p.title?.toLowerCase().includes(searchFilter) ||
|
||||
p.excerpt?.toLowerCase().includes(searchFilter) ||
|
||||
p.author?.toLowerCase().includes(searchFilter)
|
||||
);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = '<p>No posts found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(post => `
|
||||
<div class="blog-item ${post.source}">
|
||||
<div>
|
||||
<div style="display: flex; gap: 8px; align-items: center; margin-bottom: 4px;">
|
||||
<strong>${escapeHtml(post.title)}</strong>
|
||||
<span class="type-badge type-${post.type}">${post.type}</span>
|
||||
<span class="status-badge status-${post.status}">${post.status}</span>
|
||||
<span class="source-badge source-${post.source}">${post.source}</span>
|
||||
</div>
|
||||
<div class="blog-meta">
|
||||
<span>By ${escapeHtml(post.author || 'Unknown')}</span>
|
||||
<span>${formatDate(post.published_at || post.created_at)}</span>
|
||||
${post.category ? `<span>Category: ${escapeHtml(post.category)}</span>` : ''}
|
||||
${post.tags?.length ? `<span>Tags: ${post.tags.join(', ')}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button class="ghost" onclick="editPost('${post.id}')" ${post.source === 'repo' ? 'disabled title="Repo posts are read-only"' : ''}>
|
||||
${post.source === 'repo' ? 'View' : 'Edit'}
|
||||
</button>
|
||||
${post.source === 'database' ? `
|
||||
<button class="ghost danger" onclick="deletePost('${post.id}')">Delete</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Render categories list
|
||||
function renderCategoriesList() {
|
||||
const container = document.getElementById('category-list');
|
||||
|
||||
if (categories.length === 0) {
|
||||
container.innerHTML = '<p>No categories found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = categories.map(cat => `
|
||||
<div class="category-item">
|
||||
<div>
|
||||
<strong>${escapeHtml(cat.name)}</strong>
|
||||
<span style="color: var(--muted); font-size: 0.85em; margin-left: 8px;">/${escapeHtml(cat.slug)}</span>
|
||||
${cat.description ? `<p style="margin: 4px 0 0; font-size: 0.85em; color: var(--muted);">${escapeHtml(cat.description)}</p>` : ''}
|
||||
</div>
|
||||
<button class="ghost danger" onclick="deleteCategory('${cat.id}')">Delete</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Open modal for new post
|
||||
function openModal(post = null) {
|
||||
currentEditingPost = post;
|
||||
const modal = document.getElementById('post-modal');
|
||||
const titleEl = document.getElementById('modal-title');
|
||||
const sourceEl = document.getElementById('post-source');
|
||||
|
||||
if (post) {
|
||||
titleEl.textContent = post.source === 'repo' ? 'View Post (Read-Only)' : 'Edit Post';
|
||||
document.getElementById('post-id').value = post.id;
|
||||
document.getElementById('post-title').value = post.title || '';
|
||||
document.getElementById('post-slug').value = post.slug || '';
|
||||
document.getElementById('post-type').value = post.type || 'news';
|
||||
document.getElementById('post-status').value = post.status || 'draft';
|
||||
document.getElementById('post-category').value = post.category || '';
|
||||
document.getElementById('post-author').value = post.author || '';
|
||||
document.getElementById('post-excerpt').value = post.excerpt || '';
|
||||
document.getElementById('post-meta-title').value = post.meta_title || '';
|
||||
document.getElementById('post-meta-description').value = post.meta_description || '';
|
||||
document.getElementById('post-featured-image').value = post.featured_image || '';
|
||||
document.getElementById('post-tags').value = (post.tags || []).join(', ');
|
||||
sourceEl.textContent = post.source === 'repo' ? 'Read-Only (Repo)' : 'Editable (Database)';
|
||||
sourceEl.className = `source-badge source-${post.source}`;
|
||||
|
||||
// Disable form for repo posts
|
||||
const isRepo = post.source === 'repo';
|
||||
document.querySelectorAll('#post-form input, #post-form select, #post-form textarea').forEach(el => {
|
||||
el.disabled = isRepo;
|
||||
});
|
||||
document.getElementById('save-post').style.display = isRepo ? 'none' : 'inline-block';
|
||||
} else {
|
||||
titleEl.textContent = 'New Post';
|
||||
document.getElementById('post-form').reset();
|
||||
document.getElementById('post-id').value = '';
|
||||
document.getElementById('post-status').value = 'draft';
|
||||
document.getElementById('post-type').value = 'news';
|
||||
sourceEl.textContent = 'New Post';
|
||||
sourceEl.className = 'source-badge source-database';
|
||||
document.querySelectorAll('#post-form input, #post-form select, #post-form textarea').forEach(el => {
|
||||
el.disabled = false;
|
||||
});
|
||||
document.getElementById('save-post').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Initialize Editor.js
|
||||
initEditor(post?.content || null);
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal() {
|
||||
document.getElementById('post-modal').classList.add('hidden');
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
editor = null;
|
||||
}
|
||||
currentEditingPost = null;
|
||||
}
|
||||
|
||||
// Initialize Editor.js
|
||||
function initEditor(savedContent = null) {
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
}
|
||||
|
||||
const editorConfig = {
|
||||
holder: 'editor',
|
||||
tools: {
|
||||
header: {
|
||||
class: Header,
|
||||
config: {
|
||||
levels: [2, 3, 4, 5, 6],
|
||||
defaultLevel: 2
|
||||
}
|
||||
},
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar: true
|
||||
},
|
||||
list: {
|
||||
class: List,
|
||||
inlineToolbar: true
|
||||
},
|
||||
link: {
|
||||
class: LinkTool,
|
||||
config: {
|
||||
endpoint: '/api/fetch-url' // Optional: for link previews
|
||||
}
|
||||
},
|
||||
image: {
|
||||
class: ImageTool,
|
||||
config: {
|
||||
endpoints: {
|
||||
byFile: '/api/admin/blogs/upload-image'
|
||||
},
|
||||
field: 'image',
|
||||
types: 'image/*'
|
||||
}
|
||||
},
|
||||
embed: {
|
||||
class: Embed,
|
||||
config: {
|
||||
services: {
|
||||
youtube: true,
|
||||
vimeo: true,
|
||||
twitter: true,
|
||||
codepen: true
|
||||
}
|
||||
}
|
||||
},
|
||||
code: {
|
||||
class: CodeTool
|
||||
},
|
||||
quote: {
|
||||
class: Quote,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
quotePlaceholder: 'Enter a quote',
|
||||
captionPlaceholder: 'Quote\'s author'
|
||||
}
|
||||
},
|
||||
table: {
|
||||
class: Table,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
rows: 2,
|
||||
cols: 2
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
class: Marker
|
||||
}
|
||||
},
|
||||
placeholder: 'Start writing your post...',
|
||||
data: savedContent || {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
readOnly: currentEditingPost?.source === 'repo'
|
||||
};
|
||||
|
||||
editor = new EditorJS(editorConfig);
|
||||
}
|
||||
|
||||
// Handle post form submission
|
||||
async function handlePostSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!editor) {
|
||||
showStatus('Editor not initialized', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await editor.save();
|
||||
const postId = document.getElementById('post-id').value;
|
||||
|
||||
const postData = {
|
||||
title: document.getElementById('post-title').value,
|
||||
slug: document.getElementById('post-slug').value,
|
||||
type: document.getElementById('post-type').value,
|
||||
status: document.getElementById('post-status').value,
|
||||
category: document.getElementById('post-category').value,
|
||||
author: document.getElementById('post-author').value,
|
||||
excerpt: document.getElementById('post-excerpt').value,
|
||||
meta_title: document.getElementById('post-meta-title').value,
|
||||
meta_description: document.getElementById('post-meta-description').value,
|
||||
featured_image: document.getElementById('post-featured-image').value,
|
||||
tags: document.getElementById('post-tags').value.split(',').map(t => t.trim()).filter(Boolean),
|
||||
content: content
|
||||
};
|
||||
|
||||
let response;
|
||||
if (postId) {
|
||||
// Update existing post
|
||||
response = await fetch(`/api/admin/blogs/${postId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
} else {
|
||||
// Create new post
|
||||
response = await fetch('/api/admin/blogs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showStatus(postId ? 'Post updated successfully' : 'Post created successfully', 'success');
|
||||
closeModal();
|
||||
await loadPosts();
|
||||
} else {
|
||||
showStatus(result.error || 'Failed to save post', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save post:', error);
|
||||
showStatus('Failed to save post', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle category form submission
|
||||
async function handleCategorySubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const categoryData = {
|
||||
name: document.getElementById('category-name').value,
|
||||
slug: document.getElementById('category-slug').value,
|
||||
description: document.getElementById('category-description').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/blogs/categories', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(categoryData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('Category created successfully', 'success');
|
||||
document.getElementById('category-form').reset();
|
||||
await loadCategories();
|
||||
} else {
|
||||
showStatus(result.error || 'Failed to create category', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create category:', error);
|
||||
showStatus('Failed to create category', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Edit post (global function for onclick)
|
||||
window.editPost = function(postId) {
|
||||
const post = posts.find(p => p.id === postId);
|
||||
if (post) {
|
||||
openModal(post);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete post (global function for onclick)
|
||||
window.deletePost = async function(postId) {
|
||||
if (!confirm('Are you sure you want to delete this post? This cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/blogs/${postId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('Post deleted successfully', 'success');
|
||||
await loadPosts();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showStatus(result.error || 'Failed to delete post', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete post:', error);
|
||||
showStatus('Failed to delete post', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Delete category (global function for onclick)
|
||||
window.deleteCategory = async function(categoryId) {
|
||||
if (!confirm('Are you sure you want to delete this category?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/blogs/categories/${categoryId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showStatus('Category deleted successfully', 'success');
|
||||
await loadCategories();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showStatus(result.error || 'Failed to delete category', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete category:', error);
|
||||
showStatus('Failed to delete category', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function showStatus(message, type = 'info') {
|
||||
// Use the existing showStatus function from admin.js if available
|
||||
if (typeof window.showStatus === 'function' && window.showStatus !== showStatus) {
|
||||
window.showStatus(message, type);
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple fallback
|
||||
console.log(`[${type}] ${message}`);
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
291
chat/public/news.html
Normal file
291
chat/public/news.html
Normal file
@@ -0,0 +1,291 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Latest news, updates, and announcements from Plugin Compass - AI WordPress Plugin Builder">
|
||||
<title>News & Updates - Plugin Compass</title>
|
||||
<link rel="icon" type="image/png" href="/assets/Plugin.png">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
(function applyTailwindConfig(cfg, attempts = 0) {
|
||||
try {
|
||||
if (typeof window.tailwind !== 'undefined') {
|
||||
window.tailwind.config = cfg;
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
if (attempts >= 20) return;
|
||||
setTimeout(() => applyTailwindConfig(cfg, attempts + 1), 100);
|
||||
})({
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: { sans: ['Inter', 'sans-serif'] },
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc',
|
||||
400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca',
|
||||
800: '#3730a3', 900: '#312e81'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- PostHog Analytics -->
|
||||
<script src="/posthog.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900">
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass" class="h-8 w-8">
|
||||
<span class="font-bold text-xl text-gray-900">Plugin Compass</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<a href="/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
|
||||
<a href="/news" class="text-brand-600 font-medium">News</a>
|
||||
<a href="/features" class="text-gray-600 hover:text-gray-900 font-medium">Features</a>
|
||||
<a href="/pricing" class="text-gray-600 hover:text-gray-900 font-medium">Pricing</a>
|
||||
<a href="/builder" class="bg-brand-600 text-white px-4 py-2 rounded-lg hover:bg-brand-700 transition">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="bg-gradient-to-br from-brand-600 to-brand-800 text-white py-16">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-4">News & Updates</h1>
|
||||
<p class="text-xl text-brand-100 max-w-2xl mx-auto">
|
||||
Stay up to date with the latest features, improvements, and announcements from Plugin Compass
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Blog Posts Grid -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div id="posts-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<!-- Posts will be loaded here -->
|
||||
</div>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<div id="loading-indicator" class="text-center py-12">
|
||||
<i class="fas fa-spinner fa-spin text-3xl text-brand-600 loading-spinner"></i>
|
||||
<p class="mt-4 text-gray-600">Loading posts...</p>
|
||||
</div>
|
||||
|
||||
<!-- No More Posts -->
|
||||
<div id="no-more-posts" class="text-center py-12 hidden">
|
||||
<p class="text-gray-500">You've reached the end!</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" class="text-center py-16 hidden">
|
||||
<i class="fas fa-newspaper text-6xl text-gray-300 mb-4"></i>
|
||||
<h3 class="text-xl font-semibold text-gray-700 mb-2">No posts yet</h3>
|
||||
<p class="text-gray-500">Check back soon for news and updates!</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 text-gray-300 py-12 mt-12">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass" class="h-8 w-8">
|
||||
<span class="font-bold text-white text-lg">Plugin Compass</span>
|
||||
</div>
|
||||
<p class="text-sm">Build custom WordPress plugins with AI. Replace expensive subscriptions with tailored solutions.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-4">Product</h4>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/features" class="hover:text-white">Features</a></li>
|
||||
<li><a href="/pricing" class="hover:text-white">Pricing</a></li>
|
||||
<li><a href="/builder" class="hover:text-white">Builder</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-4">Resources</h4>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/news" class="hover:text-white">News</a></li>
|
||||
<li><a href="/docs" class="hover:text-white">Documentation</a></li>
|
||||
<li><a href="/faq" class="hover:text-white">FAQ</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-4">Legal</h4>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/privacy" class="hover:text-white">Privacy Policy</a></li>
|
||||
<li><a href="/terms" class="hover:text-white">Terms of Service</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-sm">
|
||||
<p>© 2026 Plugin Compass. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
let posts = [];
|
||||
let offset = 0;
|
||||
let limit = 9;
|
||||
let loading = false;
|
||||
let hasMore = true;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadPosts();
|
||||
setupInfiniteScroll();
|
||||
});
|
||||
|
||||
// Load posts
|
||||
async function loadPosts() {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
loading = true;
|
||||
document.getElementById('loading-indicator').classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/blogs?type=news&status=published&limit=${limit}&offset=${offset}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.posts && data.posts.length > 0) {
|
||||
posts = [...posts, ...data.posts];
|
||||
renderPosts(data.posts);
|
||||
offset += data.posts.length;
|
||||
|
||||
if (data.posts.length < limit) {
|
||||
hasMore = false;
|
||||
document.getElementById('no-more-posts').classList.remove('hidden');
|
||||
}
|
||||
} else {
|
||||
hasMore = false;
|
||||
if (posts.length === 0) {
|
||||
document.getElementById('empty-state').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('no-more-posts').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load posts:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
document.getElementById('loading-indicator').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Render posts
|
||||
function renderPosts(newPosts) {
|
||||
const container = document.getElementById('posts-container');
|
||||
|
||||
newPosts.forEach(post => {
|
||||
const card = createPostCard(post);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Create post card element
|
||||
function createPostCard(post) {
|
||||
const article = document.createElement('article');
|
||||
article.className = 'bg-white rounded-xl shadow-md overflow-hidden hover:shadow-lg transition-shadow fade-in';
|
||||
|
||||
const imageHtml = post.featured_image
|
||||
? `<div class="h-48 overflow-hidden"><img src="${escapeHtml(post.featured_image)}" alt="${escapeHtml(post.title)}" class="w-full h-full object-cover hover:scale-105 transition-transform duration-300"></div>`
|
||||
: `<div class="h-48 bg-gradient-to-br from-brand-100 to-brand-200 flex items-center justify-center"><i class="fas fa-newspaper text-4xl text-brand-400"></i></div>`;
|
||||
|
||||
article.innerHTML = `
|
||||
<a href="/blog/${escapeHtml(post.slug)}" class="block">
|
||||
${imageHtml}
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="px-3 py-1 bg-brand-100 text-brand-700 rounded-full text-xs font-medium">${escapeHtml(post.category || 'News')}</span>
|
||||
<span class="text-gray-400 text-sm">${formatDate(post.published_at)}</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2 line-clamp-2 hover:text-brand-600 transition-colors">${escapeHtml(post.title)}</h2>
|
||||
<p class="text-gray-600 line-clamp-3 mb-4">${escapeHtml(post.excerpt || '')}</p>
|
||||
<div class="flex items-center justify-between pt-4 border-t border-gray-100">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-brand-100 flex items-center justify-center">
|
||||
<i class="fas fa-user text-brand-600 text-sm"></i>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600">${escapeHtml(post.author || 'Plugin Compass Team')}</span>
|
||||
</div>
|
||||
<span class="text-brand-600 font-medium text-sm">Read more →</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
// Setup infinite scroll
|
||||
function setupInfiniteScroll() {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && !loading && hasMore) {
|
||||
loadPosts();
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '100px' });
|
||||
|
||||
observer.observe(document.getElementById('loading-indicator'));
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user