diff options
Diffstat (limited to 'web/src/app')
| -rw-r--r-- | web/src/app/components/Card.jsx | 7 | ||||
| -rw-r--r-- | web/src/app/components/Navbar.jsx | 83 | ||||
| -rw-r--r-- | web/src/app/components/sign-out.jsx | 15 | ||||
| -rw-r--r-- | web/src/app/dashboard/page.js | 44 | ||||
| -rw-r--r-- | web/src/app/guilds/page.js | 314 | ||||
| -rw-r--r-- | web/src/app/layout.js | 3 | ||||
| -rw-r--r-- | web/src/app/page.js | 162 | ||||
| -rw-r--r-- | web/src/app/quotes/page.js | 247 |
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> + </> + ); +} |
