@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.
Files changed (147) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/config-file.js +5 -2
  3. package/dist/custom-add-ons/starter.js +45 -28
  4. package/dist/file-helpers.js +1 -0
  5. package/dist/frameworks/react/add-ons/shadcn/assets/src/styles.css +224 -15
  6. package/dist/frameworks/react/add-ons/store/assets/src/lib/demo-store.ts +5 -6
  7. package/dist/frameworks/react/add-ons/store/assets/src/routes/demo/store.tsx.ejs +1 -1
  8. package/dist/frameworks/react/add-ons/store/package.json +2 -2
  9. package/dist/frameworks/react/add-ons/strapi/README.md +158 -8
  10. package/dist/frameworks/react/add-ons/strapi/assets/_dot_env.local.append +1 -1
  11. package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/block-renderer.tsx +55 -0
  12. package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/index.ts +14 -0
  13. package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/media.tsx +27 -0
  14. package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/quote.tsx +19 -0
  15. package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/rich-text.tsx +11 -0
  16. package/dist/frameworks/react/add-ons/strapi/assets/src/components/blocks/slider.tsx +28 -0
  17. package/dist/frameworks/react/add-ons/strapi/assets/src/components/markdown-content.tsx +74 -0
  18. package/dist/frameworks/react/add-ons/strapi/assets/src/components/pagination.tsx +120 -0
  19. package/dist/frameworks/react/add-ons/strapi/assets/src/components/search.tsx +35 -0
  20. package/dist/frameworks/react/add-ons/strapi/assets/src/components/strapi-image.tsx +47 -0
  21. package/dist/frameworks/react/add-ons/strapi/assets/src/data/loaders/articles.ts +106 -0
  22. package/dist/frameworks/react/add-ons/strapi/assets/src/data/loaders/index.ts +28 -0
  23. package/dist/frameworks/react/add-ons/strapi/assets/src/data/strapi-sdk.ts +9 -0
  24. package/dist/frameworks/react/add-ons/strapi/assets/src/lib/strapi-utils.ts +25 -0
  25. package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.$articleId.tsx +170 -0
  26. package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.tsx +269 -43
  27. package/dist/frameworks/react/add-ons/strapi/assets/src/types/strapi.ts +90 -0
  28. package/dist/frameworks/react/add-ons/strapi/info.json +3 -3
  29. package/dist/frameworks/react/add-ons/strapi/package.json +5 -2
  30. package/dist/frameworks/react/index.js +2 -2
  31. package/dist/frameworks/react/project/base/content/blog/fifth-post.mdx.ejs +54 -0
  32. package/dist/frameworks/react/project/base/content/blog/first-post.md.ejs +47 -0
  33. package/dist/frameworks/react/project/base/content/blog/fourth-post.md.ejs +42 -0
  34. package/dist/frameworks/react/project/base/content/blog/second-post.mdx.ejs +46 -0
  35. package/dist/frameworks/react/project/base/content/blog/third-post.md.ejs +49 -0
  36. package/dist/frameworks/react/project/base/content-collections.ts.ejs +37 -0
  37. package/dist/frameworks/react/project/base/package.json +8 -1
  38. package/dist/frameworks/react/project/base/public/images/lagoon-1.svg +13 -0
  39. package/dist/frameworks/react/project/base/public/images/lagoon-2.svg +12 -0
  40. package/dist/frameworks/react/project/base/public/images/lagoon-3.svg +12 -0
  41. package/dist/frameworks/react/project/base/public/images/lagoon-4.svg +12 -0
  42. package/dist/frameworks/react/project/base/public/images/lagoon-5.svg +12 -0
  43. package/dist/frameworks/react/project/base/public/images/lagoon-about.svg +14 -0
  44. package/dist/frameworks/react/project/base/src/components/Footer.tsx.ejs +42 -0
  45. package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +92 -138
  46. package/dist/frameworks/react/project/base/src/components/MdxCallout.tsx.ejs +16 -0
  47. package/dist/frameworks/react/project/base/src/components/MdxMetrics.tsx.ejs +23 -0
  48. package/dist/frameworks/react/project/base/src/components/ThemeToggle.tsx.ejs +81 -0
  49. package/dist/frameworks/react/project/base/src/lib/site.ts.ejs +4 -0
  50. package/dist/frameworks/react/project/base/src/main.tsx.ejs +0 -1
  51. package/dist/frameworks/react/project/base/src/routes/__root.tsx.ejs +10 -6
  52. package/dist/frameworks/react/project/base/src/routes/about.tsx.ejs +27 -0
  53. package/dist/frameworks/react/project/base/src/routes/blog.$slug.tsx.ejs +71 -0
  54. package/dist/frameworks/react/project/base/src/routes/blog.index.tsx.ejs +93 -0
  55. package/dist/frameworks/react/project/base/src/routes/index.tsx.ejs +58 -91
  56. package/dist/frameworks/react/project/base/src/routes/rss[.]xml.ts.ejs +35 -0
  57. package/dist/frameworks/react/project/base/src/styles.css.ejs +268 -6
  58. package/dist/frameworks/react/project/base/tsconfig.json.ejs +2 -0
  59. package/dist/frameworks/react/project/base/vite.config.ts.ejs +2 -0
  60. package/dist/frameworks/solid/add-ons/store/assets/src/lib/demo-store.ts +5 -6
  61. package/dist/frameworks/solid/add-ons/store/assets/src/routes/demo.store.tsx.ejs +2 -2
  62. package/dist/frameworks/solid/examples/tanchat/assets/src/lib/demo-store.ts +5 -6
  63. package/dist/frameworks/solid/project/base/src/components/Header.tsx.ejs +8 -6
  64. package/dist/frameworks/solid/project/base/src/routes/__root.tsx.ejs +1 -1
  65. package/dist/frameworks/solid/project/base/src/routes/index.tsx.ejs +1 -1
  66. package/dist/frameworks.js +3 -0
  67. package/dist/package-json.js +1 -1
  68. package/dist/registry.js +21 -4
  69. package/dist/types/registry.d.ts +38 -0
  70. package/package.json +1 -1
  71. package/src/config-file.ts +6 -2
  72. package/src/custom-add-ons/starter.ts +30 -10
  73. package/src/file-helpers.ts +1 -0
  74. package/src/frameworks/react/add-ons/shadcn/assets/src/styles.css +224 -15
  75. package/src/frameworks/react/add-ons/store/assets/src/lib/demo-store.ts +5 -6
  76. package/src/frameworks/react/add-ons/store/assets/src/routes/demo/store.tsx.ejs +1 -1
  77. package/src/frameworks/react/add-ons/store/package.json +2 -2
  78. package/src/frameworks/react/add-ons/strapi/README.md +158 -8
  79. package/src/frameworks/react/add-ons/strapi/assets/_dot_env.local.append +1 -1
  80. package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/block-renderer.tsx +55 -0
  81. package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/index.ts +14 -0
  82. package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/media.tsx +27 -0
  83. package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/quote.tsx +19 -0
  84. package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/rich-text.tsx +11 -0
  85. package/src/frameworks/react/add-ons/strapi/assets/src/components/blocks/slider.tsx +28 -0
  86. package/src/frameworks/react/add-ons/strapi/assets/src/components/markdown-content.tsx +74 -0
  87. package/src/frameworks/react/add-ons/strapi/assets/src/components/pagination.tsx +120 -0
  88. package/src/frameworks/react/add-ons/strapi/assets/src/components/search.tsx +35 -0
  89. package/src/frameworks/react/add-ons/strapi/assets/src/components/strapi-image.tsx +47 -0
  90. package/src/frameworks/react/add-ons/strapi/assets/src/data/loaders/articles.ts +106 -0
  91. package/src/frameworks/react/add-ons/strapi/assets/src/data/loaders/index.ts +28 -0
  92. package/src/frameworks/react/add-ons/strapi/assets/src/data/strapi-sdk.ts +9 -0
  93. package/src/frameworks/react/add-ons/strapi/assets/src/lib/strapi-utils.ts +25 -0
  94. package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.$articleId.tsx +170 -0
  95. package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi.tsx +269 -43
  96. package/src/frameworks/react/add-ons/strapi/assets/src/types/strapi.ts +90 -0
  97. package/src/frameworks/react/add-ons/strapi/info.json +3 -3
  98. package/src/frameworks/react/add-ons/strapi/package.json +5 -2
  99. package/src/frameworks/react/index.ts +2 -2
  100. package/src/frameworks/react/project/base/content/blog/fifth-post.mdx.ejs +54 -0
  101. package/src/frameworks/react/project/base/content/blog/first-post.md.ejs +47 -0
  102. package/src/frameworks/react/project/base/content/blog/fourth-post.md.ejs +42 -0
  103. package/src/frameworks/react/project/base/content/blog/second-post.mdx.ejs +46 -0
  104. package/src/frameworks/react/project/base/content/blog/third-post.md.ejs +49 -0
  105. package/src/frameworks/react/project/base/content-collections.ts.ejs +37 -0
  106. package/src/frameworks/react/project/base/package.json +8 -1
  107. package/src/frameworks/react/project/base/public/images/lagoon-1.svg +13 -0
  108. package/src/frameworks/react/project/base/public/images/lagoon-2.svg +12 -0
  109. package/src/frameworks/react/project/base/public/images/lagoon-3.svg +12 -0
  110. package/src/frameworks/react/project/base/public/images/lagoon-4.svg +12 -0
  111. package/src/frameworks/react/project/base/public/images/lagoon-5.svg +12 -0
  112. package/src/frameworks/react/project/base/public/images/lagoon-about.svg +14 -0
  113. package/src/frameworks/react/project/base/src/components/Footer.tsx.ejs +42 -0
  114. package/src/frameworks/react/project/base/src/components/Header.tsx.ejs +92 -138
  115. package/src/frameworks/react/project/base/src/components/MdxCallout.tsx.ejs +16 -0
  116. package/src/frameworks/react/project/base/src/components/MdxMetrics.tsx.ejs +23 -0
  117. package/src/frameworks/react/project/base/src/components/ThemeToggle.tsx.ejs +81 -0
  118. package/src/frameworks/react/project/base/src/lib/site.ts.ejs +4 -0
  119. package/src/frameworks/react/project/base/src/main.tsx.ejs +0 -1
  120. package/src/frameworks/react/project/base/src/routes/__root.tsx.ejs +10 -6
  121. package/src/frameworks/react/project/base/src/routes/about.tsx.ejs +27 -0
  122. package/src/frameworks/react/project/base/src/routes/blog.$slug.tsx.ejs +71 -0
  123. package/src/frameworks/react/project/base/src/routes/blog.index.tsx.ejs +93 -0
  124. package/src/frameworks/react/project/base/src/routes/index.tsx.ejs +58 -91
  125. package/src/frameworks/react/project/base/src/routes/rss[.]xml.ts.ejs +35 -0
  126. package/src/frameworks/react/project/base/src/styles.css.ejs +268 -6
  127. package/src/frameworks/react/project/base/tsconfig.json.ejs +2 -0
  128. package/src/frameworks/react/project/base/vite.config.ts.ejs +2 -0
  129. package/src/frameworks/solid/add-ons/store/assets/src/lib/demo-store.ts +5 -6
  130. package/src/frameworks/solid/add-ons/store/assets/src/routes/demo.store.tsx.ejs +2 -2
  131. package/src/frameworks/solid/examples/tanchat/assets/src/lib/demo-store.ts +5 -6
  132. package/src/frameworks/solid/project/base/src/components/Header.tsx.ejs +8 -6
  133. package/src/frameworks/solid/project/base/src/routes/__root.tsx.ejs +1 -1
  134. package/src/frameworks/solid/project/base/src/routes/index.tsx.ejs +1 -1
  135. package/src/frameworks.ts +4 -0
  136. package/src/package-json.ts +1 -1
  137. package/src/registry.ts +28 -4
  138. package/tests/add-ons.test.ts +4 -4
  139. package/tests/config-file.test.ts +3 -3
  140. package/tests/custom-add-ons/starter.test.ts +34 -2
  141. package/tests/frameworks.test.ts +24 -0
  142. package/tests/options.test.ts +4 -4
  143. package/tests/utils.test.ts +2 -2
  144. package/dist/frameworks/react/add-ons/strapi/assets/src/lib/strapiClient.ts +0 -7
  145. package/dist/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi_.$articleId.tsx +0 -78
  146. package/src/frameworks/react/add-ons/strapi/assets/src/lib/strapiClient.ts +0 -7
  147. package/src/frameworks/react/add-ons/strapi/assets/src/routes/demo/strapi_.$articleId.tsx +0 -78
