diff --git a/public/admin.html b/public/admin.html
index ed2731a..73bf0f9 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -184,7 +184,7 @@
gap: 20px;
margin-bottom: 20px;
}
-
+
.stat-card {
background: var(--card-bg);
color: var(--text-color);
@@ -193,19 +193,123 @@
box-shadow: 0 2px 4px var(--shadow);
text-align: center;
}
-
+
.stat-number {
font-size: 2em;
font-weight: bold;
color: #007bff;
}
-
+
.address-cell {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+
+ /* Tab Navigation */
+ .tab-navigation {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ border-bottom: 2px solid #ddd;
+ }
+
+ .tab-btn {
+ padding: 12px 20px;
+ background: none;
+ border: none;
+ border-bottom: 3px solid transparent;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: 500;
+ color: #666;
+ transition: all 0.3s ease;
+ }
+
+ .tab-btn.active {
+ color: #007bff;
+ border-bottom-color: #007bff;
+ background: rgba(0, 123, 255, 0.1);
+ }
+
+ .tab-btn:hover {
+ color: #007bff;
+ background: rgba(0, 123, 255, 0.05);
+ }
+
+ .tab-content {
+ display: none;
+ }
+
+ .tab-content.active {
+ display: block;
+ }
+
+ /* Profanity Management Forms */
+ .profanity-add-form,
+ .profanity-test-form {
+ background: #f8f9fa;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ }
+
+ .profanity-add-form h4,
+ .profanity-test-form h4 {
+ margin-top: 0;
+ color: #495057;
+ }
+
+ .form-row {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+
+ .form-row input,
+ .form-row select {
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+ }
+
+ .form-row input[type="text"] {
+ flex: 1;
+ min-width: 200px;
+ }
+
+ .form-row select {
+ min-width: 140px;
+ }
+
+ .test-results {
+ margin-top: 15px;
+ padding: 15px;
+ border-radius: 4px;
+ min-height: 40px;
+ font-family: monospace;
+ }
+
+ .test-results.clean {
+ background: #d4edda;
+ border: 1px solid #c3e6cb;
+ color: #155724;
+ }
+
+ .test-results.profane {
+ background: #f8d7da;
+ border: 1px solid #f5c6cb;
+ color: #721c24;
+ }
+
+ .test-results.empty {
+ background: #e2e3e5;
+ border: 1px solid #d6d8db;
+ color: #6c757d;
+ }
/* Mobile responsiveness for admin panel */
@media (max-width: 768px) {
@@ -366,26 +470,90 @@
-
diff --git a/public/admin.js b/public/admin.js
index 66c8c19..75337a7 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -71,6 +71,53 @@ document.addEventListener('DOMContentLoaded', () => {
loadLocations();
});
+ // Tab navigation logic
+ const tabs = document.querySelectorAll('.tab-btn');
+ const contents = document.querySelectorAll('.tab-content');
+
+ tabs.forEach(tab => {
+ tab.addEventListener('click', function () {
+ tabs.forEach(t => t.classList.remove('active'));
+ contents.forEach(c => c.classList.remove('active'));
+ this.classList.add('active');
+ document.querySelector(`#${this.dataset.tab}-tab`).classList.add('active');
+
+ // Load data for the active tab
+ if (this.dataset.tab === 'profanity') {
+ loadProfanityWords();
+ }
+ });
+ });
+
+ // Profanity management handlers
+ const addProfanityForm = document.getElementById('add-profanity-form');
+ const testProfanityForm = document.getElementById('test-profanity-form');
+ const profanityTableBody = document.getElementById('profanity-tbody');
+
+ if (addProfanityForm) {
+ addProfanityForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const word = document.getElementById('new-word').value.trim();
+ const severity = document.getElementById('new-severity').value;
+ const category = document.getElementById('new-category').value.trim() || 'custom';
+
+ if (!word) return;
+
+ await addProfanityWord(word, severity, category);
+ });
+ }
+
+ if (testProfanityForm) {
+ testProfanityForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const text = document.getElementById('test-text').value.trim();
+
+ if (!text) return;
+
+ await testProfanityFilter(text);
+ });
+ }
+
function showLoginSection() {
loginSection.style.display = 'block';
adminSection.style.display = 'none';
@@ -462,6 +509,158 @@ document.addEventListener('DOMContentLoaded', () => {
return reportTime.toLocaleDateString();
}
+ // Profanity management functions
+ async function loadProfanityWords() {
+ if (!profanityTableBody) return;
+
+ try {
+ const response = await fetch('/api/admin/profanity/words', {
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+
+ if (response.status === 401) {
+ logout('Session expired. Please log in again.');
+ return;
+ }
+
+ const data = await response.json();
+
+ if (response.ok) {
+ displayProfanityWords(data.words || []);
+ } else {
+ console.error('Failed to load profanity words:', data.error);
+ profanityTableBody.innerHTML = 'Failed to load words |
';
+ }
+ } catch (error) {
+ console.error('Error loading profanity words:', error);
+ profanityTableBody.innerHTML = 'Error loading words |
';
+ }
+ }
+
+ function displayProfanityWords(words) {
+ if (!profanityTableBody) return;
+
+ if (words.length === 0) {
+ profanityTableBody.innerHTML = 'No custom words added yet |
';
+ return;
+ }
+
+ profanityTableBody.innerHTML = words.map(word => `
+
+ ${word.id} |
+ ${escapeHtml(word.word)} |
+ ${word.severity} |
+ ${word.category || 'N/A'} |
+ ${new Date(word.created_at).toLocaleDateString()} |
+
+
+ |
+
+ `).join('');
+ }
+
+ async function addProfanityWord(word, severity, category) {
+ try {
+ const response = await fetch('/api/admin/profanity/words', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ },
+ body: JSON.stringify({ word, severity, category })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ // Clear form
+ document.getElementById('new-word').value = '';
+ document.getElementById('new-category').value = 'custom';
+ document.getElementById('new-severity').value = 'medium';
+
+ // Reload words list
+ loadProfanityWords();
+
+ console.log('Word added successfully');
+ } else {
+ alert('Failed to add word: ' + data.error);
+ }
+ } catch (error) {
+ console.error('Error adding profanity word:', error);
+ alert('Error adding word. Please try again.');
+ }
+ }
+
+ async function testProfanityFilter(text) {
+ const resultsDiv = document.getElementById('test-results');
+ if (!resultsDiv) return;
+
+ try {
+ const response = await fetch('/api/admin/profanity/test', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`
+ },
+ body: JSON.stringify({ text })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ if (data.isProfane) {
+ resultsDiv.className = 'test-results profane';
+ resultsDiv.innerHTML = `
+ ⚠️ Profanity Detected!
+ Detected words: ${data.detectedWords.map(w => `${escapeHtml(w)}
`).join(', ')}
+ Filtered text: "${escapeHtml(data.filteredText)}"
+ `;
+ } else {
+ resultsDiv.className = 'test-results clean';
+ resultsDiv.innerHTML = '✅ Text is clean!
No profanity detected.';
+ }
+ } else {
+ resultsDiv.className = 'test-results empty';
+ resultsDiv.innerHTML = 'Error testing text: ' + data.error;
+ }
+ } catch (error) {
+ console.error('Error testing profanity filter:', error);
+ resultsDiv.className = 'test-results empty';
+ resultsDiv.innerHTML = 'Error testing text. Please try again.';
+ }
+ }
+
+ // Make deleteProfanityWord available globally for onclick handlers
+ window.deleteProfanityWord = async function(wordId) {
+ if (!confirm('Are you sure you want to delete this word?')) return;
+
+ try {
+ const response = await fetch(`/api/admin/profanity/words/${wordId}`, {
+ method: 'DELETE',
+ headers: { 'Authorization': `Bearer ${authToken}` }
+ });
+
+ if (response.ok) {
+ loadProfanityWords();
+ console.log('Word deleted successfully');
+ } else {
+ const data = await response.json();
+ alert('Failed to delete word: ' + data.error);
+ }
+ } catch (error) {
+ console.error('Error deleting profanity word:', error);
+ alert('Error deleting word. Please try again.');
+ }
+ };
+
+ function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
// Initialize theme toggle
initializeTheme();
});