create-nextjs-stack 0.1.1 → 0.1.4

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 (79) hide show
  1. package/README.md +502 -31
  2. package/package.json +6 -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/app/layout.tsx +4 -1
  9. package/templates/admin/components/admin/Sidebar.tsx +2 -5
  10. package/templates/admin/components.json +22 -0
  11. package/templates/admin/hooks/useResource.ts +3 -0
  12. package/templates/admin/lib/services/resource.service.ts +2 -7
  13. package/templates/admin/lib/supabase/client.ts +8 -4
  14. package/templates/admin/lib/supabase/server.ts +1 -1
  15. package/templates/admin/lib/utils.ts +6 -0
  16. package/templates/admin/middleware.ts +1 -3
  17. package/templates/admin/next.config.ts +10 -2
  18. package/templates/admin/package-lock.json +13 -1
  19. package/templates/admin/package.json +11 -5
  20. package/templates/admin/src/lib/providers/StoreProvider.tsx +12 -0
  21. package/templates/admin/src/store/actions/index.ts +2 -0
  22. package/templates/admin/src/store/hooks.ts +11 -0
  23. package/templates/admin/src/store/index.ts +17 -0
  24. package/templates/admin/src/store/reducers/index.ts +11 -0
  25. package/templates/admin/src/store/types/index.ts +2 -0
  26. package/templates/admin/tsconfig.json +1 -1
  27. package/templates/web/.env.example +5 -1
  28. package/templates/web/package-lock.json +49 -18
  29. package/templates/web/package.json +3 -4
  30. package/templates/web/postcss.config.mjs +3 -1
  31. package/templates/web/src/app/api/revalidate/route.ts +46 -87
  32. package/templates/web/src/app/globals.css +1 -13
  33. package/templates/web/src/app/layout.tsx +4 -46
  34. package/templates/web/src/app/robots.ts +1 -1
  35. package/templates/web/src/app/sitemap.ts +27 -31
  36. package/templates/web/src/lib/seo/metadata.ts +5 -5
  37. package/templates/web/src/lib/seo/seo.config.ts +55 -59
  38. package/templates/web/src/lib/seo/seo.types.ts +1 -7
  39. package/templates/web/src/lib/services/categories.service.ts +3 -3
  40. package/templates/web/src/lib/services/clients.service.ts +2 -2
  41. package/templates/web/src/lib/services/products.service.ts +3 -3
  42. package/templates/web/src/lib/services/projects.service.ts +3 -3
  43. package/templates/web/src/lib/services/users.service.ts +2 -2
  44. package/templates/web/src/lib/supabase/client.ts +1 -1
  45. package/templates/web/src/lib/supabase/server.ts +1 -1
  46. package/templates/web/src/store/hooks.ts +11 -0
  47. package/templates/web/src/store/index.ts +11 -7
  48. package/templates/web/src/store/reducers/index.ts +1 -3
  49. package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +0 -22
  50. package/templates/admin/app/(dashboard)/categories/new/page.tsx +0 -5
  51. package/templates/admin/app/(dashboard)/categories/page.tsx +0 -33
  52. package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +0 -22
  53. package/templates/admin/app/(dashboard)/clients/new/page.tsx +0 -5
  54. package/templates/admin/app/(dashboard)/clients/page.tsx +0 -33
  55. package/templates/admin/app/(dashboard)/products/[id]/page.tsx +0 -22
  56. package/templates/admin/app/(dashboard)/products/new/page.tsx +0 -5
  57. package/templates/admin/app/(dashboard)/products/page.tsx +0 -33
  58. package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +0 -22
  59. package/templates/admin/app/(dashboard)/projects/new/page.tsx +0 -5
  60. package/templates/admin/app/(dashboard)/projects/page.tsx +0 -33
  61. package/templates/admin/app/(dashboard)/users/[id]/page.tsx +0 -22
  62. package/templates/admin/app/(dashboard)/users/new/page.tsx +0 -5
  63. package/templates/admin/app/(dashboard)/users/page.tsx +0 -33
  64. package/templates/admin/components/categories/CategoryForm.tsx +0 -24
  65. package/templates/admin/components/categories/CategoryList.tsx +0 -113
  66. package/templates/admin/components/clients/ClientForm.tsx +0 -24
  67. package/templates/admin/components/clients/ClientList.tsx +0 -113
  68. package/templates/admin/components/products/ProductForm.tsx +0 -24
  69. package/templates/admin/components/products/ProductList.tsx +0 -117
  70. package/templates/admin/components/projects/ProjectForm.tsx +0 -24
  71. package/templates/admin/components/projects/ProjectList.tsx +0 -121
  72. package/templates/admin/components/users/UserForm.tsx +0 -39
  73. package/templates/admin/components/users/UserList.tsx +0 -101
  74. package/templates/web/src/lib/services/categoryService.ts +0 -251
  75. package/templates/web/src/lib/services/clientService.ts +0 -132
  76. package/templates/web/src/lib/services/productService.ts +0 -261
  77. package/templates/web/src/lib/services/projectService.ts +0 -234
  78. package/templates/web/src/lib/utils/cache.ts +0 -98
  79. package/templates/web/src/lib/utils/rate-limiter.ts +0 -102
