diff --git a/index.html b/index.html index caabf63..eee5c8f 100644 --- a/index.html +++ b/index.html @@ -5,10 +5,23 @@ + + + + + + + + + + + + + + Rust Quests - diff --git a/public/assets/text/news.md b/public/assets/text/news.md index c8eac32..a5e520a 100644 --- a/public/assets/text/news.md +++ b/public/assets/text/news.md @@ -17,4 +17,10 @@ __^^ let us know if this is a feature you'd like to see! ^^__ # Other ## About -This website was created by discord user **zcog** hope you enjoy it! \ No newline at end of file +This website was created by discord user **zcog** hope you enjoy it! + +## Credits +Thanks to the following people for their contributions: +- **notacoconut** (developer) +- **Gilbert** (moderator) +- **wall2wall3** (feedback) \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index de7ebb4..4fe2061 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,9 @@ import UsernameBubble from './components/UsernameBubble'; import AuthenticatedAdminUsersPage from './pages/admin/AdminUsersPage'; import AuthenticatedAdminQuestsPage from './pages/admin/AdminQuestsPage'; import LeaderboardPage from './pages/Leaderboard'; +import AuthenticatedModeratorPage from './pages/moderator/ModeratorPage'; +import AuthenticatedModeratorSuggestionsPage from './pages/moderator/ModeratorSuggestionsPage'; +import AuthenticatedModeratorQuestsPage from './pages/moderator/ModeratorQuestsPage'; const queryClient = new QueryClient() @@ -36,6 +39,11 @@ function App() { {/* authorized routes */} {/* } /> */} + {/* moderator routes */} + } /> + } /> + } /> + {/* admin routes */} } /> } /> diff --git a/src/components/UsernameBubble/UsernameBubble.tsx b/src/components/UsernameBubble/UsernameBubble.tsx index 4c90db6..a8e5459 100644 --- a/src/components/UsernameBubble/UsernameBubble.tsx +++ b/src/components/UsernameBubble/UsernameBubble.tsx @@ -14,7 +14,7 @@ const UsernameBubble: React.FC = ({ user }) => { Hello, {user.username} { (user.role === "admin" || user.role === "moderator") && - [{user.role}] + [{user.role}] }

diff --git a/src/hocs/withAuth.tsx b/src/hocs/withAuth.tsx index bcab672..b01fbdd 100644 --- a/src/hocs/withAuth.tsx +++ b/src/hocs/withAuth.tsx @@ -1,8 +1,8 @@ -import { UserToken } from "@/models/AuthModels/userToken"; +import { role, UserToken } from "@/models/AuthModels/userToken"; import React, { useEffect, useState, ComponentType } from "react"; import { useNavigate } from "react-router-dom"; -const withAuth =

