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,24 +0,0 @@
1
- "use client";
2
-
3
- import ResourceFormClient from "@/components/admin/ResourceFormClient";
4
- import { resources } from "@/config/resources";
5
-
6
- interface ProjectFormProps {
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 ProjectForm({ mode, initialData, id }: ProjectFormProps) {
14
- const config = resources.find((r) => r.name === "projects")!;
15
-
16
- return (
17
- <ResourceFormClient
18
- config={config}
19
- mode={mode}
20
- initialData={initialData}
21
- id={id}
22
- />
23
- );
24
- }
@@ -1,121 +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 ProjectListProps {
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 ProjectList({ items, config }: ProjectListProps) {
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">Client</th>
41
- <th className="px-6 py-3">Slug</th>
42
- <th className="px-6 py-3">Published</th>
43
- <th className="px-6 py-3 text-right">Actions</th>
44
- </tr>
45
- </thead>
46
- <tbody className="divide-y divide-gray-200">
47
- {items?.map((item) => (
48
- <tr key={item.id} className="hover:bg-gray-50 transition">
49
- <td className="px-6 py-4">
50
- <div className="relative w-10 h-10 bg-gray-100 rounded overflow-hidden">
51
- {item.featured_image_url ? (
52
- <Image
53
- src={item.featured_image_url}
54
- alt={item.title}
55
- fill
56
- className="object-cover"
57
- />
58
- ) : (
59
- <span className="text-xs text-gray-400 flex items-center justify-center h-full">No Img</span>
60
- )}
61
- </div>
62
- </td>
63
- <td className="px-6 py-4 text-gray-900 font-medium">{item.title}</td>
64
- <td className="px-6 py-4 text-gray-500">
65
- {item.clients?.name || "-"}
66
- </td>
67
- <td className="px-6 py-4 text-gray-500">{item.slug}</td>
68
- <td className="px-6 py-4">
69
- <span
70
- className={`px-2 py-1 text-xs rounded-full ${
71
- item.published
72
- ? "bg-green-100 text-green-700"
73
- : "bg-gray-100 text-gray-600"
74
- }`}
75
- >
76
- {item.published ? "Published" : "Draft"}
77
- </span>
78
- </td>
79
- <td className="px-6 py-4 text-right">
80
- <div className="flex justify-end gap-2">
81
- <Link
82
- href={`/projects/${item.id}`}
83
- 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"
84
- title="Edit"
85
- >
86
- <Pencil className="w-4 h-4" />
87
- </Link>
88
- <button
89
- onClick={() => setDeleteId(item.id)}
90
- 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"
91
- title="Delete"
92
- >
93
- <Trash2 className="w-4 h-4" />
94
- </button>
95
- </div>
96
- </td>
97
- </tr>
98
- ))}
99
- {items?.length === 0 && (
100
- <tr>
101
- <td colSpan={6} className="px-6 py-8 text-center text-gray-500">
102
- No projects found.
103
- </td>
104
- </tr>
105
- )}
106
- </tbody>
107
- </table>
108
- </div>
109
- </div>
110
-
111
- <DeleteModal
112
- isOpen={!!deleteId}
113
- onClose={() => setDeleteId(null)}
114
- onConfirm={handleDelete}
115
- isLoading={isDeleting}
116
- title="Delete Project"
117
- description="Are you sure you want to delete this project? This action cannot be undone."
118
- />
119
- </>
120
- );
121
- }
@@ -1,39 +0,0 @@
1
- "use client";
2
-
3
- import ResourceFormClient from "@/components/admin/ResourceFormClient";
4
- import { resources } from "@/config/resources";
5
-
6
- interface UserFormProps {
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 UserForm({ mode, initialData, id }: UserFormProps) {
14
- const config = resources.find((r) => r.name === "users")!;
15
-
16
- return (
17
- <div>
18
- {mode === 'create' && (
19
- <div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6">
20
- <div className="flex">
21
- <div className="ml-3">
22
- <p className="text-sm text-yellow-700">
23
- Note: Creating a user here only creates a profile record.
24
- It does <strong>not</strong> create a login account.
25
- Use the Supabase Auth dashboard to invite users.
26
- </p>
27
- </div>
28
- </div>
29
- </div>
30
- )}
31
- <ResourceFormClient
32
- config={config}
33
- mode={mode}
34
- initialData={initialData}
35
- id={id}
36
- />
37
- </div>
38
- );
39
- }
@@ -1,101 +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 UserListProps {
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 UserList({ items, config }: UserListProps) {
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">Email</th>
38
- <th className="px-6 py-3">Full Name</th>
39
- <th className="px-6 py-3">Role</th>
40
- <th className="px-6 py-3 text-right">Actions</th>
41
- </tr>
42
- </thead>
43
- <tbody className="divide-y divide-gray-200">
44
- {items?.map((item) => (
45
- <tr key={item.id} className="hover:bg-gray-50 transition">
46
- <td className="px-6 py-4 text-gray-900 font-medium">{item.email}</td>
47
- <td className="px-6 py-4 text-gray-500">{item.full_name || "-"}</td>
48
- <td className="px-6 py-4">
49
- <span
50
- className={`px-2 py-1 text-xs rounded-full ${
51
- item.role === "admin"
52
- ? "bg-purple-100 text-purple-700"
53
- : "bg-blue-100 text-blue-700"
54
- }`}
55
- >
56
- {item.role}
57
- </span>
58
- </td>
59
- <td className="px-6 py-4 text-right">
60
- <div className="flex justify-end gap-2">
61
- <Link
62
- href={`/users/${item.id}`}
63
- 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"
64
- title="Edit"
65
- >
66
- <Pencil className="w-4 h-4" />
67
- </Link>
68
- <button
69
- onClick={() => setDeleteId(item.id)}
70
- 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"
71
- title="Delete"
72
- >
73
- <Trash2 className="w-4 h-4" />
74
- </button>
75
- </div>
76
- </td>
77
- </tr>
78
- ))}
79
- {items?.length === 0 && (
80
- <tr>
81
- <td colSpan={4} className="px-6 py-8 text-center text-gray-500">
82
- No users found.
83
- </td>
84
- </tr>
85
- )}
86
- </tbody>
87
- </table>
88
- </div>
89
- </div>
90
-
91
- <DeleteModal
92
- isOpen={!!deleteId}
93
- onClose={() => setDeleteId(null)}
94
- onConfirm={handleDelete}
95
- isLoading={isDeleting}
96
- title="Delete User"
97
- description="Are you sure you want to delete this user? This action cannot be undone."
98
- />
99
- </>
100
- );
101
- }
@@ -1,251 +0,0 @@
1
- import { getBrowserClient } from '@/lib/supabase/client';
2
- import { TABLE_NAMES, CACHE_TIMES } from '@/lib/supabase/constants';
3
- import { memoryCache } from '@/lib/utils/cache';
4
- import { perMinuteLimiter } from '@/lib/utils/rate-limiter';
5
- import type { Category, CategoryWithChildren } from '@/lib/supabase/types';
6
-
7
- /**
8
- * Category Service - Handles all category-related database operations
9
- * Implements singleton pattern with caching and rate limiting
10
- */
11
- export class CategoryService {
12
- private static instance: CategoryService;
13
-
14
- private constructor() {}
15
-
16
- /**
17
- * Get singleton instance
18
- */
19
- static getInstance(): CategoryService {
20
- if (!CategoryService.instance) {
21
- CategoryService.instance = new CategoryService();
22
- }
23
- return CategoryService.instance;
24
- }
25
-
26
- /**
27
- * Get all published categories
28
- */
29
- async getAll(): Promise<Category[]> {
30
- const cacheKey = 'categories:all';
31
-
32
- // Check memory cache
33
- const cached = memoryCache.get<Category[]>(cacheKey);
34
- if (cached) {
35
- return cached;
36
- }
37
-
38
- // Rate limiting
39
- const allowed = await perMinuteLimiter.checkLimit('categories:getAll');
40
- if (!allowed) {
41
- throw new Error('Rate limit exceeded. Please try again later.');
42
- }
43
-
44
- // Fetch from database
45
- const supabase = getBrowserClient();
46
- const { data, error } = await supabase
47
- .from(TABLE_NAMES.CATEGORIES)
48
- .select('*')
49
- .eq('published', true)
50
- .order('created_at', { ascending: false });
51
-
52
- if (error) {
53
- console.error('[CategoryService] Error fetching categories:', error);
54
- throw new Error(`Failed to fetch categories: ${error.message}`);
55
- }
56
-
57
- // Cache result
58
- memoryCache.set(cacheKey, data, CACHE_TIMES.CATEGORIES);
59
-
60
- return data;
61
- }
62
-
63
- /**
64
- * Get category by slug
65
- */
66
- async getBySlug(slug: string): Promise<Category | null> {
67
- const cacheKey = `categories:slug:${slug}`;
68
-
69
- const cached = memoryCache.get<Category>(cacheKey);
70
- if (cached) {
71
- return cached;
72
- }
73
-
74
- const allowed = await perMinuteLimiter.checkLimit('categories:getBySlug');
75
- if (!allowed) {
76
- throw new Error('Rate limit exceeded. Please try again later.');
77
- }
78
-
79
- const supabase = getBrowserClient();
80
- const { data, error } = await supabase
81
- .from(TABLE_NAMES.CATEGORIES)
82
- .select('*')
83
- .eq('slug', slug)
84
- .eq('published', true)
85
- .single();
86
-
87
- if (error) {
88
- if (error.code === 'PGRST116') {
89
- // Not found
90
- return null;
91
- }
92
- console.error('[CategoryService] Error fetching category:', error);
93
- throw new Error(`Failed to fetch category: ${error.message}`);
94
- }
95
-
96
- memoryCache.set(cacheKey, data, CACHE_TIMES.CATEGORIES);
97
- return data;
98
- }
99
-
100
- /**
101
- * Get featured categories
102
- */
103
- async getFeatured(): Promise<Category[]> {
104
- const cacheKey = 'categories:featured';
105
-
106
- const cached = memoryCache.get<Category[]>(cacheKey);
107
- if (cached) {
108
- return cached;
109
- }
110
-
111
- const allowed = await perMinuteLimiter.checkLimit('categories:getFeatured');
112
- if (!allowed) {
113
- throw new Error('Rate limit exceeded. Please try again later.');
114
- }
115
-
116
- const supabase = getBrowserClient();
117
- const { data, error } = await supabase
118
- .from(TABLE_NAMES.CATEGORIES)
119
- .select('*')
120
- .eq('published', true)
121
- .eq('featured', true)
122
- .order('created_at', { ascending: false });
123
-
124
- if (error) {
125
- console.error('[CategoryService] Error fetching featured categories:', error);
126
- throw new Error(`Failed to fetch featured categories: ${error.message}`);
127
- }
128
-
129
- memoryCache.set(cacheKey, data, CACHE_TIMES.CATEGORIES);
130
- return data;
131
- }
132
-
133
- /**
134
- * Get sub-categories of a parent category
135
- */
136
- async getSubCategories(parentId: string): Promise<Category[]> {
137
- const cacheKey = `categories:parent:${parentId}`;
138
-
139
- const cached = memoryCache.get<Category[]>(cacheKey);
140
- if (cached) {
141
- return cached;
142
- }
143
-
144
- const supabase = getBrowserClient();
145
- const { data, error } = await supabase
146
- .from(TABLE_NAMES.CATEGORIES)
147
- .select('*')
148
- .eq('parent_id', parentId)
149
- .eq('published', true)
150
- .order('created_at', { ascending: false });
151
-
152
- if (error) {
153
- console.error('[CategoryService] Error fetching sub-categories:', error);
154
- throw new Error(`Failed to fetch sub-categories: ${error.message}`);
155
- }
156
-
157
- memoryCache.set(cacheKey, data, CACHE_TIMES.CATEGORIES);
158
- return data;
159
- }
160
-
161
- /**
162
- * Build hierarchical category tree
163
- */
164
- async getTree(): Promise<CategoryWithChildren[]> {
165
- const cacheKey = 'categories:tree';
166
-
167
- const cached = memoryCache.get<CategoryWithChildren[]>(cacheKey);
168
- if (cached) {
169
- return cached;
170
- }
171
-
172
- // Get all categories
173
- const categories = await this.getAll();
174
-
175
- // Build category map
176
- const categoryMap = new Map<string, CategoryWithChildren>();
177
- categories.forEach((cat) => {
178
- categoryMap.set(cat.id, { ...cat, children: [] });
179
- });
180
-
181
- // Build tree structure
182
- const tree: CategoryWithChildren[] = [];
183
- categories.forEach((cat) => {
184
- const category = categoryMap.get(cat.id)!;
185
-
186
- if (cat.parent_id) {
187
- const parent = categoryMap.get(cat.parent_id);
188
- if (parent) {
189
- if (!parent.children) {
190
- parent.children = [];
191
- }
192
- parent.children.push(category);
193
- }
194
- } else {
195
- // Root level category
196
- tree.push(category);
197
- }
198
- });
199
-
200
- memoryCache.set(cacheKey, tree, CACHE_TIMES.CATEGORIES);
201
- return tree;
202
- }
203
-
204
- /**
205
- * Get category by ID
206
- */
207
- async getById(id: string): Promise<Category | null> {
208
- const cacheKey = `categories:id:${id}`;
209
-
210
- const cached = memoryCache.get<Category>(cacheKey);
211
- if (cached) {
212
- return cached;
213
- }
214
-
215
- const supabase = getBrowserClient();
216
- const { data, error } = await supabase
217
- .from(TABLE_NAMES.CATEGORIES)
218
- .select('*')
219
- .eq('id', id)
220
- .eq('published', true)
221
- .single();
222
-
223
- if (error) {
224
- if (error.code === 'PGRST116') {
225
- return null;
226
- }
227
- console.error('[CategoryService] Error fetching category by ID:', error);
228
- throw new Error(`Failed to fetch category: ${error.message}`);
229
- }
230
-
231
- memoryCache.set(cacheKey, data, CACHE_TIMES.CATEGORIES);
232
- return data;
233
- }
234
-
235
- /**
236
- * Invalidate all category caches
237
- */
238
- invalidateCache(): void {
239
- memoryCache.invalidate('categories:');
240
- }
241
-
242
- /**
243
- * Invalidate specific category cache
244
- */
245
- invalidateCategoryCache(slug: string): void {
246
- memoryCache.invalidateKey(`categories:slug:${slug}`);
247
- }
248
- }
249
-
250
- // Export singleton instance
251
- export const categoryService = CategoryService.getInstance();
@@ -1,132 +0,0 @@
1
- import { getBrowserClient } from '@/lib/supabase/client';
2
- import { TABLE_NAMES, CACHE_TIMES } from '@/lib/supabase/constants';
3
- import { memoryCache } from '@/lib/utils/cache';
4
- import { perMinuteLimiter } from '@/lib/utils/rate-limiter';
5
- import type { Client } from '@/lib/supabase/types';
6
-
7
- /**
8
- * Client Service - Handles all client-related database operations
9
- * Implements singleton pattern with caching and rate limiting
10
- */
11
- export class ClientService {
12
- private static instance: ClientService;
13
-
14
- private constructor() {}
15
-
16
- static getInstance(): ClientService {
17
- if (!ClientService.instance) {
18
- ClientService.instance = new ClientService();
19
- }
20
- return ClientService.instance;
21
- }
22
-
23
- /**
24
- * Get all published clients ordered by sort_order
25
- */
26
- async getAll(): Promise<Client[]> {
27
- const cacheKey = 'clients:all';
28
-
29
- const cached = memoryCache.get<Client[]>(cacheKey);
30
- if (cached) {
31
- return cached;
32
- }
33
-
34
- const allowed = await perMinuteLimiter.checkLimit('clients:getAll');
35
- if (!allowed) {
36
- throw new Error('Rate limit exceeded. Please try again later.');
37
- }
38
-
39
- const supabase = getBrowserClient();
40
- const { data, error } = await supabase
41
- .from(TABLE_NAMES.CLIENTS)
42
- .select('*')
43
- .eq('published', true)
44
- .order('sort_order', { ascending: true });
45
-
46
- if (error) {
47
- console.error('[ClientService] Error fetching clients:', error);
48
- throw new Error(`Failed to fetch clients: ${error.message}`);
49
- }
50
-
51
- memoryCache.set(cacheKey, data, CACHE_TIMES.CLIENTS);
52
- return data;
53
- }
54
-
55
- /**
56
- * Get client by name
57
- */
58
- async getByName(name: string): Promise<Client | null> {
59
- const cacheKey = `clients:name:${name}`;
60
-
61
- const cached = memoryCache.get<Client>(cacheKey);
62
- if (cached) {
63
- return cached;
64
- }
65
-
66
- const supabase = getBrowserClient();
67
- const { data, error } = await supabase
68
- .from(TABLE_NAMES.CLIENTS)
69
- .select('*')
70
- .eq('name', name)
71
- .eq('published', true)
72
- .single();
73
-
74
- if (error) {
75
- if (error.code === 'PGRST116') {
76
- return null;
77
- }
78
- console.error('[ClientService] Error fetching client:', error);
79
- throw new Error(`Failed to fetch client: ${error.message}`);
80
- }
81
-
82
- memoryCache.set(cacheKey, data, CACHE_TIMES.CLIENTS);
83
- return data;
84
- }
85
-
86
- /**
87
- * Get client by ID
88
- */
89
- async getById(id: string): Promise<Client | null> {
90
- const cacheKey = `clients:id:${id}`;
91
-
92
- const cached = memoryCache.get<Client>(cacheKey);
93
- if (cached) {
94
- return cached;
95
- }
96
-
97
- const supabase = getBrowserClient();
98
- const { data, error } = await supabase
99
- .from(TABLE_NAMES.CLIENTS)
100
- .select('*')
101
- .eq('id', id)
102
- .eq('published', true)
103
- .single();
104
-
105
- if (error) {
106
- if (error.code === 'PGRST116') {
107
- return null;
108
- }
109
- console.error('[ClientService] Error fetching client by ID:', error);
110
- throw new Error(`Failed to fetch client: ${error.message}`);
111
- }
112
-
113
- memoryCache.set(cacheKey, data, CACHE_TIMES.CLIENTS);
114
- return data;
115
- }
116
-
117
- /**
118
- * Invalidate all client caches
119
- */
120
- invalidateCache(): void {
121
- memoryCache.invalidate('clients:');
122
- }
123
-
124
- /**
125
- * Invalidate specific client cache
126
- */
127
- invalidateClientCache(name: string): void {
128
- memoryCache.invalidateKey(`clients:name:${name}`);
129
- }
130
- }
131
-
132
- export const clientService = ClientService.getInstance();