added admin project card
This commit is contained in:
Generated
+35
-7
@@ -37,6 +37,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tanstack/react-query": "^5.96.1",
|
||||
"@tanstack/react-query-devtools": "^5.100.9",
|
||||
"axios": "^1.14.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -2795,9 +2796,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.96.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.1.tgz",
|
||||
"integrity": "sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg==",
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz",
|
||||
"integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-devtools": {
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.9.tgz",
|
||||
"integrity": "sha512-gqiptrTIhbK2PuCaPRHmWXfJG1NGYVFpAr0HqogEqiSBNB5xDz6fmesQt7w4WgMOqOQPnPHJ3ZDMuhDaXvNO8g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2805,12 +2816,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.96.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.96.1.tgz",
|
||||
"integrity": "sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA==",
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz",
|
||||
"integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.96.1"
|
||||
"@tanstack/query-core": "5.100.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2820,6 +2831,23 @@
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query-devtools": {
|
||||
"version": "5.100.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.9.tgz",
|
||||
"integrity": "sha512-mM3slaVGXJmz+pOLgXdANj75ikgQCyudyl3kmFvm6brI1JyVeY/+IeD17uDHIvZrD8hfoO2sdZ54RFsHdYAuhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.100.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tanstack/react-query": "^5.96.1",
|
||||
"@tanstack/react-query-devtools": "^5.100.9",
|
||||
"axios": "^1.14.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -13,6 +13,9 @@ import ProjectDetails from "./pages/ProjectDetails";
|
||||
import Projects from "./pages/Projects";
|
||||
import { QueryProvider } from "./provider/QueryProvider";
|
||||
import OverviewPage from "./pages/admins/components/dashboards/OverviewPage";
|
||||
import ManageProject from "./pages/admins/components/projects/ManageProject";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
|
||||
function AnimatedRoutes() {
|
||||
const location = useLocation();
|
||||
@@ -33,6 +36,7 @@ function AnimatedRoutes() {
|
||||
{/* dashboard layouts */}
|
||||
<Route path="/dashboard" element={<DashboardLayout />}>
|
||||
<Route index element={<OverviewPage />} />
|
||||
<Route path="/dashboard/projects" element={<ManageProject/>}/>
|
||||
{/* <Route path="users" element={<Users />} />
|
||||
<Route path="settings" element={<Settings />} /> */}
|
||||
</Route>
|
||||
@@ -44,6 +48,7 @@ function AnimatedRoutes() {
|
||||
|
||||
const App = () => (
|
||||
<QueryProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
<ThemeProvider>
|
||||
<TooltipProvider>
|
||||
<Sonner />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IProjectsQueryParams, projectsService } from "@/api/services/project.service";
|
||||
import { T_projects } from "@/types/projects.type";
|
||||
import { queryKeys } from "@/utils/queryKeys";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
|
||||
export const useProjects = (params?: IProjectsQueryParams) => {
|
||||
@@ -10,6 +11,27 @@ export const useProjects = (params?: IProjectsQueryParams) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: Partial<T_projects> }) =>
|
||||
projectsService.updateProject(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.projects });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => projectsService.deleteProject(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.projects });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useProjectById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.project(id),
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const navItems = [
|
||||
{ title: "Overview", icon: LayoutDashboard, href: "/" },
|
||||
{ title: "Overview", icon: LayoutDashboard, href: "/dashboard" },
|
||||
{ title: "Manage Projects", icon: FolderKanban, href: "/dashboard/projects" },
|
||||
{ title: "Manage Team", icon: Users, href: "/dashboard/team" },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState } from "react";
|
||||
import { T_projects } from "@/types/projects.type";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export const EditProjectModal = ({
|
||||
project,
|
||||
onClose,
|
||||
onSave
|
||||
}: {
|
||||
project: T_projects;
|
||||
onClose: () => void;
|
||||
onSave: (data: Partial<T_projects>) => void
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
name: project.name,
|
||||
shortDescription: project.description,
|
||||
previewUrl: project.liveLink,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="glass-strong w-full max-w-md p-8 rounded-2xl shadow-2xl relative">
|
||||
<button onClick={onClose} className="absolute top-4 right-4 text-muted-foreground hover:text-foreground">
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-6 neon-text-glow">Edit Project</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Project Title</label>
|
||||
<input
|
||||
className="w-full bg-background border border-border p-2.5 rounded-lg focus:ring-2 focus:ring-primary outline-none"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Short Description</label>
|
||||
<textarea
|
||||
className="w-full bg-background border border-border p-2.5 rounded-lg focus:ring-2 focus:ring-primary outline-none h-24"
|
||||
value={formData.shortDescription}
|
||||
onChange={(e) => setFormData({ ...formData, shortDescription: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1 block">Preview URL</label>
|
||||
<input
|
||||
className="w-full bg-background border border-border p-2.5 rounded-lg focus:ring-2 focus:ring-primary outline-none"
|
||||
value={formData.previewUrl}
|
||||
onChange={(e) => setFormData({ ...formData, previewUrl: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 bg-primary text-primary-foreground font-bold rounded-lg mt-4 neon-glow hover:neon-glow-strong transition-all"
|
||||
>
|
||||
Update Project
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useState } from "react";
|
||||
import { useProjects, useUpdateProject, useDeleteProject } from "@/hooks/queires/useProjects";
|
||||
|
||||
import { T_projects } from "@/types/projects.type";
|
||||
import { ProjectCard } from "./ProjectCard";
|
||||
import { EditProjectModal } from "./EditProjectModal";
|
||||
|
||||
export default function ManageProject() {
|
||||
const { data: projectsData, isLoading } = useProjects();
|
||||
const updateMutation = useUpdateProject();
|
||||
const deleteMutation = useDeleteProject();
|
||||
|
||||
const [selectedProject, setSelectedProject] = useState<T_projects | null>(null);
|
||||
|
||||
const projects: T_projects[] = projectsData?.data?.data?.result || [];
|
||||
|
||||
const handleUpdate = (data: Partial<T_projects>) => {
|
||||
if (selectedProject) {
|
||||
updateMutation.mutate({ id: selectedProject.id, data }, {
|
||||
onSuccess: () => setSelectedProject(null)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (window.confirm("Are you sure you want to delete this project?")) {
|
||||
deleteMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <div className="p-10 text-center animate-pulse">Loading Projects...</div>;
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-5xl mx-auto">
|
||||
<header className="mb-10">
|
||||
<h1 className="text-4xl font-extrabold neon-text-glow">Manage Projects</h1>
|
||||
<p className="text-muted-foreground">Edit, update or remove your portfolio items.</p>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{projects.map((item) => (
|
||||
<ProjectCard
|
||||
key={item.id}
|
||||
project={item}
|
||||
onEdit={setSelectedProject}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedProject && (
|
||||
<EditProjectModal
|
||||
project={selectedProject}
|
||||
onClose={() => setSelectedProject(null)}
|
||||
onSave={handleUpdate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Edit, Trash2, ExternalLink } from "lucide-react";
|
||||
import { T_projects } from "@/types/projects.type";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface ProjectCardProps {
|
||||
project: T_projects;
|
||||
onEdit: (project: T_projects) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ProjectCard = ({ project, onEdit, onDelete }: ProjectCardProps) => {
|
||||
return (
|
||||
<div className="glass p-6 rounded-xl flex justify-between items-start gap-4 hover:border-primary/50 transition-all duration-300">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold text-foreground flex items-center gap-2">
|
||||
{project.name}
|
||||
{project.isFeatured && (
|
||||
<span className="text-[10px] bg-primary/20 text-primary px-2 py-0.5 rounded-full border border-primary/30 uppercase tracking-wider">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm line-clamp-2">
|
||||
{project.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{project.technologies.slice(0, 4).map((tech) => (
|
||||
<span key={tech} className="text-xs bg-secondary px-2 py-1 rounded text-secondary-foreground">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => onEdit(project)}
|
||||
className="p-2 rounded-lg bg-primary/10 text-primary hover:bg-primary hover:text-white transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(project.id)}
|
||||
className="p-2 rounded-lg bg-destructive/10 text-destructive hover:bg-destructive hover:text-white transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
{project.liveLink && (
|
||||
<Link
|
||||
to={project.liveLink}
|
||||
target="_blank"
|
||||
className="p-2 rounded-lg bg-secondary text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ExternalLink size={18} />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { ProjectCategory } from "@/enums/projectCategory";
|
||||
import { ProjectStatus } from "@/enums/projectStatus";
|
||||
|
||||
export type T_projects = {
|
||||
id:string;
|
||||
name: string;
|
||||
description: string;
|
||||
thumbnail?: string;
|
||||
|
||||
Reference in New Issue
Block a user