@nextsparkjs/theme-blog 0.1.0-beta.1
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/README.md +65 -0
- package/about.md +93 -0
- package/api/authors/[username]/route.ts +150 -0
- package/api/authors/route.ts +63 -0
- package/api/posts/public/route.ts +151 -0
- package/components/ExportPostsButton.tsx +102 -0
- package/components/ImportPostsDialog.tsx +284 -0
- package/components/PostsToolbar.tsx +24 -0
- package/components/editor/FeaturedImageUpload.tsx +185 -0
- package/components/editor/WysiwygEditor.tsx +340 -0
- package/components/index.ts +4 -0
- package/components/public/AuthorBio.tsx +105 -0
- package/components/public/AuthorCard.tsx +130 -0
- package/components/public/BlogFooter.tsx +185 -0
- package/components/public/BlogNavbar.tsx +201 -0
- package/components/public/PostCard.tsx +306 -0
- package/components/public/ReadingProgress.tsx +70 -0
- package/components/public/RelatedPosts.tsx +78 -0
- package/config/app.config.ts +200 -0
- package/config/billing.config.ts +146 -0
- package/config/dashboard.config.ts +333 -0
- package/config/dev.config.ts +48 -0
- package/config/features.config.ts +196 -0
- package/config/flows.config.ts +333 -0
- package/config/permissions.config.ts +101 -0
- package/config/theme.config.ts +128 -0
- package/entities/categories/categories.config.ts +60 -0
- package/entities/categories/categories.fields.ts +115 -0
- package/entities/categories/categories.service.ts +333 -0
- package/entities/categories/categories.types.ts +58 -0
- package/entities/categories/messages/en.json +33 -0
- package/entities/categories/messages/es.json +33 -0
- package/entities/posts/messages/en.json +100 -0
- package/entities/posts/messages/es.json +100 -0
- package/entities/posts/migrations/001_posts_table.sql +110 -0
- package/entities/posts/migrations/002_add_featured.sql +19 -0
- package/entities/posts/migrations/003_post_categories_pivot.sql +47 -0
- package/entities/posts/posts.config.ts +61 -0
- package/entities/posts/posts.fields.ts +234 -0
- package/entities/posts/posts.service.ts +464 -0
- package/entities/posts/posts.types.ts +80 -0
- package/lib/selectors.ts +179 -0
- package/messages/en.json +113 -0
- package/messages/es.json +113 -0
- package/migrations/002_author_profile_fields.sql +37 -0
- package/migrations/003_categories_table.sql +90 -0
- package/migrations/999_sample_data.sql +412 -0
- package/migrations/999_theme_sample_data.sql +1070 -0
- package/package.json +18 -0
- package/permissions-matrix.md +63 -0
- package/styles/article.css +333 -0
- package/styles/components.css +204 -0
- package/styles/globals.css +327 -0
- package/styles/theme.css +167 -0
- package/templates/(public)/author/[username]/page.tsx +247 -0
- package/templates/(public)/authors/page.tsx +161 -0
- package/templates/(public)/layout.tsx +44 -0
- package/templates/(public)/page.tsx +276 -0
- package/templates/(public)/posts/[slug]/page.tsx +342 -0
- package/templates/dashboard/(main)/page.tsx +385 -0
- package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +529 -0
- package/templates/dashboard/(main)/posts/[id]/page.tsx +33 -0
- package/templates/dashboard/(main)/posts/create/page.tsx +353 -0
- package/templates/dashboard/(main)/posts/page.tsx +833 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Author Profile Page
|
|
5
|
+
*
|
|
6
|
+
* Public profile page for a blog author showing their bio, social links,
|
|
7
|
+
* and all their published posts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, use } from 'react'
|
|
11
|
+
import Link from 'next/link'
|
|
12
|
+
import Image from 'next/image'
|
|
13
|
+
import { Twitter, Linkedin, Globe, User } from 'lucide-react'
|
|
14
|
+
import { PostCard } from '@/themes/blog/components/public/PostCard'
|
|
15
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
16
|
+
|
|
17
|
+
interface Author {
|
|
18
|
+
id: string
|
|
19
|
+
name: string
|
|
20
|
+
username: string
|
|
21
|
+
bio: string | null
|
|
22
|
+
image: string | null
|
|
23
|
+
socialTwitter: string | null
|
|
24
|
+
socialLinkedin: string | null
|
|
25
|
+
socialWebsite: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Post {
|
|
29
|
+
id: string
|
|
30
|
+
title: string
|
|
31
|
+
slug: string
|
|
32
|
+
excerpt: string | null
|
|
33
|
+
featuredImage: string | null
|
|
34
|
+
category: string | null
|
|
35
|
+
publishedAt: string | null
|
|
36
|
+
createdAt: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Stats {
|
|
40
|
+
totalPosts: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PageProps {
|
|
44
|
+
params: Promise<{
|
|
45
|
+
username: string
|
|
46
|
+
}>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default function AuthorPage({ params }: PageProps) {
|
|
50
|
+
const { username } = use(params)
|
|
51
|
+
const [author, setAuthor] = useState<Author | null>(null)
|
|
52
|
+
const [posts, setPosts] = useState<Post[]>([])
|
|
53
|
+
const [stats, setStats] = useState<Stats | null>(null)
|
|
54
|
+
const [loading, setLoading] = useState(true)
|
|
55
|
+
const [error, setError] = useState<string | null>(null)
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const fetchAuthorData = async () => {
|
|
59
|
+
try {
|
|
60
|
+
setLoading(true)
|
|
61
|
+
setError(null)
|
|
62
|
+
|
|
63
|
+
const response = await fetch(`/api/v1/theme/blog/authors/${username}`, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
if (response.status === 404) {
|
|
72
|
+
throw new Error('Author not found')
|
|
73
|
+
}
|
|
74
|
+
throw new Error('Failed to load author profile')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await response.json()
|
|
78
|
+
setAuthor(result.data.author)
|
|
79
|
+
setPosts(result.data.posts || [])
|
|
80
|
+
setStats(result.data.stats)
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('Error fetching author:', err)
|
|
83
|
+
setError(err instanceof Error ? err.message : 'Unable to load author profile')
|
|
84
|
+
} finally {
|
|
85
|
+
setLoading(false)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fetchAuthorData()
|
|
90
|
+
}, [username])
|
|
91
|
+
|
|
92
|
+
// Calculate reading time
|
|
93
|
+
const calculateReadingTime = (content: string | null): number => {
|
|
94
|
+
if (!content) return 3
|
|
95
|
+
const wordCount = content.split(/\s+/).length
|
|
96
|
+
return Math.max(1, Math.ceil(wordCount / 200))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (loading) {
|
|
100
|
+
return (
|
|
101
|
+
<div className="max-w-4xl mx-auto">
|
|
102
|
+
<div className="flex items-center justify-center py-16">
|
|
103
|
+
<div className="text-center">
|
|
104
|
+
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
105
|
+
<p className="text-muted-foreground">Loading author profile...</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (error || !author) {
|
|
113
|
+
return (
|
|
114
|
+
<div className="max-w-4xl mx-auto">
|
|
115
|
+
<div className="text-center py-16">
|
|
116
|
+
<h2 className="text-2xl font-bold mb-2">Author Not Found</h2>
|
|
117
|
+
<p className="text-muted-foreground mb-6">
|
|
118
|
+
{error || 'The author you are looking for does not exist.'}
|
|
119
|
+
</p>
|
|
120
|
+
<Link
|
|
121
|
+
href="/"
|
|
122
|
+
className="inline-block px-6 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
|
|
123
|
+
>
|
|
124
|
+
Back to Home
|
|
125
|
+
</Link>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="max-w-4xl mx-auto">
|
|
133
|
+
{/* Author Header */}
|
|
134
|
+
<section className="py-12 border-b border-border">
|
|
135
|
+
<div className="flex flex-col md:flex-row gap-8 items-start">
|
|
136
|
+
{/* Avatar */}
|
|
137
|
+
<div className="flex-shrink-0">
|
|
138
|
+
{author.image ? (
|
|
139
|
+
<Image
|
|
140
|
+
src={author.image}
|
|
141
|
+
alt={author.name}
|
|
142
|
+
width={128}
|
|
143
|
+
height={128}
|
|
144
|
+
className="rounded-full"
|
|
145
|
+
/>
|
|
146
|
+
) : (
|
|
147
|
+
<div className="w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center">
|
|
148
|
+
<User className="w-16 h-16 text-muted-foreground" />
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* Info */}
|
|
154
|
+
<div className="flex-1">
|
|
155
|
+
<h1 className="font-serif text-3xl md:text-4xl font-bold mb-2">
|
|
156
|
+
{author.name}
|
|
157
|
+
</h1>
|
|
158
|
+
<p className="text-muted-foreground mb-4">@{author.username}</p>
|
|
159
|
+
|
|
160
|
+
{author.bio && (
|
|
161
|
+
<p className="text-lg mb-6 leading-relaxed">{author.bio}</p>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Social Links */}
|
|
165
|
+
<div className="flex items-center gap-4">
|
|
166
|
+
{author.socialTwitter && (
|
|
167
|
+
<a
|
|
168
|
+
href={author.socialTwitter}
|
|
169
|
+
target="_blank"
|
|
170
|
+
rel="noopener noreferrer"
|
|
171
|
+
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
172
|
+
>
|
|
173
|
+
<Twitter className="w-4 h-4" />
|
|
174
|
+
<span className="sr-only">Twitter</span>
|
|
175
|
+
</a>
|
|
176
|
+
)}
|
|
177
|
+
{author.socialLinkedin && (
|
|
178
|
+
<a
|
|
179
|
+
href={author.socialLinkedin}
|
|
180
|
+
target="_blank"
|
|
181
|
+
rel="noopener noreferrer"
|
|
182
|
+
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
183
|
+
>
|
|
184
|
+
<Linkedin className="w-4 h-4" />
|
|
185
|
+
<span className="sr-only">LinkedIn</span>
|
|
186
|
+
</a>
|
|
187
|
+
)}
|
|
188
|
+
{author.socialWebsite && (
|
|
189
|
+
<a
|
|
190
|
+
href={author.socialWebsite}
|
|
191
|
+
target="_blank"
|
|
192
|
+
rel="noopener noreferrer"
|
|
193
|
+
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
194
|
+
>
|
|
195
|
+
<Globe className="w-4 h-4" />
|
|
196
|
+
<span className="sr-only">Website</span>
|
|
197
|
+
</a>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Stats */}
|
|
202
|
+
{stats && (
|
|
203
|
+
<div className="mt-6 inline-flex items-center gap-2 px-4 py-2 bg-muted rounded-full text-sm">
|
|
204
|
+
<span className="font-semibold">{stats.totalPosts}</span>
|
|
205
|
+
<span className="text-muted-foreground">
|
|
206
|
+
{stats.totalPosts === 1 ? 'published post' : 'published posts'}
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
|
|
214
|
+
{/* Author's Posts */}
|
|
215
|
+
<section className="py-12">
|
|
216
|
+
<h2 className="font-serif text-2xl font-bold mb-8">Posts by {author.name}</h2>
|
|
217
|
+
|
|
218
|
+
{posts.length === 0 ? (
|
|
219
|
+
<div className="text-center py-12 text-muted-foreground">
|
|
220
|
+
<p>No published posts yet.</p>
|
|
221
|
+
</div>
|
|
222
|
+
) : (
|
|
223
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
224
|
+
{posts.map((post) => (
|
|
225
|
+
<PostCard
|
|
226
|
+
key={post.id}
|
|
227
|
+
id={post.id}
|
|
228
|
+
title={post.title}
|
|
229
|
+
slug={post.slug}
|
|
230
|
+
excerpt={post.excerpt}
|
|
231
|
+
featuredImage={post.featuredImage}
|
|
232
|
+
category={post.category}
|
|
233
|
+
categorySlug={post.category?.toLowerCase().replace(/\s+/g, '-') || null}
|
|
234
|
+
authorName={author.name}
|
|
235
|
+
authorUsername={author.username}
|
|
236
|
+
authorAvatar={author.image}
|
|
237
|
+
publishedAt={post.publishedAt || post.createdAt}
|
|
238
|
+
readingTime={calculateReadingTime(post.excerpt)}
|
|
239
|
+
variant="default"
|
|
240
|
+
/>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</section>
|
|
245
|
+
</div>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authors List Page
|
|
5
|
+
*
|
|
6
|
+
* Public page listing all blog authors with their profile info.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect } from 'react'
|
|
10
|
+
import Link from 'next/link'
|
|
11
|
+
import Image from 'next/image'
|
|
12
|
+
import { User } from 'lucide-react'
|
|
13
|
+
|
|
14
|
+
interface Author {
|
|
15
|
+
id: string
|
|
16
|
+
name: string
|
|
17
|
+
username: string
|
|
18
|
+
bio: string | null
|
|
19
|
+
image: string | null
|
|
20
|
+
postCount: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function AuthorsPage() {
|
|
24
|
+
const [authors, setAuthors] = useState<Author[]>([])
|
|
25
|
+
const [loading, setLoading] = useState(true)
|
|
26
|
+
const [error, setError] = useState<string | null>(null)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const fetchAuthors = async () => {
|
|
30
|
+
try {
|
|
31
|
+
setLoading(true)
|
|
32
|
+
setError(null)
|
|
33
|
+
|
|
34
|
+
const response = await fetch('/api/v1/theme/blog/authors', {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error('Failed to load authors')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = await response.json()
|
|
46
|
+
setAuthors(result.data || [])
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error('Error fetching authors:', err)
|
|
49
|
+
setError(err instanceof Error ? err.message : 'Unable to load authors')
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fetchAuthors()
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
if (loading) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="max-w-4xl mx-auto">
|
|
61
|
+
<div className="flex items-center justify-center py-16">
|
|
62
|
+
<div className="text-center">
|
|
63
|
+
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
|
64
|
+
<p className="text-muted-foreground">Loading authors...</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (error) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="max-w-4xl mx-auto">
|
|
74
|
+
<div className="text-center py-16">
|
|
75
|
+
<h2 className="text-2xl font-bold mb-2">Error Loading Authors</h2>
|
|
76
|
+
<p className="text-muted-foreground mb-6">{error}</p>
|
|
77
|
+
<Link
|
|
78
|
+
href="/"
|
|
79
|
+
className="inline-block px-6 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
|
|
80
|
+
>
|
|
81
|
+
Back to Home
|
|
82
|
+
</Link>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="max-w-4xl mx-auto">
|
|
90
|
+
{/* Page Header */}
|
|
91
|
+
<section className="py-12 border-b border-border">
|
|
92
|
+
<h1 className="font-serif text-3xl md:text-4xl font-bold mb-4">
|
|
93
|
+
Our Authors
|
|
94
|
+
</h1>
|
|
95
|
+
<p className="text-lg text-muted-foreground">
|
|
96
|
+
Meet the writers who share their stories, ideas, and expertise on our platform.
|
|
97
|
+
</p>
|
|
98
|
+
</section>
|
|
99
|
+
|
|
100
|
+
{/* Authors Grid */}
|
|
101
|
+
<section className="py-12">
|
|
102
|
+
{authors.length === 0 ? (
|
|
103
|
+
<div className="text-center py-12 text-muted-foreground">
|
|
104
|
+
<p>No authors found.</p>
|
|
105
|
+
</div>
|
|
106
|
+
) : (
|
|
107
|
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
108
|
+
{authors.map((author) => (
|
|
109
|
+
<Link
|
|
110
|
+
key={author.id}
|
|
111
|
+
href={`/author/${author.username}`}
|
|
112
|
+
className="group block p-6 bg-card border border-border rounded-xl hover:border-primary/50 hover:shadow-lg transition-all"
|
|
113
|
+
>
|
|
114
|
+
{/* Avatar */}
|
|
115
|
+
<div className="flex justify-center mb-4">
|
|
116
|
+
{author.image ? (
|
|
117
|
+
<Image
|
|
118
|
+
src={author.image}
|
|
119
|
+
alt={author.name}
|
|
120
|
+
width={80}
|
|
121
|
+
height={80}
|
|
122
|
+
className="rounded-full"
|
|
123
|
+
/>
|
|
124
|
+
) : (
|
|
125
|
+
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center">
|
|
126
|
+
<User className="w-10 h-10 text-muted-foreground" />
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Info */}
|
|
132
|
+
<div className="text-center">
|
|
133
|
+
<h2 className="font-serif text-xl font-bold group-hover:text-primary transition-colors">
|
|
134
|
+
{author.name}
|
|
135
|
+
</h2>
|
|
136
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
137
|
+
@{author.username}
|
|
138
|
+
</p>
|
|
139
|
+
|
|
140
|
+
{author.bio && (
|
|
141
|
+
<p className="text-sm text-muted-foreground line-clamp-2 mb-4">
|
|
142
|
+
{author.bio}
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{/* Post Count */}
|
|
147
|
+
<div className="inline-flex items-center gap-2 px-3 py-1 bg-muted rounded-full text-xs">
|
|
148
|
+
<span className="font-semibold">{author.postCount}</span>
|
|
149
|
+
<span className="text-muted-foreground">
|
|
150
|
+
{author.postCount === 1 ? 'post' : 'posts'}
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</Link>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</section>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Blog Public Layout
|
|
5
|
+
*
|
|
6
|
+
* Clean, minimal layout for the public-facing blog.
|
|
7
|
+
* Uses custom BlogNavbar and BlogFooter components.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { BlogNavbar } from '@/themes/blog/components/public/BlogNavbar'
|
|
11
|
+
import { BlogFooter } from '@/themes/blog/components/public/BlogFooter'
|
|
12
|
+
|
|
13
|
+
interface BlogLayoutProps {
|
|
14
|
+
children: React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Navigation links for the public blog
|
|
18
|
+
const NAV_LINKS = [
|
|
19
|
+
{ name: 'Authors', href: '/authors' }
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export default function BlogLayout({ children }: BlogLayoutProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="min-h-screen flex flex-col bg-background">
|
|
25
|
+
{/* Navigation */}
|
|
26
|
+
<BlogNavbar
|
|
27
|
+
blogTitle="My Blog"
|
|
28
|
+
navLinks={NAV_LINKS}
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
{/* Main Content */}
|
|
32
|
+
<main className="flex-1 container mx-auto px-4 py-8">
|
|
33
|
+
{children}
|
|
34
|
+
</main>
|
|
35
|
+
|
|
36
|
+
{/* Footer */}
|
|
37
|
+
<BlogFooter
|
|
38
|
+
blogTitle="My Blog"
|
|
39
|
+
authorName="Your Name"
|
|
40
|
+
showNewsletter={false}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|