create-nextjs-stack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/bin/cli.js +187 -0
  4. package/package.json +48 -0
  5. package/templates/admin/.env.example +11 -0
  6. package/templates/admin/README.md +82 -0
  7. package/templates/admin/app/(auth)/login/page.tsx +84 -0
  8. package/templates/admin/app/(dashboard)/[resource]/[id]/page.tsx +45 -0
  9. package/templates/admin/app/(dashboard)/[resource]/new/page.tsx +32 -0
  10. package/templates/admin/app/(dashboard)/[resource]/page.tsx +131 -0
  11. package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +22 -0
  12. package/templates/admin/app/(dashboard)/categories/new/page.tsx +5 -0
  13. package/templates/admin/app/(dashboard)/categories/page.tsx +33 -0
  14. package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +22 -0
  15. package/templates/admin/app/(dashboard)/clients/new/page.tsx +5 -0
  16. package/templates/admin/app/(dashboard)/clients/page.tsx +33 -0
  17. package/templates/admin/app/(dashboard)/dashboard/page.tsx +45 -0
  18. package/templates/admin/app/(dashboard)/layout.tsx +13 -0
  19. package/templates/admin/app/(dashboard)/products/[id]/page.tsx +22 -0
  20. package/templates/admin/app/(dashboard)/products/new/page.tsx +5 -0
  21. package/templates/admin/app/(dashboard)/products/page.tsx +33 -0
  22. package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +22 -0
  23. package/templates/admin/app/(dashboard)/projects/new/page.tsx +5 -0
  24. package/templates/admin/app/(dashboard)/projects/page.tsx +33 -0
  25. package/templates/admin/app/(dashboard)/users/[id]/page.tsx +22 -0
  26. package/templates/admin/app/(dashboard)/users/new/page.tsx +5 -0
  27. package/templates/admin/app/(dashboard)/users/page.tsx +33 -0
  28. package/templates/admin/app/actions/resources.ts +46 -0
  29. package/templates/admin/app/actions/upload.ts +58 -0
  30. package/templates/admin/app/favicon.ico +0 -0
  31. package/templates/admin/app/globals.css +23 -0
  32. package/templates/admin/app/layout.tsx +23 -0
  33. package/templates/admin/app/page.tsx +5 -0
  34. package/templates/admin/components/admin/AdminLayoutClient.tsx +22 -0
  35. package/templates/admin/components/admin/DeleteModal.tsx +90 -0
  36. package/templates/admin/components/admin/FormLayout.tsx +113 -0
  37. package/templates/admin/components/admin/ImageUpload.tsx +137 -0
  38. package/templates/admin/components/admin/ResourceFormClient.tsx +62 -0
  39. package/templates/admin/components/admin/Sidebar.tsx +74 -0
  40. package/templates/admin/components/admin/SubmitButton.tsx +34 -0
  41. package/templates/admin/components/admin/ToastProvider.tsx +8 -0
  42. package/templates/admin/components/categories/CategoryForm.tsx +24 -0
  43. package/templates/admin/components/categories/CategoryList.tsx +113 -0
  44. package/templates/admin/components/clients/ClientForm.tsx +24 -0
  45. package/templates/admin/components/clients/ClientList.tsx +113 -0
  46. package/templates/admin/components/products/ProductForm.tsx +24 -0
  47. package/templates/admin/components/products/ProductList.tsx +117 -0
  48. package/templates/admin/components/projects/ProjectForm.tsx +24 -0
  49. package/templates/admin/components/projects/ProjectList.tsx +121 -0
  50. package/templates/admin/components/users/UserForm.tsx +39 -0
  51. package/templates/admin/components/users/UserList.tsx +101 -0
  52. package/templates/admin/config/resources.ts +123 -0
  53. package/templates/admin/eslint.config.mjs +18 -0
  54. package/templates/admin/hooks/useResource.ts +86 -0
  55. package/templates/admin/lib/services/base.service.ts +106 -0
  56. package/templates/admin/lib/services/categories.service.ts +7 -0
  57. package/templates/admin/lib/services/clients.service.ts +7 -0
  58. package/templates/admin/lib/services/index.ts +27 -0
  59. package/templates/admin/lib/services/products.service.ts +9 -0
  60. package/templates/admin/lib/services/projects.service.ts +22 -0
  61. package/templates/admin/lib/services/resource.service.ts +26 -0
  62. package/templates/admin/lib/services/users.service.ts +9 -0
  63. package/templates/admin/lib/supabase/client.ts +9 -0
  64. package/templates/admin/lib/supabase/middleware.ts +57 -0
  65. package/templates/admin/lib/supabase/server.ts +29 -0
  66. package/templates/admin/middleware.ts +15 -0
  67. package/templates/admin/next.config.ts +10 -0
  68. package/templates/admin/package-lock.json +6768 -0
  69. package/templates/admin/package.json +33 -0
  70. package/templates/admin/postcss.config.mjs +7 -0
  71. package/templates/admin/public/file.svg +1 -0
  72. package/templates/admin/public/globe.svg +1 -0
  73. package/templates/admin/public/next.svg +1 -0
  74. package/templates/admin/public/vercel.svg +1 -0
  75. package/templates/admin/public/window.svg +1 -0
  76. package/templates/admin/supabase_mock_data.sql +57 -0
  77. package/templates/admin/supabase_schema.sql +93 -0
  78. package/templates/admin/tsconfig.json +34 -0
  79. package/templates/web/.env.example +21 -0
  80. package/templates/web/README.md +129 -0
  81. package/templates/web/components.json +22 -0
  82. package/templates/web/eslint.config.mjs +25 -0
  83. package/templates/web/next.config.ts +25 -0
  84. package/templates/web/package-lock.json +6778 -0
  85. package/templates/web/package.json +45 -0
  86. package/templates/web/postcss.config.mjs +5 -0
  87. package/templates/web/src/app/api/contact/route.ts +181 -0
  88. package/templates/web/src/app/api/revalidate/route.ts +95 -0
  89. package/templates/web/src/app/error.tsx +28 -0
  90. package/templates/web/src/app/globals.css +838 -0
  91. package/templates/web/src/app/layout.tsx +126 -0
  92. package/templates/web/src/app/loading.tsx +60 -0
  93. package/templates/web/src/app/not-found.tsx +68 -0
  94. package/templates/web/src/app/page.tsx +106 -0
  95. package/templates/web/src/app/robots.ts +12 -0
  96. package/templates/web/src/app/sitemap.ts +66 -0
  97. package/templates/web/src/components/home/StatsGrid.tsx +89 -0
  98. package/templates/web/src/hooks/useIntersectionObserver.ts +39 -0
  99. package/templates/web/src/lib/providers/StoreProvider.tsx +12 -0
  100. package/templates/web/src/lib/seo/index.ts +4 -0
  101. package/templates/web/src/lib/seo/metadata.ts +103 -0
  102. package/templates/web/src/lib/seo/seo.config.ts +161 -0
  103. package/templates/web/src/lib/seo/seo.types.ts +76 -0
  104. package/templates/web/src/lib/services/categories.service.ts +38 -0
  105. package/templates/web/src/lib/services/categoryService.ts +251 -0
  106. package/templates/web/src/lib/services/clientService.ts +132 -0
  107. package/templates/web/src/lib/services/clients.service.ts +20 -0
  108. package/templates/web/src/lib/services/productService.ts +261 -0
  109. package/templates/web/src/lib/services/products.service.ts +38 -0
  110. package/templates/web/src/lib/services/projectService.ts +234 -0
  111. package/templates/web/src/lib/services/projects.service.ts +38 -0
  112. package/templates/web/src/lib/services/users.service.ts +20 -0
  113. package/templates/web/src/lib/supabase/client.ts +42 -0
  114. package/templates/web/src/lib/supabase/constants.ts +25 -0
  115. package/templates/web/src/lib/supabase/server.ts +29 -0
  116. package/templates/web/src/lib/supabase/types.ts +112 -0
  117. package/templates/web/src/lib/utils/cache.ts +98 -0
  118. package/templates/web/src/lib/utils/rate-limiter.ts +102 -0
  119. package/templates/web/src/store/actions/index.ts +2 -0
  120. package/templates/web/src/store/index.ts +13 -0
  121. package/templates/web/src/store/reducers/index.ts +13 -0
  122. package/templates/web/src/store/types/index.ts +2 -0
  123. package/templates/web/tsconfig.json +41 -0
