@tanstack/create 0.61.5 → 0.62.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/CHANGELOG.md +23 -0
- package/dist/config-file.js +5 -2
- package/dist/custom-add-ons/starter.js +45 -28
- package/dist/file-helpers.js +1 -0
- package/dist/frameworks/react/add-ons/shadcn/assets/src/styles.css +224 -15
- package/dist/frameworks/react/add-ons/store/assets/src/lib/demo-store.ts +5 -6
- package/dist/frameworks/react/add-ons/store/assets/src/routes/demo/store.tsx.ejs +1 -1
- package/dist/frameworks/react/add-ons/store/package.json +2 -2
- package/dist/frameworks/react/add-ons/strapi/README.md +158 -8
- package/dist/frameworks/react/add-ons/strapi/assets/_dot_env.local.append +1 -1
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/block-renderer.tsx +55 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/index.ts +14 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/media.tsx +27 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/quote.tsx +19 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/rich-text.tsx +11 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/slider.tsx +28 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/markdown-content.tsx +74 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/pagination.tsx +120 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/search.tsx +35 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/components/strapi-image.tsx +47 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/data/loaders/articles.ts +106 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/data/loaders/index.ts +28 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/data/strapi-sdk.ts +9 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/lib/strapi-utils.ts +25 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.$articleId.tsx +170 -0
- package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.tsx +269 -43
- package/dist/frameworks/react/add-ons/strapi/assets/src/types/strapi.ts +90 -0
- package/dist/frameworks/react/add-ons/strapi/info.json +3 -3
- package/dist/frameworks/react/add-ons/strapi/package.json +5 -2
- package/dist/frameworks/react/index.js +2 -2
- package/dist/frameworks/react/project/base/content/blog/fifth-post.mdx.ejs +54 -0
- package/dist/frameworks/react/project/base/content/blog/first-post.md.ejs +47 -0
- package/dist/frameworks/react/project/base/content/blog/fourth-post.md.ejs +42 -0
- package/dist/frameworks/react/project/base/content/blog/second-post.mdx.ejs +46 -0
- package/dist/frameworks/react/project/base/content/blog/third-post.md.ejs +49 -0
- package/dist/frameworks/react/project/base/content-collections.ts.ejs +37 -0
- package/dist/frameworks/react/project/base/package.json +8 -1
- package/dist/frameworks/react/project/base/public/images/lagoon-1.svg +13 -0
- package/dist/frameworks/react/project/base/public/images/lagoon-2.svg +12 -0
- package/dist/frameworks/react/project/base/public/images/lagoon-3.svg +12 -0
- package/dist/frameworks/react/project/base/public/images/lagoon-4.svg +12 -0
- package/dist/frameworks/react/project/base/public/images/lagoon-5.svg +12 -0
- package/dist/frameworks/react/project/base/public/images/lagoon-about.svg +14 -0
- package/dist/frameworks/react/project/base/src/components/Footer.tsx.ejs +42 -0
- package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +92 -138
- package/dist/frameworks/react/project/base/src/components/MdxCallout.tsx.ejs +16 -0
- package/dist/frameworks/react/project/base/src/components/MdxMetrics.tsx.ejs +23 -0
- package/dist/frameworks/react/project/base/src/components/ThemeToggle.tsx.ejs +81 -0
- package/dist/frameworks/react/project/base/src/lib/site.ts.ejs +4 -0
- package/dist/frameworks/react/project/base/src/main.tsx.ejs +0 -1
- package/dist/frameworks/react/project/base/src/routes/__root.tsx.ejs +10 -6
- package/dist/frameworks/react/project/base/src/routes/about.tsx.ejs +27 -0
- package/dist/frameworks/react/project/base/src/routes/blog.$slug.tsx.ejs +71 -0
- package/dist/frameworks/react/project/base/src/routes/blog.index.tsx.ejs +93 -0
- package/dist/frameworks/react/project/base/src/routes/index.tsx.ejs +58 -91
- package/dist/frameworks/react/project/base/src/routes/rss[.]xml.ts.ejs +35 -0
- package/dist/frameworks/react/project/base/src/styles.css.ejs +268 -6
- package/dist/frameworks/react/project/base/tsconfig.json.ejs +2 -0
- package/dist/frameworks/react/project/base/vite.config.ts.ejs +2 -0
- package/dist/frameworks/solid/add-ons/store/assets/src/lib/demo-store.ts +5 -6
- package/dist/frameworks/solid/add-ons/store/assets/src/routes/demo.store.tsx.ejs +2 -2
- package/dist/frameworks/solid/examples/tanchat/assets/src/lib/demo-store.ts +5 -6
- package/dist/frameworks/solid/project/base/src/components/Header.tsx.ejs +8 -6
- package/dist/frameworks/solid/project/base/src/routes/__root.tsx.ejs +1 -1
- package/dist/frameworks/solid/project/base/src/routes/index.tsx.ejs +1 -1
- package/dist/frameworks.js +3 -0
- package/dist/package-json.js +1 -1
- package/dist/registry.js +21 -4
- package/dist/types/registry.d.ts +38 -0
- package/package.json +1 -1
- package/src/config-file.ts +6 -2
- package/src/custom-add-ons/starter.ts +30 -10
- package/src/file-helpers.ts +1 -0
- package/src/frameworks/react/add-ons/shadcn/assets/src/styles.css +224 -15
- package/src/frameworks/react/add-ons/store/assets/src/lib/demo-store.ts +5 -6
- package/src/frameworks/react/add-ons/store/assets/src/routes/demo/store.tsx.ejs +1 -1
- package/src/frameworks/react/add-ons/store/package.json +2 -2
- package/src/frameworks/react/add-ons/strapi/README.md +158 -8
- package/src/frameworks/react/add-ons/strapi/assets/_dot_env.local.append +1 -1
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/block-renderer.tsx +55 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/index.ts +14 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/media.tsx +27 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/quote.tsx +19 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/rich-text.tsx +11 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/slider.tsx +28 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/markdown-content.tsx +74 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/pagination.tsx +120 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/search.tsx +35 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/components/strapi-image.tsx +47 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/data/loaders/articles.ts +106 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/data/loaders/index.ts +28 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/data/strapi-sdk.ts +9 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/lib/strapi-utils.ts +25 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.$articleId.tsx +170 -0
- package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.tsx +269 -43
- package/src/frameworks/react/add-ons/strapi/assets/src/types/strapi.ts +90 -0
- package/src/frameworks/react/add-ons/strapi/info.json +3 -3
- package/src/frameworks/react/add-ons/strapi/package.json +5 -2
- package/src/frameworks/react/index.ts +2 -2
- package/src/frameworks/react/project/base/content/blog/fifth-post.mdx.ejs +54 -0
- package/src/frameworks/react/project/base/content/blog/first-post.md.ejs +47 -0
- package/src/frameworks/react/project/base/content/blog/fourth-post.md.ejs +42 -0
- package/src/frameworks/react/project/base/content/blog/second-post.mdx.ejs +46 -0
- package/src/frameworks/react/project/base/content/blog/third-post.md.ejs +49 -0
- package/src/frameworks/react/project/base/content-collections.ts.ejs +37 -0
- package/src/frameworks/react/project/base/package.json +8 -1
- package/src/frameworks/react/project/base/public/images/lagoon-1.svg +13 -0
- package/src/frameworks/react/project/base/public/images/lagoon-2.svg +12 -0
- package/src/frameworks/react/project/base/public/images/lagoon-3.svg +12 -0
- package/src/frameworks/react/project/base/public/images/lagoon-4.svg +12 -0
- package/src/frameworks/react/project/base/public/images/lagoon-5.svg +12 -0
- package/src/frameworks/react/project/base/public/images/lagoon-about.svg +14 -0
- package/src/frameworks/react/project/base/src/components/Footer.tsx.ejs +42 -0
- package/src/frameworks/react/project/base/src/components/Header.tsx.ejs +92 -138
- package/src/frameworks/react/project/base/src/components/MdxCallout.tsx.ejs +16 -0
- package/src/frameworks/react/project/base/src/components/MdxMetrics.tsx.ejs +23 -0
- package/src/frameworks/react/project/base/src/components/ThemeToggle.tsx.ejs +81 -0
- package/src/frameworks/react/project/base/src/lib/site.ts.ejs +4 -0
- package/src/frameworks/react/project/base/src/main.tsx.ejs +0 -1
- package/src/frameworks/react/project/base/src/routes/__root.tsx.ejs +10 -6
- package/src/frameworks/react/project/base/src/routes/about.tsx.ejs +27 -0
- package/src/frameworks/react/project/base/src/routes/blog.$slug.tsx.ejs +71 -0
- package/src/frameworks/react/project/base/src/routes/blog.index.tsx.ejs +93 -0
- package/src/frameworks/react/project/base/src/routes/index.tsx.ejs +58 -91
- package/src/frameworks/react/project/base/src/routes/rss[.]xml.ts.ejs +35 -0
- package/src/frameworks/react/project/base/src/styles.css.ejs +268 -6
- package/src/frameworks/react/project/base/tsconfig.json.ejs +2 -0
- package/src/frameworks/react/project/base/vite.config.ts.ejs +2 -0
- package/src/frameworks/solid/add-ons/store/assets/src/lib/demo-store.ts +5 -6
- package/src/frameworks/solid/add-ons/store/assets/src/routes/demo.store.tsx.ejs +2 -2
- package/src/frameworks/solid/examples/tanchat/assets/src/lib/demo-store.ts +5 -6
- package/src/frameworks/solid/project/base/src/components/Header.tsx.ejs +8 -6
- package/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs +1 -1
- package/src/frameworks/solid/project/base/src/routes/index.tsx.ejs +1 -1
- package/src/frameworks.ts +4 -0
- package/src/package-json.ts +1 -1
- package/src/registry.ts +28 -4
- package/tests/add-ons.test.ts +4 -4
- package/tests/config-file.test.ts +3 -3
- package/tests/custom-add-ons/starter.test.ts +34 -2
- package/tests/frameworks.test.ts +24 -0
- package/tests/options.test.ts +4 -4
- package/tests/utils.test.ts +2 -2
- package/dist/frameworks/react/add-ons/strapi/assets/src/lib/strapiClient.ts +0 -7
- package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi_.$articleId.tsx +0 -78
- package/src/frameworks/react/add-ons/strapi/assets/src/lib/strapiClient.ts +0 -7
- package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi_.$articleId.tsx +0 -78
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Markdown from "react-markdown";
|
|
2
|
+
import remarkGfm from "remark-gfm";
|
|
3
|
+
|
|
4
|
+
interface MarkdownContentProps {
|
|
5
|
+
content: string | undefined | null;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const styles = {
|
|
10
|
+
h1: "text-3xl font-bold mb-6 text-white",
|
|
11
|
+
h2: "text-2xl font-bold mb-4 text-white",
|
|
12
|
+
h3: "text-xl font-bold mb-3 text-white",
|
|
13
|
+
p: "mb-4 leading-relaxed text-gray-300",
|
|
14
|
+
a: "text-cyan-400 hover:underline",
|
|
15
|
+
ul: "list-disc pl-6 mb-4 space-y-2 text-gray-300",
|
|
16
|
+
ol: "list-decimal pl-6 mb-4 space-y-2 text-gray-300",
|
|
17
|
+
li: "leading-relaxed",
|
|
18
|
+
blockquote: "border-l-4 border-cyan-400 pl-4 italic text-gray-400 my-4",
|
|
19
|
+
code: "bg-slate-800 px-2 py-1 rounded text-cyan-400 text-sm font-mono",
|
|
20
|
+
pre: "bg-slate-800 p-4 rounded-lg overflow-x-auto mb-4",
|
|
21
|
+
table: "w-full border-collapse mb-4",
|
|
22
|
+
th: "border border-slate-700 p-2 bg-slate-800 text-left text-white",
|
|
23
|
+
td: "border border-slate-700 p-2 text-gray-300",
|
|
24
|
+
img: "max-w-full h-auto rounded-lg my-4",
|
|
25
|
+
hr: "border-slate-700 my-8",
|
|
26
|
+
strong: "text-white font-semibold",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function MarkdownContent({ content, className = "" }: MarkdownContentProps) {
|
|
30
|
+
if (!content) return null;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={`prose prose-invert max-w-none ${className}`}>
|
|
34
|
+
<Markdown
|
|
35
|
+
remarkPlugins={[remarkGfm]}
|
|
36
|
+
components={{
|
|
37
|
+
h1: ({ children }) => <h1 className={styles.h1}>{children}</h1>,
|
|
38
|
+
h2: ({ children }) => <h2 className={styles.h2}>{children}</h2>,
|
|
39
|
+
h3: ({ children }) => <h3 className={styles.h3}>{children}</h3>,
|
|
40
|
+
p: ({ children }) => <p className={styles.p}>{children}</p>,
|
|
41
|
+
a: ({ href, children }) => (
|
|
42
|
+
<a href={href} className={styles.a} target="_blank" rel="noopener noreferrer">
|
|
43
|
+
{children}
|
|
44
|
+
</a>
|
|
45
|
+
),
|
|
46
|
+
ul: ({ children }) => <ul className={styles.ul}>{children}</ul>,
|
|
47
|
+
ol: ({ children }) => <ol className={styles.ol}>{children}</ol>,
|
|
48
|
+
li: ({ children }) => <li className={styles.li}>{children}</li>,
|
|
49
|
+
blockquote: ({ children }) => <blockquote className={styles.blockquote}>{children}</blockquote>,
|
|
50
|
+
code: ({ className, children }) => {
|
|
51
|
+
const isCodeBlock = className?.includes("language-");
|
|
52
|
+
if (isCodeBlock) {
|
|
53
|
+
return (
|
|
54
|
+
<pre className={styles.pre}>
|
|
55
|
+
<code className="text-sm font-mono text-gray-300">{children}</code>
|
|
56
|
+
</pre>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return <code className={styles.code}>{children}</code>;
|
|
60
|
+
},
|
|
61
|
+
pre: ({ children }) => <>{children}</>,
|
|
62
|
+
table: ({ children }) => <table className={styles.table}>{children}</table>,
|
|
63
|
+
th: ({ children }) => <th className={styles.th}>{children}</th>,
|
|
64
|
+
td: ({ children }) => <td className={styles.td}>{children}</td>,
|
|
65
|
+
img: ({ src, alt }) => <img src={src} alt={alt || ""} className={styles.img} />,
|
|
66
|
+
hr: () => <hr className={styles.hr} />,
|
|
67
|
+
strong: ({ children }) => <strong className={styles.strong}>{children}</strong>,
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{content}
|
|
71
|
+
</Markdown>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useRouter, useSearch } from '@tanstack/react-router'
|
|
2
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface PaginationProps {
|
|
5
|
+
pageCount: number
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Pagination({ pageCount, className = '' }: PaginationProps) {
|
|
10
|
+
const router = useRouter()
|
|
11
|
+
const search = useSearch({ strict: false })
|
|
12
|
+
const currentPage = Number((search as any)?.page) || 1
|
|
13
|
+
|
|
14
|
+
const handlePageChange = (page: number) => {
|
|
15
|
+
router.navigate({
|
|
16
|
+
to: '.',
|
|
17
|
+
search: (prev) => ({ ...prev, page }),
|
|
18
|
+
replace: true,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Generate page numbers to display
|
|
23
|
+
const getPageNumbers = () => {
|
|
24
|
+
const pages: Array<number | 'ellipsis'> = []
|
|
25
|
+
const showEllipsis = pageCount > 7
|
|
26
|
+
|
|
27
|
+
if (showEllipsis) {
|
|
28
|
+
pages.push(1)
|
|
29
|
+
|
|
30
|
+
if (currentPage > 3) {
|
|
31
|
+
pages.push('ellipsis')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const start = Math.max(2, currentPage - 1)
|
|
35
|
+
const end = Math.min(pageCount - 1, currentPage + 1)
|
|
36
|
+
|
|
37
|
+
for (let i = start; i <= end; i++) {
|
|
38
|
+
pages.push(i)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (currentPage < pageCount - 2) {
|
|
42
|
+
pages.push('ellipsis')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (pageCount > 1) {
|
|
46
|
+
pages.push(pageCount)
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
for (let i = 1; i <= pageCount; i++) {
|
|
50
|
+
pages.push(i)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return pages
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pageNumbers = getPageNumbers()
|
|
58
|
+
|
|
59
|
+
if (pageCount <= 1) return null
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<nav className={`flex items-center justify-center gap-1 ${className}`}>
|
|
63
|
+
{/* Previous Button */}
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => currentPage > 1 && handlePageChange(currentPage - 1)}
|
|
66
|
+
disabled={currentPage <= 1}
|
|
67
|
+
className={`flex items-center gap-1 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
68
|
+
currentPage <= 1
|
|
69
|
+
? 'text-gray-600 cursor-not-allowed'
|
|
70
|
+
: 'text-gray-300 hover:bg-slate-700 hover:text-white'
|
|
71
|
+
}`}
|
|
72
|
+
>
|
|
73
|
+
<ChevronLeft className="w-4 h-4" />
|
|
74
|
+
<span className="hidden sm:inline">Previous</span>
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
{/* Page Numbers */}
|
|
78
|
+
<div className="flex items-center gap-1">
|
|
79
|
+
{pageNumbers.map((page, index) =>
|
|
80
|
+
page === 'ellipsis' ? (
|
|
81
|
+
<span
|
|
82
|
+
key={`ellipsis-${index}`}
|
|
83
|
+
className="px-2 py-2 text-gray-500 hidden md:block"
|
|
84
|
+
>
|
|
85
|
+
...
|
|
86
|
+
</span>
|
|
87
|
+
) : (
|
|
88
|
+
<button
|
|
89
|
+
key={page}
|
|
90
|
+
onClick={() => handlePageChange(page)}
|
|
91
|
+
className={`min-w-[40px] px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
92
|
+
currentPage === page
|
|
93
|
+
? 'bg-cyan-500 text-white'
|
|
94
|
+
: 'text-gray-300 hover:bg-slate-700 hover:text-white'
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
{page}
|
|
98
|
+
</button>
|
|
99
|
+
)
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Next Button */}
|
|
104
|
+
<button
|
|
105
|
+
onClick={() =>
|
|
106
|
+
currentPage < pageCount && handlePageChange(currentPage + 1)
|
|
107
|
+
}
|
|
108
|
+
disabled={currentPage >= pageCount}
|
|
109
|
+
className={`flex items-center gap-1 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
110
|
+
currentPage >= pageCount
|
|
111
|
+
? 'text-gray-600 cursor-not-allowed'
|
|
112
|
+
: 'text-gray-300 hover:bg-slate-700 hover:text-white'
|
|
113
|
+
}`}
|
|
114
|
+
>
|
|
115
|
+
<span className="hidden sm:inline">Next</span>
|
|
116
|
+
<ChevronRight className="w-4 h-4" />
|
|
117
|
+
</button>
|
|
118
|
+
</nav>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useRouter, useSearch } from "@tanstack/react-router";
|
|
2
|
+
import { useDebouncedCallback } from "use-debounce";
|
|
3
|
+
|
|
4
|
+
interface SearchProps {
|
|
5
|
+
readonly className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Search({ className = "" }: SearchProps) {
|
|
9
|
+
const search = useSearch({ strict: false });
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
|
|
12
|
+
const handleSearch = useDebouncedCallback((term: string) => {
|
|
13
|
+
router.navigate({
|
|
14
|
+
to: ".",
|
|
15
|
+
search: (prev) => ({
|
|
16
|
+
...prev,
|
|
17
|
+
page: 1,
|
|
18
|
+
query: term || undefined,
|
|
19
|
+
}),
|
|
20
|
+
replace: true,
|
|
21
|
+
});
|
|
22
|
+
}, 300);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<input
|
|
26
|
+
type="text"
|
|
27
|
+
placeholder="Search articles..."
|
|
28
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
29
|
+
handleSearch(e.target.value)
|
|
30
|
+
}
|
|
31
|
+
defaultValue={(search as any)?.query || ""}
|
|
32
|
+
className={`w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition-colors ${className}`}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { getStrapiMedia } from "@/lib/strapi-utils";
|
|
3
|
+
|
|
4
|
+
interface StrapiImageProps {
|
|
5
|
+
src: string | undefined | null;
|
|
6
|
+
alt?: string | null;
|
|
7
|
+
className?: string;
|
|
8
|
+
width?: number | string;
|
|
9
|
+
height?: number | string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StrapiImage({
|
|
13
|
+
src,
|
|
14
|
+
alt,
|
|
15
|
+
className = "",
|
|
16
|
+
width,
|
|
17
|
+
height,
|
|
18
|
+
}: StrapiImageProps) {
|
|
19
|
+
const [hasError, setHasError] = useState(false);
|
|
20
|
+
|
|
21
|
+
if (!src) return null;
|
|
22
|
+
|
|
23
|
+
const imageUrl = getStrapiMedia(src);
|
|
24
|
+
|
|
25
|
+
if (hasError) {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={`bg-slate-700 flex items-center justify-center text-slate-400 text-sm ${className}`}
|
|
29
|
+
style={{ width, height }}
|
|
30
|
+
>
|
|
31
|
+
<span>Image not available</span>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<img
|
|
38
|
+
src={imageUrl}
|
|
39
|
+
alt={alt || ""}
|
|
40
|
+
width={width}
|
|
41
|
+
height={height}
|
|
42
|
+
loading="lazy"
|
|
43
|
+
className={`object-cover ${className}`}
|
|
44
|
+
onError={() => setHasError(true)}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { sdk } from "@/data/strapi-sdk";
|
|
3
|
+
import type { TArticle, TStrapiResponseCollection, TStrapiResponseSingle } from "@/types/strapi";
|
|
4
|
+
|
|
5
|
+
const PAGE_SIZE = 3;
|
|
6
|
+
|
|
7
|
+
const articles = sdk.collection("articles");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch articles with optional filtering, search, and pagination
|
|
11
|
+
*/
|
|
12
|
+
const getArticles = async (
|
|
13
|
+
page?: number,
|
|
14
|
+
category?: string,
|
|
15
|
+
query?: string
|
|
16
|
+
) => {
|
|
17
|
+
const filterConditions: Array<Record<string, unknown>> = [];
|
|
18
|
+
|
|
19
|
+
// Add search query filter
|
|
20
|
+
if (query) {
|
|
21
|
+
filterConditions.push({
|
|
22
|
+
$or: [
|
|
23
|
+
{ title: { $containsi: query } },
|
|
24
|
+
{ description: { $containsi: query } },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Add category filter
|
|
30
|
+
if (category) {
|
|
31
|
+
filterConditions.push({
|
|
32
|
+
category: {
|
|
33
|
+
slug: { $eq: category },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const filters =
|
|
39
|
+
filterConditions.length === 0
|
|
40
|
+
? undefined
|
|
41
|
+
: filterConditions.length === 1
|
|
42
|
+
? filterConditions[0]
|
|
43
|
+
: { $and: filterConditions };
|
|
44
|
+
|
|
45
|
+
return articles.find({
|
|
46
|
+
sort: ["createdAt:desc"],
|
|
47
|
+
pagination: {
|
|
48
|
+
page: page || 1,
|
|
49
|
+
pageSize: PAGE_SIZE,
|
|
50
|
+
},
|
|
51
|
+
populate: ["cover", "author", "category"],
|
|
52
|
+
filters,
|
|
53
|
+
}) as Promise<TStrapiResponseCollection<TArticle>>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Fetch a single article by documentId
|
|
58
|
+
*/
|
|
59
|
+
const getArticleById = async (documentId: string) => {
|
|
60
|
+
return articles.findOne(documentId, {
|
|
61
|
+
populate: ["cover", "author", "category", "blocks.file", "blocks.files"],
|
|
62
|
+
}) as Promise<TStrapiResponseSingle<TArticle>>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetch a single article by slug
|
|
67
|
+
*/
|
|
68
|
+
const getArticleBySlug = async (slug: string) => {
|
|
69
|
+
return articles.find({
|
|
70
|
+
filters: {
|
|
71
|
+
slug: { $eq: slug },
|
|
72
|
+
},
|
|
73
|
+
populate: ["cover", "author", "category", "blocks.file", "blocks.files"],
|
|
74
|
+
}) as Promise<TStrapiResponseCollection<TArticle>>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Server Functions - these run on the server and can be called from components
|
|
78
|
+
|
|
79
|
+
export const getArticlesData = createServerFn({
|
|
80
|
+
method: "GET",
|
|
81
|
+
})
|
|
82
|
+
.inputValidator(
|
|
83
|
+
(input?: { page?: number; category?: string; query?: string }) => input
|
|
84
|
+
)
|
|
85
|
+
.handler(async ({ data }): Promise<TStrapiResponseCollection<TArticle>> => {
|
|
86
|
+
const response = await getArticles(data?.page, data?.category, data?.query);
|
|
87
|
+
return response;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const getArticleByIdData = createServerFn({
|
|
91
|
+
method: "GET",
|
|
92
|
+
})
|
|
93
|
+
.inputValidator((documentId: string) => documentId)
|
|
94
|
+
.handler(async ({ data: documentId }): Promise<TStrapiResponseSingle<TArticle>> => {
|
|
95
|
+
const response = await getArticleById(documentId);
|
|
96
|
+
return response;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const getArticleBySlugData = createServerFn({
|
|
100
|
+
method: "GET",
|
|
101
|
+
})
|
|
102
|
+
.inputValidator((slug: string) => slug)
|
|
103
|
+
.handler(async ({ data: slug }): Promise<TStrapiResponseCollection<TArticle>> => {
|
|
104
|
+
const response = await getArticleBySlug(slug);
|
|
105
|
+
return response;
|
|
106
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getArticlesData,
|
|
3
|
+
getArticleByIdData,
|
|
4
|
+
getArticleBySlugData,
|
|
5
|
+
} from "./articles";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Strapi API - Server functions for fetching data from Strapi
|
|
9
|
+
*
|
|
10
|
+
* Usage in route loaders:
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { strapiApi } from "@/data/loaders";
|
|
13
|
+
*
|
|
14
|
+
* export const Route = createFileRoute("/articles")({
|
|
15
|
+
* loader: async () => {
|
|
16
|
+
* const { data, meta } = await strapiApi.articles.getArticlesData();
|
|
17
|
+
* return data;
|
|
18
|
+
* },
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const strapiApi = {
|
|
23
|
+
articles: {
|
|
24
|
+
getArticlesData,
|
|
25
|
+
getArticleByIdData,
|
|
26
|
+
getArticleBySlugData,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { strapi } from "@strapi/client";
|
|
2
|
+
|
|
3
|
+
// Strapi base URL (without /api)
|
|
4
|
+
const STRAPI_BASE = import.meta.env.VITE_STRAPI_URL ?? "http://localhost:1337";
|
|
5
|
+
|
|
6
|
+
// Initialize the Strapi SDK with /api endpoint
|
|
7
|
+
const sdk = strapi({ baseURL: new URL("/api", STRAPI_BASE).href });
|
|
8
|
+
|
|
9
|
+
export { sdk };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strapi URL helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DEFAULT_STRAPI_URL = "http://localhost:1337";
|
|
6
|
+
|
|
7
|
+
// Base Strapi URL (without /api)
|
|
8
|
+
export function getStrapiURL(): string {
|
|
9
|
+
// Handle SSR where import.meta.env might not be fully available
|
|
10
|
+
if (typeof import.meta !== "undefined" && import.meta.env?.VITE_STRAPI_URL) {
|
|
11
|
+
return import.meta.env.VITE_STRAPI_URL;
|
|
12
|
+
}
|
|
13
|
+
return DEFAULT_STRAPI_URL;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Get full URL for media assets
|
|
17
|
+
export function getStrapiMedia(url: string | undefined | null): string {
|
|
18
|
+
if (!url) return "";
|
|
19
|
+
if (url.startsWith("data:") || url.startsWith("http") || url.startsWith("//")) {
|
|
20
|
+
return url;
|
|
21
|
+
}
|
|
22
|
+
// Ensure we always have a valid base URL
|
|
23
|
+
const baseUrl = getStrapiURL() || DEFAULT_STRAPI_URL;
|
|
24
|
+
return `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createFileRoute, Link } from "@tanstack/react-router";
|
|
2
|
+
import { strapiApi } from "@/data/loaders";
|
|
3
|
+
import { StrapiImage } from "@/components/strapi-image";
|
|
4
|
+
import { BlockRenderer } from "@/components/blocks";
|
|
5
|
+
import type { TArticle } from "@/types/strapi";
|
|
6
|
+
|
|
7
|
+
export const Route = createFileRoute("/demo/strapi/$articleId")({
|
|
8
|
+
component: RouteComponent,
|
|
9
|
+
errorComponent: ErrorComponent,
|
|
10
|
+
loader: async ({ params }) => {
|
|
11
|
+
try {
|
|
12
|
+
const response = await strapiApi.articles.getArticleByIdData({
|
|
13
|
+
data: params.articleId,
|
|
14
|
+
});
|
|
15
|
+
return { success: true, article: response.data };
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
error: error instanceof Error ? error.message : "Failed to load article",
|
|
20
|
+
article: null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function ErrorComponent({ error }: { error: Error }) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 p-8">
|
|
29
|
+
<div className="max-w-4xl mx-auto">
|
|
30
|
+
<Link
|
|
31
|
+
to="/demo/strapi"
|
|
32
|
+
className="inline-flex items-center text-cyan-400 hover:text-cyan-300 mb-6 transition-colors"
|
|
33
|
+
>
|
|
34
|
+
← Back to Articles
|
|
35
|
+
</Link>
|
|
36
|
+
<div className="bg-red-900/20 border border-red-500/50 rounded-xl p-8 text-center">
|
|
37
|
+
<h1 className="text-2xl font-bold text-red-400 mb-4">Error Loading Article</h1>
|
|
38
|
+
<p className="text-gray-300">{error.message}</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function RouteComponent() {
|
|
46
|
+
const { success, article, error } = Route.useLoaderData() as {
|
|
47
|
+
success: boolean;
|
|
48
|
+
article: TArticle | null;
|
|
49
|
+
error?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Show error state
|
|
53
|
+
if (!success || !article) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 p-8">
|
|
56
|
+
<div className="max-w-4xl mx-auto">
|
|
57
|
+
<Link
|
|
58
|
+
to="/demo/strapi"
|
|
59
|
+
className="inline-flex items-center text-cyan-400 hover:text-cyan-300 mb-6 transition-colors"
|
|
60
|
+
>
|
|
61
|
+
<svg
|
|
62
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
63
|
+
className="h-5 w-5 mr-2"
|
|
64
|
+
viewBox="0 0 20 20"
|
|
65
|
+
fill="currentColor"
|
|
66
|
+
>
|
|
67
|
+
<path
|
|
68
|
+
fillRule="evenodd"
|
|
69
|
+
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
|
|
70
|
+
clipRule="evenodd"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
Back to Articles
|
|
74
|
+
</Link>
|
|
75
|
+
|
|
76
|
+
<div className="bg-amber-900/20 border border-amber-500/50 rounded-xl p-8">
|
|
77
|
+
<div className="flex items-start gap-4">
|
|
78
|
+
<div className="text-amber-400 text-2xl">⚠️</div>
|
|
79
|
+
<div>
|
|
80
|
+
<h2 className="text-xl font-semibold text-amber-400 mb-2">
|
|
81
|
+
{error || "Article Not Found"}
|
|
82
|
+
</h2>
|
|
83
|
+
<p className="text-gray-300">
|
|
84
|
+
Make sure the Strapi server is running and the article exists.
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 p-8">
|
|
96
|
+
<div className="max-w-4xl mx-auto">
|
|
97
|
+
<Link
|
|
98
|
+
to="/demo/strapi"
|
|
99
|
+
className="inline-flex items-center text-cyan-400 hover:text-cyan-300 mb-6 transition-colors"
|
|
100
|
+
>
|
|
101
|
+
<svg
|
|
102
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
103
|
+
className="h-5 w-5 mr-2"
|
|
104
|
+
viewBox="0 0 20 20"
|
|
105
|
+
fill="currentColor"
|
|
106
|
+
>
|
|
107
|
+
<path
|
|
108
|
+
fillRule="evenodd"
|
|
109
|
+
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
|
|
110
|
+
clipRule="evenodd"
|
|
111
|
+
/>
|
|
112
|
+
</svg>
|
|
113
|
+
Back to Articles
|
|
114
|
+
</Link>
|
|
115
|
+
|
|
116
|
+
<article className="bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl overflow-hidden">
|
|
117
|
+
<StrapiImage
|
|
118
|
+
src={article.cover?.url}
|
|
119
|
+
alt={article.cover?.alternativeText || article.title}
|
|
120
|
+
className="w-full h-64"
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<div className="p-8">
|
|
124
|
+
<h1 className="text-4xl font-bold text-white mb-4">
|
|
125
|
+
{article.title || "Untitled"}
|
|
126
|
+
</h1>
|
|
127
|
+
|
|
128
|
+
<div className="flex items-center gap-4 mb-6">
|
|
129
|
+
{article.author?.name && (
|
|
130
|
+
<span className="text-gray-400">
|
|
131
|
+
By{" "}
|
|
132
|
+
<span className="text-cyan-400">{article.author.name}</span>
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
{article.createdAt && (
|
|
136
|
+
<span className="text-sm text-cyan-400/70">
|
|
137
|
+
{new Date(article.createdAt).toLocaleDateString("en-US", {
|
|
138
|
+
year: "numeric",
|
|
139
|
+
month: "long",
|
|
140
|
+
day: "numeric",
|
|
141
|
+
})}
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{article.category?.name && (
|
|
147
|
+
<div className="mb-6">
|
|
148
|
+
<span className="text-xs px-3 py-1 bg-slate-700 text-cyan-400 rounded-full">
|
|
149
|
+
{article.category.name}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{article.description && (
|
|
155
|
+
<div className="mb-8">
|
|
156
|
+
<p className="text-xl text-gray-300 leading-relaxed">
|
|
157
|
+
{article.description}
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{article.blocks && article.blocks.length > 0 && (
|
|
163
|
+
<BlockRenderer blocks={article.blocks} />
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</article>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|