@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.
Files changed (64) hide show
  1. package/README.md +65 -0
  2. package/about.md +93 -0
  3. package/api/authors/[username]/route.ts +150 -0
  4. package/api/authors/route.ts +63 -0
  5. package/api/posts/public/route.ts +151 -0
  6. package/components/ExportPostsButton.tsx +102 -0
  7. package/components/ImportPostsDialog.tsx +284 -0
  8. package/components/PostsToolbar.tsx +24 -0
  9. package/components/editor/FeaturedImageUpload.tsx +185 -0
  10. package/components/editor/WysiwygEditor.tsx +340 -0
  11. package/components/index.ts +4 -0
  12. package/components/public/AuthorBio.tsx +105 -0
  13. package/components/public/AuthorCard.tsx +130 -0
  14. package/components/public/BlogFooter.tsx +185 -0
  15. package/components/public/BlogNavbar.tsx +201 -0
  16. package/components/public/PostCard.tsx +306 -0
  17. package/components/public/ReadingProgress.tsx +70 -0
  18. package/components/public/RelatedPosts.tsx +78 -0
  19. package/config/app.config.ts +200 -0
  20. package/config/billing.config.ts +146 -0
  21. package/config/dashboard.config.ts +333 -0
  22. package/config/dev.config.ts +48 -0
  23. package/config/features.config.ts +196 -0
  24. package/config/flows.config.ts +333 -0
  25. package/config/permissions.config.ts +101 -0
  26. package/config/theme.config.ts +128 -0
  27. package/entities/categories/categories.config.ts +60 -0
  28. package/entities/categories/categories.fields.ts +115 -0
  29. package/entities/categories/categories.service.ts +333 -0
  30. package/entities/categories/categories.types.ts +58 -0
  31. package/entities/categories/messages/en.json +33 -0
  32. package/entities/categories/messages/es.json +33 -0
  33. package/entities/posts/messages/en.json +100 -0
  34. package/entities/posts/messages/es.json +100 -0
  35. package/entities/posts/migrations/001_posts_table.sql +110 -0
  36. package/entities/posts/migrations/002_add_featured.sql +19 -0
  37. package/entities/posts/migrations/003_post_categories_pivot.sql +47 -0
  38. package/entities/posts/posts.config.ts +61 -0
  39. package/entities/posts/posts.fields.ts +234 -0
  40. package/entities/posts/posts.service.ts +464 -0
  41. package/entities/posts/posts.types.ts +80 -0
  42. package/lib/selectors.ts +179 -0
  43. package/messages/en.json +113 -0
  44. package/messages/es.json +113 -0
  45. package/migrations/002_author_profile_fields.sql +37 -0
  46. package/migrations/003_categories_table.sql +90 -0
  47. package/migrations/999_sample_data.sql +412 -0
  48. package/migrations/999_theme_sample_data.sql +1070 -0
  49. package/package.json +18 -0
  50. package/permissions-matrix.md +63 -0
  51. package/styles/article.css +333 -0
  52. package/styles/components.css +204 -0
  53. package/styles/globals.css +327 -0
  54. package/styles/theme.css +167 -0
  55. package/templates/(public)/author/[username]/page.tsx +247 -0
  56. package/templates/(public)/authors/page.tsx +161 -0
  57. package/templates/(public)/layout.tsx +44 -0
  58. package/templates/(public)/page.tsx +276 -0
  59. package/templates/(public)/posts/[slug]/page.tsx +342 -0
  60. package/templates/dashboard/(main)/page.tsx +385 -0
  61. package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +529 -0
  62. package/templates/dashboard/(main)/posts/[id]/page.tsx +33 -0
  63. package/templates/dashboard/(main)/posts/create/page.tsx +353 -0
  64. package/templates/dashboard/(main)/posts/page.tsx +833 -0
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@nextsparkjs/theme-blog",
3
+ "version": "0.1.0-beta.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./config/theme.config.ts",
7
+ "requiredPlugins": [],
8
+ "dependencies": {},
9
+ "peerDependencies": {
10
+ "@nextsparkjs/core": "workspace:*",
11
+ "lucide-react": "^0.539.0",
12
+ "next": "^15.0.0",
13
+ "next-intl": "^4.0.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "zod": "^4.0.0"
17
+ }
18
+ }
@@ -0,0 +1,63 @@
1
+ # Blog Theme - Permissions Matrix
2
+
3
+ ## Teams Mode: `single-user`
4
+
5
+ No teams functionality. Single user owns all content.
6
+
7
+ ## Roles
8
+
9
+ | Role | Description |
10
+ |------|-------------|
11
+ | owner | Blog owner - full control |
12
+
13
+ > Note: In `single-user` mode, only owner role exists. No team invitations.
14
+
15
+ ---
16
+
17
+ ## Entity Permissions
18
+
19
+ ### Posts
20
+
21
+ | Action | owner |
22
+ |--------|-------|
23
+ | create | ✅ |
24
+ | read | ✅ |
25
+ | list | ✅ |
26
+ | update | ✅ |
27
+ | delete | ✅ |
28
+ | publish | ✅ |
29
+ | archive | ✅ |
30
+
31
+ ---
32
+
33
+ ## Theme Features
34
+
35
+ | Feature | owner | Description |
36
+ |---------|-------|-------------|
37
+ | `posts.export_json` | ✅ | Export posts to JSON format |
38
+ | `posts.import_json` | ✅ | Import posts from JSON format |
39
+
40
+ ---
41
+
42
+ ## Disabled Core Permissions
43
+
44
+ - `teams.invite` - No invitations in single-user mode
45
+ - `teams.remove_member` - N/A
46
+ - `teams.change_roles` - N/A
47
+ - `teams.delete` - N/A
48
+ - `settings.billing` - No billing
49
+ - `settings.api_keys` - No API keys
50
+
51
+ ---
52
+
53
+ ## Test Scenarios
54
+
55
+ ### Owner (only user)
56
+
57
+ 1. ✅ Can create, edit, delete posts
58
+ 2. ✅ Can publish/unpublish posts
59
+ 3. ✅ Can export posts to JSON
60
+ 4. ✅ Can import posts from JSON
61
+ 5. ✅ Cannot invite other users
62
+ 6. ✅ No team switcher visible
63
+
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Blog Theme - Article Typography
3
+ *
4
+ * Clean, readable typography for long-form content.
5
+ * Optimized for reading comfort and legibility.
6
+ */
7
+
8
+ /* ============================================================================
9
+ ARTICLE CONTAINER
10
+ ============================================================================ */
11
+
12
+ .article-content {
13
+ max-width: 680px;
14
+ margin: 0 auto;
15
+ font-family: var(--font-serif, 'Merriweather', Georgia, serif);
16
+ font-size: clamp(1.0625rem, 1rem + 0.25vw, 1.1875rem);
17
+ line-height: 1.8;
18
+ color: var(--foreground);
19
+ }
20
+
21
+ /* ============================================================================
22
+ HEADINGS
23
+ ============================================================================ */
24
+
25
+ .article-content h1,
26
+ .article-content h2,
27
+ .article-content h3,
28
+ .article-content h4,
29
+ .article-content h5,
30
+ .article-content h6 {
31
+ font-family: var(--font-sans, 'Oxanium', sans-serif);
32
+ font-weight: 700;
33
+ line-height: 1.3;
34
+ letter-spacing: -0.01em;
35
+ color: var(--foreground);
36
+ }
37
+
38
+ .article-content h1 {
39
+ font-size: clamp(2rem, 1.5rem + 2vw, 2.5rem);
40
+ margin-top: 0;
41
+ margin-bottom: 1.5rem;
42
+ }
43
+
44
+ .article-content h2 {
45
+ font-size: clamp(1.5rem, 1.25rem + 1vw, 1.875rem);
46
+ margin-top: 3rem;
47
+ margin-bottom: 1rem;
48
+ padding-top: 1rem;
49
+ border-top: 1px solid var(--border);
50
+ }
51
+
52
+ .article-content h3 {
53
+ font-size: clamp(1.25rem, 1.125rem + 0.5vw, 1.5rem);
54
+ margin-top: 2.5rem;
55
+ margin-bottom: 0.75rem;
56
+ }
57
+
58
+ .article-content h4 {
59
+ font-size: 1.125rem;
60
+ margin-top: 2rem;
61
+ margin-bottom: 0.5rem;
62
+ }
63
+
64
+ /* ============================================================================
65
+ PARAGRAPHS & TEXT
66
+ ============================================================================ */
67
+
68
+ .article-content p {
69
+ margin-bottom: 1.75rem;
70
+ }
71
+
72
+ .article-content strong {
73
+ font-weight: 700;
74
+ color: var(--foreground);
75
+ }
76
+
77
+ .article-content em {
78
+ font-style: italic;
79
+ }
80
+
81
+ .article-content a {
82
+ color: var(--primary);
83
+ text-decoration: underline;
84
+ text-underline-offset: 3px;
85
+ text-decoration-thickness: 1px;
86
+ transition: text-decoration-thickness 0.2s ease;
87
+ }
88
+
89
+ .article-content a:hover {
90
+ text-decoration-thickness: 2px;
91
+ }
92
+
93
+ /* ============================================================================
94
+ LISTS
95
+ ============================================================================ */
96
+
97
+ .article-content ul,
98
+ .article-content ol {
99
+ margin-bottom: 1.75rem;
100
+ padding-left: 1.5rem;
101
+ }
102
+
103
+ .article-content li {
104
+ margin-bottom: 0.5rem;
105
+ }
106
+
107
+ .article-content li::marker {
108
+ color: var(--primary);
109
+ }
110
+
111
+ .article-content ul li {
112
+ list-style-type: disc;
113
+ }
114
+
115
+ .article-content ol li {
116
+ list-style-type: decimal;
117
+ }
118
+
119
+ /* ============================================================================
120
+ BLOCKQUOTES
121
+ ============================================================================ */
122
+
123
+ .article-content blockquote {
124
+ margin: 2rem 0;
125
+ padding: 1.5rem 2rem;
126
+ border-left: 4px solid var(--primary);
127
+ background-color: var(--muted);
128
+ border-radius: 0 0.5rem 0.5rem 0;
129
+ font-style: italic;
130
+ color: var(--muted-foreground);
131
+ }
132
+
133
+ .article-content blockquote p:last-child {
134
+ margin-bottom: 0;
135
+ }
136
+
137
+ /* Pull quotes - for emphasis */
138
+ .article-content .pullquote {
139
+ position: relative;
140
+ margin: 3rem 0;
141
+ padding: 2rem;
142
+ text-align: center;
143
+ font-size: 1.5rem;
144
+ font-weight: 500;
145
+ font-style: normal;
146
+ border: none;
147
+ background: none;
148
+ color: var(--foreground);
149
+ }
150
+
151
+ .article-content .pullquote::before,
152
+ .article-content .pullquote::after {
153
+ content: '"';
154
+ font-size: 4rem;
155
+ color: var(--primary);
156
+ opacity: 0.3;
157
+ line-height: 0;
158
+ }
159
+
160
+ .article-content .pullquote::before {
161
+ position: absolute;
162
+ top: 0;
163
+ left: 0;
164
+ }
165
+
166
+ .article-content .pullquote::after {
167
+ position: absolute;
168
+ bottom: 0;
169
+ right: 0;
170
+ }
171
+
172
+ /* ============================================================================
173
+ CODE
174
+ ============================================================================ */
175
+
176
+ .article-content code {
177
+ font-family: var(--font-mono, 'Fira Code', monospace);
178
+ font-size: 0.875em;
179
+ padding: 0.2em 0.4em;
180
+ background-color: var(--muted);
181
+ border-radius: 0.25rem;
182
+ }
183
+
184
+ .article-content pre {
185
+ margin: 2rem 0;
186
+ padding: 1.5rem;
187
+ background-color: var(--muted);
188
+ border-radius: 0.5rem;
189
+ overflow-x: auto;
190
+ font-size: 0.875rem;
191
+ line-height: 1.6;
192
+ }
193
+
194
+ .article-content pre code {
195
+ background: none;
196
+ padding: 0;
197
+ font-size: inherit;
198
+ }
199
+
200
+ /* ============================================================================
201
+ IMAGES & FIGURES
202
+ ============================================================================ */
203
+
204
+ .article-content img {
205
+ max-width: 100%;
206
+ height: auto;
207
+ border-radius: 0.5rem;
208
+ margin: 2rem 0;
209
+ }
210
+
211
+ .article-content figure {
212
+ margin: 2rem 0;
213
+ }
214
+
215
+ .article-content figure img {
216
+ margin: 0;
217
+ }
218
+
219
+ .article-content figcaption {
220
+ margin-top: 0.75rem;
221
+ font-size: 0.875rem;
222
+ color: var(--muted-foreground);
223
+ text-align: center;
224
+ font-style: italic;
225
+ }
226
+
227
+ /* Full-width images */
228
+ .article-content .full-width {
229
+ width: 100vw;
230
+ max-width: 100vw;
231
+ margin-left: calc(-50vw + 50%);
232
+ margin-right: calc(-50vw + 50%);
233
+ border-radius: 0;
234
+ }
235
+
236
+ /* ============================================================================
237
+ HORIZONTAL RULE
238
+ ============================================================================ */
239
+
240
+ .article-content hr {
241
+ margin: 3rem 0;
242
+ border: none;
243
+ border-top: 1px solid var(--border);
244
+ }
245
+
246
+ /* Decorative divider */
247
+ .article-content hr.divider {
248
+ text-align: center;
249
+ border: none;
250
+ }
251
+
252
+ .article-content hr.divider::after {
253
+ content: '• • •';
254
+ display: inline-block;
255
+ font-size: 1.5rem;
256
+ color: var(--muted-foreground);
257
+ letter-spacing: 1rem;
258
+ }
259
+
260
+ /* ============================================================================
261
+ TABLES
262
+ ============================================================================ */
263
+
264
+ .article-content table {
265
+ width: 100%;
266
+ margin: 2rem 0;
267
+ border-collapse: collapse;
268
+ font-size: 0.9375rem;
269
+ }
270
+
271
+ .article-content th,
272
+ .article-content td {
273
+ padding: 0.75rem 1rem;
274
+ text-align: left;
275
+ border-bottom: 1px solid var(--border);
276
+ }
277
+
278
+ .article-content th {
279
+ font-family: var(--font-sans);
280
+ font-weight: 600;
281
+ background-color: var(--muted);
282
+ }
283
+
284
+ .article-content tr:last-child td {
285
+ border-bottom: none;
286
+ }
287
+
288
+ /* ============================================================================
289
+ FOOTNOTES
290
+ ============================================================================ */
291
+
292
+ .article-content .footnote {
293
+ font-size: 0.875rem;
294
+ color: var(--muted-foreground);
295
+ vertical-align: super;
296
+ }
297
+
298
+ .article-content .footnotes {
299
+ margin-top: 3rem;
300
+ padding-top: 1.5rem;
301
+ border-top: 1px solid var(--border);
302
+ font-size: 0.875rem;
303
+ color: var(--muted-foreground);
304
+ }
305
+
306
+ /* ============================================================================
307
+ RESPONSIVE ADJUSTMENTS
308
+ ============================================================================ */
309
+
310
+ @media (max-width: 768px) {
311
+ .article-content {
312
+ font-size: 1rem;
313
+ line-height: 1.7;
314
+ }
315
+
316
+ .article-content blockquote {
317
+ padding: 1rem 1.25rem;
318
+ margin: 1.5rem 0;
319
+ }
320
+
321
+ .article-content pre {
322
+ padding: 1rem;
323
+ font-size: 0.8125rem;
324
+ }
325
+
326
+ .article-content h2 {
327
+ margin-top: 2rem;
328
+ }
329
+
330
+ .article-content h3 {
331
+ margin-top: 1.5rem;
332
+ }
333
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Blog Theme - Component Styles
3
+ *
4
+ * Custom component styling for blog theme.
5
+ */
6
+
7
+ /* ============================================================================
8
+ NAVIGATION
9
+ ============================================================================ */
10
+
11
+ .blog-nav {
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: space-between;
15
+ padding: 1rem 0;
16
+ border-bottom: 1px solid hsl(var(--border));
17
+ }
18
+
19
+ .blog-nav-logo {
20
+ font-size: 1.5rem;
21
+ font-weight: 700;
22
+ color: hsl(var(--foreground));
23
+ text-decoration: none;
24
+ }
25
+
26
+ .blog-nav-links {
27
+ display: flex;
28
+ gap: 1.5rem;
29
+ }
30
+
31
+ .blog-nav-link {
32
+ color: hsl(var(--muted-foreground));
33
+ text-decoration: none;
34
+ font-weight: 500;
35
+ transition: color 0.2s ease;
36
+ }
37
+
38
+ .blog-nav-link:hover {
39
+ color: hsl(var(--foreground));
40
+ }
41
+
42
+ /* ============================================================================
43
+ POST LIST
44
+ ============================================================================ */
45
+
46
+ .post-list {
47
+ display: grid;
48
+ gap: 2rem;
49
+ }
50
+
51
+ .post-list-item {
52
+ display: flex;
53
+ gap: 1.5rem;
54
+ padding-bottom: 2rem;
55
+ border-bottom: 1px solid hsl(var(--border));
56
+ }
57
+
58
+ .post-list-item:last-child {
59
+ border-bottom: none;
60
+ }
61
+
62
+ .post-list-image {
63
+ flex-shrink: 0;
64
+ width: 200px;
65
+ height: 150px;
66
+ border-radius: 0.5rem;
67
+ overflow: hidden;
68
+ }
69
+
70
+ .post-list-image img {
71
+ width: 100%;
72
+ height: 100%;
73
+ object-fit: cover;
74
+ }
75
+
76
+ .post-list-content {
77
+ flex: 1;
78
+ display: flex;
79
+ flex-direction: column;
80
+ }
81
+
82
+ .post-list-title {
83
+ font-size: 1.25rem;
84
+ font-weight: 600;
85
+ margin-bottom: 0.5rem;
86
+ color: hsl(var(--foreground));
87
+ text-decoration: none;
88
+ }
89
+
90
+ .post-list-title:hover {
91
+ color: hsl(var(--primary));
92
+ }
93
+
94
+ .post-list-excerpt {
95
+ color: hsl(var(--muted-foreground));
96
+ margin-bottom: 1rem;
97
+ display: -webkit-box;
98
+ -webkit-line-clamp: 2;
99
+ -webkit-box-orient: vertical;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .post-list-meta {
104
+ margin-top: auto;
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 1rem;
108
+ font-size: 0.875rem;
109
+ color: hsl(var(--muted-foreground));
110
+ }
111
+
112
+ /* ============================================================================
113
+ POST SINGLE
114
+ ============================================================================ */
115
+
116
+ .post-header {
117
+ text-align: center;
118
+ max-width: 48rem;
119
+ margin: 0 auto 3rem;
120
+ }
121
+
122
+ .post-title {
123
+ font-size: 3rem;
124
+ font-weight: 700;
125
+ line-height: 1.2;
126
+ margin-bottom: 1rem;
127
+ letter-spacing: -0.02em;
128
+ }
129
+
130
+ .post-meta {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ gap: 1rem;
135
+ color: hsl(var(--muted-foreground));
136
+ }
137
+
138
+ .post-author {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 0.5rem;
142
+ }
143
+
144
+ .post-author-avatar {
145
+ width: 2.5rem;
146
+ height: 2.5rem;
147
+ border-radius: 50%;
148
+ }
149
+
150
+ .post-featured-image {
151
+ width: 100%;
152
+ max-height: 500px;
153
+ border-radius: 1rem;
154
+ overflow: hidden;
155
+ margin-bottom: 3rem;
156
+ }
157
+
158
+ .post-featured-image img {
159
+ width: 100%;
160
+ height: 100%;
161
+ object-fit: cover;
162
+ }
163
+
164
+ .post-content {
165
+ max-width: 48rem;
166
+ margin: 0 auto;
167
+ }
168
+
169
+ /* ============================================================================
170
+ FOOTER
171
+ ============================================================================ */
172
+
173
+ .blog-footer {
174
+ margin-top: 4rem;
175
+ padding: 2rem 0;
176
+ border-top: 1px solid hsl(var(--border));
177
+ text-align: center;
178
+ color: hsl(var(--muted-foreground));
179
+ font-size: 0.875rem;
180
+ }
181
+
182
+ /* ============================================================================
183
+ RESPONSIVE
184
+ ============================================================================ */
185
+
186
+ @media (max-width: 768px) {
187
+ .post-list-item {
188
+ flex-direction: column;
189
+ }
190
+
191
+ .post-list-image {
192
+ width: 100%;
193
+ height: 200px;
194
+ }
195
+
196
+ .post-title {
197
+ font-size: 2rem;
198
+ }
199
+
200
+ .prose {
201
+ font-size: 1rem;
202
+ }
203
+ }
204
+