added admin project card

This commit is contained in:
sanjidarimi
2026-05-11 00:23:38 +06:00
parent 7163cdd735
commit ca96e13579
9 changed files with 258 additions and 9 deletions
+35 -7
View File
@@ -37,6 +37,7 @@
"@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.96.1", "@tanstack/react-query": "^5.96.1",
"@tanstack/react-query-devtools": "^5.100.9",
"axios": "^1.14.0", "axios": "^1.14.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -2795,9 +2796,19 @@
} }
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.96.1", "version": "5.100.9",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.1.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.9.tgz",
"integrity": "sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg==", "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", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -2805,12 +2816,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.96.1", "version": "5.100.9",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.96.1.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.9.tgz",
"integrity": "sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA==", "integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.96.1" "@tanstack/query-core": "5.100.9"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -2820,6 +2831,23 @@
"react": "^18 || ^19" "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": { "node_modules/@testing-library/dom": {
"version": "10.4.1", "version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+1
View File
@@ -42,6 +42,7 @@
"@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.96.1", "@tanstack/react-query": "^5.96.1",
"@tanstack/react-query-devtools": "^5.100.9",
"axios": "^1.14.0", "axios": "^1.14.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
+5
View File
@@ -13,6 +13,9 @@ import ProjectDetails from "./pages/ProjectDetails";
import Projects from "./pages/Projects"; import Projects from "./pages/Projects";
import { QueryProvider } from "./provider/QueryProvider"; import { QueryProvider } from "./provider/QueryProvider";
import OverviewPage from "./pages/admins/components/dashboards/OverviewPage"; 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() { function AnimatedRoutes() {
const location = useLocation(); const location = useLocation();
@@ -33,6 +36,7 @@ function AnimatedRoutes() {
{/* dashboard layouts */} {/* dashboard layouts */}
<Route path="/dashboard" element={<DashboardLayout />}> <Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<OverviewPage />} /> <Route index element={<OverviewPage />} />
<Route path="/dashboard/projects" element={<ManageProject/>}/>
{/* <Route path="users" element={<Users />} /> {/* <Route path="users" element={<Users />} />
<Route path="settings" element={<Settings />} /> */} <Route path="settings" element={<Settings />} /> */}
</Route> </Route>
@@ -44,6 +48,7 @@ function AnimatedRoutes() {
const App = () => ( const App = () => (
<QueryProvider> <QueryProvider>
<ReactQueryDevtools initialIsOpen={false} />
<ThemeProvider> <ThemeProvider>
<TooltipProvider> <TooltipProvider>
<Sonner /> <Sonner />
+23 -1
View File
@@ -1,6 +1,7 @@
import { IProjectsQueryParams, projectsService } from "@/api/services/project.service"; import { IProjectsQueryParams, projectsService } from "@/api/services/project.service";
import { T_projects } from "@/types/projects.type";
import { queryKeys } from "@/utils/queryKeys"; import { queryKeys } from "@/utils/queryKeys";
import { useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export const useProjects = (params?: IProjectsQueryParams) => { 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) => { export const useProjectById = (id: string) => {
return useQuery({ return useQuery({
queryKey: queryKeys.project(id), queryKey: queryKeys.project(id),
@@ -13,7 +13,7 @@ import {
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
const navItems = [ const navItems = [
{ title: "Overview", icon: LayoutDashboard, href: "/" }, { title: "Overview", icon: LayoutDashboard, href: "/dashboard" },
{ title: "Manage Projects", icon: FolderKanban, href: "/dashboard/projects" }, { title: "Manage Projects", icon: FolderKanban, href: "/dashboard/projects" },
{ title: "Manage Team", icon: Users, href: "/dashboard/team" }, { 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>
);
};
+1
View File
@@ -2,6 +2,7 @@ import { ProjectCategory } from "@/enums/projectCategory";
import { ProjectStatus } from "@/enums/projectStatus"; import { ProjectStatus } from "@/enums/projectStatus";
export type T_projects = { export type T_projects = {
id:string;
name: string; name: string;
description: string; description: string;
thumbnail?: string; thumbnail?: string;