setIsMenuOpen(false)}>{t('home')}
- {user && (user.plugin_count > 0 || user.role === 'admin') && (
+ {user && (user.plugin_count > 0 || user.role === 'admin' || user.role === 'moderator') && (
setIsMenuOpen(false)}>{t('dashboard')}
)}
{user && setIsMenuOpen(false)}>Publish}
- {user && user.role === 'admin' && setIsMenuOpen(false)}>Admin}
+ {user && (user.role === 'admin' || user.role === 'moderator') && setIsMenuOpen(false)}>Staff}
{!user && (
diff --git a/src/pages/admin/AdminPanel.css b/src/pages/admin/AdminPanel.css
index 2ad5a89..b0c3c2b 100644
--- a/src/pages/admin/AdminPanel.css
+++ b/src/pages/admin/AdminPanel.css
@@ -154,15 +154,162 @@ select.role-badge option {
transition: all 0.2s;
}
-.btn-delete:hover {
- background: #ff4d4d;
+.action-buttons {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.btn-view {
+ background: rgba(79, 126, 255, 0.1);
+ color: #4f7eff;
+ border: 1px solid rgba(79, 126, 255, 0.2);
+ padding: 4px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.8rem;
+ transition: all 0.2s;
+}
+
+.btn-view:hover {
+ background: #4f7eff;
color: white;
}
-.admin-loading {
- text-align: center;
- padding: 4rem;
- opacity: 0.6;
+/* Modal Styles */
+.admin-modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ backdrop-filter: blur(4px);
+}
+
+.admin-modal {
+ background: #1a1a1a;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 12px;
+ width: 90%;
+ max-width: 600px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
+ animation: modalAppear 0.3s ease-out;
+}
+
+@keyframes modalAppear {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.modal-header h2 {
+ margin: 0;
+ font-size: 1.25rem;
+ color: var(--primary, #4f7eff);
+}
+
+.close-modal {
+ background: none;
+ border: none;
+ color: #888;
+ font-size: 1.5rem;
+ cursor: pointer;
+ line-height: 1;
+}
+
+.modal-body {
+ padding: 1.5rem;
+}
+
+.details-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+}
+
+.detail-item {
+ font-size: 0.95rem;
+ color: #ccc;
+}
+
+.detail-item strong {
+ display: block;
+ color: #888;
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.05em;
+ margin-bottom: 0.25rem;
+}
+
+.detail-item.full-width {
+ grid-column: 1 / -1;
+}
+
+@media (max-width: 768px) {
+ .details-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.admin-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.admin-search {
+ flex: 1;
+ min-width: 250px;
+}
+
+.admin-search input {
+ width: 100%;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: white;
+ padding: 10px 16px;
+ border-radius: 8px;
+ font-family: inherit;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.admin-search input:focus {
+ border-color: var(--primary, #4f7eff);
+}
+
+.admin-filters {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.admin-filters select {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: white;
+ padding: 8px 12px;
+ border-radius: 8px;
+ font-family: inherit;
+ outline: none;
+ cursor: pointer;
+}
+
+.admin-filters select:focus {
+ border-color: var(--primary, #4f7eff);
}
@media (max-width: 768px) {
diff --git a/src/pages/admin/AdminPanel.tsx b/src/pages/admin/AdminPanel.tsx
index 391494a..d9ffd7c 100644
--- a/src/pages/admin/AdminPanel.tsx
+++ b/src/pages/admin/AdminPanel.tsx
@@ -1,8 +1,12 @@
import { useState, useEffect } from 'react';
import api from '../../api';
+import { useAuth } from '../../App';
import './AdminPanel.css';
const AdminPanel = () => {
+ const { user: currentUser } = useAuth();
+ const isAdmin = currentUser?.role === 'admin';
+
const [activeTab, setActiveTab] = useState<'users' | 'plugins' | 'logs'>('users');
const [users, setUsers] = useState
([]);
const [plugins, setPlugins] = useState([]);
@@ -10,8 +14,17 @@ const AdminPanel = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ // Search and Filter state
+ const [searchQuery, setSearchQuery] = useState('');
+ const [logFilterAction, setLogFilterAction] = useState('all');
+ const [logFilterModerator, setLogFilterModerator] = useState('all');
+
+ // Details Modal state
+ const [selectedItem, setSelectedItem] = useState<{type: 'user' | 'plugin' | 'log', data: any} | null>(null);
+
useEffect(() => {
fetchData();
+ setSearchQuery(''); // Reset search on tab change
}, [activeTab]);
const fetchData = async () => {
@@ -50,10 +63,9 @@ const AdminPanel = () => {
};
const changeRole = async (id: number, newRole: string) => {
- const reason = window.prompt(`Enter reason for changing role to ${newRole}:`);
- if (!reason) return;
+ const reason = window.prompt(`Enter reason for changing role to ${newRole} (Optional):`);
try {
- await api.put(`/admin/users/${id}/role`, { role: newRole, reason });
+ await api.put(`/admin/users/${id}/role`, { role: newRole, reason: reason || undefined });
setUsers(users.map(u => u.id === id ? { ...u, role: newRole } : u));
alert('Role updated successfully');
} catch (err: any) {
@@ -72,10 +84,40 @@ const AdminPanel = () => {
}
};
+ // Filtering logic
+ const filteredUsers = users.filter(u =>
+ u.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ u.email?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ u.id.toString().includes(searchQuery)
+ );
+
+ const filteredPlugins = plugins.filter(p =>
+ p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ p.dev_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ p.id.toString().includes(searchQuery)
+ );
+
+ const filteredLogs = logs.filter(l => {
+ const matchesSearch =
+ l.moderator_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ l.action.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ l.target_type.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ l.reason?.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ l.target_id.toString().includes(searchQuery);
+
+ const matchesAction = logFilterAction === 'all' || l.action === logFilterAction;
+ const matchesModerator = logFilterModerator === 'all' || l.moderator_name === logFilterModerator;
+
+ return matchesSearch && matchesAction && matchesModerator;
+ });
+
+ // Unique moderators for the filter dropdown
+ const uniqueModerators = Array.from(new Set(logs.map(l => l.moderator_name)));
+
return (
- Admin Panel
+ Staff Panel
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+ {activeTab === 'logs' && (
+
+
+
+
+ )}
+
+
{error &&
{error}
}
@@ -111,33 +179,38 @@ const AdminPanel = () => {
Username |
Email |
Role |
- Stripe |
Status |
Actions |
- {users.map(u => (
+ {filteredUsers.map(u => (
| {u.id} |
{u.username} |
{u.email} |
-
+ {isAdmin ? (
+
+ ) : (
+ {u.role}
+ )}
|
- {u.stripe_ready ? '✅' : '❌'} |
{u.deleted_at ? Deleted : Active} |
- {!u.deleted_at && }
+
+
+ {!u.deleted_at && }
+
|
))}
@@ -157,7 +230,7 @@ const AdminPanel = () => {
- {plugins.map(p => (
+ {filteredPlugins.map(p => (
| {p.id} |
{p.name} |
@@ -166,7 +239,10 @@ const AdminPanel = () => {
{p.dev_name} |
{p.deleted_at ? Deleted : Active} |
- {!p.deleted_at && }
+
+
+ {!p.deleted_at && }
+
|
))}
@@ -180,23 +256,74 @@ const AdminPanel = () => {
Moderator |
Action |
Target |
- Reason |
+ Actions |
- {logs.map(l => (
+ {filteredLogs.map(l => (
| {new Date(l.created_at).toLocaleString()} |
{l.moderator_name} |
{l.action.toUpperCase()} |
{l.target_type} ({l.target_id}) |
- {l.reason || 'No reason provided'} |
+
+
+ |
))}
)}
+
+ {selectedItem && (
+
setSelectedItem(null)}>
+
e.stopPropagation()}>
+
+ {selectedItem.type.toUpperCase()} Details
+
+
+
+ {selectedItem.type === 'user' && (
+
+
ID: {selectedItem.data.id}
+
Username: {selectedItem.data.username}
+
Email: {selectedItem.data.email}
+
Role: {selectedItem.data.role}
+
Stripe Ready: {selectedItem.data.stripe_ready ? 'Yes' : 'No'}
+
2FA Enabled: {selectedItem.data.totp_enabled ? 'Yes' : 'No'}
+
Created: {new Date(selectedItem.data.created_at).toLocaleString()}
+
Status: {selectedItem.data.deleted_at ? `Deleted at ${new Date(selectedItem.data.deleted_at).toLocaleString()}` : 'Active'}
+
+ )}
+ {selectedItem.type === 'plugin' && (
+
+
ID: {selectedItem.data.id}
+
Name: {selectedItem.data.name}
+
Developer: {selectedItem.data.dev_name}
+
Type: {selectedItem.data.type}
+
Category: {selectedItem.data.category}
+
Price: €{(selectedItem.data.price_cents / 100).toFixed(2)}
+
Downloads: {selectedItem.data.downloads}
+
Views: {selectedItem.data.views}
+
Created: {new Date(selectedItem.data.created_at).toLocaleString()}
+
Status: {selectedItem.data.deleted_at ? `Deleted at ${new Date(selectedItem.data.deleted_at).toLocaleString()}` : 'Active'}
+
+ )}
+ {selectedItem.type === 'log' && (
+
+
Date: {new Date(selectedItem.data.created_at).toLocaleString()}
+
Moderator: {selectedItem.data.moderator_name}
+
Action: {selectedItem.data.action}
+
Target Type: {selectedItem.data.target_type}
+
Target ID: {selectedItem.data.target_id}
+
Reason: {selectedItem.data.reason || 'No reason provided'}
+
+ )}
+
+
+
+ )}
);
};