(WrappedComponent: ComponentType

, requiredRole?: "admin") => { +const withAuth =

(WrappedComponent: ComponentType

, requiredRole?: role) => { const ComponentWithAuth: React.FC

= (props) => { const navigate = useNavigate(); const [isAuthenticated, setIsAuthenticated] = useState(false); @@ -23,7 +23,7 @@ const withAuth =

(WrappedComponent: ComponentType

, required return null; } - if (requiredRole && user.role !== requiredRole) { + if (user.role !== "admin" && user.role !== requiredRole) { navigate("/unauthorized"); return null; } diff --git a/src/modals/EditQuest/EditQuest.tsx b/src/modals/EditQuest/EditQuest.tsx index f99c6e0..8392fb3 100644 --- a/src/modals/EditQuest/EditQuest.tsx +++ b/src/modals/EditQuest/EditQuest.tsx @@ -7,7 +7,7 @@ import { DEFAULT_IMG_URL } from '@/constants'; interface ModalProps { onClose: () => void; - onDeleteQuest: () => void; + onDeleteQuest?: () => void; onEditQuest: (newQuest: EditQuestRequest) => void; quest: Quest; categories: Category[]; @@ -153,12 +153,15 @@ const EditQuest: React.FC = (props) => { onClick={onClose}> close - + { + onDeleteQuest && + + }

- + { + onDeleteSuggestion && + + }
@@ -169,6 +176,7 @@ const EditSuggestion: React.FC = (props) => { onCreateQuest && +
+ +
+
+

All Quests

+
+ + { + isLoading ? + : +
+ + + } + + + + { + selectedQuest && + + + + } + + { + confirmDialog && + setConfirmDialog(null)} + > + setConfirmDialog(null)} + /> + + } + + ); +} + +const AuthenticatedModeratorQuestsPage = withAuth(ModeratorQuestsPage, "moderator"); + +export default AuthenticatedModeratorQuestsPage; \ No newline at end of file diff --git a/src/pages/moderator/ModeratorSuggestionsPage.tsx b/src/pages/moderator/ModeratorSuggestionsPage.tsx new file mode 100644 index 0000000..b425164 --- /dev/null +++ b/src/pages/moderator/ModeratorSuggestionsPage.tsx @@ -0,0 +1,221 @@ +import { useNavigate } from "react-router-dom"; + +import { useCallback, useEffect, useState } from "react"; +import { toast } from "@/components/Toaster"; +import suggestionService from "@/service/suggestionService"; +import { useAuth } from "@/context/useAuth"; +import withAuth from "@/hocs/withAuth"; +import Table from "@/components/Table"; +import Button from "@/components/Button"; +import { Suggestion } from "@/models/SuggestionModels/suggestionResponse"; +import { AxiosError } from "axios"; +import Modal from "@/components/Modal"; +import EditSuggestion from "@/modals/EditSuggestions"; +import categoryService from "@/service/categoryService"; +import { Category } from "@/models/CategoryModels/categoryResponse"; +import ConfirmDialog from "@/components/ConfirmDialog"; +import { convertSuggestionIntoQuestBodyRequest } from "@/models/SuggestionModels/suggestionRequests"; +import Loader from "@/components/Loader"; + +const ModeratorSuggestionsPage = () => { + const navigate = useNavigate(); + const { accessToken, user } = useAuth(); + + const [suggestions, setSuggestions] = useState([]); + const [page, setPage] = useState(1); + const [selectedSuggestion, setSelectedSuggestion] = useState(null); + const [categories, setCategories] = useState([]); + const [confirmDialog, setConfirmDialog] = useState<{ title: string; description: string; onConfirm: () => void; } | null>(null); + const [isLoading, setIsLoading] = useState(true); + + const maxLength = 20; + + useEffect(() => { + const fetchCategories = async () => { + try { + if (!accessToken) return; + + const categoriesResponse = await categoryService.getCategories(accessToken); + + setCategories(categoriesResponse); + } catch (error) { + toast.error("Failed to get categories", error); + } + }; + + fetchCategories(); + }, [accessToken]); + + const fetchSuggestions: () => Promise = useCallback(async () => { + try { + if (!accessToken) return; + setIsLoading(true); + + const suggestionsResponse = await suggestionService.getSuggestions(accessToken, page); + + setSuggestions(suggestionsResponse); + } catch (error) { + toast.error("Failed to get suggestions", error); + } finally { + setIsLoading(false); + } + }, [accessToken, page]); + + useEffect(() => { + fetchSuggestions(); + }, [page, fetchSuggestions]); + + useEffect(() => { + if (!accessToken) { + navigate("/login"); + return; + } + fetchSuggestions(); + }, [accessToken, navigate, fetchSuggestions]); + + const handleRowClick = (index: number) => { + setSelectedSuggestion(suggestions[index]); + } + + const closeModal = () => { + setSelectedSuggestion(null); + } + + const handleCloseSuggestion = async () => { + if (confirmDialog) return; + + setConfirmDialog({ + title: "Are you sure?", + description: "If you close this suggestion, you will lose all changes.", + onConfirm: () => { + closeModal(); + setConfirmDialog(null); + } + }) + }; + + const onDeleteSuggestion = async () => { + const onConfirm = () => { + try { + if (!selectedSuggestion) return; + if (!accessToken) return; + + suggestionService.deleteSuggestion(accessToken, selectedSuggestion.id); + closeModal(); + fetchSuggestions(); + + toast.success("Suggestion deleted successfully"); + } catch (error) { + toast.error("Failed to delete suggestion", error); + } finally { + setConfirmDialog(null); + } + }; + setConfirmDialog({ + title: "Are you sure?", + description: "If you delete this suggestion, you will lose all changes.", + onConfirm: onConfirm, + }); + }; + + const handleCreateQuest = async (newQuest: convertSuggestionIntoQuestBodyRequest) => { + const handleConvertSuggestionToQuest = async () => { + try { + if (!selectedSuggestion) return; + if (!accessToken) return; + + await suggestionService.convertSuggestionIntoQuest(accessToken, selectedSuggestion.id, newQuest); + fetchSuggestions(); + + toast.success("Suggestion converted to quest"); + closeModal(); + } catch (error: unknown) { + if (error instanceof AxiosError && error.response?.data) { + return toast.error(error.response?.data.message, error); + } + toast.error("Failed to convert suggestion to quest", error); + } + } + + setConfirmDialog({ + title: "Are you sure?", + description: "If you create a quest, suggestion will be deleted.", + onConfirm: async () => { + await handleConvertSuggestionToQuest(); + setConfirmDialog(null); + } + }); + }; + + return ( +
+
+
+ +
+
+ +
+ +
+
+

All Suggestions

+
+ + { + isLoading ? + : +
+
+ + } + + + + { + selectedSuggestion && + + + + } + + { + confirmDialog && + setConfirmDialog(null)} + > + setConfirmDialog(null)} + /> + + } + + ); +} + +const AuthenticatedModeratorSuggestionsPage = withAuth(ModeratorSuggestionsPage, "moderator"); + +export default AuthenticatedModeratorSuggestionsPage; \ No newline at end of file diff --git a/src/service/suggestionService.ts b/src/service/suggestionService.ts index dce54b5..385cbba 100644 --- a/src/service/suggestionService.ts +++ b/src/service/suggestionService.ts @@ -48,4 +48,10 @@ export default { endpoint: `${baseUrl}/leaderboard`, accessToken }) as Promise, + + deleteSuggestion: async (accessToken: string, suggestionId: number) => await sendRequest({ + method: "DELETE", + endpoint: `${baseUrl}/${suggestionId}`, + accessToken + }) as Promise, } \ No newline at end of file