@@ -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();
@@ -1,261 +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 { Product, ProductWithCategory } from '@/lib/supabase/types';
6
-
7
- interface GetProductsOptions {
8
- limit?: number;
9
- offset?: number;
10
- categoryId?: string;
11
- featured?: boolean;
12
- }
13
-
14
- /**
15
- * Product Service - Handles all product-related database operations
16
- * Implements singleton pattern with caching and rate limiting
17
- */
18
- export class ProductService {
19
- private static instance: ProductService;
20
-
21
- private constructor() {}
22
-
23
- static getInstance(): ProductService {
24
- if (!ProductService.instance) {
25
- ProductService.instance = new ProductService();
26
- }
27
- return ProductService.instance;
28
- }
29
-
30
- /**
31
- * Get all published products with optional filtering
32
- */
33
- async getAll(options: GetProductsOptions = {}): Promise<Product[]> {
34
- const { limit = 100, offset = 0, categoryId, featured } = options;
35
- const cacheKey = `products:all:${limit}:${offset}:${categoryId || 'none'}:${featured || 'all'}`;
36
-
37
- const cached = memoryCache.get<Product[]>(cacheKey);
38
- if (cached) {
39
- return cached;
40
- }
41
-
42
- const allowed = await perMinuteLimiter.checkLimit('products:getAll');
43
- if (!allowed) {
44
- throw new Error('Rate limit exceeded. Please try again later.');
45
- }
46
-
47
- const supabase = getBrowserClient();
48
- let query = supabase
49
- .from(TABLE_NAMES.PRODUCTS)
50
- .select('*')
51
- .eq('published', true)
52
- .order('created_at', { ascending: false })
53
- .range(offset, offset + limit - 1);
54
-
55
- if (categoryId) {
56
- query = query.eq('category_id', categoryId);
57
- }
58
-
59
- if (featured !== undefined) {
60
- query = query.eq('featured', featured);
61
- }
62
-
63
- const { data, error } = await query;
64
-
65
- if (error) {
66
- console.error('[ProductService] Error fetching products:', error);
67
- throw new Error(`Failed to fetch products: ${error.message}`);
68
- }
69
-
70
- memoryCache.set(cacheKey, data, CACHE_TIMES.PRODUCTS);
71
- return data;
72
- }
73
-
74
- /**
75
- * Get product by slug with category information
76
- */
77
- async getBySlug(slug: string): Promise<ProductWithCategory | null> {
78
- const cacheKey = `products:slug:${slug}`;
79
-
80
- const cached = memoryCache.get<ProductWithCategory>(cacheKey);
81
- if (cached) {
82
- return cached;
83
- }
84
-
85
- const allowed = await perMinuteLimiter.checkLimit('products:getBySlug');
86
- if (!allowed) {
87
- throw new Error('Rate limit exceeded. Please try again later.');
88
- }
89
-
90
- const supabase = getBrowserClient();
91
- const { data, error } = await supabase
92
- .from(TABLE_NAMES.PRODUCTS)
93
- .select(`
94
- *,
95
- category:categories(*)
96
- `)
97
- .eq('slug', slug)
98
- .eq('published', true)
99
- .single();
100
-
101
- if (error) {
102
- if (error.code === 'PGRST116') {
103
- return null;
104
- }
105
- console.error('[ProductService] Error fetching product:', error);
106
- throw new Error(`Failed to fetch product: ${error.message}`);
107
- }
108
-
109
- const productWithCategory = data as unknown as ProductWithCategory;
110
- memoryCache.set(cacheKey, productWithCategory, CACHE_TIMES.PRODUCTS);
111
- return productWithCategory;
112
- }
113
-
114
- /**
115
- * Get featured products
116
- */
117
- async getFeatured(): Promise<Product[]> {
118
- return this.getAll({ featured: true });
119
- }
120
-
121
- /**
122
- * Get products by category
123
- */
124
- async getByCategory(categoryId: string, limit = 100): Promise<Product[]> {
125
- return this.getAll({ categoryId, limit });
126
- }
127
-
128
- /**
129
- * Get related products (same category, excluding current product)
130
- */
131
- async getRelated(productId: string, categoryId: string, limit = 4): Promise<Product[]> {
132
- const cacheKey = `products:related:${productId}:${limit}`;
133
-
134
- const cached = memoryCache.get<Product[]>(cacheKey);
135
- if (cached) {
136
- return cached;
137
- }
138
-
139
- const supabase = getBrowserClient();
140
- const { data, error } = await supabase
141
- .from(TABLE_NAMES.PRODUCTS)
142
- .select('*')
143
- .eq('category_id', categoryId)
144
- .eq('published', true)
145
- .neq('id', productId)
146
- .limit(limit);
147
-
148
- if (error) {
149
- console.error('[ProductService] Error fetching related products:', error);
150
- throw new Error(`Failed to fetch related products: ${error.message}`);
151
- }
152
-
153
- memoryCache.set(cacheKey, data, CACHE_TIMES.PRODUCTS);
154
- return data;
155
- }
156
-
157
- /**
158
- * Get product by ID
159
- */
160
- async getById(id: string): Promise<Product | null> {
161
- const cacheKey = `products:id:${id}`;
162
-
163
- const cached = memoryCache.get<Product>(cacheKey);
164
- if (cached) {
165
- return cached;
166
- }
167
-
168
- const supabase = getBrowserClient();
169
- const { data, error } = await supabase
170
- .from(TABLE_NAMES.PRODUCTS)
171
- .select('*')
172
- .eq('id', id)
173
- .eq('published', true)
174
- .single();
175
-
176
- if (error) {
177
- if (error.code === 'PGRST116') {
178
- return null;
179
- }
180
- console.error('[ProductService] Error fetching product by ID:', error);
181
- throw new Error(`Failed to fetch product: ${error.message}`);
182
- }
183
-
184
- memoryCache.set(cacheKey, data, CACHE_TIMES.PRODUCTS);
185
- return data;
186
- }
187
-
188
- /**
189
- * Get multiple products by IDs
190
- */
191
- async getByIds(ids: string[]): Promise<Product[]> {
192
- if (ids.length === 0) {
193
- return [];
194
- }
195
-
196
- const cacheKey = `products:ids:${ids.sort().join(',')}`;
197
-
198
- const cached = memoryCache.get<Product[]>(cacheKey);
199
- if (cached) {
200
- return cached;
201
- }
202
-
203
- const supabase = getBrowserClient();
204
- const { data, error } = await supabase
205
- .from(TABLE_NAMES.PRODUCTS)
206
- .select('*')
207
- .in('id', ids)
208
- .eq('published', true);
209
-
210
- if (error) {
211
- console.error('[ProductService] Error fetching products by IDs:', error);
212
- throw new Error(`Failed to fetch products: ${error.message}`);
213
- }
214
-
215
- memoryCache.set(cacheKey, data, CACHE_TIMES.PRODUCTS);
216
- return data;
217
- }
218
-
219
- /**
220
- * Get all product slugs for static generation
221
- */
222
- async getAllSlugs(): Promise<string[]> {
223
- const cacheKey = 'products:slugs:all';
224
-
225
- const cached = memoryCache.get<string[]>(cacheKey);
226
- if (cached) {
227
- return cached;
228
- }
229
-
230
- const supabase = getBrowserClient();
231
- const { data, error } = await supabase
232
- .from(TABLE_NAMES.PRODUCTS)
233
- .select('slug')
234
- .eq('published', true);
235
-
236
- if (error) {
237
- console.error('[ProductService] Error fetching product slugs:', error);
238
- throw new Error(`Failed to fetch product slugs: ${error.message}`);
239
- }
240
-
241
- const slugs = (data as { slug: string }[]).map((item) => item.slug);
242
- memoryCache.set(cacheKey, slugs, CACHE_TIMES.PRODUCTS);
243
- return slugs;
244
- }
245
-
246
- /**
247
- * Invalidate all product caches
248
- */
249
- invalidateCache(): void {
250
- memoryCache.invalidate('products:');
251
- }
252
-
253
- /**
254
- * Invalidate specific product cache
255
- */
256
- invalidateProductCache(slug: string): void {
257
- memoryCache.invalidateKey(`products:slug:${slug}`);
258
- }
259
- }
260
-
261
- export const productService = ProductService.getInstance();