create-nextjs-stack 0.1.0

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/bin/cli.js +187 -0
  4. package/package.json +48 -0
  5. package/templates/admin/.env.example +11 -0
  6. package/templates/admin/README.md +82 -0
  7. package/templates/admin/app/(auth)/login/page.tsx +84 -0
  8. package/templates/admin/app/(dashboard)/[resource]/[id]/page.tsx +45 -0
  9. package/templates/admin/app/(dashboard)/[resource]/new/page.tsx +32 -0
  10. package/templates/admin/app/(dashboard)/[resource]/page.tsx +131 -0
  11. package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +22 -0
  12. package/templates/admin/app/(dashboard)/categories/new/page.tsx +5 -0
  13. package/templates/admin/app/(dashboard)/categories/page.tsx +33 -0
  14. package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +22 -0
  15. package/templates/admin/app/(dashboard)/clients/new/page.tsx +5 -0
  16. package/templates/admin/app/(dashboard)/clients/page.tsx +33 -0
  17. package/templates/admin/app/(dashboard)/dashboard/page.tsx +45 -0
  18. package/templates/admin/app/(dashboard)/layout.tsx +13 -0
  19. package/templates/admin/app/(dashboard)/products/[id]/page.tsx +22 -0
  20. package/templates/admin/app/(dashboard)/products/new/page.tsx +5 -0
  21. package/templates/admin/app/(dashboard)/products/page.tsx +33 -0
  22. package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +22 -0
  23. package/templates/admin/app/(dashboard)/projects/new/page.tsx +5 -0
  24. package/templates/admin/app/(dashboard)/projects/page.tsx +33 -0
  25. package/templates/admin/app/(dashboard)/users/[id]/page.tsx +22 -0
  26. package/templates/admin/app/(dashboard)/users/new/page.tsx +5 -0
  27. package/templates/admin/app/(dashboard)/users/page.tsx +33 -0
  28. package/templates/admin/app/actions/resources.ts +46 -0
  29. package/templates/admin/app/actions/upload.ts +58 -0
  30. package/templates/admin/app/favicon.ico +0 -0
  31. package/templates/admin/app/globals.css +23 -0
  32. package/templates/admin/app/layout.tsx +23 -0
  33. package/templates/admin/app/page.tsx +5 -0
  34. package/templates/admin/components/admin/AdminLayoutClient.tsx +22 -0
  35. package/templates/admin/components/admin/DeleteModal.tsx +90 -0
  36. package/templates/admin/components/admin/FormLayout.tsx +113 -0
  37. package/templates/admin/components/admin/ImageUpload.tsx +137 -0
  38. package/templates/admin/components/admin/ResourceFormClient.tsx +62 -0
  39. package/templates/admin/components/admin/Sidebar.tsx +74 -0
  40. package/templates/admin/components/admin/SubmitButton.tsx +34 -0
  41. package/templates/admin/components/admin/ToastProvider.tsx +8 -0
  42. package/templates/admin/components/categories/CategoryForm.tsx +24 -0
  43. package/templates/admin/components/categories/CategoryList.tsx +113 -0
  44. package/templates/admin/components/clients/ClientForm.tsx +24 -0
  45. package/templates/admin/components/clients/ClientList.tsx +113 -0
  46. package/templates/admin/components/products/ProductForm.tsx +24 -0
  47. package/templates/admin/components/products/ProductList.tsx +117 -0
  48. package/templates/admin/components/projects/ProjectForm.tsx +24 -0
  49. package/templates/admin/components/projects/ProjectList.tsx +121 -0
  50. package/templates/admin/components/users/UserForm.tsx +39 -0
  51. package/templates/admin/components/users/UserList.tsx +101 -0
  52. package/templates/admin/config/resources.ts +123 -0
  53. package/templates/admin/eslint.config.mjs +18 -0
  54. package/templates/admin/hooks/useResource.ts +86 -0
  55. package/templates/admin/lib/services/base.service.ts +106 -0
  56. package/templates/admin/lib/services/categories.service.ts +7 -0
  57. package/templates/admin/lib/services/clients.service.ts +7 -0
  58. package/templates/admin/lib/services/index.ts +27 -0
  59. package/templates/admin/lib/services/products.service.ts +9 -0
  60. package/templates/admin/lib/services/projects.service.ts +22 -0
  61. package/templates/admin/lib/services/resource.service.ts +26 -0
  62. package/templates/admin/lib/services/users.service.ts +9 -0
  63. package/templates/admin/lib/supabase/client.ts +9 -0
  64. package/templates/admin/lib/supabase/middleware.ts +57 -0
  65. package/templates/admin/lib/supabase/server.ts +29 -0
  66. package/templates/admin/middleware.ts +15 -0
  67. package/templates/admin/next.config.ts +10 -0
  68. package/templates/admin/package-lock.json +6768 -0
  69. package/templates/admin/package.json +33 -0
  70. package/templates/admin/postcss.config.mjs +7 -0
  71. package/templates/admin/public/file.svg +1 -0
  72. package/templates/admin/public/globe.svg +1 -0
  73. package/templates/admin/public/next.svg +1 -0
  74. package/templates/admin/public/vercel.svg +1 -0
  75. package/templates/admin/public/window.svg +1 -0
  76. package/templates/admin/supabase_mock_data.sql +57 -0
  77. package/templates/admin/supabase_schema.sql +93 -0
  78. package/templates/admin/tsconfig.json +34 -0
  79. package/templates/web/.env.example +21 -0
  80. package/templates/web/README.md +129 -0
  81. package/templates/web/components.json +22 -0
  82. package/templates/web/eslint.config.mjs +25 -0
  83. package/templates/web/next.config.ts +25 -0
  84. package/templates/web/package-lock.json +6778 -0
  85. package/templates/web/package.json +45 -0
  86. package/templates/web/postcss.config.mjs +5 -0
  87. package/templates/web/src/app/api/contact/route.ts +181 -0
  88. package/templates/web/src/app/api/revalidate/route.ts +95 -0
  89. package/templates/web/src/app/error.tsx +28 -0
  90. package/templates/web/src/app/globals.css +838 -0
  91. package/templates/web/src/app/layout.tsx +126 -0
  92. package/templates/web/src/app/loading.tsx +60 -0
  93. package/templates/web/src/app/not-found.tsx +68 -0
  94. package/templates/web/src/app/page.tsx +106 -0
  95. package/templates/web/src/app/robots.ts +12 -0
  96. package/templates/web/src/app/sitemap.ts +66 -0
  97. package/templates/web/src/components/home/StatsGrid.tsx +89 -0
  98. package/templates/web/src/hooks/useIntersectionObserver.ts +39 -0
  99. package/templates/web/src/lib/providers/StoreProvider.tsx +12 -0
  100. package/templates/web/src/lib/seo/index.ts +4 -0
  101. package/templates/web/src/lib/seo/metadata.ts +103 -0
  102. package/templates/web/src/lib/seo/seo.config.ts +161 -0
  103. package/templates/web/src/lib/seo/seo.types.ts +76 -0
  104. package/templates/web/src/lib/services/categories.service.ts +38 -0
  105. package/templates/web/src/lib/services/categoryService.ts +251 -0
  106. package/templates/web/src/lib/services/clientService.ts +132 -0
  107. package/templates/web/src/lib/services/clients.service.ts +20 -0
  108. package/templates/web/src/lib/services/productService.ts +261 -0
  109. package/templates/web/src/lib/services/products.service.ts +38 -0
  110. package/templates/web/src/lib/services/projectService.ts +234 -0
  111. package/templates/web/src/lib/services/projects.service.ts +38 -0
  112. package/templates/web/src/lib/services/users.service.ts +20 -0
  113. package/templates/web/src/lib/supabase/client.ts +42 -0
  114. package/templates/web/src/lib/supabase/constants.ts +25 -0
  115. package/templates/web/src/lib/supabase/server.ts +29 -0
  116. package/templates/web/src/lib/supabase/types.ts +112 -0
  117. package/templates/web/src/lib/utils/cache.ts +98 -0
  118. package/templates/web/src/lib/utils/rate-limiter.ts +102 -0
  119. package/templates/web/src/store/actions/index.ts +2 -0
  120. package/templates/web/src/store/index.ts +13 -0
  121. package/templates/web/src/store/reducers/index.ts +13 -0
  122. package/templates/web/src/store/types/index.ts +2 -0
  123. package/templates/web/tsconfig.json +41 -0
