Add comprehensive feature request admin functionality
- Update data model to include status, adminReply, and updatedAt fields - Hide user emails from public API responses for privacy - Add admin-only endpoints: list, reply, update status, delete - Create admin-feature-requests.html with full management UI - Add status badges and admin replies to public feature requests page - Add Feature Requests link to all admin page sidebars Admin capabilities: - View all feature requests with author emails (admin only) - Reply to feature requests with admin responses visible to public - Update status: pending, planned, in-progress, completed, declined - Delete feature requests - Filter and sort by status, votes, date
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -247,6 +247,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost active" href="/admin/blogs">Blog Management</a>
|
<a class="ghost active" href="/admin/blogs">Blog Management</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</a>
|
<a class="ghost" href="/admin/external-testing">External Testing</a>
|
||||||
<a class="ghost active" href="/admin/contact-messages">Contact Messages</a>
|
<a class="ghost active" href="/admin/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
537
chat/public/admin-feature-requests.html
Normal file
537
chat/public/admin-feature-requests.html
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Feature Requests - Admin Panel</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
<style>
|
||||||
|
body[data-page="feature-requests"] .admin-grid {
|
||||||
|
grid-template-columns: none !important;
|
||||||
|
gap: 12px !important;
|
||||||
|
}
|
||||||
|
body[data-page="feature-requests"] .admin-grid .admin-card {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.fr-card {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.fr-card:hover {
|
||||||
|
border-color: rgba(0, 66, 37, 0.3);
|
||||||
|
}
|
||||||
|
.fr-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.fr-meta {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.fr-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.fr-email {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.fr-votes {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(0, 128, 96, 0.1);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.fr-description {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.fr-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.fr-status.pending {
|
||||||
|
background: rgba(108, 117, 125, 0.1);
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.fr-status.planned {
|
||||||
|
background: rgba(0, 123, 255, 0.1);
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
.fr-status.in-progress {
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
.fr-status.completed {
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
.fr-status.declined {
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
.fr-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.fr-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.fr-admin-reply {
|
||||||
|
background: rgba(0, 128, 96, 0.05);
|
||||||
|
border: 1px solid rgba(0, 128, 96, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.fr-admin-reply-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.fr-admin-reply-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.reply-form {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.reply-form.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.reply-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 80px;
|
||||||
|
resize: vertical;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.reply-form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.status-select {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.status-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.empty-state svg {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.search-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.search-box input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.filter-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.filter-box select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.filter-box select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="/posthog.js"></script>
|
||||||
|
</head>
|
||||||
|
<body data-page="feature-requests">
|
||||||
|
<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/feature-requests">Feature Requests</a>
|
||||||
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main class="main">
|
||||||
|
<div class="admin-shell">
|
||||||
|
<div class="topbar">
|
||||||
|
<button id="menu-toggle">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<div class="pill">Admin</div>
|
||||||
|
<div class="title" style="margin-top: 6px;">Feature Requests</div>
|
||||||
|
<div class="crumb">Manage and respond to feature requests</div>
|
||||||
|
</div>
|
||||||
|
<div class="admin-actions">
|
||||||
|
<button id="admin-refresh" class="ghost">Refresh</button>
|
||||||
|
<button id="admin-logout" class="primary">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-grid">
|
||||||
|
<div class="admin-card" style="grid-column: span 2;">
|
||||||
|
<header>
|
||||||
|
<h3>All Feature Requests</h3>
|
||||||
|
<div class="pill" id="fr-count">0</div>
|
||||||
|
</header>
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="search-input" placeholder="Search feature requests..." />
|
||||||
|
</div>
|
||||||
|
<div class="filter-box">
|
||||||
|
<select id="status-filter">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="planned">Planned</option>
|
||||||
|
<option value="in-progress">In Progress</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
<option value="declined">Declined</option>
|
||||||
|
</select>
|
||||||
|
<select id="sort-filter">
|
||||||
|
<option value="votes">Most Voted</option>
|
||||||
|
<option value="newest">Newest First</option>
|
||||||
|
<option value="oldest">Oldest First</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="fr-list">
|
||||||
|
<div class="empty-state">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
|
||||||
|
</svg>
|
||||||
|
<p>No feature requests yet</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script src="/admin.js"></script>
|
||||||
|
<script>
|
||||||
|
(async function() {
|
||||||
|
const frList = document.getElementById('fr-list');
|
||||||
|
const frCount = document.getElementById('fr-count');
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
const statusFilter = document.getElementById('status-filter');
|
||||||
|
const sortFilter = document.getElementById('sort-filter');
|
||||||
|
let featureRequests = [];
|
||||||
|
|
||||||
|
async function loadFeatureRequests() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/feature-requests');
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.featureRequests) {
|
||||||
|
featureRequests = data.featureRequests;
|
||||||
|
renderFeatureRequests(featureRequests);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load feature requests:', error);
|
||||||
|
frList.innerHTML = '<div class="empty-state"><p>Failed to load feature requests</p></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusLabel(status) {
|
||||||
|
const labels = {
|
||||||
|
'pending': 'Pending',
|
||||||
|
'planned': 'Planned',
|
||||||
|
'in-progress': 'In Progress',
|
||||||
|
'completed': 'Completed',
|
||||||
|
'declined': 'Declined'
|
||||||
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFeatureRequests(frs) {
|
||||||
|
frCount.textContent = frs.length;
|
||||||
|
|
||||||
|
if (frs.length === 0) {
|
||||||
|
frList.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
|
||||||
|
</svg>
|
||||||
|
<p>No feature requests found</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frList.innerHTML = frs.map(fr => `
|
||||||
|
<div class="fr-card" data-id="${fr.id}">
|
||||||
|
<div class="fr-header">
|
||||||
|
<div class="fr-meta">
|
||||||
|
<div class="fr-title">${escapeHtml(fr.title)}</div>
|
||||||
|
<div class="fr-email">${escapeHtml(fr.authorEmail)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<div class="fr-votes">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="18 15 12 9 6 15"></polyline>
|
||||||
|
</svg>
|
||||||
|
${fr.votes} votes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fr-description">${escapeHtml(fr.description)}</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<span class="fr-status ${fr.status}">${getStatusLabel(fr.status)}</span>
|
||||||
|
<span class="fr-date" style="margin-left: 8px;">${formatDate(fr.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${fr.adminReply ? `
|
||||||
|
<div class="fr-admin-reply">
|
||||||
|
<div class="fr-admin-reply-label">Admin Reply</div>
|
||||||
|
<div class="fr-admin-reply-text">${escapeHtml(fr.adminReply)}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="reply-form" id="reply-form-${fr.id}">
|
||||||
|
<textarea id="reply-text-${fr.id}" placeholder="Enter your reply...">${escapeHtml(fr.adminReply || '')}</textarea>
|
||||||
|
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
||||||
|
<button class="ghost cancel-reply-btn" data-id="${fr.id}">Cancel</button>
|
||||||
|
<button class="primary save-reply-btn" data-id="${fr.id}">Save Reply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fr-actions">
|
||||||
|
<select class="status-select status-change-btn" data-id="${fr.id}">
|
||||||
|
<option value="pending" ${fr.status === 'pending' ? 'selected' : ''}>Pending</option>
|
||||||
|
<option value="planned" ${fr.status === 'planned' ? 'selected' : ''}>Planned</option>
|
||||||
|
<option value="in-progress" ${fr.status === 'in-progress' ? 'selected' : ''}>In Progress</option>
|
||||||
|
<option value="completed" ${fr.status === 'completed' ? 'selected' : ''}>Completed</option>
|
||||||
|
<option value="declined" ${fr.status === 'declined' ? 'selected' : ''}>Declined</option>
|
||||||
|
</select>
|
||||||
|
<button class="ghost reply-btn" data-id="${fr.id}">${fr.adminReply ? 'Edit Reply' : 'Add Reply'}</button>
|
||||||
|
<button class="danger delete-btn" data-id="${fr.id}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Status change
|
||||||
|
document.querySelectorAll('.status-change-btn').forEach(select => {
|
||||||
|
select.addEventListener('change', async (e) => {
|
||||||
|
const id = e.target.dataset.id;
|
||||||
|
const status = e.target.value;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/feature-requests/${id}/status`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status })
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
await loadFeatureRequests();
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(data.error || 'Failed to update status');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update status:', error);
|
||||||
|
alert('Failed to update status');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reply buttons
|
||||||
|
document.querySelectorAll('.reply-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const id = e.target.dataset.id;
|
||||||
|
const form = document.getElementById(`reply-form-${id}`);
|
||||||
|
form.classList.add('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel reply
|
||||||
|
document.querySelectorAll('.cancel-reply-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const id = e.target.dataset.id;
|
||||||
|
const form = document.getElementById(`reply-form-${id}`);
|
||||||
|
form.classList.remove('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save reply
|
||||||
|
document.querySelectorAll('.save-reply-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
const id = e.target.dataset.id;
|
||||||
|
const textarea = document.getElementById(`reply-text-${id}`);
|
||||||
|
const reply = textarea.value.trim();
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/feature-requests/${id}/reply`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ reply })
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
await loadFeatureRequests();
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(data.error || 'Failed to save reply');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save reply:', error);
|
||||||
|
alert('Failed to save reply');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete buttons
|
||||||
|
document.querySelectorAll('.delete-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
const id = e.target.dataset.id;
|
||||||
|
if (confirm('Are you sure you want to delete this feature request? This action cannot be undone.')) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/feature-requests/${id}`, { method: 'DELETE' });
|
||||||
|
if (response.ok) {
|
||||||
|
await loadFeatureRequests();
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
alert(data.error || 'Failed to delete');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete feature request:', error);
|
||||||
|
alert('Failed to delete feature request');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterAndSort() {
|
||||||
|
let filtered = [...featureRequests];
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
const query = searchInput.value.toLowerCase();
|
||||||
|
if (query) {
|
||||||
|
filtered = filtered.filter(fr =>
|
||||||
|
fr.title.toLowerCase().includes(query) ||
|
||||||
|
fr.description.toLowerCase().includes(query) ||
|
||||||
|
fr.authorEmail.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
const status = statusFilter.value;
|
||||||
|
if (status) {
|
||||||
|
filtered = filtered.filter(fr => fr.status === status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
const sort = sortFilter.value;
|
||||||
|
if (sort === 'votes') {
|
||||||
|
filtered.sort((a, b) => b.votes - a.votes || new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
|
} else if (sort === 'newest') {
|
||||||
|
filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
|
} else if (sort === 'oldest') {
|
||||||
|
filtered.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFeatureRequests(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', filterAndSort);
|
||||||
|
statusFilter.addEventListener('change', filterAndSort);
|
||||||
|
sortFilter.addEventListener('change', filterAndSort);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
loadFeatureRequests();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -226,6 +226,7 @@
|
|||||||
<a class="ghost active" href="/admin/resources">Resources</a>
|
<a class="ghost active" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -186,6 +186,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<a class="ghost" href="/admin/resources">Resources</a>
|
<a class="ghost" href="/admin/resources">Resources</a>
|
||||||
<a class="ghost" href="/admin/external-testing">External Testing</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/contact-messages">Contact Messages</a>
|
||||||
|
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
|
||||||
<a class="ghost" href="/admin/blogs">Blog Management</a>
|
<a class="ghost" href="/admin/blogs">Blog Management</a>
|
||||||
<a class="ghost" href="/admin/login">Login</a>
|
<a class="ghost" href="/admin/login">Login</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -271,6 +271,66 @@
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #adb5bd;
|
color: #adb5bd;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-status.pending {
|
||||||
|
background: rgba(108, 117, 125, 0.1);
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-status.planned {
|
||||||
|
background: rgba(0, 123, 255, 0.1);
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-status.in-progress {
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-status.completed {
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-status.declined {
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-admin-reply {
|
||||||
|
background: linear-gradient(135deg, rgba(0, 128, 96, 0.05), rgba(0, 76, 63, 0.05));
|
||||||
|
border: 1px solid rgba(0, 128, 96, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-admin-reply-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--shopify-green);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-admin-reply-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fr-empty {
|
.fr-empty {
|
||||||
@@ -697,6 +757,17 @@
|
|||||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStatusLabel(status) {
|
||||||
|
const labels = {
|
||||||
|
'pending': 'Pending',
|
||||||
|
'planned': 'Planned',
|
||||||
|
'in-progress': 'In Progress',
|
||||||
|
'completed': 'Completed',
|
||||||
|
'declined': 'Declined'
|
||||||
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
function renderFeatureRequests() {
|
function renderFeatureRequests() {
|
||||||
if (state.featureRequests.length === 0) {
|
if (state.featureRequests.length === 0) {
|
||||||
frList.innerHTML = `
|
frList.innerHTML = `
|
||||||
@@ -725,10 +796,21 @@
|
|||||||
<h3 class="fr-title">${escapeHtml(fr.title)}</h3>
|
<h3 class="fr-title">${escapeHtml(fr.title)}</h3>
|
||||||
<p class="fr-description">${escapeHtml(fr.description)}</p>
|
<p class="fr-description">${escapeHtml(fr.description)}</p>
|
||||||
<div class="fr-meta">
|
<div class="fr-meta">
|
||||||
<span>Submitted by ${escapeHtml(fr.authorEmail || 'Anonymous')}</span>
|
<span class="fr-status ${fr.status || 'pending'}">${getStatusLabel(fr.status || 'pending')}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>${formatDate(fr.createdAt)}</span>
|
<span>${formatDate(fr.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${fr.adminReply ? `
|
||||||
|
<div class="fr-admin-reply">
|
||||||
|
<div class="fr-admin-reply-label">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
Admin Response
|
||||||
|
</div>
|
||||||
|
<div class="fr-admin-reply-text">${escapeHtml(fr.adminReply)}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
121
chat/server.js
121
chat/server.js
@@ -11217,7 +11217,6 @@ async function handleAdminWithdrawalUpdate(req, res) {
|
|||||||
async function handleFeatureRequestsList(req, res) {
|
async function handleFeatureRequestsList(req, res) {
|
||||||
const session = getUserSession(req);
|
const session = getUserSession(req);
|
||||||
const userId = session?.userId || '';
|
const userId = session?.userId || '';
|
||||||
const userEmail = userId ? (findUserById(userId)?.email || '') : '';
|
|
||||||
|
|
||||||
const sorted = [...featureRequestsDb].sort((a, b) => {
|
const sorted = [...featureRequestsDb].sort((a, b) => {
|
||||||
if (b.votes !== a.votes) return b.votes - a.votes;
|
if (b.votes !== a.votes) return b.votes - a.votes;
|
||||||
@@ -11230,7 +11229,8 @@ async function handleFeatureRequestsList(req, res) {
|
|||||||
description: fr.description,
|
description: fr.description,
|
||||||
votes: fr.votes,
|
votes: fr.votes,
|
||||||
createdAt: fr.createdAt,
|
createdAt: fr.createdAt,
|
||||||
authorEmail: fr.authorEmail,
|
status: fr.status || 'pending',
|
||||||
|
adminReply: fr.adminReply || '',
|
||||||
hasVoted: userId ? fr.upvoters.includes(userId) : false,
|
hasVoted: userId ? fr.upvoters.includes(userId) : false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -11264,7 +11264,10 @@ async function handleFeatureRequestCreate(req, res) {
|
|||||||
upvoters: [session.userId],
|
upvoters: [session.userId],
|
||||||
authorEmail: user.email || '',
|
authorEmail: user.email || '',
|
||||||
authorId: session.userId,
|
authorId: session.userId,
|
||||||
|
status: 'pending',
|
||||||
|
adminReply: '',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
featureRequestsDb.push(featureRequest);
|
featureRequestsDb.push(featureRequest);
|
||||||
@@ -11279,7 +11282,8 @@ async function handleFeatureRequestCreate(req, res) {
|
|||||||
description: featureRequest.description,
|
description: featureRequest.description,
|
||||||
votes: featureRequest.votes,
|
votes: featureRequest.votes,
|
||||||
createdAt: featureRequest.createdAt,
|
createdAt: featureRequest.createdAt,
|
||||||
authorEmail: featureRequest.authorEmail,
|
status: featureRequest.status,
|
||||||
|
adminReply: featureRequest.adminReply,
|
||||||
hasVoted: true,
|
hasVoted: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -11315,6 +11319,109 @@ async function handleFeatureRequestUpvote(req, res, id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAdminFeatureRequestsList(req, res) {
|
||||||
|
const session = getAdminSession(req);
|
||||||
|
if (!session) {
|
||||||
|
return sendJson(res, 403, { error: 'Admin access required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = [...featureRequestsDb].sort((a, b) => {
|
||||||
|
if (b.votes !== a.votes) return b.votes - a.votes;
|
||||||
|
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = sorted.map(fr => ({
|
||||||
|
id: fr.id,
|
||||||
|
title: fr.title,
|
||||||
|
description: fr.description,
|
||||||
|
votes: fr.votes,
|
||||||
|
createdAt: fr.createdAt,
|
||||||
|
updatedAt: fr.updatedAt,
|
||||||
|
authorEmail: fr.authorEmail,
|
||||||
|
authorId: fr.authorId,
|
||||||
|
status: fr.status || 'pending',
|
||||||
|
adminReply: fr.adminReply || '',
|
||||||
|
upvoters: fr.upvoters || [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
sendJson(res, 200, { featureRequests: result });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFeatureRequestReply(req, res, id) {
|
||||||
|
const session = getAdminSession(req);
|
||||||
|
if (!session) {
|
||||||
|
return sendJson(res, 403, { error: 'Admin access required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureRequest = featureRequestsDb.find(fr => fr.id === id);
|
||||||
|
if (!featureRequest) {
|
||||||
|
return sendJson(res, 404, { error: 'Feature request not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await parseJsonBody(req);
|
||||||
|
const reply = (body.reply || '').toString().trim();
|
||||||
|
|
||||||
|
featureRequest.adminReply = reply;
|
||||||
|
featureRequest.updatedAt = new Date().toISOString();
|
||||||
|
await persistFeatureRequestsDb();
|
||||||
|
|
||||||
|
log('Feature request reply added', { id, adminId: session.userId });
|
||||||
|
sendJson(res, 200, { ok: true, adminReply: reply });
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(res, 400, { error: error.message || 'Unable to add reply' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFeatureRequestUpdateStatus(req, res, id) {
|
||||||
|
const session = getAdminSession(req);
|
||||||
|
if (!session) {
|
||||||
|
return sendJson(res, 403, { error: 'Admin access required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureRequest = featureRequestsDb.find(fr => fr.id === id);
|
||||||
|
if (!featureRequest) {
|
||||||
|
return sendJson(res, 404, { error: 'Feature request not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await parseJsonBody(req);
|
||||||
|
const status = (body.status || '').toString().trim();
|
||||||
|
const validStatuses = ['pending', 'planned', 'in-progress', 'completed', 'declined'];
|
||||||
|
|
||||||
|
if (!validStatuses.includes(status)) {
|
||||||
|
return sendJson(res, 400, { error: 'Invalid status. Must be one of: ' + validStatuses.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
featureRequest.status = status;
|
||||||
|
featureRequest.updatedAt = new Date().toISOString();
|
||||||
|
await persistFeatureRequestsDb();
|
||||||
|
|
||||||
|
log('Feature request status updated', { id, status, adminId: session.userId });
|
||||||
|
sendJson(res, 200, { ok: true, status });
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(res, 400, { error: error.message || 'Unable to update status' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFeatureRequestDelete(req, res, id) {
|
||||||
|
const session = getAdminSession(req);
|
||||||
|
if (!session) {
|
||||||
|
return sendJson(res, 403, { error: 'Admin access required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = featureRequestsDb.findIndex(fr => fr.id === id);
|
||||||
|
if (index === -1) {
|
||||||
|
return sendJson(res, 404, { error: 'Feature request not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
featureRequestsDb.splice(index, 1);
|
||||||
|
await persistFeatureRequestsDb();
|
||||||
|
|
||||||
|
log('Feature request deleted', { id, adminId: session.userId });
|
||||||
|
sendJson(res, 200, { ok: true });
|
||||||
|
}
|
||||||
|
|
||||||
async function handleContactMessagesList(req, res) {
|
async function handleContactMessagesList(req, res) {
|
||||||
const session = getAdminSession(req);
|
const session = getAdminSession(req);
|
||||||
|
|
||||||
@@ -17369,6 +17476,14 @@ async function routeInternal(req, res, url, pathname) {
|
|||||||
if (req.method === 'POST' && pathname === '/api/feature-requests') return handleFeatureRequestCreate(req, res);
|
if (req.method === 'POST' && pathname === '/api/feature-requests') return handleFeatureRequestCreate(req, res);
|
||||||
const featureUpvoteMatch = pathname.match(/^\/api\/feature-requests\/([a-f0-9\-]+)\/upvote$/i);
|
const featureUpvoteMatch = pathname.match(/^\/api\/feature-requests\/([a-f0-9\-]+)\/upvote$/i);
|
||||||
if (req.method === 'POST' && featureUpvoteMatch) return handleFeatureRequestUpvote(req, res, featureUpvoteMatch[1]);
|
if (req.method === 'POST' && featureUpvoteMatch) return handleFeatureRequestUpvote(req, res, featureUpvoteMatch[1]);
|
||||||
|
// Admin feature request endpoints
|
||||||
|
if (req.method === 'GET' && pathname === '/api/admin/feature-requests') return handleAdminFeatureRequestsList(req, res);
|
||||||
|
const featureRequestReplyMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)\/reply$/i);
|
||||||
|
if (req.method === 'POST' && featureRequestReplyMatch) return handleFeatureRequestReply(req, res, featureRequestReplyMatch[1]);
|
||||||
|
const featureRequestStatusMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)\/status$/i);
|
||||||
|
if (req.method === 'POST' && featureRequestStatusMatch) return handleFeatureRequestUpdateStatus(req, res, featureRequestStatusMatch[1]);
|
||||||
|
const featureRequestDeleteMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)$/i);
|
||||||
|
if (req.method === 'DELETE' && featureRequestDeleteMatch) return handleFeatureRequestDelete(req, res, featureRequestDeleteMatch[1]);
|
||||||
if (req.method === 'POST' && pathname === '/api/contact') return handleContactMessageCreate(req, res);
|
if (req.method === 'POST' && pathname === '/api/contact') return handleContactMessageCreate(req, res);
|
||||||
const contactMessagesMatch = pathname.match(/^\/api\/contact\/messages$/i);
|
const contactMessagesMatch = pathname.match(/^\/api\/contact\/messages$/i);
|
||||||
if (req.method === 'GET' && contactMessagesMatch) return handleContactMessagesList(req, res);
|
if (req.method === 'GET' && contactMessagesMatch) return handleContactMessagesList(req, res);
|
||||||
|
|||||||
Reference in New Issue
Block a user