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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/bin/cli.js +187 -0
- package/package.json +48 -0
- package/templates/admin/.env.example +11 -0
- package/templates/admin/README.md +82 -0
- package/templates/admin/app/(auth)/login/page.tsx +84 -0
- package/templates/admin/app/(dashboard)/[resource]/[id]/page.tsx +45 -0
- package/templates/admin/app/(dashboard)/[resource]/new/page.tsx +32 -0
- package/templates/admin/app/(dashboard)/[resource]/page.tsx +131 -0
- package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/categories/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/categories/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/clients/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/clients/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/dashboard/page.tsx +45 -0
- package/templates/admin/app/(dashboard)/layout.tsx +13 -0
- package/templates/admin/app/(dashboard)/products/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/products/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/products/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/projects/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/projects/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/users/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/users/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/users/page.tsx +33 -0
- package/templates/admin/app/actions/resources.ts +46 -0
- package/templates/admin/app/actions/upload.ts +58 -0
- package/templates/admin/app/favicon.ico +0 -0
- package/templates/admin/app/globals.css +23 -0
- package/templates/admin/app/layout.tsx +23 -0
- package/templates/admin/app/page.tsx +5 -0
- package/templates/admin/components/admin/AdminLayoutClient.tsx +22 -0
- package/templates/admin/components/admin/DeleteModal.tsx +90 -0
- package/templates/admin/components/admin/FormLayout.tsx +113 -0
- package/templates/admin/components/admin/ImageUpload.tsx +137 -0
- package/templates/admin/components/admin/ResourceFormClient.tsx +62 -0
- package/templates/admin/components/admin/Sidebar.tsx +74 -0
- package/templates/admin/components/admin/SubmitButton.tsx +34 -0
- package/templates/admin/components/admin/ToastProvider.tsx +8 -0
- package/templates/admin/components/categories/CategoryForm.tsx +24 -0
- package/templates/admin/components/categories/CategoryList.tsx +113 -0
- package/templates/admin/components/clients/ClientForm.tsx +24 -0
- package/templates/admin/components/clients/ClientList.tsx +113 -0
- package/templates/admin/components/products/ProductForm.tsx +24 -0
- package/templates/admin/components/products/ProductList.tsx +117 -0
- package/templates/admin/components/projects/ProjectForm.tsx +24 -0
- package/templates/admin/components/projects/ProjectList.tsx +121 -0
- package/templates/admin/components/users/UserForm.tsx +39 -0
- package/templates/admin/components/users/UserList.tsx +101 -0
- package/templates/admin/config/resources.ts +123 -0
- package/templates/admin/eslint.config.mjs +18 -0
- package/templates/admin/hooks/useResource.ts +86 -0
- package/templates/admin/lib/services/base.service.ts +106 -0
- package/templates/admin/lib/services/categories.service.ts +7 -0
- package/templates/admin/lib/services/clients.service.ts +7 -0
- package/templates/admin/lib/services/index.ts +27 -0
- package/templates/admin/lib/services/products.service.ts +9 -0
- package/templates/admin/lib/services/projects.service.ts +22 -0
- package/templates/admin/lib/services/resource.service.ts +26 -0
- package/templates/admin/lib/services/users.service.ts +9 -0
- package/templates/admin/lib/supabase/client.ts +9 -0
- package/templates/admin/lib/supabase/middleware.ts +57 -0
- package/templates/admin/lib/supabase/server.ts +29 -0
- package/templates/admin/middleware.ts +15 -0
- package/templates/admin/next.config.ts +10 -0
- package/templates/admin/package-lock.json +6768 -0
- package/templates/admin/package.json +33 -0
- package/templates/admin/postcss.config.mjs +7 -0
- package/templates/admin/public/file.svg +1 -0
- package/templates/admin/public/globe.svg +1 -0
- package/templates/admin/public/next.svg +1 -0
- package/templates/admin/public/vercel.svg +1 -0
- package/templates/admin/public/window.svg +1 -0
- package/templates/admin/supabase_mock_data.sql +57 -0
- package/templates/admin/supabase_schema.sql +93 -0
- package/templates/admin/tsconfig.json +34 -0
- package/templates/web/.env.example +21 -0
- package/templates/web/README.md +129 -0
- package/templates/web/components.json +22 -0
- package/templates/web/eslint.config.mjs +25 -0
- package/templates/web/next.config.ts +25 -0
- package/templates/web/package-lock.json +6778 -0
- package/templates/web/package.json +45 -0
- package/templates/web/postcss.config.mjs +5 -0
- package/templates/web/src/app/api/contact/route.ts +181 -0
- package/templates/web/src/app/api/revalidate/route.ts +95 -0
- package/templates/web/src/app/error.tsx +28 -0
- package/templates/web/src/app/globals.css +838 -0
- package/templates/web/src/app/layout.tsx +126 -0
- package/templates/web/src/app/loading.tsx +60 -0
- package/templates/web/src/app/not-found.tsx +68 -0
- package/templates/web/src/app/page.tsx +106 -0
- package/templates/web/src/app/robots.ts +12 -0
- package/templates/web/src/app/sitemap.ts +66 -0
- package/templates/web/src/components/home/StatsGrid.tsx +89 -0
- package/templates/web/src/hooks/useIntersectionObserver.ts +39 -0
- package/templates/web/src/lib/providers/StoreProvider.tsx +12 -0
- package/templates/web/src/lib/seo/index.ts +4 -0
- package/templates/web/src/lib/seo/metadata.ts +103 -0
- package/templates/web/src/lib/seo/seo.config.ts +161 -0
- package/templates/web/src/lib/seo/seo.types.ts +76 -0
- package/templates/web/src/lib/services/categories.service.ts +38 -0
- package/templates/web/src/lib/services/categoryService.ts +251 -0
- package/templates/web/src/lib/services/clientService.ts +132 -0
- package/templates/web/src/lib/services/clients.service.ts +20 -0
- package/templates/web/src/lib/services/productService.ts +261 -0
- package/templates/web/src/lib/services/products.service.ts +38 -0
- package/templates/web/src/lib/services/projectService.ts +234 -0
- package/templates/web/src/lib/services/projects.service.ts +38 -0
- package/templates/web/src/lib/services/users.service.ts +20 -0
- package/templates/web/src/lib/supabase/client.ts +42 -0
- package/templates/web/src/lib/supabase/constants.ts +25 -0
- package/templates/web/src/lib/supabase/server.ts +29 -0
- package/templates/web/src/lib/supabase/types.ts +112 -0
- package/templates/web/src/lib/utils/cache.ts +98 -0
- package/templates/web/src/lib/utils/rate-limiter.ts +102 -0
- package/templates/web/src/store/actions/index.ts +2 -0
- package/templates/web/src/store/index.ts +13 -0
- package/templates/web/src/store/reducers/index.ts +13 -0
- package/templates/web/src/store/types/index.ts +2 -0
- 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();
|