@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
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Reading Progress Component
5
+ *
6
+ * Fixed progress bar at the top of the page that tracks
7
+ * scroll progress through an article.
8
+ */
9
+
10
+ import { useState, useEffect } from 'react'
11
+ import { cn } from '@nextsparkjs/core/lib/utils'
12
+
13
+ interface ReadingProgressProps {
14
+ className?: string
15
+ height?: number
16
+ }
17
+
18
+ export function ReadingProgress({
19
+ className,
20
+ height = 3
21
+ }: ReadingProgressProps) {
22
+ const [progress, setProgress] = useState(0)
23
+ const [isVisible, setIsVisible] = useState(false)
24
+
25
+ useEffect(() => {
26
+ const calculateProgress = () => {
27
+ const scrollTop = window.scrollY
28
+ const docHeight = document.documentElement.scrollHeight - window.innerHeight
29
+ const scrollPercent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0
30
+
31
+ setProgress(Math.min(100, Math.max(0, scrollPercent)))
32
+ setIsVisible(scrollTop > 100)
33
+ }
34
+
35
+ // Calculate initial progress
36
+ calculateProgress()
37
+
38
+ // Add scroll listener with passive for better performance
39
+ window.addEventListener('scroll', calculateProgress, { passive: true })
40
+
41
+ return () => {
42
+ window.removeEventListener('scroll', calculateProgress)
43
+ }
44
+ }, [])
45
+
46
+ return (
47
+ <div
48
+ data-cy="reading-progress"
49
+ className={cn(
50
+ 'fixed top-0 left-0 right-0 z-[100] transition-opacity duration-300',
51
+ isVisible ? 'opacity-100' : 'opacity-0',
52
+ className
53
+ )}
54
+ style={{ height: `${height}px` }}
55
+ role="progressbar"
56
+ aria-valuenow={Math.round(progress)}
57
+ aria-valuemin={0}
58
+ aria-valuemax={100}
59
+ aria-label="Reading progress"
60
+ >
61
+ <div
62
+ data-cy="reading-progress-bar"
63
+ className="h-full bg-primary transition-[width] duration-100 ease-out"
64
+ style={{ width: `${progress}%` }}
65
+ />
66
+ </div>
67
+ )
68
+ }
69
+
70
+ export default ReadingProgress
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Related Posts Component
5
+ *
6
+ * Displays a horizontal list of related posts from the same category.
7
+ */
8
+
9
+ import Link from 'next/link'
10
+ import Image from 'next/image'
11
+ import { Clock } from 'lucide-react'
12
+
13
+ interface RelatedPost {
14
+ id: string
15
+ title: string
16
+ slug: string
17
+ featuredImage?: string | null
18
+ readingTime?: number
19
+ }
20
+
21
+ interface RelatedPostsProps {
22
+ posts: RelatedPost[]
23
+ title?: string
24
+ }
25
+
26
+ export function RelatedPosts({
27
+ posts,
28
+ title = 'Related Articles'
29
+ }: RelatedPostsProps) {
30
+ if (posts.length === 0) return null
31
+
32
+ return (
33
+ <section data-cy="related-posts" className="py-8">
34
+ <h2 data-cy="related-posts-title" className="font-serif text-xl font-bold mb-6">{title}</h2>
35
+
36
+ <div data-cy="related-posts-grid" className="grid gap-4 md:grid-cols-3">
37
+ {posts.slice(0, 3).map((post) => (
38
+ <Link
39
+ key={post.id}
40
+ href={`/posts/${post.slug || post.id}`}
41
+ data-cy={`related-post-${post.id}`}
42
+ className="group flex flex-col overflow-hidden rounded-lg border border-border bg-card hover:shadow-md transition-all duration-200"
43
+ >
44
+ {/* Image */}
45
+ <div data-cy={`related-post-image-${post.id}`} className="aspect-[16/9] relative overflow-hidden">
46
+ {post.featuredImage ? (
47
+ <Image
48
+ src={post.featuredImage}
49
+ alt={post.title}
50
+ fill
51
+ className="object-cover transition-transform duration-300 group-hover:scale-105"
52
+ sizes="(max-width: 768px) 100vw, 33vw"
53
+ />
54
+ ) : (
55
+ <div className="w-full h-full bg-gradient-to-br from-primary/20 to-primary/5" />
56
+ )}
57
+ </div>
58
+
59
+ {/* Content */}
60
+ <div data-cy={`related-post-content-${post.id}`} className="p-4">
61
+ <h3 data-cy={`related-post-title-${post.id}`} className="font-medium text-sm line-clamp-2 group-hover:text-primary transition-colors">
62
+ {post.title}
63
+ </h3>
64
+ {post.readingTime && (
65
+ <div data-cy={`related-post-reading-time-${post.id}`} className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
66
+ <Clock className="w-3 h-3" />
67
+ <span>{post.readingTime} min read</span>
68
+ </div>
69
+ )}
70
+ </div>
71
+ </Link>
72
+ ))}
73
+ </div>
74
+ </section>
75
+ )
76
+ }
77
+
78
+ export default RelatedPosts
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Blog Theme - Application Configuration
3
+ *
4
+ * Multi-author blog platform with single-user mode.
5
+ * Each author has their own isolated team but content is aggregated publicly.
6
+ */
7
+
8
+ export const APP_CONFIG_OVERRIDES = {
9
+ // =============================================================================
10
+ // APPLICATION METADATA
11
+ // =============================================================================
12
+ app: {
13
+ name: 'Blog Platform',
14
+ version: '2.0.0',
15
+ },
16
+
17
+ // =============================================================================
18
+ // TEAMS CONFIGURATION - SINGLE USER MODE
19
+ // =============================================================================
20
+ /**
21
+ * Single-user mode:
22
+ * - No team switching
23
+ * - No invitations
24
+ * - No team creation
25
+ * - Each author has their personal team
26
+ * - Content aggregates publicly across all authors
27
+ */
28
+ teams: {
29
+ mode: 'single-user' as const,
30
+ options: {
31
+ maxTeamsPerUser: 1,
32
+ maxMembersPerTeam: 1,
33
+ allowLeaveAllTeams: false,
34
+ },
35
+ },
36
+
37
+ // =============================================================================
38
+ // TOPBAR CONFIGURATION
39
+ // =============================================================================
40
+ topbar: {
41
+ features: {
42
+ quickCreate: true,
43
+ search: false, // No search in MVP
44
+ notifications: false,
45
+ help: false,
46
+ theme: true,
47
+ sector7: false,
48
+ userMenu: true,
49
+ },
50
+ },
51
+
52
+ // =============================================================================
53
+ // SETTINGS PAGES CONFIGURATION
54
+ // =============================================================================
55
+ settings: {
56
+ pages: {
57
+ profile: { enabled: true, order: 1 },
58
+ billing: { enabled: true, order: 2 }, // Billing enabled for Pro/Enterprise plans
59
+ apiKeys: { enabled: false },
60
+ password: { enabled: true, order: 3 },
61
+ teams: { enabled: false }, // Hidden in single-user mode
62
+ },
63
+ },
64
+
65
+ // =============================================================================
66
+ // INTERNATIONALIZATION
67
+ // =============================================================================
68
+ i18n: {
69
+ supportedLocales: ['en', 'es'],
70
+ defaultLocale: 'en' as const,
71
+ namespaces: [
72
+ 'common',
73
+ 'dashboard',
74
+ 'settings',
75
+ 'auth',
76
+ 'public',
77
+ 'validation',
78
+ // Theme specific
79
+ 'posts',
80
+ 'blog',
81
+ ],
82
+ },
83
+
84
+ // =============================================================================
85
+ // API CONFIGURATION
86
+ // =============================================================================
87
+ api: {
88
+ cors: {
89
+ allowedOrigins: {
90
+ development: [
91
+ 'http://localhost:3000',
92
+ 'http://localhost:5173',
93
+ ],
94
+ production: [],
95
+ },
96
+ },
97
+ },
98
+
99
+ // =============================================================================
100
+ // DOCUMENTATION
101
+ // =============================================================================
102
+ docs: {
103
+ enabled: true,
104
+ public: true,
105
+ searchEnabled: true,
106
+ breadcrumbs: true,
107
+ theme: {
108
+ enabled: true,
109
+ open: true,
110
+ label: "Blog Theme",
111
+ },
112
+ plugins: {
113
+ enabled: false,
114
+ open: false,
115
+ label: "Plugins",
116
+ },
117
+ core: {
118
+ enabled: true,
119
+ open: false,
120
+ label: "Core",
121
+ },
122
+ showPluginsDocsInProd: false,
123
+ },
124
+
125
+ // =============================================================================
126
+ // SCHEDULED ACTIONS
127
+ // =============================================================================
128
+ scheduledActions: {
129
+ enabled: true,
130
+ retentionDays: 7,
131
+ batchSize: 10,
132
+ defaultTimeout: 30000,
133
+ },
134
+
135
+ // =============================================================================
136
+ // MOBILE NAVIGATION
137
+ // =============================================================================
138
+ mobileNav: {
139
+ items: [
140
+ {
141
+ id: 'home',
142
+ labelKey: 'common.mobileNav.home',
143
+ href: '/dashboard',
144
+ icon: 'Home',
145
+ enabled: true,
146
+ },
147
+ {
148
+ id: 'posts',
149
+ labelKey: 'posts.title',
150
+ href: '/dashboard/posts',
151
+ icon: 'FileText',
152
+ enabled: true,
153
+ },
154
+ {
155
+ id: 'create',
156
+ labelKey: 'common.mobileNav.create',
157
+ icon: 'Plus',
158
+ isCentral: true,
159
+ action: 'quickCreate',
160
+ enabled: true,
161
+ },
162
+ {
163
+ id: 'settings',
164
+ labelKey: 'common.mobileNav.settings',
165
+ href: '/dashboard/settings',
166
+ icon: 'Settings',
167
+ enabled: true,
168
+ },
169
+ {
170
+ id: 'view-blog',
171
+ labelKey: 'blog.viewBlog',
172
+ href: '/',
173
+ icon: 'ExternalLink',
174
+ enabled: true,
175
+ },
176
+ ],
177
+ moreSheetItems: [
178
+ {
179
+ id: 'profile',
180
+ labelKey: 'common.navigation.profile',
181
+ href: '/dashboard/settings/profile',
182
+ icon: 'User',
183
+ enabled: true,
184
+ },
185
+ {
186
+ id: 'export',
187
+ labelKey: 'blog.export',
188
+ href: '/dashboard/settings/export',
189
+ icon: 'Download',
190
+ enabled: true,
191
+ },
192
+ ],
193
+ },
194
+
195
+ // =============================================================================
196
+ // DEV KEYRING - MOVED TO dev.config.ts
197
+ // =============================================================================
198
+ // DevKeyring configuration has been moved to config/dev.config.ts
199
+ // This separates development-only settings from production configuration.
200
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Billing Configuration - Blog Theme
3
+ *
4
+ * Defines plans, features, limits, and action mappings for the Blog theme.
5
+ * Customized for content publishing and blogging.
6
+ */
7
+
8
+ import type { BillingConfig } from '@nextsparkjs/core/lib/billing/config-types'
9
+
10
+ export const billingConfig: BillingConfig = {
11
+ provider: 'stripe',
12
+ currency: 'usd',
13
+ defaultPlan: 'free',
14
+
15
+ // ===========================================
16
+ // FEATURE DEFINITIONS (Blog-specific)
17
+ // ===========================================
18
+ features: {
19
+ basic_analytics: {
20
+ name: 'billing.features.basic_analytics',
21
+ description: 'billing.features.basic_analytics_description',
22
+ },
23
+ custom_domain: {
24
+ name: 'billing.features.custom_domain',
25
+ description: 'billing.features.custom_domain_description',
26
+ },
27
+ advanced_seo: {
28
+ name: 'billing.features.advanced_seo',
29
+ description: 'billing.features.advanced_seo_description',
30
+ },
31
+ priority_support: {
32
+ name: 'billing.features.priority_support',
33
+ description: 'billing.features.priority_support_description',
34
+ },
35
+ },
36
+
37
+ // ===========================================
38
+ // LIMIT DEFINITIONS (Blog-specific)
39
+ // ===========================================
40
+ limits: {
41
+ posts: {
42
+ name: 'billing.limits.posts',
43
+ unit: 'count',
44
+ resetPeriod: 'never',
45
+ },
46
+ monthly_views: {
47
+ name: 'billing.limits.monthly_views',
48
+ unit: 'count',
49
+ resetPeriod: 'monthly',
50
+ },
51
+ storage_gb: {
52
+ name: 'billing.limits.storage',
53
+ unit: 'bytes',
54
+ resetPeriod: 'never',
55
+ },
56
+ authors: {
57
+ name: 'billing.limits.authors',
58
+ unit: 'count',
59
+ resetPeriod: 'never',
60
+ },
61
+ },
62
+
63
+ // ===========================================
64
+ // PLAN DEFINITIONS (Blog-optimized)
65
+ // ===========================================
66
+ plans: [
67
+ {
68
+ slug: 'free',
69
+ name: 'billing.plans.free.name',
70
+ description: 'billing.plans.free.description',
71
+ type: 'free',
72
+ visibility: 'public',
73
+ price: { monthly: 0, yearly: 0 },
74
+ trialDays: 0,
75
+ features: ['basic_analytics'],
76
+ limits: {
77
+ posts: 10,
78
+ monthly_views: 10000,
79
+ storage_gb: 1,
80
+ authors: 1,
81
+ },
82
+ stripePriceIdMonthly: null,
83
+ stripePriceIdYearly: null,
84
+ },
85
+ {
86
+ slug: 'pro',
87
+ name: 'billing.plans.pro.name',
88
+ description: 'billing.plans.pro.description',
89
+ type: 'paid',
90
+ visibility: 'public',
91
+ price: {
92
+ monthly: 2900, // $29.00 - Same as default theme
93
+ yearly: 29000, // $290.00 (16% savings)
94
+ },
95
+ trialDays: 14,
96
+ features: ['basic_analytics', 'custom_domain', 'advanced_seo'],
97
+ limits: {
98
+ posts: -1, // Unlimited posts
99
+ monthly_views: 100000,
100
+ storage_gb: 10,
101
+ authors: 5,
102
+ },
103
+ // Configure these in Stripe Dashboard
104
+ stripePriceIdMonthly: 'price_blog_pro_monthly',
105
+ stripePriceIdYearly: 'price_blog_pro_yearly',
106
+ },
107
+ {
108
+ slug: 'enterprise',
109
+ name: 'billing.plans.enterprise.name',
110
+ description: 'billing.plans.enterprise.description',
111
+ type: 'enterprise',
112
+ visibility: 'hidden',
113
+ trialDays: 30,
114
+ features: ['*'], // All features
115
+ limits: {
116
+ posts: -1, // Unlimited
117
+ monthly_views: -1,
118
+ storage_gb: -1,
119
+ authors: -1,
120
+ },
121
+ stripePriceIdMonthly: null,
122
+ stripePriceIdYearly: null,
123
+ },
124
+ ],
125
+
126
+ // ===========================================
127
+ // ACTION MAPPINGS (Blog-specific)
128
+ // ===========================================
129
+ actionMappings: {
130
+ permissions: {
131
+ 'team.billing.manage': 'team.billing.manage',
132
+ },
133
+
134
+ features: {
135
+ 'domain.customize': 'custom_domain',
136
+ 'seo.advanced': 'advanced_seo',
137
+ 'support.priority_access': 'priority_support',
138
+ },
139
+
140
+ limits: {
141
+ 'posts.create': 'posts',
142
+ 'files.upload': 'storage_gb',
143
+ 'authors.invite': 'authors',
144
+ },
145
+ },
146
+ }