@@ -0,0 +1,132 @@
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();
@@ -0,0 +1,20 @@
1
+ import { getServerClient } from "@/lib/supabase/server";
2
+
3
+ export class ClientService {
4
+ private static table = "clients";
5
+
6
+ static async getAll() {
7
+ const supabase = await getServerClient();
8
+ const { data, error } = await supabase
9
+ .from(this.table)
10
+ .select("*")
11
+ .order("name");
12
+
13
+ if (error) {
14
+ console.error(`Error fetching ${this.table}:`, error);
15
+ return [];
16
+ }
17
+
18
+ return data;
19
+ }
20
+ }
@@ -0,0 +1,261 @@
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();
@@ -0,0 +1,38 @@
1
+ import { getServerClient } from "@/lib/supabase/server";
2
+
3
+ export class ProductService {
4
+ private static table = "products";
5
+
6
+ static async getAll() {
7
+ const supabase = await getServerClient();
8
+ const { data, error } = await supabase
9
+ .from(this.table)
10
+ .select("*, categories(title, slug)")
11
+ .eq("published", true)
12
+ .order("created_at", { ascending: false });
13
+
14
+ if (error) {
15
+ console.error(`Error fetching ${this.table}:`, error);
16
+ return [];
17
+ }
18
+
19
+ return data;
20
+ }
21
+
22
+ static async getBySlug(slug: string) {
23
+ const supabase = await getServerClient();
24
+ const { data, error } = await supabase
25
+ .from(this.table)
26
+ .select("*, categories(title, slug)")
27
+ .eq("slug", slug)
28
+ .eq("published", true)
29
+ .single();
30
+
31
+ if (error) {
32
+ console.error(`Error fetching ${this.table} ${slug}:`, error);
33
+ return null;
34
+ }
35
+
36
+ return data;
37
+ }
38
+ }
@@ -0,0 +1,234 @@
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 { Project, ProjectWithProducts } from '@/lib/supabase/types';
6
+ import { productService } from './productService';
7
+
8
+ interface GetProjectsOptions {
9
+ limit?: number;
10
+ offset?: number;
11
+ year?: number;
12
+ featured?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Project Service - Handles all project-related database operations
17
+ * Implements singleton pattern with caching and rate limiting
18
+ */
19
+ export class ProjectService {
20
+ private static instance: ProjectService;
21
+
22
+ private constructor() {}
23
+
24
+ static getInstance(): ProjectService {
25
+ if (!ProjectService.instance) {
26
+ ProjectService.instance = new ProjectService();
27
+ }
28
+ return ProjectService.instance;
29
+ }
30
+
31
+ /**
32
+ * Get all published projects with optional filtering
33
+ */
34
+ async getAll(options: GetProjectsOptions = {}): Promise<Project[]> {
35
+ const { limit = 100, offset = 0, year, featured } = options;
36
+ const cacheKey = `projects:all:${limit}:${offset}:${year || 'all'}:${featured || 'all'}`;
37
+
38
+ const cached = memoryCache.get<Project[]>(cacheKey);
39
+ if (cached) {
40
+ return cached;
41
+ }
42
+
43
+ const allowed = await perMinuteLimiter.checkLimit('projects:getAll');
44
+ if (!allowed) {
45
+ throw new Error('Rate limit exceeded. Please try again later.');
46
+ }
47
+
48
+ const supabase = getBrowserClient();
49
+ let query = supabase
50
+ .from(TABLE_NAMES.PROJECTS)
51
+ .select('*')
52
+ .eq('published', true)
53
+ .order('year', { ascending: false })
54
+ .order('created_at', { ascending: false })
55
+ .range(offset, offset + limit - 1);
56
+
57
+ if (year !== undefined) {
58
+ query = query.eq('year', year);
59
+ }
60
+
61
+ if (featured !== undefined) {
62
+ query = query.eq('featured', featured);
63
+ }
64
+
65
+ const { data, error } = await query;
66
+
67
+ if (error) {
68
+ console.error('[ProjectService] Error fetching projects:', error);
69
+ throw new Error(`Failed to fetch projects: ${error.message}`);
70
+ }
71
+
72
+ memoryCache.set(cacheKey, data, CACHE_TIMES.PROJECTS);
73
+ return data as Project[];
74
+ }
75
+
76
+ /**
77
+ * Get project by slug with related products
78
+ */
79
+ async getBySlug(slug: string): Promise<ProjectWithProducts | null> {
80
+ const cacheKey = `projects:slug:${slug}`;
81
+
82
+ const cached = memoryCache.get<ProjectWithProducts>(cacheKey);
83
+ if (cached) {
84
+ return cached;
85
+ }
86
+
87
+ const allowed = await perMinuteLimiter.checkLimit('projects:getBySlug');
88
+ if (!allowed) {
89
+ throw new Error('Rate limit exceeded. Please try again later.');
90
+ }
91
+
92
+ const supabase = getBrowserClient();
93
+ const { data, error } = await supabase
94
+ .from(TABLE_NAMES.PROJECTS)
95
+ .select('*')
96
+ .eq('slug', slug)
97
+ .eq('published', true)
98
+ .single();
99
+
100
+ if (error) {
101
+ if (error.code === 'PGRST116') {
102
+ return null;
103
+ }
104
+ console.error('[ProjectService] Error fetching project:', error);
105
+ throw new Error(`Failed to fetch project: ${error.message}`);
106
+ }
107
+
108
+ // Fetch related products
109
+ let products: import('@/lib/supabase/types').Product[] = [];
110
+ const projectData = data as Project;
111
+ if (projectData.product_ids && projectData.product_ids.length > 0) {
112
+ try {
113
+ products = await productService.getByIds(projectData.product_ids);
114
+ } catch (error) {
115
+ console.error('[ProjectService] Error fetching related products:', error);
116
+ // Continue without products rather than failing
117
+ }
118
+ }
119
+
120
+ const projectWithProducts: ProjectWithProducts = {
121
+ ...projectData,
122
+ products,
123
+ };
124
+
125
+ memoryCache.set(cacheKey, projectWithProducts, CACHE_TIMES.PROJECTS);
126
+ return projectWithProducts;
127
+ }
128
+
129
+ /**
130
+ * Get featured projects
131
+ */
132
+ async getFeatured(): Promise<Project[]> {
133
+ return this.getAll({ featured: true });
134
+ }
135
+
136
+ /**
137
+ * Get projects by year
138
+ */
139
+ async getByYear(year: number): Promise<Project[]> {
140
+ return this.getAll({ year });
141
+ }
142
+
143
+ /**
144
+ * Get project by ID
145
+ */
146
+ async getById(id: string): Promise<Project | null> {
147
+ const cacheKey = `projects:id:${id}`;
148
+
149
+ const cached = memoryCache.get<Project>(cacheKey);
150
+ if (cached) {
151
+ return cached;
152
+ }
153
+
154
+ const supabase = getBrowserClient();
155
+ const { data, error } = await supabase
156
+ .from(TABLE_NAMES.PROJECTS)
157
+ .select('*')
158
+ .eq('id', id)
159
+ .eq('published', true)
160
+ .single();
161
+
162
+ if (error) {
163
+ if (error.code === 'PGRST116') {
164
+ return null;
165
+ }
166
+ console.error('[ProjectService] Error fetching project by ID:', error);
167
+ throw new Error(`Failed to fetch project: ${error.message}`);
168
+ }
169
+
170
+ memoryCache.set(cacheKey, data, CACHE_TIMES.PROJECTS);
171
+ return data;
172
+ }
173
+
174
+ /**
175
+ * Get all project slugs for static generation
176
+ */
177
+ async getAllSlugs(): Promise<string[]> {
178
+ const cacheKey = 'projects:slugs:all';
179
+
180
+ const cached = memoryCache.get<string[]>(cacheKey);
181
+ if (cached) {
182
+ return cached;
183
+ }
184
+
185
+ const supabase = getBrowserClient();
186
+ const { data, error } = await supabase
187
+ .from(TABLE_NAMES.PROJECTS)
188
+ .select('slug')
189
+ .eq('published', true);
190
+
191
+ if (error) {
192
+ console.error('[ProjectService] Error fetching project slugs:', error);
193
+ throw new Error(`Failed to fetch project slugs: ${error.message}`);
194
+ }
195
+
196
+ const slugs = (data as { slug: string }[]).map((item) => item.slug);
197
+ memoryCache.set(cacheKey, slugs, CACHE_TIMES.PROJECTS);
198
+ return slugs;
199
+ }
200
+
201
+ /**
202
+ * Get unique years from all projects
203
+ */
204
+ async getYears(): Promise<number[]> {
205
+ const cacheKey = 'projects:years';
206
+
207
+ const cached = memoryCache.get<number[]>(cacheKey);
208
+ if (cached) {
209
+ return cached;
210
+ }
211
+
212
+ const projects = await this.getAll();
213
+ const years = [...new Set(projects.map((p) => p.year))].sort((a, b) => b - a);
214
+
215
+ memoryCache.set(cacheKey, years, CACHE_TIMES.PROJECTS);
216
+ return years;
217
+ }
218
+
219
+ /**
220
+ * Invalidate all project caches
221
+ */
222
+ invalidateCache(): void {
223
+ memoryCache.invalidate('projects:');
224
+ }
225
+
226
+ /**
227
+ * Invalidate specific project cache
228
+ */
229
+ invalidateProjectCache(slug: string): void {
230
+ memoryCache.invalidateKey(`projects:slug:${slug}`);
231
+ }
232
+ }
233
+
234
+ export const projectService = ProjectService.getInstance();