diff --git a/src/MainLayout/MainLayout.tsx b/src/MainLayout/MainLayout.tsx
index 4af7691..62ad6b1 100644
--- a/src/MainLayout/MainLayout.tsx
+++ b/src/MainLayout/MainLayout.tsx
@@ -4,14 +4,16 @@ import { LayoutWrapper } from "./Layout.styles";
import { StyledDistance } from "../components/shared/StyledDistance";
import Header from "../components/Header/Header";
import { AdminSidebar } from "../components/AdminSidebar/AdminSidebar";
+import { useAuth } from "../context/AuthContext";
const MainLayout = () => {
+ const { user } = useAuth();
return (
<>
-
+ {user && }
diff --git a/src/api/publications.ts b/src/api/publications.ts
index e69de29..da2ee04 100644
--- a/src/api/publications.ts
+++ b/src/api/publications.ts
@@ -0,0 +1,123 @@
+import type { User } from "../components/shared/types/AuthTypes";
+import axiosInstance from "./axiosInstance";
+import type { CategoryType } from "./categories";
+
+export type PublicationType = {
+ pk: number;
+ image?: string;
+ video?: string;
+ content_type: "image" | "video";
+ status: "public" | "pending";
+ average_score: number;
+ description: string;
+ is_pinned: boolean;
+ user: number;
+ user_detail: User;
+ category: number;
+ category_detail: CategoryType;
+ time_created: string;
+ time_updated: string;
+};
+
+export type PublicationsResponse = {
+ count: number;
+ page_size: number;
+ page: number;
+ total_pages: number;
+ results: PublicationType[];
+};
+
+export type PublicationPayload = {
+ file: File;
+ content_type: "image" | "video";
+ status: "public" | "pending";
+ description: string;
+ is_pinned: boolean;
+ category: number;
+};
+
+export type UpdatePublicationPayload = {
+ file?: File;
+ content_type: "image" | "video";
+ status: "public" | "pending";
+ description: string;
+ is_pinned: boolean;
+ category: number;
+};
+
+export const getPublications = async (page: number) => {
+ const res = await axiosInstance.get(
+ "/api/admin/publications",
+ { params: { page } }
+ );
+ return res.data;
+};
+
+export const getPublicationById = async (id: number) => {
+ const res = await axiosInstance.get(
+ `/api/admin/publications/${id}`
+ );
+ return res.data;
+};
+
+export const createPublication = async (payload: PublicationPayload) => {
+ const formData = new FormData();
+
+ if (payload.content_type === "image") {
+ formData.append("image", payload.file);
+ } else {
+ formData.append("video", payload.file);
+ }
+
+ formData.append("content_type", payload.content_type);
+ formData.append("status", payload.status);
+ formData.append("description", payload.description);
+ formData.append("is_pinned", String(payload.is_pinned));
+ formData.append("category", String(payload.category));
+
+ const res = await axiosInstance.post(
+ "/api/admin/publications/",
+ formData,
+ {
+ headers: { "Content-Type": "multipart/form-data" },
+ }
+ );
+
+ return res.data;
+};
+
+export const updatePublication = async (
+ id: number,
+ payload: UpdatePublicationPayload
+) => {
+ const formData = new FormData();
+
+ if (payload.file) {
+ if (payload.content_type === "image") {
+ formData.append("image", payload.file);
+ } else {
+ formData.append("video", payload.file);
+ }
+ }
+
+ formData.append("content_type", payload.content_type);
+ formData.append("status", payload.status);
+ formData.append("description", payload.description);
+ formData.append("is_pinned", String(payload.is_pinned));
+ formData.append("category", String(payload.category));
+
+ const res = await axiosInstance.patch(
+ `/api/admin/publications/${id}`,
+ formData,
+ {
+ headers: { "Content-Type": "multipart/form-data" },
+ }
+ );
+
+ return res.data;
+};
+
+export const deletePublication = async (id: number) => {
+ const res = await axiosInstance.delete(`/api/admin/publications/${id}`);
+ return res.data;
+};
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 710dcbf..c53df56 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -31,7 +31,6 @@ function Header() {
setAnchorElNav(null);
};
- const goToAdmin = () => navigate("/admin");
return (
@@ -42,7 +41,7 @@ function Header() {
variant="h6"
noWrap
component="a"
- href="/"
+ href= {user ? "/publications": "/"}
sx={{
mr: 2,
display: { xs: "none", md: "flex" },
@@ -100,16 +99,7 @@ function Header() {
)}
- {user && user.is_superuser && (
-
- )}
+
@@ -131,7 +121,7 @@ function Header() {
textDecoration: "none",
}}
>
- HoshiAI
+ Gallery
@@ -153,14 +143,7 @@ function Header() {
)}
- {user?.is_superuser && (
-
- )}
+
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
index ce79d52..cd49662 100644
--- a/src/context/AuthContext.tsx
+++ b/src/context/AuthContext.tsx
@@ -34,7 +34,7 @@ export const AuthProvider = ({ children }: OnlyChildrenProps) => {
localStorage.removeItem("access_token");
queryClient.removeQueries({ queryKey: ["me"] });
toast.info("Logged out");
- window.location.href = "/login";
+ window.location.href = "/";
};
return (
diff --git a/src/hooks/publications/useCreatePublication.ts b/src/hooks/publications/useCreatePublication.ts
new file mode 100644
index 0000000..1bd4d71
--- /dev/null
+++ b/src/hooks/publications/useCreatePublication.ts
@@ -0,0 +1,32 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import {
+ createPublication,
+ type PublicationPayload,
+} from "../../api/publications";
+import { toast } from "react-toastify";
+
+export const useCreatePublication = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (payload: PublicationPayload) => createPublication(payload),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["publications"] });
+ },
+ onError: (error: any) => {
+ const data = error?.response?.data;
+
+ if (data?.image?.length) {
+ toast.error(data.image[0]);
+ return;
+ }
+
+ if (data?.video?.length) {
+ toast.error(data.video[0]);
+ return;
+ }
+
+ toast.error("Failed to create publication");
+ },
+ });
+};
diff --git a/src/hooks/publications/useDeletePublication.ts b/src/hooks/publications/useDeletePublication.ts
new file mode 100644
index 0000000..a2dd88d
--- /dev/null
+++ b/src/hooks/publications/useDeletePublication.ts
@@ -0,0 +1,17 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { deletePublication } from "../../api/publications";
+import { toast } from "react-toastify";
+
+export const useDeletePublication = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: number) => deletePublication(id),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["publications"] });
+ toast.success("Publication Deleted");
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ });
+};
diff --git a/src/hooks/publications/useGetPublicationById.ts b/src/hooks/publications/useGetPublicationById.ts
new file mode 100644
index 0000000..7893a88
--- /dev/null
+++ b/src/hooks/publications/useGetPublicationById.ts
@@ -0,0 +1,10 @@
+import { useQuery } from "@tanstack/react-query";
+import { getPublicationById } from "../../api/publications";
+
+export const useGetPublicationsById = (id?: number) => {
+ return useQuery({
+ queryKey: ["publication", id],
+ queryFn: () => getPublicationById(id!),
+ enabled: !!id,
+ });
+};
diff --git a/src/hooks/publications/useGetPublications.ts b/src/hooks/publications/useGetPublications.ts
new file mode 100644
index 0000000..5c61b4b
--- /dev/null
+++ b/src/hooks/publications/useGetPublications.ts
@@ -0,0 +1,8 @@
+import { useQuery } from "@tanstack/react-query";
+import { getPublications } from "../../api/publications";
+
+export const useGetPublications = (page: number) =>
+ useQuery({
+ queryKey: ["publications", page],
+ queryFn: () => getPublications(page),
+ });
diff --git a/src/hooks/publications/useUpdatePublication.ts b/src/hooks/publications/useUpdatePublication.ts
new file mode 100644
index 0000000..3ab43af
--- /dev/null
+++ b/src/hooks/publications/useUpdatePublication.ts
@@ -0,0 +1,38 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import {
+ updatePublication,
+ type UpdatePublicationPayload,
+} from "../../api/publications";
+import { toast } from "react-toastify";
+
+export const useUpdatePublication = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ id,
+ payload,
+ }: {
+ id: number;
+ payload: UpdatePublicationPayload;
+ }) => updatePublication(id, payload),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["publications"] });
+ },
+ onError: (error: any) => {
+ const data = error?.response?.data;
+
+ if (data?.image?.length) {
+ toast.error(data.image[0]);
+ return;
+ }
+
+ if (data?.video?.length) {
+ toast.error(data.video[0]);
+ return;
+ }
+
+ toast.error("Failed to create publication");
+ },
+ });
+};
diff --git a/src/pages/IndexesPage/IndexesPage.tsx b/src/pages/IndexesPage/IndexesPage.tsx
index 9e99d13..b359358 100644
--- a/src/pages/IndexesPage/IndexesPage.tsx
+++ b/src/pages/IndexesPage/IndexesPage.tsx
@@ -30,7 +30,7 @@ const IndexesPage = () => {
const deleteMutation = useDeleteIndex();
const navigate = useNavigate();
- const handleCreateIndex = () => {
+ const handleCreateIndex = () => {
navigate("/indexes/create");
};
@@ -54,11 +54,7 @@ const IndexesPage = () => {
Indexes
-
diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx
index 6fd0966..99dae62 100644
--- a/src/pages/LoginPage/LoginPage.tsx
+++ b/src/pages/LoginPage/LoginPage.tsx
@@ -14,7 +14,7 @@ const LoginPage = () => {
useEffect(() => {
if (user) {
- navigate("/");
+ navigate("/profile");
}
}, [user, navigate]);
diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx
index 001732e..552b26e 100644
--- a/src/pages/ProfilePage/ProfilePage.tsx
+++ b/src/pages/ProfilePage/ProfilePage.tsx
@@ -43,13 +43,6 @@ const ProfilePage = () => {
Logout
- navigate("/admin")}
- >
- Admin Dashboard
-
);
diff --git a/src/pages/PublicationsForm/PublicationsForm.tsx b/src/pages/PublicationsForm/PublicationsForm.tsx
new file mode 100644
index 0000000..9dba173
--- /dev/null
+++ b/src/pages/PublicationsForm/PublicationsForm.tsx
@@ -0,0 +1,254 @@
+import { useState, useEffect } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import {
+ Box,
+ Button,
+ CircularProgress,
+ TextField,
+ Typography,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ Checkbox,
+ FormControlLabel,
+} from "@mui/material";
+import { useGetPublicationsById } from "../../hooks/publications/useGetPublicationById";
+import { useCreatePublication } from "../../hooks/publications/useCreatePublication";
+import { useUpdatePublication } from "../../hooks/publications/useUpdatePublication";
+import type {
+ PublicationPayload,
+ UpdatePublicationPayload,
+} from "../../api/publications";
+import { useCategories } from "../../hooks/categories/useCategories";
+
+const PublicationForm = () => {
+ const { id } = useParams<{ id: string }>();
+ const isUpdate = Boolean(id);
+ const navigate = useNavigate();
+
+ const [file, setFile] = useState(null);
+ const [contentType, setContentType] = useState<"image" | "video">("image");
+ const [status, setStatus] = useState<"public" | "pending">("public");
+ const [description, setDescription] = useState("");
+ const [isPinned, setIsPinned] = useState(false);
+ const [category, setCategory] = useState(0);
+
+ const { data: publication, isLoading } = useGetPublicationsById(
+ id ? Number(id) : undefined
+ );
+ const { data: categories } = useCategories();
+ const createMutation = useCreatePublication();
+ const updateMutation = useUpdatePublication();
+ const previewUrl = file ? URL.createObjectURL(file) : null;
+
+ useEffect(() => {
+ return () => {
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
+ };
+ }, [previewUrl]);
+
+ useEffect(() => {
+ if (publication) {
+ setContentType(publication.content_type);
+ setStatus(publication.status);
+ setDescription(publication.description);
+ setIsPinned(publication.is_pinned);
+ setCategory(publication.category);
+ }
+ }, [publication]);
+
+ if (isUpdate && isLoading) return ;
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (isUpdate && id) {
+ const payload: UpdatePublicationPayload = {
+ content_type: contentType,
+ status,
+ description,
+ is_pinned: isPinned,
+ category,
+ file: file ?? undefined,
+ };
+
+ updateMutation.mutate(
+ { id: Number(id), payload },
+ { onSuccess: () => navigate("/publications") }
+ );
+ } else {
+ if (!file) {
+ alert("File is required");
+ return;
+ }
+
+ const payload: PublicationPayload = {
+ file,
+ content_type: contentType,
+ status,
+ description,
+ is_pinned: isPinned,
+ category,
+ };
+
+ createMutation.mutate(payload, {
+ onSuccess: () => navigate("/publications"),
+ });
+ }
+ };
+
+ return (
+
+
+ {isUpdate ? "Update Publication" : "Create Publication"}
+
+
+
+
+ );
+};
+
+export default PublicationForm;
diff --git a/src/pages/PublicationsPage/PublicationsPage.tsx b/src/pages/PublicationsPage/PublicationsPage.tsx
new file mode 100644
index 0000000..89918ca
--- /dev/null
+++ b/src/pages/PublicationsPage/PublicationsPage.tsx
@@ -0,0 +1,157 @@
+import { useState } from "react";
+import {
+ Box,
+ Button,
+ CircularProgress,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography,
+ Pagination,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+} from "@mui/material";
+import { useNavigate } from "react-router-dom";
+
+import Container from "../../components/shared/Container/Container";
+import { useGetPublications } from "../../hooks/publications/useGetPublications";
+import { useDeletePublication } from "../../hooks/publications/useDeletePublication";
+import type { PublicationType } from "../../api/publications";
+
+const PublicationsPage = () => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [deletePublicationId, setDeletePublicationId] = useState(
+ null
+ );
+
+ const { data, isLoading, isError } = useGetPublications(currentPage);
+ const deleteMutation = useDeletePublication();
+ const navigate = useNavigate();
+
+ const handleCreatePublication = () => {
+ navigate("/publications/create");
+ };
+
+ const handleUpdate = (id: number) => {
+ navigate(`/publications/${id}/update`);
+ };
+
+ const handleDelete = () => {
+ if (deletePublicationId !== null) {
+ deleteMutation.mutate(deletePublicationId, {
+ onSuccess: () => setDeletePublicationId(null),
+ });
+ }
+ };
+
+ if (isLoading) return ;
+ if (isError)
+ return Error loading publications.;
+
+ return (
+
+
+ Publications
+
+ Create Publication
+
+
+
+
+
+
+
+ ID
+ Type
+ Description
+ Status
+ Average Score
+ Pinned
+ Category
+ User
+ Created
+ Actions
+
+
+
+
+ {data?.results.map((pub: PublicationType) => (
+
+ {pub.pk}
+ {pub.content_type}
+ {pub.description}
+ {pub.status}
+ {pub.average_score}
+ {pub.is_pinned ? "Yes" : "No"}
+ {pub.category_detail.name}
+ {pub.user_detail.username}
+
+ {new Date(pub.time_created).toLocaleDateString()}
+
+
+ handleUpdate(pub.pk)}
+ >
+ Update
+
+ setDeletePublicationId(pub.pk)}
+ >
+ Delete
+
+
+
+ ))}
+
+
+
+
+ setCurrentPage(value)}
+ sx={{ mt: 3, mb: 3, display: "flex", justifyContent: "center" }}
+ />
+
+
+
+ );
+};
+
+export default PublicationsPage;
diff --git a/src/router/router.tsx b/src/router/router.tsx
index 30abd1f..5640033 100644
--- a/src/router/router.tsx
+++ b/src/router/router.tsx
@@ -1,6 +1,5 @@
import { createBrowserRouter } from "react-router-dom";
import MainLayout from "../MainLayout/MainLayout";
-import { Container } from "@mui/material";
import LoginPage from "../pages/LoginPage/LoginPage";
import ProfilePage from "../pages/ProfilePage/ProfilePage";
import UsersPage from "../pages/UsersPage/UsersPage";
@@ -9,6 +8,8 @@ import IndexesPage from "../pages/IndexesPage/IndexesPage";
import IndexForm from "../pages/IndexForm/IndexForm";
import CategoriesPage from "../pages/CategoriesPage/CategoriesPage";
import CategoryForm from "../pages/CategoryForm/CategoryForm";
+import PublicationsPage from "../pages/PublicationsPage/PublicationsPage";
+import PublicationForm from "../pages/PublicationsForm/PublicationsForm";
const router = createBrowserRouter([
{
@@ -17,10 +18,6 @@ const router = createBrowserRouter([
children: [
{
index: true,
- element: index page,
- },
- {
- path: "/login",
element: ,
},
{
@@ -44,7 +41,11 @@ const router = createBrowserRouter([
},
{
path: "/publications",
- element: Publications Page,
+ children: [
+ { index: true, element: },
+ { path: "create", element: },
+ { path: ":id/update", element: },
+ ],
},
{
path: "/categories",