From 2c783bdb703e4ad69c9f3f846c2c9e6a527ccc80 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 25 Mar 2025 17:23:30 -0400 Subject: Implemented admin dashboard --- web/src/app/components/Card.jsx | 7 + web/src/app/components/Navbar.jsx | 83 +++++++++- web/src/app/components/sign-out.jsx | 15 +- web/src/app/dashboard/page.js | 44 ----- web/src/app/guilds/page.js | 314 ++++++++++++++++++++++++++++++++++++ web/src/app/layout.js | 3 + web/src/app/page.js | 162 +++++++++++++++---- web/src/app/quotes/page.js | 247 ++++++++++++++++++++++++++++ 8 files changed, 792 insertions(+), 83 deletions(-) create mode 100644 web/src/app/components/Card.jsx delete mode 100644 web/src/app/dashboard/page.js create mode 100644 web/src/app/guilds/page.js create mode 100644 web/src/app/quotes/page.js (limited to 'web/src/app') diff --git a/web/src/app/components/Card.jsx b/web/src/app/components/Card.jsx new file mode 100644 index 0000000..e6d55fd --- /dev/null +++ b/web/src/app/components/Card.jsx @@ -0,0 +1,7 @@ +export default function Card({children}) { + return ( +
+ {children} +
+ ) +} diff --git a/web/src/app/components/Navbar.jsx b/web/src/app/components/Navbar.jsx index 242175c..f07718c 100644 --- a/web/src/app/components/Navbar.jsx +++ b/web/src/app/components/Navbar.jsx @@ -1,25 +1,94 @@ +'use client'; +import { useState, useEffect } from 'react'; import SignOut from "@/app/components/sign-out"; +import Link from "next/link"; +import { fetchWithAuth } from '@/utils/api'; export default function Navbar() { + const [versionInfo, setVersionInfo] = useState({ + ab_version: 'Loading...', + api_version: 'Loading...' + }); + const [uptime, setUptime] = useState('Loading...'); + + useEffect(() => { + // Get the API URL + const apiUrl = localStorage.getItem('apiUrl'); + if (!apiUrl) return; + + // Fetch version information - no auth required + fetch(`${apiUrl}/api/version`) + .then(response => response.json()) + .then(data => { + setVersionInfo({ + ab_version: data.ab_version, + api_version: data.api_version + }); + }) + .catch(error => { + console.error('Failed to fetch version info:', error); + setVersionInfo({ + ab_version: 'Error', + api_version: 'Error' + }); + }); + + // Fetch uptime information - no auth required + fetch(`${apiUrl}/api/uptime`) + .then(response => response.json()) + .then(data => { + // Convert milliseconds to a readable format + const ms = data.uptime; + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + let uptimeText = ''; + if (days > 0) { + uptimeText = `${days} day${days !== 1 ? 's' : ''}`; + } else if (hours > 0) { + uptimeText = `${hours} hour${hours !== 1 ? 's' : ''}`; + } else if (minutes > 0) { + uptimeText = `${minutes} minute${minutes !== 1 ? 's' : ''}`; + } else { + uptimeText = `${seconds} second${seconds !== 1 ? 's' : ''}`; + } + + setUptime(uptimeText); + }) + .catch(error => { + console.error('Failed to fetch uptime:', error); + setUptime('Error'); + }); + }, []); + return ( - ) + ); } diff --git a/web/src/app/components/sign-out.jsx b/web/src/app/components/sign-out.jsx index dd6693d..b0762d8 100644 --- a/web/src/app/components/sign-out.jsx +++ b/web/src/app/components/sign-out.jsx @@ -1,5 +1,16 @@ +'use client'; +import { useAuth } from '@/context/middleware'; + export default function SignOut() { + const { logout } = useAuth(); + return ( - - ) + + ); } diff --git a/web/src/app/dashboard/page.js b/web/src/app/dashboard/page.js deleted file mode 100644 index a252958..0000000 --- a/web/src/app/dashboard/page.js +++ /dev/null @@ -1,44 +0,0 @@ -import Navbar from "@/app/components/Navbar"; - -export default function Dashboard() { - return ( - <> - -
-

Guilds

-
-
-

Server 1

-

ID: 23893249843983489 - Members: 30

- Leave {/* Add an "are you sure prompt" */} -
-
-

Server 2

-

ID: 23893249843983489 - Members: 30

- Leave -
-
-

Server 3

-

ID: 23893249843983489 - Members: 30

- Leave -
-
-

Server 4

-

ID: 23893249843983489 - Members: 30

- Leave -
-
-

Server 5

-

ID: 23893249843983489 - Members: 30

- Leave -
-
-

Server 6

-

ID: 23893249843983489 - Members: 30

- Leave -
-
-
- - ); -} diff --git a/web/src/app/guilds/page.js b/web/src/app/guilds/page.js new file mode 100644 index 0000000..23ec00d --- /dev/null +++ b/web/src/app/guilds/page.js @@ -0,0 +1,314 @@ +'use client'; +import { useState, useEffect } from "react"; +import { fetchWithAuth } from "@/utils/api"; +import Link from "next/link"; +import Card from "@/app/components/Card"; +import Navbar from "@/app/components/Navbar"; + +export default function Guilds() { + const [guilds, setGuilds] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [clientId, setClientId] = useState(null); + const [selectedGuild, setSelectedGuild] = useState(null); + const [showModal, setShowModal] = useState(false); + const [formData, setFormData] = useState({ + guildID: '', + logChannelID: '', + suggestionsChannelID: '', + qotdChannelID: '', + qotdToggle: false, + ollamaEnabled: false + }); + const [formMessage, setFormMessage] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + // Fetch guilds + const guildsResponse = await fetchWithAuth('/api/servers'); + if (!guildsResponse.ok) { + throw new Error('Failed to fetch guilds'); + } + const guildsData = await guildsResponse.json(); + setGuilds(guildsData); + + // Fetch client ID + const apiUrl = localStorage.getItem('apiUrl') || ''; + const versionResponse = await fetch(`${apiUrl}/api/version`); + if (!versionResponse.ok) { + throw new Error('Failed to fetch version data'); + } + const versionData = await versionResponse.json(); + setClientId(versionData.client_id); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const handleLeaveServer = async (serverId) => { + if (window.confirm('Are you sure you want to leave this server?')) { + try { + const response = await fetchWithAuth(`/api/leave`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: serverId + }) + }); + + if (!response.ok) { + throw new Error('Failed to leave server'); + } + + // Remove server from list + setGuilds(guilds.filter(guild => guild.id !== serverId)); + + // Close modal if the guild was selected + if (selectedGuild === serverId) { + closeModal(); + } + } catch (err) { + setError(err.message); + } + } + }; + + const handleGuildSelect = async (guildId) => { + try { + setSelectedGuild(guildId); + const response = await fetchWithAuth(`/api/settings/guilds/${guildId}`); + + if (!response.ok) { + throw new Error('Failed to fetch guild settings'); + } + + const data = await response.json(); + setFormData({ + guildID: data.settings.guildID || '', + logChannelID: data.settings.logChannelID || '', + suggestionsChannelID: data.settings.suggestionsChannelID || '', + qotdChannelID: data.settings.qotdChannelID || '', + qotdToggle: data.settings.qotdToggle || false, + ollamaEnabled: data.settings.ollamaEnabled || false + }); + setShowModal(true); + } catch (err) { + setError(err.message); + setFormData({ + guildID: guildId, + logChannelID: '', + suggestionsChannelID: '', + qotdChannelID: '', + qotdToggle: false, + ollamaEnabled: false + }); + setShowModal(true); + } + }; + + const handleInputChange = (e) => { + const { name, value, type, checked } = e.target; + setFormData({ + ...formData, + [name]: type === 'checkbox' ? checked : value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setFormMessage(null); + + try { + const response = await fetchWithAuth(`/api/settings/guilds/${selectedGuild}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (!response.ok) { + throw new Error('Failed to update guild settings'); + } + + setFormMessage({ + type: 'success', + text: 'Settings updated successfully' + }); + + // Clear message after 3 seconds + setTimeout(() => { + setFormMessage(null); + }, 3000); + } catch (err) { + setFormMessage({ + type: 'error', + text: err.message + }); + } + }; + + const closeModal = () => { + setShowModal(false); + setSelectedGuild(null); + setFormMessage(null); + }; + + return ( + <> + +
+

Guilds

+ + {loading &&

Loading guilds...

} + {error &&

Error: {error}

} + + {!loading && guilds.length === 0 && ( +

No guilds found. Invite AleeBot to your server.

+ )} + +
+ {guilds.map(guild => ( + +

{guild.name}

+

ID: {guild.id} - Members: {guild.members || 'N/A'}

+
+ +
+
+ ))} +
+ +
+ + Invite + +
+
+ + {/* Settings Modal */} + {showModal && ( +
+
+
+

+ Guild Settings: {guilds.find(g => g.id === selectedGuild)?.name} +

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {formMessage && ( +
+ {formMessage.text} +
+ )} + +
+ + +
+
+
+
+ )} + + ); +} diff --git a/web/src/app/layout.js b/web/src/app/layout.js index bd8d805..2b6d521 100644 --- a/web/src/app/layout.js +++ b/web/src/app/layout.js @@ -1,5 +1,6 @@ import { Exo_2, JetBrains_Mono } from "next/font/google"; import "./globals.css"; +import { AuthProvider } from "@/context/middleware"; const exoSans = Exo_2({ variable: "--font-exo-sans", @@ -22,7 +23,9 @@ export default function RootLayout({ children }) { + {children} + ); diff --git a/web/src/app/page.js b/web/src/app/page.js index 99556e8..3b6ebd9 100644 --- a/web/src/app/page.js +++ b/web/src/app/page.js @@ -1,33 +1,135 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { fetchWithAuth } from '@/utils/api'; + export default function Home() { + const [formData, setFormData] = useState({ + username: '', + password: '', + url: '' + }); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(true); + const router = useRouter(); + + useEffect(() => { + // Check if token and API URL exist + const token = localStorage.getItem('token'); + const apiUrl = localStorage.getItem('apiUrl'); + + if (token && apiUrl) { + // Verify token is still valid + fetchWithAuth('/api/servers') + .then(response => { + if (response.ok) { + router.push('/guilds'); + } else { + // Clear invalid token + localStorage.removeItem('token'); + localStorage.removeItem('apiUrl'); + setLoading(false); + } + }) + .catch(() => { + // Error means token is invalid or other issue + localStorage.removeItem('token'); + localStorage.removeItem('apiUrl'); + setLoading(false); + }); + } else { + setLoading(false); + } + }, [router]); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const response = await fetch(`${formData.url}/api/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: formData.username, + password: formData.password + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Login failed'); + } + + // Save token and API URL to localStorage + localStorage.setItem('token', data.token); + localStorage.setItem('apiUrl', formData.url); + + // Redirect to guilds page + router.push('/guilds'); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( +
+

Loading...

+
+ ); + } - return ( -
-

AleeBot

-
- - - - -
-
- ); + return ( +
+

AleeBot

+ {error &&

{error}

} +
+ + + + +
+
+ ); } diff --git a/web/src/app/quotes/page.js b/web/src/app/quotes/page.js new file mode 100644 index 0000000..27ae439 --- /dev/null +++ b/web/src/app/quotes/page.js @@ -0,0 +1,247 @@ +'use client'; +import { useState, useEffect } from "react"; +import Card from "@/app/components/Card"; +import Navbar from "@/app/components/Navbar"; +import { fetchWithAuth } from "@/utils/api"; + +export default function Quotes() { + const [pendingQuotes, setPendingQuotes] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [message, setMessage] = useState(null); + const [formData, setFormData] = useState({ + author: '', + authorImage: '', + quote: '', + year: '', + submitterID: '' + }); + + useEffect(() => { + fetchPendingQuotes(); + }, []); + + const fetchPendingQuotes = async () => { + try { + setLoading(true); + const response = await fetchWithAuth('/api/quotes/pending'); + + if (!response.ok) { + throw new Error('Failed to fetch pending quotes'); + } + + const data = await response.json(); + setPendingQuotes(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setMessage(null); + + try { + const response = await fetchWithAuth('/api/quotes/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (!response.ok) { + throw new Error('Failed to submit quote'); + } + + setMessage({ + type: 'success', + text: 'Quote submitted successfully' + }); + + // Reset form + setFormData({ + author: '', + authorImage: '', + quote: '', + year: '', + submitterID: '' + }); + } catch (err) { + setMessage({ + type: 'error', + text: err.message + }); + } + }; + + const handleApproveQuote = async (id) => { + if (window.confirm('Are you sure you want to approve this quote?')) { + try { + const response = await fetchWithAuth('/api/quotes/approve', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id }) + }); + + if (!response.ok) { + throw new Error('Failed to approve quote'); + } + + setMessage({ + type: 'success', + text: 'Quote approved successfully' + }); + + // Refresh quotes + fetchPendingQuotes(); + } catch (err) { + setMessage({ + type: 'error', + text: err.message + }); + } + } + }; + + const handleRejectQuote = async (id) => { + if (window.confirm('Are you sure you want to reject this quote?')) { + try { + const response = await fetchWithAuth('/api/quotes/reject', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id }) + }); + + if (!response.ok) { + throw new Error('Failed to reject quote'); + } + + setMessage({ + type: 'success', + text: 'Quote rejected successfully' + }); + + // Refresh quotes + fetchPendingQuotes(); + } catch (err) { + setMessage({ + type: 'error', + text: err.message + }); + } + } + }; + + return ( + <> + +
+

Submit New Quote

+
+ + +