create-nextjs-stack 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +502 -31
  2. package/package.json +1 -1
  3. package/templates/admin/app/(dashboard)/[resource]/[id]/page.tsx +6 -5
  4. package/templates/admin/app/(dashboard)/[resource]/new/page.tsx +11 -12
  5. package/templates/admin/app/(dashboard)/[resource]/page.tsx +4 -3
  6. package/templates/admin/app/actions/upload.ts +0 -7
  7. package/templates/admin/app/globals.css +112 -14
  8. package/templates/admin/components/admin/Sidebar.tsx +2 -5
  9. package/templates/admin/components.json +22 -0
  10. package/templates/admin/hooks/useResource.ts +3 -0
  11. package/templates/admin/lib/services/resource.service.ts +2 -7
  12. package/templates/admin/lib/supabase/client.ts +8 -4
  13. package/templates/admin/lib/supabase/server.ts +1 -1
  14. package/templates/admin/lib/utils.ts +6 -0
  15. package/templates/admin/middleware.ts +1 -3
  16. package/templates/admin/next.config.ts +10 -2
  17. package/templates/admin/package-lock.json +13 -1
  18. package/templates/admin/package.json +3 -1
  19. package/templates/web/.env.example +5 -1
  20. package/templates/web/package-lock.json +49 -18
  21. package/templates/web/package.json +1 -2
  22. package/templates/web/postcss.config.mjs +3 -1
  23. package/templates/web/src/app/api/revalidate/route.ts +46 -87
  24. package/templates/web/src/app/globals.css +1 -13
  25. package/templates/web/src/app/layout.tsx +4 -46
  26. package/templates/web/src/app/robots.ts +1 -1
  27. package/templates/web/src/app/sitemap.ts +27 -31
  28. package/templates/web/src/lib/seo/metadata.ts +5 -5
  29. package/templates/web/src/lib/seo/seo.config.ts +55 -59
  30. package/templates/web/src/lib/seo/seo.types.ts +1 -7
  31. package/templates/web/src/lib/services/categories.service.ts +3 -3
  32. package/templates/web/src/lib/services/clients.service.ts +2 -2
  33. package/templates/web/src/lib/services/products.service.ts +3 -3
  34. package/templates/web/src/lib/services/projects.service.ts +3 -3
  35. package/templates/web/src/lib/services/users.service.ts +2 -2
  36. package/templates/web/src/lib/supabase/client.ts +1 -1
  37. package/templates/web/src/lib/supabase/server.ts +1 -1
  38. package/templates/web/src/store/index.ts +4 -9
  39. package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +0 -22
  40. package/templates/admin/app/(dashboard)/categories/new/page.tsx +0 -5
  41. package/templates/admin/app/(dashboard)/categories/page.tsx +0 -33
  42. package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +0 -22
  43. package/templates/admin/app/(dashboard)/clients/new/page.tsx +0 -5
  44. package/templates/admin/app/(dashboard)/clients/page.tsx +0 -33
  45. package/templates/admin/app/(dashboard)/products/[id]/page.tsx +0 -22
  46. package/templates/admin/app/(dashboard)/products/new/page.tsx +0 -5
  47. package/templates/admin/app/(dashboard)/products/page.tsx +0 -33
  48. package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +0 -22
  49. package/templates/admin/app/(dashboard)/projects/new/page.tsx +0 -5
  50. package/templates/admin/app/(dashboard)/projects/page.tsx +0 -33
  51. package/templates/admin/app/(dashboard)/users/[id]/page.tsx +0 -22
  52. package/templates/admin/app/(dashboard)/users/new/page.tsx +0 -5
  53. package/templates/admin/app/(dashboard)/users/page.tsx +0 -33
  54. package/templates/admin/components/categories/CategoryForm.tsx +0 -24
  55. package/templates/admin/components/categories/CategoryList.tsx +0 -113
  56. package/templates/admin/components/clients/ClientForm.tsx +0 -24
  57. package/templates/admin/components/clients/ClientList.tsx +0 -113
  58. package/templates/admin/components/products/ProductForm.tsx +0 -24
  59. package/templates/admin/components/products/ProductList.tsx +0 -117
  60. package/templates/admin/components/projects/ProjectForm.tsx +0 -24
  61. package/templates/admin/components/projects/ProjectList.tsx +0 -121
  62. package/templates/admin/components/users/UserForm.tsx +0 -39
  63. package/templates/admin/components/users/UserList.tsx +0 -101
  64. package/templates/web/src/lib/services/categoryService.ts +0 -251
  65. package/templates/web/src/lib/services/clientService.ts +0 -132
  66. package/templates/web/src/lib/services/productService.ts +0 -261
  67. package/templates/web/src/lib/services/projectService.ts +0 -234
  68. package/templates/web/src/lib/utils/cache.ts +0 -98
  69. package/templates/web/src/lib/utils/rate-limiter.ts +0 -102