@@ -0,0 +1,131 @@
1
+ import { resources } from "@/config/resources";
2
+ import { notFound } from "next/navigation";
3
+ import Link from "next/link";
4
+ import * as Icons from "lucide-react";
5
+ import Image from "next/image";
6
+ import { getService } from "@/lib/services";
7
+
8
+ interface PageProps {
9
+ params: {
10
+ resource: string;
11
+ };
12
+ }
13
+
14
+ export default async function ResourceListPage({ params }: PageProps) {
15
+ const resourceName = params.resource;
16
+ const config = resources.find((r) => r.name === resourceName);
17
+
18
+ if (!config) {
19
+ return notFound();
20
+ }
21
+
22
+ const service = getService(config.table);
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ let items: any[] = [];
25
+ try {
26
+ items = await service.getAll();
27
+ } catch (error) {
28
+ console.error(error);
29
+ }
30
+
31
+ // Helper to resolve related data (basic implementation)
32
+ // For production, you might want to join tables in the query
33
+ // or fetch relations separately.
34
+
35
+ return (
36
+ <div>
37
+ <div className="flex justify-between items-center mb-6">
38
+ <h1 className="text-2xl font-bold">{config.plural}</h1>
39
+ <Link
40
+ href={`/${resourceName}/new`}
41
+ className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
42
+ >
43
+ Add {config.singular}
44
+ </Link>
45
+ </div>
46
+
47
+ <div className="bg-white rounded-lg shadow overflow-hidden">
48
+ <div className="overflow-x-auto">
49
+ <table className="w-full text-left">
50
+ <thead className="bg-gray-50 text-gray-500 uppercase text-xs font-semibold">
51
+ <tr>
52
+ {config.fields
53
+ .filter((f) => f.type !== "textarea") // Skip long text in table
54
+ .slice(0, 5) // Limit columns
55
+ .map((field) => (
56
+ <th key={field.name} className="px-6 py-3">
57
+ {field.label}
58
+ </th>
59
+ ))}
60
+ <th className="px-6 py-3 text-right">Actions</th>
61
+ </tr>
62
+ </thead>
63
+ <tbody className="divide-y divide-gray-200">
64
+ {items?.map((item) => (
65
+ <tr
66
+ key={item.id}
67
+ className="hover:bg-gray-50 transition"
68
+ >
69
+ {config.fields
70
+ .filter((f) => f.type !== "textarea")
71
+ .slice(0, 5)
72
+ .map((field) => (
73
+ <td
74
+ key={field.name}
75
+ className="px-6 py-4 text-gray-900"
76
+ >
77
+ {field.type === "image" ? (
78
+ <div className="relative w-10 h-10 bg-gray-100 rounded overflow-hidden">
79
+ {item[field.name] ? (
80
+ <Image
81
+ src={item[field.name]}
82
+ alt="Thumbnail"
83
+ fill
84
+ className="object-cover"
85
+ />
86
+ ) : (
87
+ <span className="text-xs text-gray-400 flex items-center justify-center h-full">No Img</span>
88
+ )}
89
+ </div>
90
+ ) : field.type === "boolean" ? (
91
+ <span
92
+ className={`px-2 py-1 text-xs rounded-full ${
93
+ item[field.name]
94
+ ? "bg-green-100 text-green-700"
95
+ : "bg-gray-100 text-gray-600"
96
+ }`}
97
+ >
98
+ {item[field.name] ? "Yes" : "No"}
99
+ </span>
100
+ ) : (
101
+ <span className="text-sm">
102
+ {/* Handle objects/arrays if needed */}
103
+ {String(item[field.name] || "-")}
104
+ </span>
105
+ )}
106
+ </td>
107
+ ))}
108
+ <td className="px-6 py-4 text-right">
109
+ <Link
110
+ href={`/${resourceName}/${item.id}`}
111
+ className="text-blue-600 hover:text-blue-800 font-medium text-sm"
112
+ >
113
+ Edit
114
+ </Link>
115
+ </td>
116
+ </tr>
117
+ ))}
118
+ {items?.length === 0 && (
119
+ <tr>
120
+ <td colSpan={10} className="px-6 py-8 text-center text-gray-500">
121
+ No records found.
122
+ </td>
123
+ </tr>
124
+ )}
125
+ </tbody>
126
+ </table>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,22 @@
1
+ import { getService } from "@/lib/services";
2
+ import { notFound } from "next/navigation";
3
+ import CategoryForm from "@/components/categories/CategoryForm";
4
+
5
+ export default async function EditCategoryPage({ params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params;
7
+ const service = getService("categories");
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 <CategoryForm mode="update" initialData={item} id={id} />;
22
+ }
@@ -0,0 +1,5 @@
1
+ import CategoryForm from "@/components/categories/CategoryForm";
2
+
3
+ export default function NewCategoryPage() {
4
+ return <CategoryForm mode="create" />;
5
+ }
@@ -0,0 +1,33 @@
1
+ import Link from "next/link";
2
+ import { getService } from "@/lib/services";
3
+ import CategoryList from "@/components/categories/CategoryList";
4
+ import { resources } from "@/config/resources";
5
+
6
+ export default async function CategoriesPage() {
7
+ const service = getService("categories");
8
+ const config = resources.find((r) => r.name === "categories")!;
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">Categories</h1>
22
+ <Link
23
+ href="/categories/new"
24
+ className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
25
+ >
26
+ Add Category
27
+ </Link>
28
+ </div>
29
+
30
+ <CategoryList items={items} config={config} />
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ import ClientForm from "@/components/clients/ClientForm";
2
+
3
+ export default function NewClientPage() {
4
+ return <ClientForm mode="create" />;
5
+ }
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,45 @@
1
+ import { resources } from "@/config/resources";
2
+ import { getServerClient } from "@/lib/supabase/server";
3
+ import * as Icons from "lucide-react";
4
+
5
+ export default async function DashboardPage() {
6
+ const supabase = await getServerClient();
7
+
8
+ const stats = await Promise.all(
9
+ resources.map(async (resource) => {
10
+ const { count } = await supabase
11
+ .from(resource.table)
12
+ .select("*", { count: "exact", head: true });
13
+ return {
14
+ label: resource.plural,
15
+ count: count || 0,
16
+ icon: resource.icon,
17
+ };
18
+ })
19
+ );
20
+
21
+ return (
22
+ <div>
23
+ <h1 className="text-2xl font-bold mb-6">Dashboard</h1>
24
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
25
+ {stats.map((stat) => {
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ const Icon = (Icons as any)[stat.icon] || Icons.Box;
28
+ return (
29
+ <div key={stat.label} className="bg-white p-6 rounded-lg shadow">
30
+ <div className="flex items-center justify-between">
31
+ <div>
32
+ <h3 className="text-gray-500 text-sm font-medium">{stat.label}</h3>
33
+ <p className="text-3xl font-bold text-gray-900 mt-1">{stat.count}</p>
34
+ </div>
35
+ <div className="p-3 bg-blue-100 rounded-full">
36
+ <Icon className="w-6 h-6 text-blue-600" />
37
+ </div>
38
+ </div>
39
+ </div>
40
+ );
41
+ })}
42
+ </div>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,13 @@
1
+ import AdminLayoutClient from "@/components/admin/AdminLayoutClient";
2
+
3
+ export default function DashboardLayout({
4
+ children,
5
+ }: {
6
+ children: React.ReactNode;
7
+ }) {
8
+ return (
9
+ <AdminLayoutClient>
10
+ {children}
11
+ </AdminLayoutClient>
12
+ );
13
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ import ProductForm from "@/components/products/ProductForm";
2
+
3
+ export default function NewProductPage() {
4
+ return <ProductForm mode="create" />;
5
+ }
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ import ProjectForm from "@/components/projects/ProjectForm";
2
+
3
+ export default function NewProjectPage() {
4
+ return <ProjectForm mode="create" />;
5
+ }
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ import UserForm from "@/components/users/UserForm";
2
+
3
+ export default function NewUserPage() {
4
+ return <UserForm mode="create" />;
5
+ }
@@ -0,0 +1,33 @@
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
+ }
@@ -0,0 +1,46 @@
1
+ "use server";
2
+
3
+ import { getService } from "@/lib/services";
4
+ import { revalidatePath } from "next/cache";
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ export async function getRelationOptions(table: string, displayField: string): Promise<any[]> {
8
+ const service = getService(table);
9
+ return await service.getOptions(displayField);
10
+ }
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ export async function createResource(table: string, data: any) {
14
+ try {
15
+ const service = getService(table);
16
+ const result = await service.create(data);
17
+ revalidatePath(`/${table}`);
18
+ return { data: result };
19
+ } catch (error: any) {
20
+ return { error: error.message || "Failed to create resource" };
21
+ }
22
+ }
23
+
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ export async function updateResource(table: string, id: string, data: any) {
26
+ try {
27
+ const service = getService(table);
28
+ const result = await service.update(id, data);
29
+ revalidatePath(`/${table}`);
30
+ revalidatePath(`/${table}/${id}`);
31
+ return { data: result };
32
+ } catch (error: any) {
33
+ return { error: error.message || "Failed to update resource" };
34
+ }
35
+ }
36
+
37
+ export async function deleteResource(table: string, id: string) {
38
+ try {
39
+ const service = getService(table);
40
+ await service.delete(id);
41
+ revalidatePath(`/${table}`);
42
+ return { success: true };
43
+ } catch (error: any) {
44
+ return { error: error.message || "Failed to delete resource" };
45
+ }
46
+ }
@@ -0,0 +1,58 @@
1
+ "use server";
2
+
3
+ import { v2 as cloudinary } from "cloudinary";
4
+
5
+ // Parse CLOUDINARY_URL manually to ensure config is correct
6
+ // This runs when the module is loaded
7
+ if (process.env.CLOUDINARY_URL) {
8
+ const matcher = /cloudinary:\/\/([^:]+):([^@]+)@(.+)/;
9
+ const match = process.env.CLOUDINARY_URL.match(matcher);
10
+ if (match) {
11
+ const [, apiKey, apiSecret, cloudName] = match;
12
+ cloudinary.config({
13
+ cloud_name: cloudName,
14
+ api_key: apiKey,
15
+ api_secret: apiSecret,
16
+ });
17
+ }
18
+ } else {
19
+ cloudinary.config({
20
+ cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
21
+ api_key: process.env.CLOUDINARY_API_KEY,
22
+ api_secret: process.env.CLOUDINARY_API_SECRET,
23
+ });
24
+ }
25
+
26
+ export async function uploadImage(formData: FormData) {
27
+ const file = formData.get("file") as File;
28
+
29
+ if (!file) {
30
+ return { error: "No file provided" };
31
+ }
32
+
33
+ // Debug: Log config state (safe)
34
+ const config = cloudinary.config();
35
+ console.log("Active Cloudinary Config:", {
36
+ cloud_name: config.cloud_name,
37
+ api_key: config.api_key ? "HIDDEN (Set)" : "MISSING",
38
+ api_secret: config.api_secret ? "HIDDEN (Set)" : "MISSING",
39
+ });
40
+
41
+ try {
42
+ const arrayBuffer = await file.arrayBuffer();
43
+ const buffer = Buffer.from(arrayBuffer);
44
+
45
+ // Try using upload instead of upload_stream for better error handling
46
+ const base64 = `data:${file.type};base64,${buffer.toString('base64')}`;
47
+
48
+ const result = await cloudinary.uploader.upload(base64, {
49
+ folder: "admin_uploads",
50
+ resource_type: "auto"
51
+ });
52
+
53
+ return { url: result.secure_url };
54
+ } catch (error) {
55
+ console.error("Upload error:", error);
56
+ return { error: "Failed to upload image" };
57
+ }
58
+ }
Binary file
@@ -0,0 +1,23 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --foreground-rgb: 0, 0, 0;
5
+ --background-start-rgb: 214, 219, 220;
6
+ --background-end-rgb: 255, 255, 255;
7
+ }
8
+
9
+ body {
10
+ color: rgb(var(--foreground-rgb));
11
+ background: linear-gradient(
12
+ to bottom,
13
+ transparent,
14
+ rgb(var(--background-end-rgb))
15
+ )
16
+ rgb(var(--background-start-rgb));
17
+ }
18
+
19
+ @layer utilities {
20
+ .text-balance {
21
+ text-wrap: balance;
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "./globals.css";
4
+ import "react-toastify/dist/ReactToastify.css";
5
+
6
+ const inter = Inter({ subsets: ["latin"] });
7
+
8
+ export const metadata: Metadata = {
9
+ title: "Admin Panel",
10
+ description: "Supabase Admin Template",
11
+ };
12
+
13
+ export default function RootLayout({
14
+ children,
15
+ }: Readonly<{
16
+ children: React.ReactNode;
17
+ }>) {
18
+ return (
19
+ <html lang="en">
20
+ <body className={inter.className}>{children}</body>
21
+ </html>
22
+ );
23
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ export default function Home() {
4
+ redirect("/dashboard");
5
+ }