@nextsparkjs/theme-blog 0.1.0-beta.189 → 0.1.0-beta.190
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/components/ExportPostsButton.tsx +2 -1
- package/components/ImportPostsDialog.tsx +2 -1
- package/components/editor/FeaturedImageUpload.tsx +2 -1
- package/components/public/AuthorBio.tsx +3 -2
- package/components/public/AuthorCard.tsx +3 -2
- package/components/public/BlogFooter.tsx +2 -1
- package/components/public/PostCard.tsx +8 -7
- package/components/public/RelatedPosts.tsx +2 -1
- package/config/app.config.ts +4 -10
- package/migrations/090_billing_sample_data.sql +99 -0
- package/migrations/999_theme_sample_data.sql +13 -13
- package/package.json +2 -2
- package/templates/(public)/author/[username]/page.tsx +6 -5
- package/templates/(public)/authors/page.tsx +3 -2
- package/templates/(public)/page.tsx +2 -1
- package/templates/(public)/posts/[slug]/page.tsx +6 -5
- package/templates/dashboard/(main)/page.tsx +2 -1
- package/templates/dashboard/(main)/posts/[id]/edit/page.tsx +4 -3
- package/templates/dashboard/(main)/posts/create/page.tsx +2 -1
- package/templates/dashboard/(main)/posts/page.tsx +7 -6
|
@@ -4,6 +4,7 @@ import { useState } from 'react'
|
|
|
4
4
|
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
5
5
|
import { Download, Loader2 } from 'lucide-react'
|
|
6
6
|
import { useToast } from '@nextsparkjs/core/hooks/useToast'
|
|
7
|
+
import { withBasePath } from '@nextsparkjs/core/lib/base-path'
|
|
7
8
|
|
|
8
9
|
interface ExportPostsButtonProps {
|
|
9
10
|
className?: string
|
|
@@ -17,7 +18,7 @@ export function ExportPostsButton({ className }: ExportPostsButtonProps) {
|
|
|
17
18
|
setIsExporting(true)
|
|
18
19
|
|
|
19
20
|
try {
|
|
20
|
-
const response = await fetch('/api/v1/posts?limit=1000', {
|
|
21
|
+
const response = await fetch(withBasePath('/api/v1/posts?limit=1000'), {
|
|
21
22
|
method: 'GET',
|
|
22
23
|
headers: {
|
|
23
24
|
'Content-Type': 'application/json',
|
|
@@ -16,6 +16,7 @@ import { Badge } from '@nextsparkjs/core/components/ui/badge'
|
|
|
16
16
|
import { ScrollArea } from '@nextsparkjs/core/components/ui/scroll-area'
|
|
17
17
|
import { Upload, FileJson, AlertCircle, CheckCircle2, Loader2 } from 'lucide-react'
|
|
18
18
|
import { useToast } from '@nextsparkjs/core/hooks/useToast'
|
|
19
|
+
import { withBasePath } from '@nextsparkjs/core/lib/base-path'
|
|
19
20
|
|
|
20
21
|
interface PostImport {
|
|
21
22
|
title: string
|
|
@@ -108,7 +109,7 @@ export function ImportPostsDialog({ className, onImportComplete }: ImportPostsDi
|
|
|
108
109
|
const post = posts[i]
|
|
109
110
|
|
|
110
111
|
try {
|
|
111
|
-
const response = await fetch('/api/v1/posts', {
|
|
112
|
+
const response = await fetch(withBasePath('/api/v1/posts'), {
|
|
112
113
|
method: 'POST',
|
|
113
114
|
headers: {
|
|
114
115
|
'Content-Type': 'application/json',
|
|
@@ -12,6 +12,7 @@ import NextImage from 'next/image'
|
|
|
12
12
|
import { X, Upload, Image as ImageIcon } from 'lucide-react'
|
|
13
13
|
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
14
14
|
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
15
|
+
import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
15
16
|
|
|
16
17
|
interface FeaturedImageUploadProps {
|
|
17
18
|
value: string // URL string
|
|
@@ -111,7 +112,7 @@ export function FeaturedImageUpload({
|
|
|
111
112
|
// Image Preview
|
|
112
113
|
<div className="relative aspect-video rounded-lg overflow-hidden border border-border bg-muted" data-cy="featured-image-preview">
|
|
113
114
|
<NextImage
|
|
114
|
-
src={value}
|
|
115
|
+
src={withBasePathIfInApp(value)}
|
|
115
116
|
alt="Featured image preview"
|
|
116
117
|
fill
|
|
117
118
|
className="object-cover"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import Image from 'next/image'
|
|
10
10
|
import Link from 'next/link'
|
|
11
11
|
import { Twitter, Github, Linkedin, Globe, User } from 'lucide-react'
|
|
12
|
+
import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
12
13
|
|
|
13
14
|
interface SocialLink {
|
|
14
15
|
type: 'twitter' | 'github' | 'linkedin' | 'website'
|
|
@@ -43,7 +44,7 @@ export function AuthorBio({
|
|
|
43
44
|
<div data-cy="author-bio-avatar" className="flex-shrink-0">
|
|
44
45
|
{avatar ? (
|
|
45
46
|
<Image
|
|
46
|
-
src={avatar}
|
|
47
|
+
src={withBasePathIfInApp(avatar)}
|
|
47
48
|
alt={name}
|
|
48
49
|
width={64}
|
|
49
50
|
height={64}
|
|
@@ -84,7 +85,7 @@ export function AuthorBio({
|
|
|
84
85
|
return (
|
|
85
86
|
<a
|
|
86
87
|
key={link.type}
|
|
87
|
-
href={link.url}
|
|
88
|
+
href={withBasePathIfInApp(link.url)}
|
|
88
89
|
data-cy={`author-bio-social-${link.type}`}
|
|
89
90
|
target="_blank"
|
|
90
91
|
rel="noopener noreferrer"
|
|
@@ -11,6 +11,7 @@ import Link from 'next/link'
|
|
|
11
11
|
import Image from 'next/image'
|
|
12
12
|
import { User } from 'lucide-react'
|
|
13
13
|
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
14
|
+
import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
14
15
|
|
|
15
16
|
interface AuthorCardProps {
|
|
16
17
|
username: string
|
|
@@ -48,7 +49,7 @@ export function AuthorCard({
|
|
|
48
49
|
<div data-cy={`author-card-avatar-${username}`} className="flex-shrink-0">
|
|
49
50
|
{avatar ? (
|
|
50
51
|
<Image
|
|
51
|
-
src={avatar}
|
|
52
|
+
src={withBasePathIfInApp(avatar)}
|
|
52
53
|
alt={name}
|
|
53
54
|
width={48}
|
|
54
55
|
height={48}
|
|
@@ -89,7 +90,7 @@ export function AuthorCard({
|
|
|
89
90
|
<div data-cy={`author-card-avatar-${username}`} className="flex justify-center mb-4">
|
|
90
91
|
{avatar ? (
|
|
91
92
|
<Image
|
|
92
|
-
src={avatar}
|
|
93
|
+
src={withBasePathIfInApp(avatar)}
|
|
93
94
|
alt={name}
|
|
94
95
|
width={96}
|
|
95
96
|
height={96}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import Link from 'next/link'
|
|
10
10
|
import { Github, Twitter, Linkedin, Mail, Heart } from 'lucide-react'
|
|
11
|
+
import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
11
12
|
|
|
12
13
|
interface SocialLink {
|
|
13
14
|
name: string
|
|
@@ -117,7 +118,7 @@ export function BlogFooter({
|
|
|
117
118
|
return (
|
|
118
119
|
<a
|
|
119
120
|
key={link.name}
|
|
120
|
-
href={link.href}
|
|
121
|
+
href={withBasePathIfInApp(link.href)}
|
|
121
122
|
data-cy={`blog-footer-social-${link.icon}`}
|
|
122
123
|
target="_blank"
|
|
123
124
|
rel="noopener noreferrer"
|
|
@@ -11,6 +11,7 @@ import Link from 'next/link'
|
|
|
11
11
|
import Image from 'next/image'
|
|
12
12
|
import { Calendar, Clock, User } from 'lucide-react'
|
|
13
13
|
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
14
|
+
import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
14
15
|
|
|
15
16
|
interface PostCardProps {
|
|
16
17
|
id: string
|
|
@@ -73,7 +74,7 @@ export function PostCard({
|
|
|
73
74
|
<div data-cy={`post-card-image-${id}`} className="aspect-[21/9] relative overflow-hidden">
|
|
74
75
|
{featuredImage ? (
|
|
75
76
|
<Image
|
|
76
|
-
src={featuredImage}
|
|
77
|
+
src={withBasePathIfInApp(featuredImage)}
|
|
77
78
|
alt={title}
|
|
78
79
|
fill
|
|
79
80
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
|
@@ -115,7 +116,7 @@ export function PostCard({
|
|
|
115
116
|
<Link href={authorUrl} data-cy={`post-card-author-${id}`} className="flex items-center gap-2 hover:text-foreground transition-colors">
|
|
116
117
|
{authorAvatar ? (
|
|
117
118
|
<Image
|
|
118
|
-
src={authorAvatar}
|
|
119
|
+
src={withBasePathIfInApp(authorAvatar)}
|
|
119
120
|
alt={authorName}
|
|
120
121
|
width={24}
|
|
121
122
|
height={24}
|
|
@@ -132,7 +133,7 @@ export function PostCard({
|
|
|
132
133
|
<div data-cy={`post-card-author-${id}`} className="flex items-center gap-2">
|
|
133
134
|
{authorAvatar ? (
|
|
134
135
|
<Image
|
|
135
|
-
src={authorAvatar}
|
|
136
|
+
src={withBasePathIfInApp(authorAvatar)}
|
|
136
137
|
alt={authorName}
|
|
137
138
|
width={24}
|
|
138
139
|
height={24}
|
|
@@ -174,7 +175,7 @@ export function PostCard({
|
|
|
174
175
|
<div data-cy={`post-card-image-${id}`} className="flex-shrink-0 w-24 h-24 relative overflow-hidden rounded-md">
|
|
175
176
|
{featuredImage ? (
|
|
176
177
|
<Image
|
|
177
|
-
src={featuredImage}
|
|
178
|
+
src={withBasePathIfInApp(featuredImage)}
|
|
178
179
|
alt={title}
|
|
179
180
|
fill
|
|
180
181
|
className="object-cover"
|
|
@@ -216,7 +217,7 @@ export function PostCard({
|
|
|
216
217
|
<div data-cy={`post-card-image-${id}`} className="aspect-[16/10] relative overflow-hidden">
|
|
217
218
|
{featuredImage ? (
|
|
218
219
|
<Image
|
|
219
|
-
src={featuredImage}
|
|
220
|
+
src={withBasePathIfInApp(featuredImage)}
|
|
220
221
|
alt={title}
|
|
221
222
|
fill
|
|
222
223
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
|
@@ -256,7 +257,7 @@ export function PostCard({
|
|
|
256
257
|
<Link href={authorUrl} data-cy={`post-card-author-${id}`} className="flex items-center gap-2 hover:text-foreground transition-colors">
|
|
257
258
|
{authorAvatar ? (
|
|
258
259
|
<Image
|
|
259
|
-
src={authorAvatar}
|
|
260
|
+
src={withBasePathIfInApp(authorAvatar)}
|
|
260
261
|
alt={authorName}
|
|
261
262
|
width={20}
|
|
262
263
|
height={20}
|
|
@@ -273,7 +274,7 @@ export function PostCard({
|
|
|
273
274
|
<div data-cy={`post-card-author-${id}`} className="flex items-center gap-2">
|
|
274
275
|
{authorAvatar ? (
|
|
275
276
|
<Image
|
|
276
|
-
src={authorAvatar}
|
|
277
|
+
src={withBasePathIfInApp(authorAvatar)}
|
|
277
278
|
alt={authorName}
|
|
278
279
|
width={20}
|
|
279
280
|
height={20}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import Link from 'next/link'
|
|
10
10
|
import Image from 'next/image'
|
|
11
11
|
import { Clock } from 'lucide-react'
|
|
12
|
+
import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
12
13
|
|
|
13
14
|
interface RelatedPost {
|
|
14
15
|
id: string
|
|
@@ -45,7 +46,7 @@ export function RelatedPosts({
|
|
|
45
46
|
<div data-cy={`related-post-image-${post.id}`} className="aspect-[16/9] relative overflow-hidden">
|
|
46
47
|
{post.featuredImage ? (
|
|
47
48
|
<Image
|
|
48
|
-
src={post.featuredImage}
|
|
49
|
+
src={withBasePathIfInApp(post.featuredImage)}
|
|
49
50
|
alt={post.title}
|
|
50
51
|
fill
|
|
51
52
|
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
package/config/app.config.ts
CHANGED
|
@@ -100,25 +100,19 @@ export const APP_CONFIG_OVERRIDES = {
|
|
|
100
100
|
// =============================================================================
|
|
101
101
|
docs: {
|
|
102
102
|
enabled: true,
|
|
103
|
-
|
|
103
|
+
publicAccess: true,
|
|
104
104
|
searchEnabled: true,
|
|
105
105
|
breadcrumbs: true,
|
|
106
|
-
|
|
106
|
+
public: {
|
|
107
107
|
enabled: true,
|
|
108
108
|
open: true,
|
|
109
109
|
label: "Blog Theme",
|
|
110
110
|
},
|
|
111
|
-
|
|
112
|
-
enabled: false,
|
|
113
|
-
open: false,
|
|
114
|
-
label: "Plugins",
|
|
115
|
-
},
|
|
116
|
-
core: {
|
|
111
|
+
superadmin: {
|
|
117
112
|
enabled: true,
|
|
118
113
|
open: false,
|
|
119
|
-
label: "
|
|
114
|
+
label: "Admin Docs",
|
|
120
115
|
},
|
|
121
|
-
showPluginsDocsInProd: false,
|
|
122
116
|
},
|
|
123
117
|
|
|
124
118
|
// =============================================================================
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
-- Migration: 090_billing_sample_data.sql
|
|
2
|
+
-- Description: Sample billing plans for the blog theme (free, pro, enterprise)
|
|
3
|
+
-- Theme: blog
|
|
4
|
+
-- Phase: Theme sample data - runs before 999_theme_sample_data.sql, whose
|
|
5
|
+
-- subscriptions reference these plans by slug.
|
|
6
|
+
--
|
|
7
|
+
-- Mirrors themes/blog/config/billing.config.ts (slugs, prices, features,
|
|
8
|
+
-- limits) so the seeded plans match what the app's billing config declares.
|
|
9
|
+
|
|
10
|
+
-- ============================================
|
|
11
|
+
-- SAMPLE PLANS
|
|
12
|
+
-- ============================================
|
|
13
|
+
|
|
14
|
+
-- Plan: Free
|
|
15
|
+
INSERT INTO public."plans" (
|
|
16
|
+
id,
|
|
17
|
+
slug,
|
|
18
|
+
name,
|
|
19
|
+
description,
|
|
20
|
+
type,
|
|
21
|
+
visibility,
|
|
22
|
+
"priceMonthly",
|
|
23
|
+
"priceYearly",
|
|
24
|
+
"trialDays",
|
|
25
|
+
features,
|
|
26
|
+
limits,
|
|
27
|
+
"sortOrder"
|
|
28
|
+
) VALUES (
|
|
29
|
+
'plan_blog_free',
|
|
30
|
+
'free',
|
|
31
|
+
'Free',
|
|
32
|
+
'Free plan to get started',
|
|
33
|
+
'free',
|
|
34
|
+
'public',
|
|
35
|
+
0,
|
|
36
|
+
0,
|
|
37
|
+
0,
|
|
38
|
+
'["basic_analytics"]'::jsonb,
|
|
39
|
+
'{"posts": 10, "monthly_views": 10000, "storage_gb": 1, "authors": 1}'::jsonb,
|
|
40
|
+
1
|
|
41
|
+
) ON CONFLICT (slug) DO NOTHING;
|
|
42
|
+
|
|
43
|
+
-- Plan: Pro
|
|
44
|
+
INSERT INTO public."plans" (
|
|
45
|
+
id,
|
|
46
|
+
slug,
|
|
47
|
+
name,
|
|
48
|
+
description,
|
|
49
|
+
type,
|
|
50
|
+
visibility,
|
|
51
|
+
"priceMonthly",
|
|
52
|
+
"priceYearly",
|
|
53
|
+
"trialDays",
|
|
54
|
+
features,
|
|
55
|
+
limits,
|
|
56
|
+
"sortOrder"
|
|
57
|
+
) VALUES (
|
|
58
|
+
'plan_blog_pro',
|
|
59
|
+
'pro',
|
|
60
|
+
'Pro',
|
|
61
|
+
'Professional plan for bloggers',
|
|
62
|
+
'paid',
|
|
63
|
+
'public',
|
|
64
|
+
2900,
|
|
65
|
+
29000,
|
|
66
|
+
14,
|
|
67
|
+
'["basic_analytics", "custom_domain", "advanced_seo"]'::jsonb,
|
|
68
|
+
'{"posts": -1, "monthly_views": 100000, "storage_gb": 10, "authors": 5}'::jsonb,
|
|
69
|
+
2
|
|
70
|
+
) ON CONFLICT (slug) DO NOTHING;
|
|
71
|
+
|
|
72
|
+
-- Plan: Enterprise
|
|
73
|
+
INSERT INTO public."plans" (
|
|
74
|
+
id,
|
|
75
|
+
slug,
|
|
76
|
+
name,
|
|
77
|
+
description,
|
|
78
|
+
type,
|
|
79
|
+
visibility,
|
|
80
|
+
"priceMonthly",
|
|
81
|
+
"priceYearly",
|
|
82
|
+
"trialDays",
|
|
83
|
+
features,
|
|
84
|
+
limits,
|
|
85
|
+
"sortOrder"
|
|
86
|
+
) VALUES (
|
|
87
|
+
'plan_blog_enterprise',
|
|
88
|
+
'enterprise',
|
|
89
|
+
'Enterprise',
|
|
90
|
+
'Custom enterprise plan with unlimited features',
|
|
91
|
+
'enterprise',
|
|
92
|
+
'hidden',
|
|
93
|
+
NULL,
|
|
94
|
+
NULL,
|
|
95
|
+
30,
|
|
96
|
+
'["*"]'::jsonb,
|
|
97
|
+
'{"posts": -1, "monthly_views": -1, "storage_gb": -1, "authors": -1}'::jsonb,
|
|
98
|
+
3
|
|
99
|
+
) ON CONFLICT (slug) DO NOTHING;
|
|
@@ -662,15 +662,15 @@ INSERT INTO public."subscriptions" (
|
|
|
662
662
|
"externalSubscriptionId", "externalCustomerId", "createdAt"
|
|
663
663
|
) VALUES
|
|
664
664
|
-- Marcos Tech → Pro Plan $29/mo (active, monthly)
|
|
665
|
-
('sub-blog-marcos', 'team-blog-marcos', 'usr-blog-marcos', '
|
|
665
|
+
('sub-blog-marcos', 'team-blog-marcos', 'usr-blog-marcos', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
|
|
666
666
|
NOW() - INTERVAL '30 days', NOW() + INTERVAL '30 days', 'monthly', 'stripe',
|
|
667
667
|
'sub_stripe_marcos', 'cus_marcos', NOW() - INTERVAL '6 months'),
|
|
668
668
|
-- Lucia Lifestyle → Pro Plan $29/mo (active, monthly)
|
|
669
|
-
('sub-blog-lucia', 'team-blog-lucia', 'usr-blog-lucia', '
|
|
669
|
+
('sub-blog-lucia', 'team-blog-lucia', 'usr-blog-lucia', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
|
|
670
670
|
NOW() - INTERVAL '15 days', NOW() + INTERVAL '15 days', 'monthly', 'stripe',
|
|
671
671
|
'sub_stripe_lucia', 'cus_lucia', NOW() - INTERVAL '5 months'),
|
|
672
672
|
-- Carlos Finance → Pro Plan (trialing, monthly)
|
|
673
|
-
('sub-blog-carlos', 'team-blog-carlos', 'usr-blog-carlos', '
|
|
673
|
+
('sub-blog-carlos', 'team-blog-carlos', 'usr-blog-carlos', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'trialing',
|
|
674
674
|
NOW(), NOW() + INTERVAL '14 days', 'monthly', NULL, NULL, NULL, NOW() - INTERVAL '4 months')
|
|
675
675
|
ON CONFLICT (id) DO NOTHING;
|
|
676
676
|
|
|
@@ -843,64 +843,64 @@ INSERT INTO public."subscriptions" (
|
|
|
843
843
|
"createdAt", "canceledAt", "cancelAtPeriodEnd", metadata
|
|
844
844
|
) VALUES
|
|
845
845
|
-- 1. Diana Design - Pro Monthly ACTIVE (8 months customer)
|
|
846
|
-
('sub-blog-diana', 'team-blog-diana', 'usr-blog-diana', '
|
|
846
|
+
('sub-blog-diana', 'team-blog-diana', 'usr-blog-diana', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
|
|
847
847
|
NOW() - INTERVAL '25 days', NOW() + INTERVAL '5 days', 'monthly',
|
|
848
848
|
'stripe', 'sub_stripe_diana', 'cus_diana',
|
|
849
849
|
NOW() - INTERVAL '8 months', NULL, false, '{}'::jsonb),
|
|
850
850
|
|
|
851
851
|
-- 2. Elena Eco - Pro Monthly ACTIVE (7 months customer)
|
|
852
|
-
('sub-blog-elena', 'team-blog-elena', 'usr-blog-elena', '
|
|
852
|
+
('sub-blog-elena', 'team-blog-elena', 'usr-blog-elena', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
|
|
853
853
|
NOW() - INTERVAL '20 days', NOW() + INTERVAL '10 days', 'monthly',
|
|
854
854
|
'stripe', 'sub_stripe_elena', 'cus_elena',
|
|
855
855
|
NOW() - INTERVAL '7 months', NULL, false, '{}'::jsonb),
|
|
856
856
|
|
|
857
857
|
-- 3. Felix Fitness - Pro Yearly ACTIVE (14 months = 2nd year)
|
|
858
|
-
('sub-blog-felix', 'team-blog-felix', 'usr-blog-felix', '
|
|
858
|
+
('sub-blog-felix', 'team-blog-felix', 'usr-blog-felix', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
|
|
859
859
|
NOW() - INTERVAL '60 days', NOW() + INTERVAL '305 days', 'yearly',
|
|
860
860
|
'stripe', 'sub_stripe_felix', 'cus_felix',
|
|
861
861
|
NOW() - INTERVAL '14 months', NULL, false, '{}'::jsonb),
|
|
862
862
|
|
|
863
863
|
-- 4. Gloria Gourmet - Pro Yearly ACTIVE (12 months)
|
|
864
|
-
('sub-blog-gloria', 'team-blog-gloria', 'usr-blog-gloria', '
|
|
864
|
+
('sub-blog-gloria', 'team-blog-gloria', 'usr-blog-gloria', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
|
|
865
865
|
NOW() - INTERVAL '30 days', NOW() + INTERVAL '335 days', 'yearly',
|
|
866
866
|
'stripe', 'sub_stripe_gloria', 'cus_gloria',
|
|
867
867
|
NOW() - INTERVAL '12 months', NULL, false, '{}'::jsonb),
|
|
868
868
|
|
|
869
869
|
-- 5. Hugo Health - Pro Monthly ACTIVE (6 months customer)
|
|
870
|
-
('sub-blog-hugo', 'team-blog-hugo', 'usr-blog-hugo', '
|
|
870
|
+
('sub-blog-hugo', 'team-blog-hugo', 'usr-blog-hugo', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
|
|
871
871
|
NOW() - INTERVAL '18 days', NOW() + INTERVAL '12 days', 'monthly',
|
|
872
872
|
'stripe', 'sub_stripe_hugo', 'cus_hugo',
|
|
873
873
|
NOW() - INTERVAL '6 months', NULL, false, '{}'::jsonb),
|
|
874
874
|
|
|
875
875
|
-- 6. Iris Inspire - Pro Monthly PAST_DUE (failed payment)
|
|
876
|
-
('sub-blog-iris', 'team-blog-iris', 'usr-blog-iris', '
|
|
876
|
+
('sub-blog-iris', 'team-blog-iris', 'usr-blog-iris', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'past_due',
|
|
877
877
|
NOW() - INTERVAL '35 days', NOW() - INTERVAL '5 days', 'monthly',
|
|
878
878
|
'stripe', 'sub_stripe_iris', 'cus_iris',
|
|
879
879
|
NOW() - INTERVAL '5 months', NULL, false,
|
|
880
880
|
'{"lastPaymentAttempt": "failed", "retryCount": 2}'::jsonb),
|
|
881
881
|
|
|
882
882
|
-- 7. Jorge Journey - Pro Monthly CANCELED (voluntary - user decided to cancel)
|
|
883
|
-
('sub-blog-jorge', 'team-blog-jorge', 'usr-blog-jorge', '
|
|
883
|
+
('sub-blog-jorge', 'team-blog-jorge', 'usr-blog-jorge', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'canceled',
|
|
884
884
|
NOW() - INTERVAL '45 days', NOW() - INTERVAL '15 days', 'monthly',
|
|
885
885
|
'stripe', 'sub_stripe_jorge', 'cus_jorge',
|
|
886
886
|
NOW() - INTERVAL '9 months', NOW() - INTERVAL '15 days', false,
|
|
887
887
|
'{"cancelReason": "voluntary", "canceledBy": "user", "feedback": "taking a break"}'::jsonb),
|
|
888
888
|
|
|
889
889
|
-- 8. Karen Kitchen - Pro Yearly CANCELED (payment failed 4 times)
|
|
890
|
-
('sub-blog-karen', 'team-blog-karen', 'usr-blog-karen', '
|
|
890
|
+
('sub-blog-karen', 'team-blog-karen', 'usr-blog-karen', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'canceled',
|
|
891
891
|
NOW() - INTERVAL '400 days', NOW() - INTERVAL '35 days', 'yearly',
|
|
892
892
|
'stripe', 'sub_stripe_karen', 'cus_karen',
|
|
893
893
|
NOW() - INTERVAL '15 months', NOW() - INTERVAL '35 days', false,
|
|
894
894
|
'{"cancelReason": "payment_failed", "failedAttempts": 4, "lastCardEnding": "4242"}'::jsonb),
|
|
895
895
|
|
|
896
896
|
-- 9. Leo Learn - Free Plan ACTIVE (no billing)
|
|
897
|
-
('sub-blog-leo', 'team-blog-leo', 'usr-blog-leo', '
|
|
897
|
+
('sub-blog-leo', 'team-blog-leo', 'usr-blog-leo', (SELECT id FROM public."plans" WHERE slug = 'free'), 'active',
|
|
898
898
|
NOW() - INTERVAL '2 months', NOW() + INTERVAL '100 years', 'monthly',
|
|
899
899
|
NULL, NULL, NULL,
|
|
900
900
|
NOW() - INTERVAL '2 months', NULL, false, '{}'::jsonb),
|
|
901
901
|
|
|
902
902
|
-- 10. Maria Music - Pro Monthly TRIALING (5 days in)
|
|
903
|
-
('sub-blog-maria', 'team-blog-maria', 'usr-blog-maria', '
|
|
903
|
+
('sub-blog-maria', 'team-blog-maria', 'usr-blog-maria', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'trialing',
|
|
904
904
|
NOW() - INTERVAL '5 days', NOW() + INTERVAL '25 days', 'monthly',
|
|
905
905
|
NULL, NULL, NULL,
|
|
906
906
|
NOW() - INTERVAL '5 days', NULL, false, '{}'::jsonb)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextsparkjs/theme-blog",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.190",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./config/theme.config.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@nextsparkjs/core": ">=0.1.0-0",
|
|
15
15
|
"@nextsparkjs/testing": ">=0.1.0-0",
|
|
16
16
|
"lucide-react": "^0.539.0",
|
|
17
|
-
"next": "^15.0.0",
|
|
17
|
+
"next": "^15.0.0 || ^16.0.0",
|
|
18
18
|
"next-intl": "^4.0.0",
|
|
19
19
|
"react": "^19.0.0",
|
|
20
20
|
"react-dom": "^19.0.0",
|
|
@@ -13,6 +13,7 @@ import Image from 'next/image'
|
|
|
13
13
|
import { Twitter, Linkedin, Globe, User } from 'lucide-react'
|
|
14
14
|
import { PostCard } from '@/themes/blog/components/public/PostCard'
|
|
15
15
|
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
16
|
+
import { withBasePath, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
16
17
|
|
|
17
18
|
interface Author {
|
|
18
19
|
id: string
|
|
@@ -60,7 +61,7 @@ export default function AuthorPage({ params }: PageProps) {
|
|
|
60
61
|
setLoading(true)
|
|
61
62
|
setError(null)
|
|
62
63
|
|
|
63
|
-
const response = await fetch(`/api/v1/theme/blog/authors/${username}
|
|
64
|
+
const response = await fetch(withBasePath(`/api/v1/theme/blog/authors/${username}`), {
|
|
64
65
|
method: 'GET',
|
|
65
66
|
headers: {
|
|
66
67
|
'Content-Type': 'application/json',
|
|
@@ -137,7 +138,7 @@ export default function AuthorPage({ params }: PageProps) {
|
|
|
137
138
|
<div className="flex-shrink-0">
|
|
138
139
|
{author.image ? (
|
|
139
140
|
<Image
|
|
140
|
-
src={author.image}
|
|
141
|
+
src={withBasePathIfInApp(author.image)}
|
|
141
142
|
alt={author.name}
|
|
142
143
|
width={128}
|
|
143
144
|
height={128}
|
|
@@ -165,7 +166,7 @@ export default function AuthorPage({ params }: PageProps) {
|
|
|
165
166
|
<div className="flex items-center gap-4">
|
|
166
167
|
{author.socialTwitter && (
|
|
167
168
|
<a
|
|
168
|
-
href={author.socialTwitter}
|
|
169
|
+
href={withBasePathIfInApp(author.socialTwitter)}
|
|
169
170
|
target="_blank"
|
|
170
171
|
rel="noopener noreferrer"
|
|
171
172
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
@@ -176,7 +177,7 @@ export default function AuthorPage({ params }: PageProps) {
|
|
|
176
177
|
)}
|
|
177
178
|
{author.socialLinkedin && (
|
|
178
179
|
<a
|
|
179
|
-
href={author.socialLinkedin}
|
|
180
|
+
href={withBasePathIfInApp(author.socialLinkedin)}
|
|
180
181
|
target="_blank"
|
|
181
182
|
rel="noopener noreferrer"
|
|
182
183
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
@@ -187,7 +188,7 @@ export default function AuthorPage({ params }: PageProps) {
|
|
|
187
188
|
)}
|
|
188
189
|
{author.socialWebsite && (
|
|
189
190
|
<a
|
|
190
|
-
href={author.socialWebsite}
|
|
191
|
+
href={withBasePathIfInApp(author.socialWebsite)}
|
|
191
192
|
target="_blank"
|
|
192
193
|
rel="noopener noreferrer"
|
|
193
194
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
@@ -10,6 +10,7 @@ import { useState, useEffect } from 'react'
|
|
|
10
10
|
import Link from 'next/link'
|
|
11
11
|
import Image from 'next/image'
|
|
12
12
|
import { User } from 'lucide-react'
|
|
13
|
+
import { withBasePath, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
13
14
|
|
|
14
15
|
interface Author {
|
|
15
16
|
id: string
|
|
@@ -31,7 +32,7 @@ export default function AuthorsPage() {
|
|
|
31
32
|
setLoading(true)
|
|
32
33
|
setError(null)
|
|
33
34
|
|
|
34
|
-
const response = await fetch('/api/v1/theme/blog/authors', {
|
|
35
|
+
const response = await fetch(withBasePath('/api/v1/theme/blog/authors'), {
|
|
35
36
|
method: 'GET',
|
|
36
37
|
headers: {
|
|
37
38
|
'Content-Type': 'application/json',
|
|
@@ -115,7 +116,7 @@ export default function AuthorsPage() {
|
|
|
115
116
|
<div className="flex justify-center mb-4">
|
|
116
117
|
{author.image ? (
|
|
117
118
|
<Image
|
|
118
|
-
src={author.image}
|
|
119
|
+
src={withBasePathIfInApp(author.image)}
|
|
119
120
|
alt={author.name}
|
|
120
121
|
width={80}
|
|
121
122
|
height={80}
|
|
@@ -14,6 +14,7 @@ import { useTranslations } from 'next-intl'
|
|
|
14
14
|
import { PostCard } from '@/themes/blog/components/public/PostCard'
|
|
15
15
|
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
16
16
|
import { Loader2 } from 'lucide-react'
|
|
17
|
+
import { withBasePath } from '@nextsparkjs/core/lib/base-path'
|
|
17
18
|
|
|
18
19
|
interface Post {
|
|
19
20
|
id: string
|
|
@@ -65,7 +66,7 @@ export default function BlogHomePage() {
|
|
|
65
66
|
|
|
66
67
|
// Fetch published posts from ALL authors via public endpoint
|
|
67
68
|
// This endpoint does not require authentication and aggregates posts cross-team
|
|
68
|
-
const response = await fetch('/api/v1/theme/blog/posts/public?limit=20', {
|
|
69
|
+
const response = await fetch(withBasePath('/api/v1/theme/blog/posts/public?limit=20'), {
|
|
69
70
|
method: 'GET',
|
|
70
71
|
headers: {
|
|
71
72
|
'Content-Type': 'application/json',
|
|
@@ -17,6 +17,7 @@ import { ReadingProgress } from '@/themes/blog/components/public/ReadingProgress
|
|
|
17
17
|
import { AuthorBio } from '@/themes/blog/components/public/AuthorBio'
|
|
18
18
|
import { RelatedPosts } from '@/themes/blog/components/public/RelatedPosts'
|
|
19
19
|
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
20
|
+
import { withBasePath, withBasePathInHtml, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
20
21
|
|
|
21
22
|
interface Post {
|
|
22
23
|
id: string
|
|
@@ -58,7 +59,7 @@ async function fetchPost(identifier: string): Promise<Post | null> {
|
|
|
58
59
|
try {
|
|
59
60
|
const headers = buildHeaders()
|
|
60
61
|
|
|
61
|
-
const byIdResponse = await fetch(`/api/v1/posts?ids=${encodeURIComponent(identifier)}&status=published&limit=1
|
|
62
|
+
const byIdResponse = await fetch(withBasePath(`/api/v1/posts?ids=${encodeURIComponent(identifier)}&status=published&limit=1`), {
|
|
62
63
|
credentials: 'include',
|
|
63
64
|
headers,
|
|
64
65
|
})
|
|
@@ -70,7 +71,7 @@ async function fetchPost(identifier: string): Promise<Post | null> {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
const bySlugResponse = await fetch(`/api/v1/posts?slug=${encodeURIComponent(identifier)}&status=published&limit=1
|
|
74
|
+
const bySlugResponse = await fetch(withBasePath(`/api/v1/posts?slug=${encodeURIComponent(identifier)}&status=published&limit=1`), {
|
|
74
75
|
credentials: 'include',
|
|
75
76
|
headers,
|
|
76
77
|
})
|
|
@@ -146,7 +147,7 @@ async function fetchRelatedPosts(currentPostId: string): Promise<RelatedPost[]>
|
|
|
146
147
|
const headers = buildHeaders()
|
|
147
148
|
|
|
148
149
|
// Fetch latest published posts
|
|
149
|
-
const response = await fetch('/api/v1/posts?status=published&limit=4&sortBy=publishedAt&sortOrder=desc', {
|
|
150
|
+
const response = await fetch(withBasePath('/api/v1/posts?status=published&limit=4&sortBy=publishedAt&sortOrder=desc'), {
|
|
150
151
|
credentials: 'include',
|
|
151
152
|
headers,
|
|
152
153
|
})
|
|
@@ -282,7 +283,7 @@ export default function PostPage({ params }: PageProps) {
|
|
|
282
283
|
<figure className="mb-12">
|
|
283
284
|
<div className="aspect-[21/9] relative overflow-hidden rounded-xl">
|
|
284
285
|
<Image
|
|
285
|
-
src={post.featuredImage}
|
|
286
|
+
src={withBasePathIfInApp(post.featuredImage)}
|
|
286
287
|
alt={post.title}
|
|
287
288
|
fill
|
|
288
289
|
className="object-cover"
|
|
@@ -295,7 +296,7 @@ export default function PostPage({ params }: PageProps) {
|
|
|
295
296
|
|
|
296
297
|
{/* Article Content */}
|
|
297
298
|
<div className="article-content">
|
|
298
|
-
<div dangerouslySetInnerHTML={{ __html: sanitizePostHtml(renderContent(post.content)) }} />
|
|
299
|
+
<div dangerouslySetInnerHTML={{ __html: withBasePathInHtml(sanitizePostHtml(renderContent(post.content))) }} />
|
|
299
300
|
</div>
|
|
300
301
|
|
|
301
302
|
{/* Divider */}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
Eye,
|
|
30
30
|
MoreHorizontal
|
|
31
31
|
} from 'lucide-react'
|
|
32
|
+
import { withBasePath } from '@nextsparkjs/core/lib/base-path'
|
|
32
33
|
|
|
33
34
|
interface PostStats {
|
|
34
35
|
total: number
|
|
@@ -65,7 +66,7 @@ async function fetchPosts(): Promise<{ stats: PostStats; recent: RecentPost[] }>
|
|
|
65
66
|
const headers = buildHeaders()
|
|
66
67
|
|
|
67
68
|
// Fetch all posts to get stats
|
|
68
|
-
const response = await fetch('/api/v1/posts?limit=100&sortBy=updatedAt&sortOrder=desc', {
|
|
69
|
+
const response = await fetch(withBasePath('/api/v1/posts?limit=100&sortBy=updatedAt&sortOrder=desc'), {
|
|
69
70
|
credentials: 'include',
|
|
70
71
|
headers,
|
|
71
72
|
})
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { Switch } from '@nextsparkjs/core/components/ui/switch'
|
|
40
40
|
import { WysiwygEditor } from '@/themes/blog/components/editor/WysiwygEditor'
|
|
41
41
|
import { FeaturedImageUpload } from '@/themes/blog/components/editor/FeaturedImageUpload'
|
|
42
|
+
import { withBasePath } from '@nextsparkjs/core/lib/base-path'
|
|
42
43
|
|
|
43
44
|
interface PostData {
|
|
44
45
|
id: string
|
|
@@ -102,7 +103,7 @@ export default function EditPostPage({ params }: PageProps) {
|
|
|
102
103
|
async function fetchPost() {
|
|
103
104
|
try {
|
|
104
105
|
const headers = buildHeaders()
|
|
105
|
-
const response = await fetch(`/api/v1/posts/${id}
|
|
106
|
+
const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
|
|
106
107
|
credentials: 'include',
|
|
107
108
|
headers,
|
|
108
109
|
})
|
|
@@ -178,7 +179,7 @@ export default function EditPostPage({ params }: PageProps) {
|
|
|
178
179
|
|
|
179
180
|
try {
|
|
180
181
|
const headers = buildHeaders()
|
|
181
|
-
const response = await fetch(`/api/v1/posts/${id}
|
|
182
|
+
const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
|
|
182
183
|
method: 'PATCH',
|
|
183
184
|
headers,
|
|
184
185
|
credentials: 'include',
|
|
@@ -232,7 +233,7 @@ export default function EditPostPage({ params }: PageProps) {
|
|
|
232
233
|
|
|
233
234
|
try {
|
|
234
235
|
const headers = buildHeaders()
|
|
235
|
-
const response = await fetch(`/api/v1/posts/${id}
|
|
236
|
+
const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
|
|
236
237
|
method: 'DELETE',
|
|
237
238
|
headers,
|
|
238
239
|
credentials: 'include',
|
|
@@ -29,6 +29,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@nextsparkjs/core/comp
|
|
|
29
29
|
import { Switch } from '@nextsparkjs/core/components/ui/switch'
|
|
30
30
|
import { WysiwygEditor } from '@/themes/blog/components/editor/WysiwygEditor'
|
|
31
31
|
import { FeaturedImageUpload } from '@/themes/blog/components/editor/FeaturedImageUpload'
|
|
32
|
+
import { withBasePath } from '@nextsparkjs/core/lib/base-path'
|
|
32
33
|
|
|
33
34
|
interface PostData {
|
|
34
35
|
title: string
|
|
@@ -122,7 +123,7 @@ export default function CreatePostPage() {
|
|
|
122
123
|
|
|
123
124
|
try {
|
|
124
125
|
const headers = buildHeaders()
|
|
125
|
-
const response = await fetch('/api/v1/posts', {
|
|
126
|
+
const response = await fetch(withBasePath('/api/v1/posts'), {
|
|
126
127
|
method: 'POST',
|
|
127
128
|
headers,
|
|
128
129
|
credentials: 'include',
|
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
Send,
|
|
64
64
|
ImageIcon,
|
|
65
65
|
} from 'lucide-react'
|
|
66
|
+
import { withBasePath, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
|
|
66
67
|
|
|
67
68
|
interface Post {
|
|
68
69
|
id: string
|
|
@@ -174,7 +175,7 @@ export default function PostsListPage() {
|
|
|
174
175
|
sortOrder,
|
|
175
176
|
})
|
|
176
177
|
|
|
177
|
-
const response = await fetch(`/api/v1/posts?${params}
|
|
178
|
+
const response = await fetch(withBasePath(`/api/v1/posts?${params}`), {
|
|
178
179
|
credentials: 'include',
|
|
179
180
|
headers,
|
|
180
181
|
})
|
|
@@ -246,7 +247,7 @@ export default function PostsListPage() {
|
|
|
246
247
|
setActionLoading(post.id)
|
|
247
248
|
try {
|
|
248
249
|
const headers = buildHeaders()
|
|
249
|
-
const response = await fetch(`/api/v1/posts/${post.id}
|
|
250
|
+
const response = await fetch(withBasePath(`/api/v1/posts/${post.id}`), {
|
|
250
251
|
method: 'PATCH',
|
|
251
252
|
headers,
|
|
252
253
|
credentials: 'include',
|
|
@@ -269,7 +270,7 @@ export default function PostsListPage() {
|
|
|
269
270
|
setActionLoading(post.id)
|
|
270
271
|
try {
|
|
271
272
|
const headers = buildHeaders()
|
|
272
|
-
const response = await fetch(`/api/v1/posts/${post.id}
|
|
273
|
+
const response = await fetch(withBasePath(`/api/v1/posts/${post.id}`), {
|
|
273
274
|
method: 'PATCH',
|
|
274
275
|
headers,
|
|
275
276
|
credentials: 'include',
|
|
@@ -291,7 +292,7 @@ export default function PostsListPage() {
|
|
|
291
292
|
setActionLoading(postToDelete.id)
|
|
292
293
|
try {
|
|
293
294
|
const headers = buildHeaders()
|
|
294
|
-
const response = await fetch(`/api/v1/posts/${postToDelete.id}
|
|
295
|
+
const response = await fetch(withBasePath(`/api/v1/posts/${postToDelete.id}`), {
|
|
295
296
|
method: 'DELETE',
|
|
296
297
|
headers,
|
|
297
298
|
credentials: 'include',
|
|
@@ -555,7 +556,7 @@ export default function PostsListPage() {
|
|
|
555
556
|
<div className="hidden sm:block flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-muted">
|
|
556
557
|
{post.featuredImage ? (
|
|
557
558
|
<Image
|
|
558
|
-
src={post.featuredImage}
|
|
559
|
+
src={withBasePathIfInApp(post.featuredImage)}
|
|
559
560
|
alt=""
|
|
560
561
|
width={64}
|
|
561
562
|
height={48}
|
|
@@ -690,7 +691,7 @@ export default function PostsListPage() {
|
|
|
690
691
|
<div className="aspect-video relative bg-muted">
|
|
691
692
|
{post.featuredImage ? (
|
|
692
693
|
<Image
|
|
693
|
-
src={post.featuredImage}
|
|
694
|
+
src={withBasePathIfInApp(post.featuredImage)}
|
|
694
695
|
alt=""
|
|
695
696
|
fill
|
|
696
697
|
className="object-cover"
|