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,234 +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 { 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();
@@ -1,98 +0,0 @@
1
- interface CacheEntry<T> {
2
- data: T;
3
- timestamp: number;
4
- ttl: number;
5
- }
6
-
7
- /**
8
- * In-memory cache with TTL (Time To Live) support
9
- * Provides fast access to frequently requested data
10
- */
11
- class MemoryCache {
12
- private cache: Map<string, CacheEntry<unknown>> = new Map();
13
-
14
- /**
15
- * Store data in cache with TTL
16
- * @param key - Cache key
17
- * @param data - Data to cache
18
- * @param ttl - Time to live in seconds
19
- */
20
- set<T>(key: string, data: T, ttl: number): void {
21
- this.cache.set(key, {
22
- data,
23
- timestamp: Date.now(),
24
- ttl,
25
- });
26
- }
27
-
28
- /**
29
- * Retrieve data from cache
30
- * @param key - Cache key
31
- * @returns Cached data or null if expired/not found
32
- */
33
- get<T>(key: string): T | null {
34
- const entry = this.cache.get(key) as CacheEntry<T> | undefined;
35
-
36
- if (!entry) {
37
- return null;
38
- }
39
-
40
- // Check if entry has expired
41
- const age = Date.now() - entry.timestamp;
42
- if (age > entry.ttl * 1000) {
43
- this.cache.delete(key);
44
- return null;
45
- }
46
-
47
- return entry.data;
48
- }
49
-
50
- /**
51
- * Check if key exists and is not expired
52
- * @param key - Cache key
53
- */
54
- has(key: string): boolean {
55
- return this.get(key) !== null;
56
- }
57
-
58
- /**
59
- * Invalidate cache entries matching a pattern
60
- * @param pattern - String pattern to match against keys
61
- */
62
- invalidate(pattern: string): void {
63
- const keys = Array.from(this.cache.keys());
64
- keys.forEach((key) => {
65
- if (key.includes(pattern)) {
66
- this.cache.delete(key);
67
- }
68
- });
69
- }
70
-
71
- /**
72
- * Invalidate specific cache key
73
- * @param key - Cache key to invalidate
74
- */
75
- invalidateKey(key: string): void {
76
- this.cache.delete(key);
77
- }
78
-
79
- /**
80
- * Clear all cache entries
81
- */
82
- clear(): void {
83
- this.cache.clear();
84
- }
85
-
86
- /**
87
- * Get cache statistics
88
- */
89
- getStats() {
90
- return {
91
- size: this.cache.size,
92
- keys: Array.from(this.cache.keys()),
93
- };
94
- }
95
- }
96
-
97
- // Export singleton instance
98
- export const memoryCache = new MemoryCache();
@@ -1,102 +0,0 @@
1
- import { RATE_LIMITS } from '@/lib/supabase/constants';
2
-
3
- interface RateLimitEntry {
4
- tokens: number;
5
- lastRefill: number;
6
- }
7
-
8
- /**
9
- * Token Bucket Rate Limiter
10
- * Implements the token bucket algorithm for rate limiting
11
- */
12
- class RateLimiter {
13
- private buckets: Map<string, RateLimitEntry> = new Map();
14
- private maxTokens: number;
15
- private refillRate: number; // tokens per second
16
-
17
- /**
18
- * @param maxTokens - Maximum number of tokens in the bucket
19
- * @param refillRate - Number of tokens added per second
20
- */
21
- constructor(maxTokens: number, refillRate: number) {
22
- this.maxTokens = maxTokens;
23
- this.refillRate = refillRate;
24
- }
25
-
26
- /**
27
- * Check if request is allowed under rate limit
28
- * @param key - Unique identifier for the rate limit bucket (e.g., user ID, IP, endpoint)
29
- * @returns true if request is allowed, false if rate limited
30
- */
31
- async checkLimit(key: string): Promise<boolean> {
32
- const now = Date.now();
33
- let bucket = this.buckets.get(key);
34
-
35
- // Initialize bucket if it doesn't exist
36
- if (!bucket) {
37
- bucket = {
38
- tokens: this.maxTokens - 1,
39
- lastRefill: now,
40
- };
41
- this.buckets.set(key, bucket);
42
- return true;
43
- }
44
-
45
- // Refill tokens based on time passed
46
- const timePassed = (now - bucket.lastRefill) / 1000; // in seconds
47
- const tokensToAdd = timePassed * this.refillRate;
48
- bucket.tokens = Math.min(this.maxTokens, bucket.tokens + tokensToAdd);
49
- bucket.lastRefill = now;
50
-
51
- // Check if we have tokens available
52
- if (bucket.tokens >= 1) {
53
- bucket.tokens -= 1;
54
- return true;
55
- }
56
-
57
- // Rate limit exceeded
58
- return false;
59
- }
60
-
61
- /**
62
- * Get remaining tokens for a key
63
- * @param key - Rate limit bucket key
64
- */
65
- getRemainingTokens(key: string): number {
66
- const bucket = this.buckets.get(key);
67
- if (!bucket) {
68
- return this.maxTokens;
69
- }
70
-
71
- const now = Date.now();
72
- const timePassed = (now - bucket.lastRefill) / 1000;
73
- const tokensToAdd = timePassed * this.refillRate;
74
- return Math.min(this.maxTokens, bucket.tokens + tokensToAdd);
75
- }
76
-
77
- /**
78
- * Reset rate limit for a specific key
79
- * @param key - Rate limit bucket key
80
- */
81
- reset(key: string): void {
82
- this.buckets.delete(key);
83
- }
84
-
85
- /**
86
- * Clear all rate limit buckets
87
- */
88
- clearAll(): void {
89
- this.buckets.clear();
90
- }
91
- }
92
-
93
- // Global rate limiters
94
- export const perMinuteLimiter = new RateLimiter(
95
- RATE_LIMITS.REQUESTS_PER_MINUTE,
96
- RATE_LIMITS.REQUESTS_PER_MINUTE / 60 // tokens per second
97
- );
98
-
99
- export const perHourLimiter = new RateLimiter(
100
- RATE_LIMITS.REQUESTS_PER_HOUR,
101
- RATE_LIMITS.REQUESTS_PER_HOUR / 3600 // tokens per second
102
- );