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,103 @@
1
+ import { Metadata } from 'next';
2
+ import { PageSEOConfig } from './seo.types';
3
+ import { seoConfig } from './seo.config';
4
+
5
+ /**
6
+ * Generate Next.js Metadata from SEO Config
7
+ * @param pageConfig - Page-specific SEO configuration
8
+ * @returns Next.js Metadata object
9
+ */
10
+ export function generateMetadata(pageConfig: PageSEOConfig): Metadata {
11
+ const { site } = seoConfig;
12
+
13
+ const metadata: Metadata = {
14
+ title: pageConfig.title,
15
+ description: pageConfig.description,
16
+ keywords: pageConfig.keywords,
17
+
18
+ // Robots
19
+ robots: pageConfig.robots
20
+ ? {
21
+ index: pageConfig.robots.index ?? true,
22
+ follow: pageConfig.robots.follow ?? true,
23
+ googleBot: pageConfig.robots.googleBot,
24
+ }
25
+ : undefined,
26
+
27
+ // Open Graph
28
+ openGraph: {
29
+ title: pageConfig.openGraph?.title || pageConfig.title,
30
+ description: pageConfig.openGraph?.description || pageConfig.description,
31
+ siteName: site.siteName,
32
+ locale: site.defaultLocale,
33
+ type: pageConfig.openGraph?.type || 'website',
34
+ url: pageConfig.canonical || site.siteUrl,
35
+ images: pageConfig.openGraph?.images?.map((img) => ({
36
+ url: img,
37
+ width: 1200,
38
+ height: 630,
39
+ alt: site.siteName,
40
+ })) || [
41
+ {
42
+ url: site.defaultOGImage,
43
+ width: 1200,
44
+ height: 630,
45
+ alt: site.siteName,
46
+ },
47
+ ],
48
+ },
49
+
50
+ // Twitter
51
+ twitter: {
52
+ card: pageConfig.twitter?.card || 'summary_large_image',
53
+ title: pageConfig.twitter?.title || pageConfig.title,
54
+ description: pageConfig.twitter?.description || pageConfig.description,
55
+ creator: site.twitterHandle,
56
+ images: pageConfig.twitter?.images?.[0] || site.defaultOGImage,
57
+ },
58
+
59
+ // Alternates
60
+ alternates: pageConfig.alternates
61
+ ? {
62
+ canonical: pageConfig.canonical,
63
+ languages: pageConfig.alternates.languages,
64
+ }
65
+ : pageConfig.canonical
66
+ ? { canonical: pageConfig.canonical }
67
+ : undefined,
68
+
69
+ // Additional Meta Tags
70
+ other: {
71
+ 'fb:app_id': site.facebookAppId || '',
72
+ },
73
+ };
74
+
75
+ return metadata;
76
+ }
77
+
78
+ /**
79
+ * Generate JSON-LD Structured Data for Organization
80
+ */
81
+ export function generateOrganizationSchema() {
82
+ const { site } = seoConfig;
83
+
84
+ return {
85
+ '@context': 'https://schema.org',
86
+ '@type': 'Organization',
87
+ name: site.siteName,
88
+ url: site.siteUrl,
89
+ logo: `${site.siteUrl}/images/logo.png`,
90
+ description:
91
+ 'Eurodeco Panel Systems GmbH - Innovative Panel-Systeme für Architektur der Zukunft',
92
+ contactPoint: {
93
+ '@type': 'ContactPoint',
94
+ contactType: 'Sales',
95
+ availableLanguage: ['German', 'English'],
96
+ },
97
+ sameAs: [
98
+ // Sosyal medya linklerinizi buraya ekleyin
99
+ // 'https://www.linkedin.com/company/eurodeco-panel-systems',
100
+ // 'https://www.instagram.com/eurodeco',
101
+ ],
102
+ };
103
+ }
@@ -0,0 +1,161 @@
1
+ import { SEOConfig } from './seo.types';
2
+
3
+ /**
4
+ * Centralized SEO Configuration
5
+ * Tüm sayfa SEO ayarları burada yönetilir
6
+ */
7
+ export const seoConfig: SEOConfig = {
8
+ site: {
9
+ siteName: 'Eurodeco Panel Systems GmbH',
10
+ siteUrl: 'https://www.eurodecopanel.de',
11
+ defaultLocale: 'de',
12
+ locales: ['de', 'en'],
13
+ defaultOGImage: '/images/og-default.jpg',
14
+ twitterHandle: '@eurodecopanel', // Placeholder handle
15
+ },
16
+
17
+ pages: {
18
+ // Home Page SEO
19
+ home: {
20
+ title: 'Eurodeco Panel Systems GmbH - Innovative Panel-Systeme',
21
+ description:
22
+ 'Eurodeco Panel Systems GmbH entwickelt und vertreibt moderne Hochleistungs-Paneelsysteme für anspruchsvolle Architektur, Innenausbau und Gewerbeprojekte.',
23
+ keywords: [
24
+ 'Eurodeco',
25
+ 'Panel-Systeme',
26
+ 'Wandpaneele',
27
+ 'Deckenpaneele',
28
+ 'Brandschutz',
29
+ 'Leichtbau',
30
+ 'Architektur',
31
+ 'Gewerbeprojekte',
32
+ ],
33
+ openGraph: {
34
+ title: 'Eurodeco Panel Systems GmbH - Innovative Panel-Systeme',
35
+ description:
36
+ 'Entwickelt und vertreibt moderne Hochleistungs-Paneelsysteme für anspruchsvolle Architektur.',
37
+ images: ['/images/og-home.jpg'],
38
+ type: 'website',
39
+ },
40
+ twitter: {
41
+ card: 'summary_large_image',
42
+ title: 'Eurodeco Panel Systems GmbH - Innovative Panel-Systeme',
43
+ description:
44
+ 'Entwickelt und vertreibt moderne Hochleistungs-Paneelsysteme für anspruchsvolle Architektur.',
45
+ images: ['/images/twitter-home.jpg'],
46
+ },
47
+ robots: {
48
+ index: true,
49
+ follow: true,
50
+ googleBot: {
51
+ index: true,
52
+ follow: true,
53
+ },
54
+ },
55
+ },
56
+
57
+ // About Page SEO
58
+ about: {
59
+ title: 'Über Uns - Eurodeco Panel Systems GmbH',
60
+ description:
61
+ 'Erfahren Sie mehr über Eurodeco Panel Systems GmbH, unsere Mission, Vision und Technologie für moderne Paneelsysteme.',
62
+ keywords: [
63
+ 'Über Eurodeco',
64
+ 'Mission',
65
+ 'Vision',
66
+ 'Technologie',
67
+ 'Paneelsysteme',
68
+ 'Unternehmensprofil',
69
+ ],
70
+ canonical: 'https://www.eurodecopanel.de/about',
71
+ openGraph: {
72
+ title: 'Über Uns - Eurodeco Panel Systems GmbH',
73
+ description:
74
+ 'Erfahren Sie mehr über Eurodeco Panel Systems GmbH und unsere innovativen Lösungen.',
75
+ images: ['/images/og-about.jpg'],
76
+ type: 'website',
77
+ },
78
+ robots: {
79
+ index: true,
80
+ follow: true,
81
+ },
82
+ },
83
+
84
+ // Works/Projects Page SEO
85
+ works: {
86
+ title: 'Referenzen - Eurodeco Panel Systems GmbH',
87
+ description:
88
+ 'Entdecken Sie unsere erfolgreichen Projekte und Referenzen in den Bereichen Gewerbe, Büro, Hotel und öffentliche Gebäude.',
89
+ keywords: [
90
+ 'Referenzen',
91
+ 'Projekte',
92
+ 'Eurodeco Projekte',
93
+ 'Architektur Referenzen',
94
+ 'Bauprojekte',
95
+ ],
96
+ canonical: 'https://www.eurodecopanel.de/references',
97
+ openGraph: {
98
+ title: 'Referenzen - Eurodeco Panel Systems GmbH',
99
+ description:
100
+ 'Entdecken Sie unsere erfolgreichen Projekte und Referenzen.',
101
+ images: ['/images/og-works.jpg'],
102
+ type: 'website',
103
+ },
104
+ robots: {
105
+ index: true,
106
+ follow: true,
107
+ },
108
+ },
109
+
110
+ // Blog Page SEO (Keeping structurally but updating content generic/german if needed, or removing if not in list. Task.md didn't ask to remove, just update config. I'll translate to German generic).
111
+ blog: {
112
+ title: 'Aktuelles - Eurodeco Panel Systems GmbH',
113
+ description:
114
+ 'Neuigkeiten und Trends zu Paneelsystemen, Architektur und Innovationen von Eurodeco.',
115
+ keywords: [
116
+ 'Eurodeco News',
117
+ 'Architektur Trends',
118
+ 'Paneel Innovationen',
119
+ 'Baubranche',
120
+ ],
121
+ canonical: 'https://www.eurodecopanel.de/blog',
122
+ openGraph: {
123
+ title: 'Aktuelles - Eurodeco Panel Systems GmbH',
124
+ description:
125
+ 'Neuigkeiten und Trends zu Paneelsystemen und Architektur.',
126
+ images: ['/images/og-blog.jpg'],
127
+ type: 'website',
128
+ },
129
+ robots: {
130
+ index: true,
131
+ follow: true,
132
+ },
133
+ },
134
+
135
+ // Contact Page SEO
136
+ contact: {
137
+ title: 'Kontakt - Eurodeco Panel Systems GmbH',
138
+ description:
139
+ 'Kontaktieren Sie Eurodeco Panel Systems GmbH für Ihr nächstes Architektur- oder Bauprojekt. Wir freuen uns auf den Austausch.',
140
+ keywords: [
141
+ 'Kontakt Eurodeco',
142
+ 'Anfrage',
143
+ 'Projekt starten',
144
+ 'Eurodeco Adresse',
145
+ 'Eurodeco Email',
146
+ ],
147
+ canonical: 'https://www.eurodecopanel.de/contact',
148
+ openGraph: {
149
+ title: 'Kontakt - Eurodeco Panel Systems GmbH',
150
+ description:
151
+ 'Kontaktieren Sie uns für Ihr nächstes Projekt.',
152
+ images: ['/images/og-contact.jpg'],
153
+ type: 'website',
154
+ },
155
+ robots: {
156
+ index: true,
157
+ follow: true,
158
+ },
159
+ },
160
+ },
161
+ };
@@ -0,0 +1,76 @@
1
+
2
+
3
+ /**
4
+ * SEO Configuration for individual pages
5
+ */
6
+ export interface PageSEOConfig {
7
+ /** Page title */
8
+ title: string;
9
+ /** Page description */
10
+ description: string;
11
+ /** Page keywords */
12
+ keywords?: string[];
13
+ /** Canonical URL */
14
+ canonical?: string;
15
+ /** Open Graph specific overrides */
16
+ openGraph?: {
17
+ title?: string;
18
+ description?: string;
19
+ images?: string[];
20
+ type?: 'website' | 'article';
21
+ };
22
+ /** Twitter Card specific overrides */
23
+ twitter?: {
24
+ card?: 'summary' | 'summary_large_image' | 'app' | 'player';
25
+ title?: string;
26
+ description?: string;
27
+ images?: string[];
28
+ };
29
+ /** Robots meta tag */
30
+ robots?: {
31
+ index?: boolean;
32
+ follow?: boolean;
33
+ googleBot?: {
34
+ index?: boolean;
35
+ follow?: boolean;
36
+ };
37
+ };
38
+ /** Alternate languages */
39
+ alternates?: {
40
+ languages?: Record<string, string>;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Site-wide SEO Configuration
46
+ */
47
+ export interface SiteSEOConfig {
48
+ /** Site name */
49
+ siteName: string;
50
+ /** Base URL */
51
+ siteUrl: string;
52
+ /** Default locale */
53
+ defaultLocale: string;
54
+ /** Available locales */
55
+ locales: string[];
56
+ /** Default OG image */
57
+ defaultOGImage: string;
58
+ /** Twitter handle */
59
+ twitterHandle?: string;
60
+ /** Facebook App ID */
61
+ facebookAppId?: string;
62
+ }
63
+
64
+ /**
65
+ * Complete SEO Configuration including site config and page configs
66
+ */
67
+ export interface SEOConfig {
68
+ site: SiteSEOConfig;
69
+ pages: {
70
+ home: PageSEOConfig;
71
+ about: PageSEOConfig;
72
+ works: PageSEOConfig;
73
+ blog: PageSEOConfig;
74
+ contact: PageSEOConfig;
75
+ };
76
+ }
@@ -0,0 +1,38 @@
1
+ import { getServerClient } from "@/lib/supabase/server";
2
+
3
+ export class CategoryService {
4
+ private static table = "categories";
5
+
6
+ static async getAll() {
7
+ const supabase = await getServerClient();
8
+ const { data, error } = await supabase
9
+ .from(this.table)
10
+ .select("*")
11
+ .eq("published", true)
12
+ .order("title");
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("*")
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,251 @@
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();