@@ -1,22 +0,0 @@
1
- import { getService } from "@/lib/services";
2
- import { notFound } from "next/navigation";
3
- import ClientForm from "@/components/clients/ClientForm";
4
-
5
- export default async function EditClientPage({ params }: { params: Promise<{ id: string }> }) {
6
- const { id } = await params;
7
- const service = getService("clients");
8
-
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- let item: any = null;
11
- try {
12
- item = await service.getById(id);
13
- } catch (error) {
14
- console.error(error);
15
- }
16
-
17
- if (!item) {
18
- return notFound();
19
- }
20
-
21
- return <ClientForm mode="update" initialData={item} id={id} />;
22
- }
@@ -1,5 +0,0 @@
1
- import ClientForm from "@/components/clients/ClientForm";
2
-
3
- export default function NewClientPage() {
4
- return <ClientForm mode="create" />;
5
- }
@@ -1,33 +0,0 @@
1
- import Link from "next/link";
2
- import { getService } from "@/lib/services";
3
- import ClientList from "@/components/clients/ClientList";
4
- import { resources } from "@/config/resources";
5
-
6
- export default async function ClientsPage() {
7
- const service = getService("clients");
8
- const config = resources.find((r) => r.name === "clients")!;
9
-
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- let items: any[] = [];
12
- try {
13
- items = await service.getAll();
14
- } catch (error) {
15
- console.error(error);
16
- }
17
-
18
- return (
19
- <div>
20
- <div className="flex justify-between items-center mb-6">
21
- <h1 className="text-2xl font-bold">Clients</h1>
22
- <Link
23
- href="/clients/new"
24
- className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
25
- >
26
- Add Client
27
- </Link>
28
- </div>
29
-
30
- <ClientList items={items} config={config} />
31
- </div>
32
- );
33
- }
@@ -1,22 +0,0 @@
1
- import { getService } from "@/lib/services";
2
- import { notFound } from "next/navigation";
3
- import ProductForm from "@/components/products/ProductForm";
4
-
5
- export default async function EditProductPage({ params }: { params: Promise<{ id: string }> }) {
6
- const { id } = await params;
7
- const service = getService("products");
8
-
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- let item: any = null;
11
- try {
12
- item = await service.getById(id);
13
- } catch (error) {
14
- console.error(error);
15
- }
16
-
17
- if (!item) {
18
- return notFound();
19
- }
20
-
21
- return <ProductForm mode="update" initialData={item} id={id} />;
22
- }
@@ -1,5 +0,0 @@
1
- import ProductForm from "@/components/products/ProductForm";
2
-
3
- export default function NewProductPage() {
4
- return <ProductForm mode="create" />;
5
- }
@@ -1,33 +0,0 @@
1
- import Link from "next/link";
2
- import { getService } from "@/lib/services";
3
- import ProductList from "@/components/products/ProductList";
4
- import { resources } from "@/config/resources";
5
-
6
- export default async function ProductsPage() {
7
- const service = getService("products");
8
- const config = resources.find((r) => r.name === "products")!;
9
-
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- let items: any[] = [];
12
- try {
13
- items = await service.getAll();
14
- } catch (error) {
15
- console.error(error);
16
- }
17
-
18
- return (
19
- <div>
20
- <div className="flex justify-between items-center mb-6">
21
- <h1 className="text-2xl font-bold">Products</h1>
22
- <Link
23
- href="/products/new"
24
- className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
25
- >
26
- Add Product
27
- </Link>
28
- </div>
29
-
30
- <ProductList items={items} config={config} />
31
- </div>
32
- );
33
- }
@@ -1,22 +0,0 @@
1
- import { getService } from "@/lib/services";
2
- import { notFound } from "next/navigation";
3
- import ProjectForm from "@/components/projects/ProjectForm";
4
-
5
- export default async function EditProjectPage({ params }: { params: Promise<{ id: string }> }) {
6
- const { id } = await params;
7
- const service = getService("projects");
8
-
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- let item: any = null;
11
- try {
12
- item = await service.getById(id);
13
- } catch (error) {
14
- console.error(error);
15
- }
16
-
17
- if (!item) {
18
- return notFound();
19
- }
20
-
21
- return <ProjectForm mode="update" initialData={item} id={id} />;
22
- }
@@ -1,5 +0,0 @@
1
- import ProjectForm from "@/components/projects/ProjectForm";
2
-
3
- export default function NewProjectPage() {
4
- return <ProjectForm mode="create" />;
5
- }
@@ -1,33 +0,0 @@
1
- import Link from "next/link";
2
- import { getService } from "@/lib/services";
3
- import ProjectList from "@/components/projects/ProjectList";
4
- import { resources } from "@/config/resources";
5
-
6
- export default async function ProjectsPage() {
7
- const service = getService("projects");
8
- const config = resources.find((r) => r.name === "projects")!;
9
-
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- let items: any[] = [];
12
- try {
13
- items = await service.getAll();
14
- } catch (error) {
15
- console.error(error);
16
- }
17
-
18
- return (
19
- <div>
20
- <div className="flex justify-between items-center mb-6">
21
- <h1 className="text-2xl font-bold">Projects</h1>
22
- <Link
23
- href="/projects/new"
24
- className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
25
- >
26
- Add Project
27
- </Link>
28
- </div>
29
-
30
- <ProjectList items={items} config={config} />
31
- </div>
32
- );
33
- }
@@ -1,22 +0,0 @@
1
- import { getService } from "@/lib/services";
2
- import { notFound } from "next/navigation";
3
- import UserForm from "@/components/users/UserForm";
4
-
5
- export default async function EditUserPage({ params }: { params: Promise<{ id: string }> }) {
6
- const { id } = await params;
7
- const service = getService("users");
8
-
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- let item: any = null;
11
- try {
12
- item = await service.getById(id);
13
- } catch (error) {
14
- console.error(error);
15
- }
16
-
17
- if (!item) {
18
- return notFound();
19
- }
20
-
21
- return <UserForm mode="update" initialData={item} id={id} />;
22
- }
@@ -1,5 +0,0 @@
1
- import UserForm from "@/components/users/UserForm";
2
-
3
- export default function NewUserPage() {
4
- return <UserForm mode="create" />;
5
- }
@@ -1,33 +0,0 @@
1
- import Link from "next/link";
2
- import { getService } from "@/lib/services";
3
- import UserList from "@/components/users/UserList";
4
- import { resources } from "@/config/resources";
5
-
6
- export default async function UsersPage() {
7
- const service = getService("users");
8
- const config = resources.find((r) => r.name === "users")!;
9
-
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- let items: any[] = [];
12
- try {
13
- items = await service.getAll();
14
- } catch (error) {
15
- console.error(error);
16
- }
17
-
18
- return (
19
- <div>
20
- <div className="flex justify-between items-center mb-6">
21
- <h1 className="text-2xl font-bold">Users</h1>
22
- <Link
23
- href="/users/new"
24
- className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
25
- >
26
- Add User
27
- </Link>
28
- </div>
29
-
30
- <UserList items={items} config={config} />
31
- </div>
32
- );
33
- }
@@ -1,24 +0,0 @@
1
- "use client";
2
-
3
- import ResourceFormClient from "@/components/admin/ResourceFormClient";
4
- import { resources } from "@/config/resources";
5
-
6
- interface CategoryFormProps {
7
- mode: "create" | "update";
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- initialData?: any;
10
- id?: string;
11
- }
12
-
13
- export default function CategoryForm({ mode, initialData, id }: CategoryFormProps) {
14
- const config = resources.find((r) => r.name === "categories")!;
15
-
16
- return (
17
- <ResourceFormClient
18
- config={config}
19
- mode={mode}
20
- initialData={initialData}
21
- id={id}
22
- />
23
- );
24
- }
@@ -1,113 +0,0 @@
1
- "use client";
2
-
3
- import Link from "next/link";
4
- import { ResourceConfig } from "@/config/resources";
5
- import { useResource } from "@/hooks/useResource";
6
- import { Pencil, Trash2 } from "lucide-react";
7
-
8
- interface CategoryListProps {
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
- items: any[];
11
- config: ResourceConfig;
12
- }
13
-
14
- import { useState } from "react";
15
- import DeleteModal from "@/components/admin/DeleteModal";
16
-
17
- export default function CategoryList({ items, config }: CategoryListProps) {
18
- const { remove, isDeleting } = useResource(config);
19
- const [deleteId, setDeleteId] = useState<string | null>(null);
20
-
21
- const handleDelete = async () => {
22
- if (deleteId) {
23
- const success = await remove(deleteId);
24
- if (success) {
25
- setDeleteId(null);
26
- }
27
- }
28
- };
29
-
30
- return (
31
- <>
32
- <div className="bg-white rounded-lg shadow overflow-hidden">
33
- <div className="overflow-x-auto">
34
- <table className="w-full text-left">
35
- <thead className="bg-gray-50 text-gray-500 uppercase text-xs font-semibold">
36
- <tr>
37
- <th className="px-6 py-3">Title</th>
38
- <th className="px-6 py-3">Slug</th>
39
- <th className="px-6 py-3">Featured</th>
40
- <th className="px-6 py-3">Published</th>
41
- <th className="px-6 py-3 text-right">Actions</th>
42
- </tr>
43
- </thead>
44
- <tbody className="divide-y divide-gray-200">
45
- {items?.map((item) => (
46
- <tr key={item.id} className="hover:bg-gray-50 transition">
47
- <td className="px-6 py-4 text-gray-900 font-medium">{item.title}</td>
48
- <td className="px-6 py-4 text-gray-500">{item.slug}</td>
49
- <td className="px-6 py-4">
50
- <span
51
- className={`px-2 py-1 text-xs rounded-full ${
52
- item.featured
53
- ? "bg-yellow-100 text-yellow-700"
54
- : "bg-gray-100 text-gray-600"
55
- }`}
56
- >
57
- {item.featured ? "Yes" : "No"}
58
- </span>
59
- </td>
60
- <td className="px-6 py-4">
61
- <span
62
- className={`px-2 py-1 text-xs rounded-full ${
63
- item.published
64
- ? "bg-green-100 text-green-700"
65
- : "bg-gray-100 text-gray-600"
66
- }`}
67
- >
68
- {item.published ? "Published" : "Draft"}
69
- </span>
70
- </td>
71
- <td className="px-6 py-4 text-right">
72
- <div className="flex justify-end gap-2">
73
- <Link
74
- href={`/categories/${item.id}`}
75
- className="flex items-center text-indigo-600 hover:text-indigo-900 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-md transition-colors duration-200"
76
- title="Edit"
77
- >
78
- <Pencil className="w-4 h-4" />
79
- </Link>
80
- <button
81
- onClick={() => setDeleteId(item.id)}
82
- className="flex items-center text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-md transition-colors duration-200"
83
- title="Delete"
84
- >
85
- <Trash2 className="w-4 h-4" />
86
- </button>
87
- </div>
88
- </td>
89
- </tr>
90
- ))}
91
- {items?.length === 0 && (
92
- <tr>
93
- <td colSpan={5} className="px-6 py-8 text-center text-gray-500">
94
- No categories found.
95
- </td>
96
- </tr>
97
- )}
98
- </tbody>
99
- </table>
100
- </div>
101
- </div>
102
-
103
- <DeleteModal
104
- isOpen={!!deleteId}
105
- onClose={() => setDeleteId(null)}
106
- onConfirm={handleDelete}
107
- isLoading={isDeleting}
108
- title="Delete Category"
109
- description="Are you sure you want to delete this category? This action cannot be undone."
110
- />
111
- </>
112
- );
113
- }
@@ -1,24 +0,0 @@
1
- "use client";
2
-
3
- import ResourceFormClient from "@/components/admin/ResourceFormClient";
4
- import { resources } from "@/config/resources";
5
-
6
- interface ClientFormProps {
7
- mode: "create" | "update";
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- initialData?: any;
10
- id?: string;
11
- }
12
-
13
- export default function ClientForm({ mode, initialData, id }: ClientFormProps) {
14
- const config = resources.find((r) => r.name === "clients")!;
15
-
16
- return (
17
- <ResourceFormClient
18
- config={config}
19
- mode={mode}
20
- initialData={initialData}
21
- id={id}
22
- />
23
- );
24
- }
@@ -1,113 +0,0 @@
1
- "use client";
2
-
3
- import Link from "next/link";
4
- import Image from "next/image";
5
- import { ResourceConfig } from "@/config/resources";
6
- import { useResource } from "@/hooks/useResource";
7
- import { Pencil, Trash2 } from "lucide-react";
8
-
9
- interface ClientListProps {
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- items: any[];
12
- config: ResourceConfig;
13
- }
14
-
15
- import { useState } from "react";
16
- import DeleteModal from "@/components/admin/DeleteModal";
17
-
18
- export default function ClientList({ items, config }: ClientListProps) {
19
- const { remove, isDeleting } = useResource(config);
20
- const [deleteId, setDeleteId] = useState<string | null>(null);
21
-
22
- const handleDelete = async () => {
23
- if (deleteId) {
24
- const success = await remove(deleteId);
25
- if (success) {
26
- setDeleteId(null);
27
- }
28
- }
29
- };
30
-
31
- return (
32
- <>
33
- <div className="bg-white rounded-lg shadow overflow-hidden">
34
- <div className="overflow-x-auto">
35
- <table className="w-full text-left">
36
- <thead className="bg-gray-50 text-gray-500 uppercase text-xs font-semibold">
37
- <tr>
38
- <th className="px-6 py-3">Logo</th>
39
- <th className="px-6 py-3">Name</th>
40
- <th className="px-6 py-3">Website</th>
41
- <th className="px-6 py-3 text-right">Actions</th>
42
- </tr>
43
- </thead>
44
- <tbody className="divide-y divide-gray-200">
45
- {items?.map((item) => (
46
- <tr key={item.id} className="hover:bg-gray-50 transition">
47
- <td className="px-6 py-4">
48
- <div className="relative w-10 h-10 bg-gray-100 rounded overflow-hidden">
49
- {item.logo_url ? (
50
- <Image
51
- src={item.logo_url}
52
- alt={item.name}
53
- fill
54
- className="object-contain"
55
- />
56
- ) : (
57
- <span className="text-xs text-gray-400 flex items-center justify-center h-full">No Logo</span>
58
- )}
59
- </div>
60
- </td>
61
- <td className="px-6 py-4 text-gray-900 font-medium">{item.name}</td>
62
- <td className="px-6 py-4 text-gray-500">
63
- {item.website ? (
64
- <a href={item.website} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
65
- {item.website}
66
- </a>
67
- ) : (
68
- "-"
69
- )}
70
- </td>
71
- <td className="px-6 py-4 text-right">
72
- <div className="flex justify-end gap-2">
73
- <Link
74
- href={`/clients/${item.id}`}
75
- className="flex items-center text-indigo-600 hover:text-indigo-900 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-md transition-colors duration-200"
76
- title="Edit"
77
- >
78
- <Pencil className="w-4 h-4" />
79
- </Link>
80
- <button
81
- onClick={() => setDeleteId(item.id)}
82
- className="flex items-center text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-md transition-colors duration-200"
83
- title="Delete"
84
- >
85
- <Trash2 className="w-4 h-4" />
86
- </button>
87
- </div>
88
- </td>
89
- </tr>
90
- ))}
91
- {items?.length === 0 && (
92
- <tr>
93
- <td colSpan={4} className="px-6 py-8 text-center text-gray-500">
94
- No clients found.
95
- </td>
96
- </tr>
97
- )}
98
- </tbody>
99
- </table>
100
- </div>
101
- </div>
102
-
103
- <DeleteModal
104
- isOpen={!!deleteId}
105
- onClose={() => setDeleteId(null)}
106
- onConfirm={handleDelete}
107
- isLoading={isDeleting}
108
- title="Delete Client"
109
- description="Are you sure you want to delete this client? This action cannot be undone."
110
- />
111
- </>
112
- );
113
- }
@@ -1,24 +0,0 @@
1
- "use client";
2
-
3
- import ResourceFormClient from "@/components/admin/ResourceFormClient";
4
- import { resources } from "@/config/resources";
5
-
6
- interface ProductFormProps {
7
- mode: "create" | "update";
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- initialData?: any;
10
- id?: string;
11
- }
12
-
13
- export default function ProductForm({ mode, initialData, id }: ProductFormProps) {
14
- const config = resources.find((r) => r.name === "products")!;
15
-
16
- return (
17
- <ResourceFormClient
18
- config={config}
19
- mode={mode}
20
- initialData={initialData}
21
- id={id}
22
- />
23
- );
24
- }
@@ -1,117 +0,0 @@
1
- "use client";
2
-
3
- import Link from "next/link";
4
- import Image from "next/image";
5
- import { ResourceConfig } from "@/config/resources";
6
- import { useResource } from "@/hooks/useResource";
7
- import { Pencil, Trash2 } from "lucide-react";
8
-
9
- interface ProductListProps {
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- items: any[];
12
- config: ResourceConfig;
13
- }
14
-
15
- import { useState } from "react";
16
- import DeleteModal from "@/components/admin/DeleteModal";
17
-
18
- export default function ProductList({ items, config }: ProductListProps) {
19
- const { remove, isDeleting } = useResource(config);
20
- const [deleteId, setDeleteId] = useState<string | null>(null);
21
-
22
- const handleDelete = async () => {
23
- if (deleteId) {
24
- const success = await remove(deleteId);
25
- if (success) {
26
- setDeleteId(null);
27
- }
28
- }
29
- };
30
-
31
- return (
32
- <>
33
- <div className="bg-white rounded-lg shadow overflow-hidden">
34
- <div className="overflow-x-auto">
35
- <table className="w-full text-left">
36
- <thead className="bg-gray-50 text-gray-500 uppercase text-xs font-semibold">
37
- <tr>
38
- <th className="px-6 py-3">Image</th>
39
- <th className="px-6 py-3">Title</th>
40
- <th className="px-6 py-3">Slug</th>
41
- <th className="px-6 py-3">Published</th>
42
- <th className="px-6 py-3 text-right">Actions</th>
43
- </tr>
44
- </thead>
45
- <tbody className="divide-y divide-gray-200">
46
- {items?.map((item) => (
47
- <tr key={item.id} className="hover:bg-gray-50 transition">
48
- <td className="px-6 py-4">
49
- <div className="relative w-10 h-10 bg-gray-100 rounded overflow-hidden">
50
- {item.featured_image_url ? (
51
- <Image
52
- src={item.featured_image_url}
53
- alt={item.title}
54
- fill
55
- className="object-cover"
56
- />
57
- ) : (
58
- <span className="text-xs text-gray-400 flex items-center justify-center h-full">No Img</span>
59
- )}
60
- </div>
61
- </td>
62
- <td className="px-6 py-4 text-gray-900 font-medium">{item.title}</td>
63
- <td className="px-6 py-4 text-gray-500">{item.slug}</td>
64
- <td className="px-6 py-4">
65
- <span
66
- className={`px-2 py-1 text-xs rounded-full ${
67
- item.published
68
- ? "bg-green-100 text-green-700"
69
- : "bg-gray-100 text-gray-600"
70
- }`}
71
- >
72
- {item.published ? "Published" : "Draft"}
73
- </span>
74
- </td>
75
- <td className="px-6 py-4 text-right">
76
- <div className="flex justify-end gap-2">
77
- <Link
78
- href={`/products/${item.id}`}
79
- className="flex items-center text-indigo-600 hover:text-indigo-900 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-md transition-colors duration-200"
80
- title="Edit"
81
- >
82
- <Pencil className="w-4 h-4" />
83
- </Link>
84
- <button
85
- onClick={() => setDeleteId(item.id)}
86
- className="flex items-center text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-md transition-colors duration-200"
87
- title="Delete"
88
- >
89
- <Trash2 className="w-4 h-4" />
90
- </button>
91
- </div>
92
- </td>
93
- </tr>
94
- ))}
95
- {items?.length === 0 && (
96
- <tr>
97
- <td colSpan={5} className="px-6 py-8 text-center text-gray-500">
98
- No products found.
99
- </td>
100
- </tr>
101
- )}
102
- </tbody>
103
- </table>
104
- </div>
105
- </div>
106
-
107
- <DeleteModal
108
- isOpen={!!deleteId}
109
- onClose={() => setDeleteId(null)}
110
- onConfirm={handleDelete}
111
- isLoading={isDeleting}
112
- title="Delete Product"
113
- description="Are you sure you want to delete this product? This action cannot be undone."
114
- />
115
- </>
116
- );
117
- }