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-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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user