logs categories and hitcounts
This commit is contained in:
parent
9121f49a9b
commit
4ee508f01b
@ -6,6 +6,7 @@ import { ToastContainer } from "react-toastify";
|
|||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { AuthProvider } from "./context/AuthContext";
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|||||||
@ -9,3 +9,24 @@ export const getCategories = async () => {
|
|||||||
const res = await axiosInstance.get<CategoriesResponse>("/api/categories");
|
const res = await axiosInstance.get<CategoriesResponse>("/api/categories");
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCategoryById = async (id: number) => {
|
||||||
|
const res = await axiosInstance.get<{data:CategoryType}>(`/api/categories/${id}`);
|
||||||
|
return res.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCategory = async (name: string) => {
|
||||||
|
const res = await axiosInstance.post(`/api/categories`, { name });
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCategory = async (id: number, name: string) => {
|
||||||
|
const res = await axiosInstance.put(`/api/categories/${id}`, { name });
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteCategory = async (id: number) => {
|
||||||
|
const res = await axiosInstance.delete(`/api/categories/${id}`);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
24
src/api/logsApi.ts
Normal file
24
src/api/logsApi.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axiosInstance from "./axiosInstance";
|
||||||
|
|
||||||
|
export type LogType = {
|
||||||
|
id: number;
|
||||||
|
description: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LogsResponse {
|
||||||
|
data: LogType[];
|
||||||
|
meta: {
|
||||||
|
current_page: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLogs = async (page = 1) => {
|
||||||
|
const res = await axiosInstance.get<LogsResponse>("/api/logs", {
|
||||||
|
params: { page },
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
39
src/api/metricsService.ts
Normal file
39
src/api/metricsService.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import axiosInstance from "./axiosInstance";
|
||||||
|
|
||||||
|
export const hitApi = async (url: string): Promise<void> => {
|
||||||
|
await axiosInstance.post("/api/hit", { url });
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface HitCountType {
|
||||||
|
id: number;
|
||||||
|
ip: string;
|
||||||
|
device_type: string;
|
||||||
|
user_agent: string;
|
||||||
|
country: string | null;
|
||||||
|
url: string;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HitCountsResponse {
|
||||||
|
data: HitCountType[];
|
||||||
|
links: {
|
||||||
|
first: string;
|
||||||
|
last: string;
|
||||||
|
prev: string | null;
|
||||||
|
next: string | null;
|
||||||
|
};
|
||||||
|
meta: {
|
||||||
|
current_page: number;
|
||||||
|
from: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHitCounts = async (page = 1) => {
|
||||||
|
const res = await axiosInstance.get<HitCountsResponse>("/api/hitcounts", {
|
||||||
|
params: { page },
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
@ -13,6 +13,7 @@ import QuizIcon from "@mui/icons-material/Quiz";
|
|||||||
import CategoryIcon from "@mui/icons-material/Category";
|
import CategoryIcon from "@mui/icons-material/Category";
|
||||||
import ArticleIcon from "@mui/icons-material/Article";
|
import ArticleIcon from "@mui/icons-material/Article";
|
||||||
import AssignmentIndIcon from '@mui/icons-material/AssignmentInd';
|
import AssignmentIndIcon from '@mui/icons-material/AssignmentInd';
|
||||||
|
import FmdBadIcon from '@mui/icons-material/FmdBad';
|
||||||
|
|
||||||
const drawerWidth = 240;
|
const drawerWidth = 240;
|
||||||
|
|
||||||
@ -35,12 +36,17 @@ const menuItems = [
|
|||||||
{
|
{
|
||||||
label: "User Tests",
|
label: "User Tests",
|
||||||
icon: <AssignmentIndIcon />,
|
icon: <AssignmentIndIcon />,
|
||||||
to: "/dashboard/userTests",
|
to: "/dashboard/user-tests",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Categories",
|
label: "Categories",
|
||||||
icon: <CategoryIcon />,
|
icon: <CategoryIcon />,
|
||||||
to: "/dashboard/categories",
|
to: "/dashboard/categories",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hitcounts",
|
||||||
|
icon: <FmdBadIcon />,
|
||||||
|
to: "/dashboard/hitcounts",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Logs",
|
label: "Logs",
|
||||||
|
|||||||
15
src/components/Cholecounter/Cholecounter.tsx
Normal file
15
src/components/Cholecounter/Cholecounter.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { hitApi } from "../../api/metricsService";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
const Cholecounter = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
hitApi(window.location.href);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cholecounter;
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { StartTestWrapper } from "./StartTestForm.styles";
|
import { StartTestWrapper } from "./StartTestForm.styles";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useCategories } from "../../hooks/Question/useCategories";
|
import { useCategories } from "../../hooks/categories/useCategories";
|
||||||
import type { Category } from "../shared/types/QuestionTypes";
|
import type { Category } from "../shared/types/QuestionTypes";
|
||||||
import { useStartTest } from "../../hooks/Tests/useStartTest";
|
import { useStartTest } from "../../hooks/Tests/useStartTest";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|||||||
17
src/hooks/categories/useCreateCategory.ts
Normal file
17
src/hooks/categories/useCreateCategory.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { createCategory } from "../../api/categoryApi";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
export const useCreateCategory = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (name: string) => createCategory(name),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["categories"] });
|
||||||
|
toast.success("Category Created");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
17
src/hooks/categories/useDeleteCategory.ts
Normal file
17
src/hooks/categories/useDeleteCategory.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { deleteCategory } from "../../api/categoryApi";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
export const useDeleteCategory = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => deleteCategory(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["categories"] });
|
||||||
|
toast.success("Category Deleted");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
10
src/hooks/categories/useGetCategoryById.ts
Normal file
10
src/hooks/categories/useGetCategoryById.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getCategoryById } from "../../api/categoryApi";
|
||||||
|
|
||||||
|
export const useCategoryById = (id?: number) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["categories", id],
|
||||||
|
queryFn: () => getCategoryById(id!),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
18
src/hooks/categories/useUpdateCategory.ts
Normal file
18
src/hooks/categories/useUpdateCategory.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { updateCategory } from "../../api/categoryApi";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
export const useUpdateCategory = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, name }: { id: number; name: string }) =>
|
||||||
|
updateCategory(id, name),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["categories"] });
|
||||||
|
toast.success("Category Updated");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
9
src/hooks/hitcounts/useHitcounts.ts
Normal file
9
src/hooks/hitcounts/useHitcounts.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getHitCounts } from "../../api/metricsService";
|
||||||
|
|
||||||
|
export const useHitcounts = (page: number) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["hitcounts", page],
|
||||||
|
queryFn: () => getHitCounts(page),
|
||||||
|
});
|
||||||
|
};
|
||||||
9
src/hooks/logsHook/useLogs.ts
Normal file
9
src/hooks/logsHook/useLogs.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getLogs } from "../../api/logsApi";
|
||||||
|
|
||||||
|
export const useLogs = (page: number) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["logs", page],
|
||||||
|
queryFn: () => getLogs(page),
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -3,6 +3,7 @@ import { AdminSidebar } from "../../components/AdminSidebar/AdminSidebar";
|
|||||||
import { LayoutWrapper } from "../MainLayout/Layout.styles";
|
import { LayoutWrapper } from "../MainLayout/Layout.styles";
|
||||||
import Header from "../../components/Header/Header";
|
import Header from "../../components/Header/Header";
|
||||||
import { StyledDistance } from "../../components/shared/StyledDistance";
|
import { StyledDistance } from "../../components/shared/StyledDistance";
|
||||||
|
import Cholecounter from "../../components/Cholecounter/Cholecounter";
|
||||||
|
|
||||||
const AdminLayout = () => {
|
const AdminLayout = () => {
|
||||||
return (
|
return (
|
||||||
@ -10,6 +11,7 @@ const AdminLayout = () => {
|
|||||||
<Header />
|
<Header />
|
||||||
<StyledDistance />
|
<StyledDistance />
|
||||||
<AdminSidebar />
|
<AdminSidebar />
|
||||||
|
<Cholecounter/>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</LayoutWrapper>
|
</LayoutWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Outlet } from "react-router-dom";
|
|||||||
import Header from "../../components/Header/Header";
|
import Header from "../../components/Header/Header";
|
||||||
import { LayoutWrapper } from "./Layout.styles";
|
import { LayoutWrapper } from "./Layout.styles";
|
||||||
import { StyledDistance } from "../../components/shared/StyledDistance";
|
import { StyledDistance } from "../../components/shared/StyledDistance";
|
||||||
|
import Cholecounter from "../../components/Cholecounter/Cholecounter";
|
||||||
|
|
||||||
const MainLayout = () => {
|
const MainLayout = () => {
|
||||||
return (
|
return (
|
||||||
@ -9,6 +10,7 @@ const MainLayout = () => {
|
|||||||
<LayoutWrapper>
|
<LayoutWrapper>
|
||||||
<Header />
|
<Header />
|
||||||
<StyledDistance />
|
<StyledDistance />
|
||||||
|
<Cholecounter/>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</LayoutWrapper>
|
</LayoutWrapper>
|
||||||
</>
|
</>
|
||||||
|
|||||||
136
src/pages/CategoriesPage/CategoriesPage.tsx
Normal file
136
src/pages/CategoriesPage/CategoriesPage.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Container from "../../components/shared/Container";
|
||||||
|
import { useCategories } from "../../hooks/categories/useCategories";
|
||||||
|
import { useDeleteCategory } from "../../hooks/categories/useDeleteCategory";
|
||||||
|
|
||||||
|
const CategoriesPage = () => {
|
||||||
|
const [deleteCategoryId, setDeleteCategoryId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useCategories();
|
||||||
|
const deleteMutation = useDeleteCategory();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleCreateCategory = () => {
|
||||||
|
navigate("/dashboard/categories/create");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateCategory = (id: number) => {
|
||||||
|
navigate(`/dashboard/categories/${id}/update`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCategory = () => {
|
||||||
|
if (deleteCategoryId !== null) {
|
||||||
|
deleteMutation.mutate(deleteCategoryId, {
|
||||||
|
onSuccess: () => setDeleteCategoryId(null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <CircularProgress />;
|
||||||
|
if (isError)
|
||||||
|
return <Typography color="error">Error loading categories.</Typography>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 3 }}>
|
||||||
|
<Typography variant="h4">Categories</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleCreateCategory}
|
||||||
|
>
|
||||||
|
Create Category
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>ID</TableCell>
|
||||||
|
<TableCell>Name</TableCell>
|
||||||
|
<TableCell>Questions</TableCell>
|
||||||
|
<TableCell>User Tests</TableCell>
|
||||||
|
<TableCell>Created At</TableCell>
|
||||||
|
<TableCell>Actions</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((category) => (
|
||||||
|
<TableRow key={category.id}>
|
||||||
|
<TableCell>{category.id}</TableCell>
|
||||||
|
<TableCell>{category.name}</TableCell>
|
||||||
|
<TableCell>{category.questions_count ?? "-"}</TableCell>
|
||||||
|
<TableCell>{category.user_tests_count ?? "-"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{category.created_at
|
||||||
|
? new Date(category.created_at).toLocaleDateString()
|
||||||
|
: "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
onClick={() => handleUpdateCategory(category.id)}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setDeleteCategoryId(category.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={deleteCategoryId !== null}
|
||||||
|
onClose={() => setDeleteCategoryId(null)}
|
||||||
|
>
|
||||||
|
<DialogTitle>Confirm Delete</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
Are you sure you want to delete this category?
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteCategoryId(null)}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
color="error"
|
||||||
|
onClick={handleDeleteCategory}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoriesPage;
|
||||||
77
src/pages/CategoryForm/CategoryForm.tsx
Normal file
77
src/pages/CategoryForm/CategoryForm.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import { useCreateCategory } from "../../hooks/categories/useCreateCategory";
|
||||||
|
import { useUpdateCategory } from "../../hooks/categories/useUpdateCategory";
|
||||||
|
import { useCategoryById } from "../../hooks/categories/useGetCategoryById";
|
||||||
|
|
||||||
|
const CategoryForm = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const isUpdate = Boolean(id);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
|
||||||
|
const { data: category, isLoading } = useCategoryById(
|
||||||
|
id ? Number(id) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const createCategoryMutation = useCreateCategory();
|
||||||
|
const updateCategoryMutation = useUpdateCategory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (category) {
|
||||||
|
setName(category.name);
|
||||||
|
}
|
||||||
|
}, [category]);
|
||||||
|
|
||||||
|
if (isUpdate && isLoading) return <CircularProgress />;
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isUpdate && id) {
|
||||||
|
updateCategoryMutation.mutate(
|
||||||
|
{ id: Number(id), name },
|
||||||
|
{ onSuccess: () => navigate("/dashboard/categories") }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
createCategoryMutation.mutate(name, {
|
||||||
|
onSuccess: () => navigate("/dashboard/categories"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ maxWidth: 500, mx: "auto", mt: 5 }}>
|
||||||
|
<Typography variant="h5" mb={3}>
|
||||||
|
{isUpdate ? "Update Category" : "Create Category"}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="contained" color="primary" type="submit">
|
||||||
|
{isUpdate ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryForm;
|
||||||
|
|
||||||
71
src/pages/HitcountsPage/HitcountsPage.tsx
Normal file
71
src/pages/HitcountsPage/HitcountsPage.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
Pagination,
|
||||||
|
} from "@mui/material";
|
||||||
|
import Container from "../../components/shared/Container";
|
||||||
|
import { useHitcounts } from "../../hooks/hitcounts/useHitcounts";
|
||||||
|
|
||||||
|
const HitCountsPage = () => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const { data, isLoading, isError } = useHitcounts(currentPage);
|
||||||
|
|
||||||
|
if (isLoading) return <CircularProgress />;
|
||||||
|
if (isError)
|
||||||
|
return <Typography color="error">Error loading hitcounts.</Typography>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="h4">Hit Counts</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>ID</TableCell>
|
||||||
|
<TableCell>IP</TableCell>
|
||||||
|
<TableCell>Device</TableCell>
|
||||||
|
<TableCell>User Agent</TableCell>
|
||||||
|
<TableCell>Country</TableCell>
|
||||||
|
<TableCell>URL</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data?.data.map((hit) => (
|
||||||
|
<TableRow key={hit.id}>
|
||||||
|
<TableCell>{hit.id}</TableCell>
|
||||||
|
<TableCell>{hit.ip}</TableCell>
|
||||||
|
<TableCell>{hit.device_type}</TableCell>
|
||||||
|
<TableCell>{hit.user_agent}</TableCell>
|
||||||
|
<TableCell>{hit.country || "-"}</TableCell>
|
||||||
|
<TableCell>{hit.url}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
color="primary"
|
||||||
|
shape="rounded"
|
||||||
|
count={data?.meta.last_page || 1}
|
||||||
|
page={currentPage}
|
||||||
|
onChange={(_, value) => setCurrentPage(value)}
|
||||||
|
sx={{ mt: 3, display: "flex", justifyContent: "center" }}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HitCountsPage;
|
||||||
@ -15,7 +15,7 @@ import Question from "../../components/Question/Question";
|
|||||||
import { useQuestions } from "../../hooks/Question/useQuestions";
|
import { useQuestions } from "../../hooks/Question/useQuestions";
|
||||||
import type { QuestionType } from "../../components/shared/types/QuestionTypes";
|
import type { QuestionType } from "../../components/shared/types/QuestionTypes";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
import { useAuth } from "../../context/AuthContext";
|
||||||
import { useCategories } from "../../hooks/Question/useCategories";
|
import { useCategories } from "../../hooks/categories/useCategories";
|
||||||
|
|
||||||
const IndexPage = () => {
|
const IndexPage = () => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|||||||
67
src/pages/LogsPage/LogsPage.tsx
Normal file
67
src/pages/LogsPage/LogsPage.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
CircularProgress,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
Pagination,
|
||||||
|
} from "@mui/material";
|
||||||
|
import Container from "../../components/shared/Container";
|
||||||
|
import { useLogs } from "../../hooks/logsHook/useLogs";
|
||||||
|
|
||||||
|
const LogsPage = () => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const { data, isLoading, isError } = useLogs(currentPage);
|
||||||
|
|
||||||
|
if (isLoading) return <CircularProgress />;
|
||||||
|
if (isError)
|
||||||
|
return <Typography color="error">Error loading logs.</Typography>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Typography variant="h4" mb={3}>
|
||||||
|
Activity Logs
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>ID</TableCell>
|
||||||
|
<TableCell>Description</TableCell>
|
||||||
|
<TableCell>Created At</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{data?.data.map((log) => (
|
||||||
|
<TableRow key={log.id}>
|
||||||
|
<TableCell>{log.id}</TableCell>
|
||||||
|
<TableCell>{log.description}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{new Date(log.created_at).toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
color="primary"
|
||||||
|
shape="rounded"
|
||||||
|
count={data?.meta.last_page}
|
||||||
|
page={currentPage}
|
||||||
|
onChange={(_, value) => setCurrentPage(value)}
|
||||||
|
sx={{ mt: 3, display: "flex", justifyContent: "center" }}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsPage;
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
type SelectChangeEvent,
|
type SelectChangeEvent,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useCategories } from "../../hooks/Question/useCategories";
|
import { useCategories } from "../../hooks/categories/useCategories";
|
||||||
|
|
||||||
const TestsPage = () => {
|
const TestsPage = () => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|||||||
@ -16,6 +16,10 @@ import AdminLayout from "../layouts/AdminLayout/AdminLayout";
|
|||||||
import Container from "../components/shared/Container";
|
import Container from "../components/shared/Container";
|
||||||
import UsersPage from "../pages/UsersPage/UsersPage";
|
import UsersPage from "../pages/UsersPage/UsersPage";
|
||||||
import UserForm from "../pages/UserForm/UserForm";
|
import UserForm from "../pages/UserForm/UserForm";
|
||||||
|
import CategoriesPage from "../pages/CategoriesPage/CategoriesPage";
|
||||||
|
import CategoryForm from "../pages/CategoryForm/CategoryForm";
|
||||||
|
import LogsPage from "../pages/LogsPage/LogsPage";
|
||||||
|
import HitCountsPage from "../pages/HitcountsPage/HitcountsPage";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -92,8 +96,16 @@ const router = createBrowserRouter([
|
|||||||
{ path: "questions", element: <Container>questions</Container> },
|
{ path: "questions", element: <Container>questions</Container> },
|
||||||
{ path: "tests", element: <Container>tests</Container> },
|
{ path: "tests", element: <Container>tests</Container> },
|
||||||
{ path: "user-tests", element: <Container>User Tests</Container> },
|
{ path: "user-tests", element: <Container>User Tests</Container> },
|
||||||
{ path: "categories", element: <Container>categories</Container> },
|
{
|
||||||
{ path: "logs", element: <Container>logs</Container> },
|
path: "categories",
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <CategoriesPage /> },
|
||||||
|
{ path: "create", element: <CategoryForm /> },
|
||||||
|
{ path: ":id/update", element: <CategoryForm /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: "logs", element: <LogsPage /> },
|
||||||
|
{path: "hitcounts", element: <HitCountsPage/>}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user