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,123 @@
|
|
|
1
|
+
export interface ResourceConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
singular: string;
|
|
4
|
+
plural: string;
|
|
5
|
+
icon: string;
|
|
6
|
+
path: string;
|
|
7
|
+
table: string;
|
|
8
|
+
fields: ResourceField[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ResourceField {
|
|
12
|
+
name: string;
|
|
13
|
+
type:
|
|
14
|
+
| "text"
|
|
15
|
+
| "textarea"
|
|
16
|
+
| "number"
|
|
17
|
+
| "boolean"
|
|
18
|
+
| "select"
|
|
19
|
+
| "image"
|
|
20
|
+
| "date";
|
|
21
|
+
label: string;
|
|
22
|
+
required?: boolean;
|
|
23
|
+
options?: { value: string; label: string }[];
|
|
24
|
+
relation?: {
|
|
25
|
+
table: string;
|
|
26
|
+
display: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const resources: ResourceConfig[] = [
|
|
31
|
+
{
|
|
32
|
+
name: "categories",
|
|
33
|
+
singular: "Category",
|
|
34
|
+
plural: "Categories",
|
|
35
|
+
icon: "Layers",
|
|
36
|
+
path: "/categories",
|
|
37
|
+
table: "categories",
|
|
38
|
+
fields: [
|
|
39
|
+
{ name: "title", type: "text", label: "Title", required: true },
|
|
40
|
+
{ name: "slug", type: "text", label: "Slug", required: true },
|
|
41
|
+
{ name: "description", type: "textarea", label: "Description" },
|
|
42
|
+
{ name: "featured", type: "boolean", label: "Featured" },
|
|
43
|
+
{ name: "published", type: "boolean", label: "Published" },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "clients",
|
|
48
|
+
singular: "Client",
|
|
49
|
+
plural: "Clients",
|
|
50
|
+
icon: "Users",
|
|
51
|
+
path: "/clients",
|
|
52
|
+
table: "clients",
|
|
53
|
+
fields: [
|
|
54
|
+
{ name: "name", type: "text", label: "Name", required: true },
|
|
55
|
+
{ name: "logo_url", type: "image", label: "Logo" },
|
|
56
|
+
{ name: "website", type: "text", label: "Website" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "products",
|
|
61
|
+
singular: "Product",
|
|
62
|
+
plural: "Products",
|
|
63
|
+
icon: "Package",
|
|
64
|
+
path: "/products",
|
|
65
|
+
table: "products",
|
|
66
|
+
fields: [
|
|
67
|
+
{ name: "title", type: "text", label: "Title", required: true },
|
|
68
|
+
{ name: "slug", type: "text", label: "Slug", required: true },
|
|
69
|
+
{ name: "description", type: "textarea", label: "Description" },
|
|
70
|
+
{ name: "featured_image_url", type: "image", label: "Featured Image" },
|
|
71
|
+
{
|
|
72
|
+
name: "category_id",
|
|
73
|
+
type: "select",
|
|
74
|
+
label: "Category",
|
|
75
|
+
relation: { table: "categories", display: "title" },
|
|
76
|
+
},
|
|
77
|
+
{ name: "featured", type: "boolean", label: "Featured" },
|
|
78
|
+
{ name: "published", type: "boolean", label: "Published" },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "projects",
|
|
83
|
+
singular: "Project",
|
|
84
|
+
plural: "Projects",
|
|
85
|
+
icon: "Briefcase",
|
|
86
|
+
path: "/projects",
|
|
87
|
+
table: "projects",
|
|
88
|
+
fields: [
|
|
89
|
+
{ name: "title", type: "text", label: "Title", required: true },
|
|
90
|
+
{ name: "slug", type: "text", label: "Slug", required: true },
|
|
91
|
+
{ name: "description", type: "textarea", label: "Description" },
|
|
92
|
+
{
|
|
93
|
+
name: "client_id",
|
|
94
|
+
type: "select",
|
|
95
|
+
label: "Client",
|
|
96
|
+
relation: { table: "clients", display: "name" },
|
|
97
|
+
},
|
|
98
|
+
{ name: "featured_image_url", type: "image", label: "Featured Image" },
|
|
99
|
+
{ name: "published", type: "boolean", label: "Published" },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "users",
|
|
104
|
+
singular: "User",
|
|
105
|
+
plural: "Users",
|
|
106
|
+
icon: "Settings",
|
|
107
|
+
path: "/users",
|
|
108
|
+
table: "users",
|
|
109
|
+
fields: [
|
|
110
|
+
{ name: "email", type: "text", label: "Email", required: true },
|
|
111
|
+
{ name: "full_name", type: "text", label: "Full Name" },
|
|
112
|
+
{
|
|
113
|
+
name: "role",
|
|
114
|
+
type: "select",
|
|
115
|
+
label: "Role",
|
|
116
|
+
options: [
|
|
117
|
+
{ value: "admin", label: "Admin" },
|
|
118
|
+
{ value: "editor", label: "Editor" },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { toast } from "react-toastify";
|
|
6
|
+
import { createResource, updateResource, deleteResource } from "@/app/actions/resources";
|
|
7
|
+
import { ResourceConfig } from "@/config/resources";
|
|
8
|
+
|
|
9
|
+
export function useResource(config: ResourceConfig) {
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
12
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
const create = async (data: any) => {
|
|
16
|
+
setIsSubmitting(true);
|
|
17
|
+
try {
|
|
18
|
+
const result = await createResource(config.table, data);
|
|
19
|
+
if (result.error) {
|
|
20
|
+
toast.error(result.error);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
toast.success(`${config.singular} created successfully`);
|
|
24
|
+
router.push(`/${config.name}`);
|
|
25
|
+
router.refresh();
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(error);
|
|
29
|
+
toast.error("Something went wrong");
|
|
30
|
+
return false;
|
|
31
|
+
} finally {
|
|
32
|
+
setIsSubmitting(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
const update = async (id: string, data: any) => {
|
|
38
|
+
setIsSubmitting(true);
|
|
39
|
+
try {
|
|
40
|
+
const result = await updateResource(config.table, id, data);
|
|
41
|
+
if (result.error) {
|
|
42
|
+
toast.error(result.error);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
toast.success(`${config.singular} updated successfully`);
|
|
46
|
+
router.push(`/${config.name}`);
|
|
47
|
+
router.refresh();
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(error);
|
|
51
|
+
toast.error("Something went wrong");
|
|
52
|
+
return false;
|
|
53
|
+
} finally {
|
|
54
|
+
setIsSubmitting(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const remove = async (id: string) => {
|
|
59
|
+
setIsDeleting(true);
|
|
60
|
+
try {
|
|
61
|
+
const result = await deleteResource(config.table, id);
|
|
62
|
+
if (result.error) {
|
|
63
|
+
toast.error(result.error);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
toast.success("Record deleted");
|
|
67
|
+
router.push(`/${config.name}`);
|
|
68
|
+
router.refresh();
|
|
69
|
+
return true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(error);
|
|
72
|
+
toast.error("Failed to delete");
|
|
73
|
+
return false;
|
|
74
|
+
} finally {
|
|
75
|
+
setIsDeleting(false);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
create,
|
|
81
|
+
update,
|
|
82
|
+
remove,
|
|
83
|
+
isSubmitting,
|
|
84
|
+
isDeleting
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { getServerClient } from "@/lib/supabase/server";
|
|
3
|
+
|
|
4
|
+
export class BaseService<T = any> {
|
|
5
|
+
protected table: string;
|
|
6
|
+
|
|
7
|
+
constructor(table: string) {
|
|
8
|
+
this.table = table;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
protected async getClient(): Promise<SupabaseClient> {
|
|
12
|
+
return getServerClient();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async getAll() {
|
|
16
|
+
const supabase = await this.getClient();
|
|
17
|
+
const { data, error } = await supabase
|
|
18
|
+
.from(this.table)
|
|
19
|
+
.select("*")
|
|
20
|
+
.order("created_at", { ascending: false });
|
|
21
|
+
|
|
22
|
+
if (error) {
|
|
23
|
+
console.error(`Error fetching ${this.table}:`, error);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return data as T[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getById(id: string) {
|
|
31
|
+
const supabase = await this.getClient();
|
|
32
|
+
const { data, error } = await supabase
|
|
33
|
+
.from(this.table)
|
|
34
|
+
.select("*")
|
|
35
|
+
.eq("id", id)
|
|
36
|
+
.single();
|
|
37
|
+
|
|
38
|
+
if (error) {
|
|
39
|
+
console.error(`Error fetching ${this.table} ${id}:`, error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return data as T;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async create(payload: Partial<T>) {
|
|
47
|
+
const supabase = await this.getClient();
|
|
48
|
+
const { data, error } = await supabase
|
|
49
|
+
.from(this.table)
|
|
50
|
+
.insert(payload)
|
|
51
|
+
.select()
|
|
52
|
+
.single();
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
console.error(`Error creating ${this.table}:`, error);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return data as T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async update(id: string, payload: Partial<T>) {
|
|
63
|
+
const supabase = await this.getClient();
|
|
64
|
+
const { data, error } = await supabase
|
|
65
|
+
.from(this.table)
|
|
66
|
+
.update(payload)
|
|
67
|
+
.eq("id", id)
|
|
68
|
+
.select()
|
|
69
|
+
.single();
|
|
70
|
+
|
|
71
|
+
if (error) {
|
|
72
|
+
console.error(`Error updating ${this.table} ${id}:`, error);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return data as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async delete(id: string) {
|
|
80
|
+
const supabase = await this.getClient();
|
|
81
|
+
const { error } = await supabase.from(this.table).delete().eq("id", id);
|
|
82
|
+
|
|
83
|
+
if (error) {
|
|
84
|
+
console.error(`Error deleting ${this.table} ${id}:`, error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
async getOptions(displayField: string): Promise<any[]> {
|
|
93
|
+
const supabase = await this.getClient();
|
|
94
|
+
const { data, error } = await supabase
|
|
95
|
+
.from(this.table)
|
|
96
|
+
.select(`id, ${displayField}`)
|
|
97
|
+
.order(displayField);
|
|
98
|
+
|
|
99
|
+
if (error) {
|
|
100
|
+
console.error(`Error fetching options for ${this.table}:`, error);
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return data || [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BaseService } from "./base.service";
|
|
2
|
+
import { ProductService } from "./products.service";
|
|
3
|
+
import { UserService } from "./users.service";
|
|
4
|
+
import { ClientService } from "./clients.service";
|
|
5
|
+
import { ProjectService } from "./projects.service";
|
|
6
|
+
import { CategoryService } from "./categories.service";
|
|
7
|
+
|
|
8
|
+
// Registry mapping resource names to Service instances
|
|
9
|
+
const services: Record<string, BaseService> = {
|
|
10
|
+
products: new ProductService(),
|
|
11
|
+
users: new UserService(),
|
|
12
|
+
clients: new ClientService(),
|
|
13
|
+
projects: new ProjectService(),
|
|
14
|
+
categories: new CategoryService(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getService = (resourceName: string): BaseService => {
|
|
18
|
+
const service = services[resourceName];
|
|
19
|
+
if (!service) {
|
|
20
|
+
// Fallback or throw error. For template flexibility, we can return a generic BaseService
|
|
21
|
+
// or strictly throw if the resource isn't configured.
|
|
22
|
+
// For now, let's return a generic BaseService to support new tables added to config/resources.ts
|
|
23
|
+
// without needing a dedicated file immediately.
|
|
24
|
+
return new BaseService(resourceName);
|
|
25
|
+
}
|
|
26
|
+
return service;
|
|
27
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BaseService } from "./base.service";
|
|
2
|
+
|
|
3
|
+
export class ProjectService extends BaseService {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("projects");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async getAll() {
|
|
9
|
+
const supabase = await this.getClient();
|
|
10
|
+
const { data, error } = await supabase
|
|
11
|
+
.from(this.table)
|
|
12
|
+
.select("*, clients(name)")
|
|
13
|
+
.order("created_at", { ascending: false });
|
|
14
|
+
|
|
15
|
+
if (error) {
|
|
16
|
+
console.error(`Error fetching ${this.table}:`, error);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseService } from './base.service';
|
|
2
|
+
import { getServerClient } from "@/lib/supabase/server";
|
|
3
|
+
|
|
4
|
+
export class ResourceService extends BaseService {
|
|
5
|
+
constructor(table: string) {
|
|
6
|
+
super(table);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Fetch options for relation fields (e.g. categories for products)
|
|
10
|
+
async getRelationOptions(table: string, displayField: string) {
|
|
11
|
+
const supabase = await getServerClient();
|
|
12
|
+
const { data, error } = await supabase
|
|
13
|
+
.from(table)
|
|
14
|
+
.select(`id, ${displayField}`)
|
|
15
|
+
.order(displayField);
|
|
16
|
+
|
|
17
|
+
if (error) throw error;
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const resourceService = new ResourceService(''); // Placeholder, instantiated per use really, strictly for static methods if needed or we change design.
|
|
23
|
+
|
|
24
|
+
// Improved design: We don't export a single instance but rather helper functions or the class itself.
|
|
25
|
+
// But for FormLayout client-side fetching, we need a server action or API route.
|
|
26
|
+
// Let's create a server action for this instead.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createServerClient } from '@supabase/ssr'
|
|
2
|
+
import { NextResponse, type NextRequest } from 'next/server'
|
|
3
|
+
|
|
4
|
+
export async function updateSession(request: NextRequest) {
|
|
5
|
+
let supabaseResponse = NextResponse.next({
|
|
6
|
+
request,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const supabase = createServerClient(
|
|
10
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
11
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
12
|
+
{
|
|
13
|
+
cookies: {
|
|
14
|
+
getAll() {
|
|
15
|
+
return request.cookies.getAll()
|
|
16
|
+
},
|
|
17
|
+
setAll(cookiesToSet) {
|
|
18
|
+
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
|
|
19
|
+
supabaseResponse = NextResponse.next({
|
|
20
|
+
request,
|
|
21
|
+
})
|
|
22
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
23
|
+
supabaseResponse.cookies.set(name, value, options)
|
|
24
|
+
)
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// IMPORTANT: Avoid writing any logic between createServerClient and
|
|
31
|
+
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
|
|
32
|
+
// issues with users being randomly logged out.
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
data: { user },
|
|
36
|
+
} = await supabase.auth.getUser()
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
!user &&
|
|
40
|
+
!request.nextUrl.pathname.startsWith('/login') &&
|
|
41
|
+
!request.nextUrl.pathname.startsWith('/auth')
|
|
42
|
+
) {
|
|
43
|
+
// no user, potentially respond by redirecting the user to the login page
|
|
44
|
+
const url = request.nextUrl.clone()
|
|
45
|
+
url.pathname = '/login'
|
|
46
|
+
return NextResponse.redirect(url)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Redirect to dashboard if authenticated and on login page
|
|
50
|
+
if (user && request.nextUrl.pathname === '/login') {
|
|
51
|
+
const url = request.nextUrl.clone()
|
|
52
|
+
url.pathname = '/dashboard'
|
|
53
|
+
return NextResponse.redirect(url)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return supabaseResponse
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createServerClient } from "@supabase/ssr";
|
|
2
|
+
import { cookies } from "next/headers";
|
|
3
|
+
|
|
4
|
+
export async function getServerClient() {
|
|
5
|
+
const cookieStore = await cookies();
|
|
6
|
+
|
|
7
|
+
return createServerClient(
|
|
8
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
9
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
10
|
+
{
|
|
11
|
+
cookies: {
|
|
12
|
+
getAll() {
|
|
13
|
+
return cookieStore.getAll();
|
|
14
|
+
},
|
|
15
|
+
setAll(cookiesToSet) {
|
|
16
|
+
try {
|
|
17
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
18
|
+
cookieStore.set(name, value, options)
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
// The `setAll` method was called from a Server Component.
|
|
22
|
+
// This can be ignored if you have middleware refreshing
|
|
23
|
+
// user sessions.
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type NextRequest } from "next/server";
|
|
2
|
+
import { updateSession } from "@/lib/supabase/middleware";
|
|
3
|
+
|
|
4
|
+
export async function middleware(request: NextRequest) {
|
|
5
|
+
console.log("Middleware running");
|
|
6
|
+
console.log("URL:", process.env.NEXT_PUBLIC_SUPABASE_URL ? "Defined" : "Undefined");
|
|
7
|
+
console.log("Anon Key:", process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ? "Defined" : "Undefined");
|
|
8
|
+
return await updateSession(request);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const config = {
|
|
12
|
+
matcher: [
|
|
13
|
+
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
|
14
|
+
],
|
|
15
|
+
};
|