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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/bin/cli.js +187 -0
- package/package.json +48 -0
- package/templates/admin/.env.example +11 -0
- package/templates/admin/README.md +82 -0
- package/templates/admin/app/(auth)/login/page.tsx +84 -0
- package/templates/admin/app/(dashboard)/[resource]/[id]/page.tsx +45 -0
- package/templates/admin/app/(dashboard)/[resource]/new/page.tsx +32 -0
- package/templates/admin/app/(dashboard)/[resource]/page.tsx +131 -0
- package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/categories/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/categories/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/clients/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/clients/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/dashboard/page.tsx +45 -0
- package/templates/admin/app/(dashboard)/layout.tsx +13 -0
- package/templates/admin/app/(dashboard)/products/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/products/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/products/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/projects/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/projects/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/users/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/users/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/users/page.tsx +33 -0
- package/templates/admin/app/actions/resources.ts +46 -0
- package/templates/admin/app/actions/upload.ts +58 -0
- package/templates/admin/app/favicon.ico +0 -0
- package/templates/admin/app/globals.css +23 -0
- package/templates/admin/app/layout.tsx +23 -0
- package/templates/admin/app/page.tsx +5 -0
- package/templates/admin/components/admin/AdminLayoutClient.tsx +22 -0
- package/templates/admin/components/admin/DeleteModal.tsx +90 -0
- package/templates/admin/components/admin/FormLayout.tsx +113 -0
- package/templates/admin/components/admin/ImageUpload.tsx +137 -0
- package/templates/admin/components/admin/ResourceFormClient.tsx +62 -0
- package/templates/admin/components/admin/Sidebar.tsx +74 -0
- package/templates/admin/components/admin/SubmitButton.tsx +34 -0
- package/templates/admin/components/admin/ToastProvider.tsx +8 -0
- package/templates/admin/components/categories/CategoryForm.tsx +24 -0
- package/templates/admin/components/categories/CategoryList.tsx +113 -0
- package/templates/admin/components/clients/ClientForm.tsx +24 -0
- package/templates/admin/components/clients/ClientList.tsx +113 -0
- package/templates/admin/components/products/ProductForm.tsx +24 -0
- package/templates/admin/components/products/ProductList.tsx +117 -0
- package/templates/admin/components/projects/ProjectForm.tsx +24 -0
- package/templates/admin/components/projects/ProjectList.tsx +121 -0
- package/templates/admin/components/users/UserForm.tsx +39 -0
- package/templates/admin/components/users/UserList.tsx +101 -0
- package/templates/admin/config/resources.ts +123 -0
- package/templates/admin/eslint.config.mjs +18 -0
- package/templates/admin/hooks/useResource.ts +86 -0
- package/templates/admin/lib/services/base.service.ts +106 -0
- package/templates/admin/lib/services/categories.service.ts +7 -0
- package/templates/admin/lib/services/clients.service.ts +7 -0
- package/templates/admin/lib/services/index.ts +27 -0
- package/templates/admin/lib/services/products.service.ts +9 -0
- package/templates/admin/lib/services/projects.service.ts +22 -0
- package/templates/admin/lib/services/resource.service.ts +26 -0
- package/templates/admin/lib/services/users.service.ts +9 -0
- package/templates/admin/lib/supabase/client.ts +9 -0
- package/templates/admin/lib/supabase/middleware.ts +57 -0
- package/templates/admin/lib/supabase/server.ts +29 -0
- package/templates/admin/middleware.ts +15 -0
- package/templates/admin/next.config.ts +10 -0
- package/templates/admin/package-lock.json +6768 -0
- package/templates/admin/package.json +33 -0
- package/templates/admin/postcss.config.mjs +7 -0
- package/templates/admin/public/file.svg +1 -0
- package/templates/admin/public/globe.svg +1 -0
- package/templates/admin/public/next.svg +1 -0
- package/templates/admin/public/vercel.svg +1 -0
- package/templates/admin/public/window.svg +1 -0
- package/templates/admin/supabase_mock_data.sql +57 -0
- package/templates/admin/supabase_schema.sql +93 -0
- package/templates/admin/tsconfig.json +34 -0
- package/templates/web/.env.example +21 -0
- package/templates/web/README.md +129 -0
- package/templates/web/components.json +22 -0
- package/templates/web/eslint.config.mjs +25 -0
- package/templates/web/next.config.ts +25 -0
- package/templates/web/package-lock.json +6778 -0
- package/templates/web/package.json +45 -0
- package/templates/web/postcss.config.mjs +5 -0
- package/templates/web/src/app/api/contact/route.ts +181 -0
- package/templates/web/src/app/api/revalidate/route.ts +95 -0
- package/templates/web/src/app/error.tsx +28 -0
- package/templates/web/src/app/globals.css +838 -0
- package/templates/web/src/app/layout.tsx +126 -0
- package/templates/web/src/app/loading.tsx +60 -0
- package/templates/web/src/app/not-found.tsx +68 -0
- package/templates/web/src/app/page.tsx +106 -0
- package/templates/web/src/app/robots.ts +12 -0
- package/templates/web/src/app/sitemap.ts +66 -0
- package/templates/web/src/components/home/StatsGrid.tsx +89 -0
- package/templates/web/src/hooks/useIntersectionObserver.ts +39 -0
- package/templates/web/src/lib/providers/StoreProvider.tsx +12 -0
- package/templates/web/src/lib/seo/index.ts +4 -0
- package/templates/web/src/lib/seo/metadata.ts +103 -0
- package/templates/web/src/lib/seo/seo.config.ts +161 -0
- package/templates/web/src/lib/seo/seo.types.ts +76 -0
- package/templates/web/src/lib/services/categories.service.ts +38 -0
- package/templates/web/src/lib/services/categoryService.ts +251 -0
- package/templates/web/src/lib/services/clientService.ts +132 -0
- package/templates/web/src/lib/services/clients.service.ts +20 -0
- package/templates/web/src/lib/services/productService.ts +261 -0
- package/templates/web/src/lib/services/products.service.ts +38 -0
- package/templates/web/src/lib/services/projectService.ts +234 -0
- package/templates/web/src/lib/services/projects.service.ts +38 -0
- package/templates/web/src/lib/services/users.service.ts +20 -0
- package/templates/web/src/lib/supabase/client.ts +42 -0
- package/templates/web/src/lib/supabase/constants.ts +25 -0
- package/templates/web/src/lib/supabase/server.ts +29 -0
- package/templates/web/src/lib/supabase/types.ts +112 -0
- package/templates/web/src/lib/utils/cache.ts +98 -0
- package/templates/web/src/lib/utils/rate-limiter.ts +102 -0
- package/templates/web/src/store/actions/index.ts +2 -0
- package/templates/web/src/store/index.ts +13 -0
- package/templates/web/src/store/reducers/index.ts +13 -0
- package/templates/web/src/store/types/index.ts +2 -0
- 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,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,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,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,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,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,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
|
+
}
|