@@ -0,0 +1,55 @@
1
+ import { RichText } from "./rich-text";
2
+ import { Quote } from "./quote";
3
+ import { Media } from "./media";
4
+ import { Slider } from "./slider";
5
+
6
+ import type { IRichText } from "./rich-text";
7
+ import type { IQuote } from "./quote";
8
+ import type { IMedia } from "./media";
9
+ import type { ISlider } from "./slider";
10
+
11
+ // Union type of all block types
12
+ export type Block = IRichText | IQuote | IMedia | ISlider;
13
+
14
+ interface BlockRendererProps {
15
+ blocks: Array<Block>;
16
+ }
17
+
18
+ /**
19
+ * BlockRenderer - Renders dynamic content blocks from Strapi
20
+ *
21
+ * Usage:
22
+ * ```tsx
23
+ * <BlockRenderer blocks={article.blocks} />
24
+ * ```
25
+ */
26
+ export function BlockRenderer({ blocks }: Readonly<BlockRendererProps>) {
27
+ if (!blocks || blocks.length === 0) return null;
28
+
29
+ const renderBlock = (block: Block) => {
30
+ switch (block.__component) {
31
+ case "shared.rich-text":
32
+ return <RichText {...block} />;
33
+ case "shared.quote":
34
+ return <Quote {...block} />;
35
+ case "shared.media":
36
+ return <Media {...block} />;
37
+ case "shared.slider":
38
+ return <Slider {...block} />;
39
+ default:
40
+ // Log unknown block types in development
41
+ console.warn("Unknown block type:", (block as any).__component);
42
+ return null;
43
+ }
44
+ };
45
+
46
+ return (
47
+ <div className="space-y-6">
48
+ {blocks.map((block, index) => (
49
+ <div key={`${block.__component}-${block.id}-${index}`}>
50
+ {renderBlock(block)}
51
+ </div>
52
+ ))}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,14 @@
1
+ export { BlockRenderer } from "./block-renderer";
2
+ export type { Block } from "./block-renderer";
3
+
4
+ export { RichText } from "./rich-text";
5
+ export type { IRichText } from "./rich-text";
6
+
7
+ export { Quote } from "./quote";
8
+ export type { IQuote } from "./quote";
9
+
10
+ export { Media } from "./media";
11
+ export type { IMedia } from "./media";
12
+
13
+ export { Slider } from "./slider";
14
+ export type { ISlider } from "./slider";
@@ -0,0 +1,27 @@
1
+ import { StrapiImage } from "@/components/strapi-image";
2
+ import type { TImage } from "@/types/strapi";
3
+
4
+ export interface IMedia {
5
+ __component: "shared.media";
6
+ id: number;
7
+ file?: TImage;
8
+ }
9
+
10
+ export function Media({ file }: Readonly<IMedia>) {
11
+ if (!file) return null;
12
+
13
+ return (
14
+ <figure className="my-8">
15
+ <StrapiImage
16
+ src={file.url}
17
+ alt={file.alternativeText || ""}
18
+ className="rounded-lg w-full"
19
+ />
20
+ {file.alternativeText && (
21
+ <figcaption className="mt-2 text-center text-sm text-gray-500">
22
+ {file.alternativeText}
23
+ </figcaption>
24
+ )}
25
+ </figure>
26
+ );
27
+ }
@@ -0,0 +1,19 @@
1
+ export interface IQuote {
2
+ __component: "shared.quote";
3
+ id: number;
4
+ body: string;
5
+ title?: string;
6
+ }
7
+
8
+ export function Quote({ body, title }: Readonly<IQuote>) {
9
+ return (
10
+ <blockquote className="border-l-4 border-cyan-400 pl-6 py-4 my-6 bg-slate-800/30 rounded-r-lg">
11
+ <p className="text-xl italic text-gray-300 leading-relaxed">{body}</p>
12
+ {title && (
13
+ <cite className="block mt-4 text-cyan-400 not-italic font-medium">
14
+ — {title}
15
+ </cite>
16
+ )}
17
+ </blockquote>
18
+ );
19
+ }
@@ -0,0 +1,11 @@
1
+ import { MarkdownContent } from "@/components/markdown-content";
2
+
3
+ export interface IRichText {
4
+ __component: "shared.rich-text";
5
+ id: number;
6
+ body: string;
7
+ }
8
+
9
+ export function RichText({ body }: Readonly<IRichText>) {
10
+ return <MarkdownContent content={body} />;
11
+ }
@@ -0,0 +1,28 @@
1
+ import { StrapiImage } from "@/components/strapi-image";
2
+ import type { TImage } from "@/types/strapi";
3
+
4
+ export interface ISlider {
5
+ __component: "shared.slider";
6
+ id: number;
7
+ files?: Array<TImage>;
8
+ }
9
+
10
+ export function Slider({ files }: Readonly<ISlider>) {
11
+ if (!files || files.length === 0) return null;
12
+
13
+ return (
14
+ <div className="my-8">
15
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
16
+ {files.map((file, index) => (
17
+ <figure key={file.id || index}>
18
+ <StrapiImage
19
+ src={file.url}
20
+ alt={file.alternativeText || ""}
21
+ className="rounded-lg w-full h-48 object-cover"
22
+ />
23
+ </figure>
24
+ ))}
25
+ </div>
26
+ </div>
27
+ );
28
+ }
@@ -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
+ }