aboutsummaryrefslogtreecommitdiff
path: root/web/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/app')
-rw-r--r--web/src/app/components/Card.jsx7
-rw-r--r--web/src/app/components/Navbar.jsx83
-rw-r--r--web/src/app/components/sign-out.jsx15
-rw-r--r--web/src/app/dashboard/page.js44
-rw-r--r--web/src/app/guilds/page.js314
-rw-r--r--web/src/app/layout.js3
-rw-r--r--web/src/app/page.js162
-rw-r--r--web/src/app/quotes/page.js247
8 files changed, 792 insertions, 83 deletions
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 (
+ <div className="p-4 bg-gray-900 rounded-md">
+ {children}
+ </div>
+ )
+}
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 (
<nav className="bg-gray-900 text-white">
<div className="max-w-screen-xl flex items-center justify-between mx-auto p-4">
<div className="flex items-center space-x-4">
<h1 className="text-xl font-medium">AleeBot</h1>
<ul>
- <li className="inline-block mx-2">Guilds</li>
- <li className="inline-block mx-2">Quotes</li>
- <li className="inline-block mx-2">Settings</li>
+ {[
+ ['Guilds', '/guilds'],
+ ['Quotes', '/quotes'],
+ ].map(([title, url]) => (
+ <li key={title} className="inline-block mx-2">
+ <Link href={url}>
+ {title}
+ </Link>
+ </li>
+ ))}
</ul>
</div>
<div className="flex items-center space-x-4">
- <span>Uptime: 1 day</span>
- <span>API v(version)</span>
- <span>4.0.0 Beta</span>
+ <span>Uptime: {uptime}</span>
+ <span>API v{versionInfo.api_version}</span>
+ <span>{versionInfo.ab_version}</span>
<SignOut />
</div>
</div>
</nav>
- )
+ );
}
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 (
- <button type="submit" className="py-2 px-4 rounded-md text-md bg-red-700 hover:bg-red-500">Sign out</button>
- )
+ <button
+ type="button"
+ onClick={logout}
+ className="py-2 px-4 rounded-md text-md bg-red-700 hover:bg-red-500"
+ >
+ Sign out
+ </button>
+ );
}
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 (
- <>
- <Navbar />
- <div className="flex flex-col gap-4 p-12">
- <h1 className="text-3xl">Guilds</h1>
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
- <div className="p-4 bg-gray-900 rounded-md">
- <h2 className="text-lg font-medium">Server 1</h2>
- <p>ID: 23893249843983489 - Members: 30</p>
- <span>Leave</span> {/* Add an "are you sure prompt" */}
- </div>
- <div className="p-4 bg-gray-900 rounded-md">
- <h2 className="text-lg font-medium">Server 2</h2>
- <p>ID: 23893249843983489 - Members: 30</p>
- <span>Leave</span>
- </div>
- <div className="p-4 bg-gray-900 rounded-md">
- <h2 className="text-lg font-medium">Server 3</h2>
- <p>ID: 23893249843983489 - Members: 30</p>
- <span>Leave</span>
- </div>
- <div className="p-4 bg-gray-900 rounded-md">
- <h2 className="text-lg font-medium">Server 4</h2>
- <p>ID: 23893249843983489 - Members: 30</p>
- <span>Leave</span>
- </div>
- <div className="p-4 bg-gray-900 rounded-md">
- <h2 className="text-lg font-medium">Server 5</h2>
- <p>ID: 23893249843983489 - Members: 30</p>
- <span>Leave</span>
- </div>
- <div className="p-4 bg-gray-900 rounded-md">
- <h2 className="text-lg font-medium">Server 6</h2>
- <p>ID: 23893249843983489 - Members: 30</p>
- <span>Leave</span>
- </div>
- </div>
- </div>
- </>
- );
-}
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 (
+ <>
+ <Navbar />
+ <div className="flex flex-col gap-4 p-12">
+ <h1 className="text-3xl">Guilds</h1>
+
+ {loading && <p>Loading guilds...</p>}
+ {error && <p className="text-red-500">Error: {error}</p>}
+
+ {!loading && guilds.length === 0 && (
+ <p>No guilds found. Invite AleeBot to your server.</p>
+ )}
+
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
+ {guilds.map(guild => (
+ <Card key={guild.id}>
+ <h2 className="text-lg font-medium">{guild.name}</h2>
+ <p>ID: {guild.id} - Members: {guild.members || 'N/A'}</p>
+ <div className="flex justify-between mt-2"> <button
+ onClick={() => handleGuildSelect(guild.id)}
+ className="text-blue-500 hover:text-blue-700 cursor-pointer"
+ >
+ Settings
+ </button>
+ <button
+ onClick={() => handleLeaveServer(guild.id)}
+ className="text-red-500 hover:text-red-700 cursor-pointer"
+ >
+ Leave
+ </button>
+ </div>
+ </Card>
+ ))}
+ </div>
+
+ <div className="flex gap-4 mt-4">
+ <Link
+ href={clientId ? `https://discord.com/oauth2/authorize?client_id=${clientId}&integration_type=0&scope=bot+applications.commands` : "#"}
+ className={`py-2 px-4 rounded-md text-md ${clientId ? 'bg-blue-600 hover:bg-blue-500' : 'bg-gray-400 cursor-not-allowed'}`}
+ >
+ Invite
+ </Link>
+ </div>
+ </div>
+
+ {/* Settings Modal */}
+ {showModal && (
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+ <div className="bg-gray-800 p-6 rounded-lg w-full max-w-md mx-4">
+ <div className="flex justify-between items-center mb-4">
+ <h2 className="text-xl font-semibold">
+ Guild Settings: {guilds.find(g => g.id === selectedGuild)?.name}
+ </h2>
+ <button
+ onClick={closeModal}
+ className="text-gray-400 hover:text-white"
+ >
+ ✕
+ </button>
+ </div>
+
+ <form className="flex flex-col gap-4" onSubmit={handleSubmit}>
+ <div className="mb-2">
+ <label className="block text-sm text-gray-400 mb-1">Log Channel ID</label>
+ <input
+ name="logChannelID"
+ type="text"
+ placeholder="Enter channel ID"
+ value={formData.logChannelID}
+ onChange={handleInputChange}
+ className="w-full bg-gray-700 rounded p-2"
+ />
+ </div>
+
+ <div className="mb-2">
+ <label className="block text-sm text-gray-400 mb-1">Suggestions Channel ID</label>
+ <input
+ name="suggestionsChannelID"
+ type="text"
+ placeholder="Enter channel ID"
+ value={formData.suggestionsChannelID}
+ onChange={handleInputChange}
+ className="w-full bg-gray-700 rounded p-2"
+ />
+ </div>
+
+ <div className="mb-2">
+ <label className="block text-sm text-gray-400 mb-1">QOTD Channel ID</label>
+ <input
+ name="qotdChannelID"
+ type="text"
+ placeholder="Enter channel ID"
+ value={formData.qotdChannelID}
+ onChange={handleInputChange}
+ className="w-full bg-gray-700 rounded p-2"
+ />
+ </div>
+
+ <div className="flex items-center mb-2">
+ <input
+ id="qotdToggle"
+ name="qotdToggle"
+ type="checkbox"
+ checked={formData.qotdToggle}
+ onChange={handleInputChange}
+ className="mr-2"
+ />
+ <label htmlFor="qotdToggle">QOTD Toggle</label>
+ </div>
+
+ <div className="flex items-center mb-4">
+ <input
+ id="ollamaEnabled"
+ name="ollamaEnabled"
+ type="checkbox"
+ checked={formData.ollamaEnabled}
+ onChange={handleInputChange}
+ className="mr-2"
+ />
+ <label htmlFor="ollamaEnabled">Ollama Toggle</label>
+ </div>
+
+ {formMessage && (
+ <div className={`p-2 mb-4 rounded ${formMessage.type === 'success' ? 'bg-green-800 text-green-200' : 'bg-red-800 text-red-200'}`}>
+ {formMessage.text}
+ </div>
+ )}
+
+ <div className="flex justify-end gap-2">
+ <button
+ type="button"
+ onClick={closeModal}
+ className="bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded"
+ >
+ Cancel
+ </button>
+ <button
+ type="submit"
+ className="bg-blue-600 hover:bg-blue-500 text-white py-2 px-4 rounded"
+ >
+ Save
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ )}
+ </>
+ );
+}
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 }) {
<body
className={`${exoSans.variable} ${jetbrainsMono.variable} antialiased`}
>
+ <AuthProvider>
{children}
+ </AuthProvider>
</body>
</html>
);
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 (
+ <main className="flex justify-center items-center h-screen">
+ <p>Loading...</p>
+ </main>
+ );
+ }
- return (
- <main className="flex flex-col space-y-5 justify-center items-center h-screen">
- <h1 className="text-4xl font-medium">AleeBot</h1>
- <form className="flex flex-col gap-4 w-80">
- <input
- name="username"
- type="text"
- placeholder="Username"
- required
- />
- <input
- name="password"
- type="password"
- placeholder="Password"
- required
- />
- <input
- name="apiUrl"
- type="url"
- placeholder="API URL"
- required
- />
- <button
- type="submit"
- className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded"
- >Login
- </button>
- </form>
- </main>
- );
+ return (
+ <main className="flex flex-col space-y-5 justify-center items-center h-screen">
+ <h1 className="text-4xl font-medium">AleeBot</h1>
+ {error && <p className="text-red-500">{error}</p>}
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4 w-80">
+ <input
+ name="username"
+ type="text"
+ placeholder="Username"
+ required
+ value={formData.username}
+ onChange={handleChange}
+ className="p-2 border rounded"
+ />
+ <input
+ name="password"
+ type="password"
+ placeholder="Password"
+ required
+ value={formData.password}
+ onChange={handleChange}
+ className="p-2 border rounded"
+ />
+ <input
+ name="url"
+ type="url"
+ placeholder="API URL"
+ required
+ value={formData.url}
+ onChange={handleChange}
+ className="p-2 border rounded"
+ />
+ <button
+ type="submit"
+ disabled={loading}
+ className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded disabled:bg-blue-300"
+ >
+ {loading ? 'Logging in...' : 'Login'}
+ </button>
+ </form>
+ </main>
+ );
}
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 (
+ <>
+ <Navbar />
+ <div className="flex flex-col gap-4 p-12">
+ <h1 className="text-3xl">Submit New Quote</h1>
+ <form className="flex flex-col gap-4 px-20" onSubmit={handleSubmit}>
+ <input
+ name="author"
+ type="text"
+ placeholder="Author"
+ value={formData.author}
+ onChange={handleInputChange}
+ required
+ />
+ <input
+ name="authorImage"
+ type="url"
+ placeholder="Author URL"
+ value={formData.authorImage}
+ onChange={handleInputChange}
+ required
+ />
+ <textarea
+ name="quote"
+ placeholder="Quote"
+ value={formData.quote}
+ onChange={handleInputChange}
+ required
+ />
+ <input
+ name="year"
+ type="number"
+ placeholder="Year"
+ value={formData.year}
+ onChange={handleInputChange}
+ required
+ />
+ <input
+ name="submitterID"
+ type="text"
+ placeholder="Submitter ID"
+ value={formData.submitterID}
+ onChange={handleInputChange}
+ required
+ />
+ <button
+ type="submit"
+ className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded"
+ >
+ Submit
+ </button>
+ </form>
+
+ {message && (
+ <div className={`p-4 rounded ${message.type === 'success' ? 'bg-green-800 text-green-200' : 'bg-red-800 text-red-200'}`}>
+ {message.text}
+ </div>
+ )}
+
+
+ <h1 className="text-3xl">Pending Quotes</h1>
+
+ {loading && <p>Loading quotes...</p>}
+ {error && <p className="text-red-500">Error: {error}</p>}
+
+ {!loading && pendingQuotes.length === 0 && (
+ <p>No pending quotes found.</p>
+ )}
+
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
+ {pendingQuotes.map(quote => (
+ <Card key={quote.id}>
+ <h2 className="text-xl font-medium">{quote.author}</h2>
+ <p>Author URL: {quote.authorImage}</p>
+ <p>{quote.quote}</p>
+ <small className="block mb-3">- {quote.year}</small>
+ <p className="text-sm text-gray-400 mb-2">Submitted by: {quote.submitterAuthor || quote.submitterID}</p>
+ <div className="flex gap-3 mt-2">
+ <button
+ onClick={() => handleApproveQuote(quote.id)}
+ className="bg-green-600 hover:bg-green-500 text-white py-1 px-3 rounded"
+ >
+ Approve
+ </button>
+ <button
+ onClick={() => handleRejectQuote(quote.id)}
+ className="bg-red-600 hover:bg-red-500 text-white py-1 px-3 rounded"
+ >
+ Reject
+ </button>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </div>
+ </>
+ );
+}