@haroonwaves/blog-kit-react 0.0.4
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/README.md +38 -0
- package/dist/index.cjs +224 -0
- package/dist/index.css +3 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +183 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @haroonwaves/blog-kit-react
|
|
2
|
+
|
|
3
|
+
React components and hooks for rendering markdown blogs. Includes blog renderer with syntax
|
|
4
|
+
highlighting, blog cards, lists, and search functionality.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @haroonwaves/blog-kit-react
|
|
10
|
+
# or
|
|
11
|
+
pnpm add @haroonwaves/blog-kit-react
|
|
12
|
+
# or
|
|
13
|
+
yarn add @haroonwaves/blog-kit-react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Note:** This package requires React 18+ as a peer dependency.
|
|
17
|
+
|
|
18
|
+
## Documentation
|
|
19
|
+
|
|
20
|
+
For complete documentation, API reference, and examples, please visit the
|
|
21
|
+
[main documentation](https://github.com/haroonwaves/blog-kit).
|
|
22
|
+
|
|
23
|
+
## Quick Links
|
|
24
|
+
|
|
25
|
+
- [React Package Usage](https://github.com/haroonwaves/blog-kit#react-package)
|
|
26
|
+
- [Components Reference](https://github.com/haroonwaves/blog-kit#react-package)
|
|
27
|
+
- [useBlogs Hook](https://github.com/haroonwaves/blog-kit#useblogs-hook)
|
|
28
|
+
- [Complete Example](https://github.com/haroonwaves/blog-kit#complete-example)
|
|
29
|
+
- [Next.js Integration](https://github.com/haroonwaves/blog-kit#nextjs-integration)
|
|
30
|
+
|
|
31
|
+
## Related Packages
|
|
32
|
+
|
|
33
|
+
- [@haroonwaves/blog-kit-core](https://www.npmjs.com/package/@haroonwaves/blog-kit-core) - Core
|
|
34
|
+
utilities for parsing markdown files
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
ISC
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
BlogCard: () => BlogCard,
|
|
34
|
+
BlogList: () => BlogList,
|
|
35
|
+
BlogPlaceholder: () => BlogPlaceholder,
|
|
36
|
+
MarkdownRenderer: () => BlogRenderer,
|
|
37
|
+
useBlogs: () => useBlogs
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(src_exports);
|
|
40
|
+
|
|
41
|
+
// src/components/MarkdownRenderer.tsx
|
|
42
|
+
var import_react_markdown = __toESM(require("react-markdown"), 1);
|
|
43
|
+
var import_remark_gfm = __toESM(require("remark-gfm"), 1);
|
|
44
|
+
var import_rehype_prism_plus = __toESM(require("rehype-prism-plus"), 1);
|
|
45
|
+
var import_rehype_raw = __toESM(require("rehype-raw"), 1);
|
|
46
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
47
|
+
function BlogRenderer({ content, className = "", components }) {
|
|
48
|
+
const defaultComponents = {
|
|
49
|
+
h1: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-4xl font-bold mb-4 mt-8 text-gray-800", ...props }),
|
|
50
|
+
h2: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-3xl font-bold mb-3 mt-6 text-gray-800", ...props }),
|
|
51
|
+
h3: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-2xl font-semibold mb-2 mt-4 text-gray-800", ...props }),
|
|
52
|
+
h4: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { className: "text-xl font-semibold mb-2 mt-4 text-gray-800", ...props }),
|
|
53
|
+
h5: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h5", { className: "text-lg font-semibold mb-2 mt-3 text-gray-800", ...props }),
|
|
54
|
+
h6: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h6", { className: "text-base font-semibold mb-2 mt-3 text-gray-800", ...props }),
|
|
55
|
+
p: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mb-4 leading-7 text-gray-600", ...props }),
|
|
56
|
+
ul: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "mb-4 ml-6 list-disc text-gray-600", ...props }),
|
|
57
|
+
ol: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "mb-4 ml-6 list-decimal text-gray-600", ...props }),
|
|
58
|
+
li: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "mb-2", ...props }),
|
|
59
|
+
code: ({ className: codeClassName, children, ...props }) => {
|
|
60
|
+
const isInline = !codeClassName;
|
|
61
|
+
return isInline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "px-1.5 py-0.5 bg-gray-100 rounded text-sm text-red-600", ...props, children }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: codeClassName, ...props, children });
|
|
62
|
+
},
|
|
63
|
+
pre: ({ className: preClassName, children, ...props }) => {
|
|
64
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
65
|
+
"pre",
|
|
66
|
+
{
|
|
67
|
+
className: `mb-4 rounded-lg overflow-x-auto [&>code]:block [&>code]:p-4 ${preClassName || ""}`,
|
|
68
|
+
...props,
|
|
69
|
+
children
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
blockquote: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
74
|
+
"blockquote",
|
|
75
|
+
{
|
|
76
|
+
className: "border-l-4 border-blue-500 pl-4 italic my-4 text-gray-600",
|
|
77
|
+
...props
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
a: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: "text-blue-600 hover:text-blue-800 underline", ...props }),
|
|
81
|
+
strong: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { className: "font-semibold text-gray-800", ...props }),
|
|
82
|
+
em: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { className: "italic", ...props }),
|
|
83
|
+
del: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("del", { className: "line-through text-gray-500", ...props }),
|
|
84
|
+
hr: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { className: "my-8 border-gray-300", ...props }),
|
|
85
|
+
br: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", { ...props }),
|
|
86
|
+
img: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { className: "max-w-full h-auto rounded-lg my-4", ...props }),
|
|
87
|
+
table: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "overflow-x-auto my-4", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("table", { className: "min-w-full border border-gray-300 rounded", ...props }) }),
|
|
88
|
+
thead: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { className: "bg-gray-50", ...props }),
|
|
89
|
+
tbody: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { ...props }),
|
|
90
|
+
tr: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { className: "border-b border-gray-300", ...props }),
|
|
91
|
+
th: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { className: "px-4 py-2 text-left font-semibold border border-gray-300", ...props }),
|
|
92
|
+
td: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { className: "px-4 py-2 border border-gray-300", ...props }),
|
|
93
|
+
input: ({ ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "mr-2", type: "checkbox", disabled: true, ...props })
|
|
94
|
+
};
|
|
95
|
+
const mergedComponents = { ...defaultComponents, ...components };
|
|
96
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `prose prose-slate max-w-none ${className}`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
97
|
+
import_react_markdown.default,
|
|
98
|
+
{
|
|
99
|
+
remarkPlugins: [import_remark_gfm.default],
|
|
100
|
+
rehypePlugins: [import_rehype_raw.default, import_rehype_prism_plus.default],
|
|
101
|
+
components: mergedComponents,
|
|
102
|
+
children: content
|
|
103
|
+
}
|
|
104
|
+
) });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/components/BlogCard.tsx
|
|
108
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
109
|
+
function BlogCard({
|
|
110
|
+
blog,
|
|
111
|
+
basePath = "/blog",
|
|
112
|
+
renderLink,
|
|
113
|
+
className = "",
|
|
114
|
+
showCategory = true,
|
|
115
|
+
showReadingTime = true,
|
|
116
|
+
showDate = true
|
|
117
|
+
}) {
|
|
118
|
+
const href = `${basePath}/${blog.slug}`;
|
|
119
|
+
const defaultLink = (href2, children) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: href2, children });
|
|
120
|
+
const Link = renderLink || defaultLink;
|
|
121
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
122
|
+
"article",
|
|
123
|
+
{
|
|
124
|
+
className: `bg-white rounded-lg border border-gray-200 hover:border-gray-300 p-6 transition-colors ${className}`,
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex items-center justify-between mb-3", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-3", children: [
|
|
127
|
+
showCategory && blog.category && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex items-center rounded-full border border-transparent bg-orange-100 px-2.5 py-0.5 text-xs font-semibold text-orange-500", children: blog.category }),
|
|
128
|
+
(showReadingTime || showDate) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [
|
|
129
|
+
showReadingTime && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: blog.readingTime }),
|
|
130
|
+
showReadingTime && showDate && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u2022" }),
|
|
131
|
+
showDate && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("time", { dateTime: blog.date, children: new Date(blog.date).toLocaleDateString("en-US", {
|
|
132
|
+
year: "numeric",
|
|
133
|
+
month: "long",
|
|
134
|
+
day: "numeric"
|
|
135
|
+
}) })
|
|
136
|
+
] })
|
|
137
|
+
] }) }),
|
|
138
|
+
Link(
|
|
139
|
+
href,
|
|
140
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "font-semibold text-xl text-gray-700 mb-2 hover:underline transition-colors", children: blog.title })
|
|
141
|
+
),
|
|
142
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-sm text-gray-500 leading-6 mb-4", children: blog.description }),
|
|
143
|
+
Link(
|
|
144
|
+
href,
|
|
145
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-flex items-center text-blue-600 hover:text-blue-700 font-medium text-sm", children: "Read more \u2192" })
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/components/BlogList.tsx
|
|
153
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
154
|
+
function BlogList({
|
|
155
|
+
blogs,
|
|
156
|
+
basePath = "/blog",
|
|
157
|
+
renderLink,
|
|
158
|
+
className = "",
|
|
159
|
+
emptyMessage = "No blog posts found.",
|
|
160
|
+
cardProps
|
|
161
|
+
}) {
|
|
162
|
+
if (blogs.length === 0) {
|
|
163
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `text-center text-gray-500 py-12 ${className}`, children: emptyMessage });
|
|
164
|
+
}
|
|
165
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: `space-y-6 ${className}`, children: blogs.map((blog) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
166
|
+
BlogCard,
|
|
167
|
+
{
|
|
168
|
+
blog,
|
|
169
|
+
basePath,
|
|
170
|
+
renderLink,
|
|
171
|
+
...cardProps
|
|
172
|
+
},
|
|
173
|
+
blog.slug
|
|
174
|
+
)) });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/components/BlogPlaceholder.tsx
|
|
178
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
179
|
+
function BlogPlaceholder({ count = 3, className = "" }) {
|
|
180
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `space-y-6 ${className}`, children: Array.from({ length: count }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "bg-white rounded-lg border border-gray-200 p-6 animate-pulse", children: [
|
|
181
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-4 bg-gray-200 rounded mb-3" }),
|
|
182
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-6 bg-gray-200 rounded mb-2" }),
|
|
183
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "h-4 bg-gray-200 rounded w-3/4" })
|
|
184
|
+
] }, i)) });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/hooks/useBlogs.ts
|
|
188
|
+
var import_react = require("react");
|
|
189
|
+
function useBlogs(blogs) {
|
|
190
|
+
const [filteredBlogs, setFilteredBlogs] = (0, import_react.useState)(blogs);
|
|
191
|
+
const [searchTerm, setSearchTerm] = (0, import_react.useState)("");
|
|
192
|
+
const [selectedCategory, setSelectedCategory] = (0, import_react.useState)(null);
|
|
193
|
+
(0, import_react.useEffect)(() => {
|
|
194
|
+
let filtered = blogs;
|
|
195
|
+
if (searchTerm) {
|
|
196
|
+
filtered = filtered.filter(
|
|
197
|
+
(blog) => blog.title.toLowerCase().includes(searchTerm.toLowerCase()) || blog.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (selectedCategory) {
|
|
201
|
+
filtered = filtered.filter((blog) => blog.category === selectedCategory);
|
|
202
|
+
}
|
|
203
|
+
setFilteredBlogs(filtered);
|
|
204
|
+
}, [blogs, searchTerm, selectedCategory]);
|
|
205
|
+
const categories = Array.from(
|
|
206
|
+
new Set(blogs.map((blog) => blog.category).filter(Boolean))
|
|
207
|
+
);
|
|
208
|
+
return {
|
|
209
|
+
blogs: filteredBlogs,
|
|
210
|
+
searchTerm,
|
|
211
|
+
setSearchTerm,
|
|
212
|
+
selectedCategory,
|
|
213
|
+
setSelectedCategory,
|
|
214
|
+
categories
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
218
|
+
0 && (module.exports = {
|
|
219
|
+
BlogCard,
|
|
220
|
+
BlogList,
|
|
221
|
+
BlogPlaceholder,
|
|
222
|
+
MarkdownRenderer,
|
|
223
|
+
useBlogs
|
|
224
|
+
});
|
package/dist/index.css
ADDED
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import react__default from 'react';
|
|
4
|
+
|
|
5
|
+
interface BlogMeta {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
date: string;
|
|
9
|
+
category?: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
readingTime: string;
|
|
12
|
+
}
|
|
13
|
+
interface Blog {
|
|
14
|
+
metadata: BlogMeta;
|
|
15
|
+
content: string;
|
|
16
|
+
readingTime: string;
|
|
17
|
+
}
|
|
18
|
+
interface BlogConfig {
|
|
19
|
+
contentDirectory: string;
|
|
20
|
+
blogSubdirectory?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface BlogRendererProps {
|
|
24
|
+
content: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
components?: Record<string, react__default.ComponentType<any>>;
|
|
27
|
+
}
|
|
28
|
+
declare function BlogRenderer({ content, className, components }: BlogRendererProps): react_jsx_runtime.JSX.Element;
|
|
29
|
+
|
|
30
|
+
interface BlogCardProps {
|
|
31
|
+
blog: BlogMeta;
|
|
32
|
+
basePath?: string;
|
|
33
|
+
renderLink?: (href: string, children: react__default.ReactNode) => react__default.ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
showCategory?: boolean;
|
|
36
|
+
showReadingTime?: boolean;
|
|
37
|
+
showDate?: boolean;
|
|
38
|
+
}
|
|
39
|
+
declare function BlogCard({ blog, basePath, renderLink, className, showCategory, showReadingTime, showDate, }: BlogCardProps): react_jsx_runtime.JSX.Element;
|
|
40
|
+
|
|
41
|
+
interface BlogListProps {
|
|
42
|
+
blogs: BlogMeta[];
|
|
43
|
+
basePath?: string;
|
|
44
|
+
renderLink?: BlogCardProps['renderLink'];
|
|
45
|
+
className?: string;
|
|
46
|
+
emptyMessage?: string;
|
|
47
|
+
cardProps?: Omit<BlogCardProps, 'blog' | 'basePath' | 'renderLink'>;
|
|
48
|
+
}
|
|
49
|
+
declare function BlogList({ blogs, basePath, renderLink, className, emptyMessage, cardProps, }: BlogListProps): react_jsx_runtime.JSX.Element;
|
|
50
|
+
|
|
51
|
+
interface BlogPlaceholderProps {
|
|
52
|
+
count?: number;
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
declare function BlogPlaceholder({ count, className }: BlogPlaceholderProps): react_jsx_runtime.JSX.Element;
|
|
56
|
+
|
|
57
|
+
declare function useBlogs(blogs: BlogMeta[]): {
|
|
58
|
+
blogs: BlogMeta[];
|
|
59
|
+
searchTerm: string;
|
|
60
|
+
setSearchTerm: react.Dispatch<react.SetStateAction<string>>;
|
|
61
|
+
selectedCategory: string | null;
|
|
62
|
+
setSelectedCategory: react.Dispatch<react.SetStateAction<string | null>>;
|
|
63
|
+
categories: string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export { type Blog, BlogCard, type BlogCardProps, type BlogConfig, BlogList, type BlogListProps, type BlogMeta, BlogPlaceholder, type BlogPlaceholderProps, BlogRenderer as MarkdownRenderer, type BlogRendererProps as MarkdownRendererProps, useBlogs };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import react__default from 'react';
|
|
4
|
+
|
|
5
|
+
interface BlogMeta {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
date: string;
|
|
9
|
+
category?: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
readingTime: string;
|
|
12
|
+
}
|
|
13
|
+
interface Blog {
|
|
14
|
+
metadata: BlogMeta;
|
|
15
|
+
content: string;
|
|
16
|
+
readingTime: string;
|
|
17
|
+
}
|
|
18
|
+
interface BlogConfig {
|
|
19
|
+
contentDirectory: string;
|
|
20
|
+
blogSubdirectory?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface BlogRendererProps {
|
|
24
|
+
content: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
components?: Record<string, react__default.ComponentType<any>>;
|
|
27
|
+
}
|
|
28
|
+
declare function BlogRenderer({ content, className, components }: BlogRendererProps): react_jsx_runtime.JSX.Element;
|
|
29
|
+
|
|
30
|
+
interface BlogCardProps {
|
|
31
|
+
blog: BlogMeta;
|
|
32
|
+
basePath?: string;
|
|
33
|
+
renderLink?: (href: string, children: react__default.ReactNode) => react__default.ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
showCategory?: boolean;
|
|
36
|
+
showReadingTime?: boolean;
|
|
37
|
+
showDate?: boolean;
|
|
38
|
+
}
|
|
39
|
+
declare function BlogCard({ blog, basePath, renderLink, className, showCategory, showReadingTime, showDate, }: BlogCardProps): react_jsx_runtime.JSX.Element;
|
|
40
|
+
|
|
41
|
+
interface BlogListProps {
|
|
42
|
+
blogs: BlogMeta[];
|
|
43
|
+
basePath?: string;
|
|
44
|
+
renderLink?: BlogCardProps['renderLink'];
|
|
45
|
+
className?: string;
|
|
46
|
+
emptyMessage?: string;
|
|
47
|
+
cardProps?: Omit<BlogCardProps, 'blog' | 'basePath' | 'renderLink'>;
|
|
48
|
+
}
|
|
49
|
+
declare function BlogList({ blogs, basePath, renderLink, className, emptyMessage, cardProps, }: BlogListProps): react_jsx_runtime.JSX.Element;
|
|
50
|
+
|
|
51
|
+
interface BlogPlaceholderProps {
|
|
52
|
+
count?: number;
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
declare function BlogPlaceholder({ count, className }: BlogPlaceholderProps): react_jsx_runtime.JSX.Element;
|
|
56
|
+
|
|
57
|
+
declare function useBlogs(blogs: BlogMeta[]): {
|
|
58
|
+
blogs: BlogMeta[];
|
|
59
|
+
searchTerm: string;
|
|
60
|
+
setSearchTerm: react.Dispatch<react.SetStateAction<string>>;
|
|
61
|
+
selectedCategory: string | null;
|
|
62
|
+
setSelectedCategory: react.Dispatch<react.SetStateAction<string | null>>;
|
|
63
|
+
categories: string[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export { type Blog, BlogCard, type BlogCardProps, type BlogConfig, BlogList, type BlogListProps, type BlogMeta, BlogPlaceholder, type BlogPlaceholderProps, BlogRenderer as MarkdownRenderer, type BlogRendererProps as MarkdownRendererProps, useBlogs };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// src/components/MarkdownRenderer.tsx
|
|
2
|
+
import ReactMarkdown from "react-markdown";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import rehypePrismPlus from "rehype-prism-plus";
|
|
5
|
+
import rehypeRaw from "rehype-raw";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
function BlogRenderer({ content, className = "", components }) {
|
|
8
|
+
const defaultComponents = {
|
|
9
|
+
h1: ({ ...props }) => /* @__PURE__ */ jsx("h1", { className: "text-4xl font-bold mb-4 mt-8 text-gray-800", ...props }),
|
|
10
|
+
h2: ({ ...props }) => /* @__PURE__ */ jsx("h2", { className: "text-3xl font-bold mb-3 mt-6 text-gray-800", ...props }),
|
|
11
|
+
h3: ({ ...props }) => /* @__PURE__ */ jsx("h3", { className: "text-2xl font-semibold mb-2 mt-4 text-gray-800", ...props }),
|
|
12
|
+
h4: ({ ...props }) => /* @__PURE__ */ jsx("h4", { className: "text-xl font-semibold mb-2 mt-4 text-gray-800", ...props }),
|
|
13
|
+
h5: ({ ...props }) => /* @__PURE__ */ jsx("h5", { className: "text-lg font-semibold mb-2 mt-3 text-gray-800", ...props }),
|
|
14
|
+
h6: ({ ...props }) => /* @__PURE__ */ jsx("h6", { className: "text-base font-semibold mb-2 mt-3 text-gray-800", ...props }),
|
|
15
|
+
p: ({ ...props }) => /* @__PURE__ */ jsx("p", { className: "mb-4 leading-7 text-gray-600", ...props }),
|
|
16
|
+
ul: ({ ...props }) => /* @__PURE__ */ jsx("ul", { className: "mb-4 ml-6 list-disc text-gray-600", ...props }),
|
|
17
|
+
ol: ({ ...props }) => /* @__PURE__ */ jsx("ol", { className: "mb-4 ml-6 list-decimal text-gray-600", ...props }),
|
|
18
|
+
li: ({ ...props }) => /* @__PURE__ */ jsx("li", { className: "mb-2", ...props }),
|
|
19
|
+
code: ({ className: codeClassName, children, ...props }) => {
|
|
20
|
+
const isInline = !codeClassName;
|
|
21
|
+
return isInline ? /* @__PURE__ */ jsx("code", { className: "px-1.5 py-0.5 bg-gray-100 rounded text-sm text-red-600", ...props, children }) : /* @__PURE__ */ jsx("code", { className: codeClassName, ...props, children });
|
|
22
|
+
},
|
|
23
|
+
pre: ({ className: preClassName, children, ...props }) => {
|
|
24
|
+
return /* @__PURE__ */ jsx(
|
|
25
|
+
"pre",
|
|
26
|
+
{
|
|
27
|
+
className: `mb-4 rounded-lg overflow-x-auto [&>code]:block [&>code]:p-4 ${preClassName || ""}`,
|
|
28
|
+
...props,
|
|
29
|
+
children
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
blockquote: ({ ...props }) => /* @__PURE__ */ jsx(
|
|
34
|
+
"blockquote",
|
|
35
|
+
{
|
|
36
|
+
className: "border-l-4 border-blue-500 pl-4 italic my-4 text-gray-600",
|
|
37
|
+
...props
|
|
38
|
+
}
|
|
39
|
+
),
|
|
40
|
+
a: ({ ...props }) => /* @__PURE__ */ jsx("a", { className: "text-blue-600 hover:text-blue-800 underline", ...props }),
|
|
41
|
+
strong: ({ ...props }) => /* @__PURE__ */ jsx("strong", { className: "font-semibold text-gray-800", ...props }),
|
|
42
|
+
em: ({ ...props }) => /* @__PURE__ */ jsx("em", { className: "italic", ...props }),
|
|
43
|
+
del: ({ ...props }) => /* @__PURE__ */ jsx("del", { className: "line-through text-gray-500", ...props }),
|
|
44
|
+
hr: ({ ...props }) => /* @__PURE__ */ jsx("hr", { className: "my-8 border-gray-300", ...props }),
|
|
45
|
+
br: ({ ...props }) => /* @__PURE__ */ jsx("br", { ...props }),
|
|
46
|
+
img: ({ ...props }) => /* @__PURE__ */ jsx("img", { className: "max-w-full h-auto rounded-lg my-4", ...props }),
|
|
47
|
+
table: ({ ...props }) => /* @__PURE__ */ jsx("div", { className: "overflow-x-auto my-4", children: /* @__PURE__ */ jsx("table", { className: "min-w-full border border-gray-300 rounded", ...props }) }),
|
|
48
|
+
thead: ({ ...props }) => /* @__PURE__ */ jsx("thead", { className: "bg-gray-50", ...props }),
|
|
49
|
+
tbody: ({ ...props }) => /* @__PURE__ */ jsx("tbody", { ...props }),
|
|
50
|
+
tr: ({ ...props }) => /* @__PURE__ */ jsx("tr", { className: "border-b border-gray-300", ...props }),
|
|
51
|
+
th: ({ ...props }) => /* @__PURE__ */ jsx("th", { className: "px-4 py-2 text-left font-semibold border border-gray-300", ...props }),
|
|
52
|
+
td: ({ ...props }) => /* @__PURE__ */ jsx("td", { className: "px-4 py-2 border border-gray-300", ...props }),
|
|
53
|
+
input: ({ ...props }) => /* @__PURE__ */ jsx("input", { className: "mr-2", type: "checkbox", disabled: true, ...props })
|
|
54
|
+
};
|
|
55
|
+
const mergedComponents = { ...defaultComponents, ...components };
|
|
56
|
+
return /* @__PURE__ */ jsx("div", { className: `prose prose-slate max-w-none ${className}`, children: /* @__PURE__ */ jsx(
|
|
57
|
+
ReactMarkdown,
|
|
58
|
+
{
|
|
59
|
+
remarkPlugins: [remarkGfm],
|
|
60
|
+
rehypePlugins: [rehypeRaw, rehypePrismPlus],
|
|
61
|
+
components: mergedComponents,
|
|
62
|
+
children: content
|
|
63
|
+
}
|
|
64
|
+
) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/components/BlogCard.tsx
|
|
68
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
69
|
+
function BlogCard({
|
|
70
|
+
blog,
|
|
71
|
+
basePath = "/blog",
|
|
72
|
+
renderLink,
|
|
73
|
+
className = "",
|
|
74
|
+
showCategory = true,
|
|
75
|
+
showReadingTime = true,
|
|
76
|
+
showDate = true
|
|
77
|
+
}) {
|
|
78
|
+
const href = `${basePath}/${blog.slug}`;
|
|
79
|
+
const defaultLink = (href2, children) => /* @__PURE__ */ jsx2("a", { href: href2, children });
|
|
80
|
+
const Link = renderLink || defaultLink;
|
|
81
|
+
return /* @__PURE__ */ jsxs(
|
|
82
|
+
"article",
|
|
83
|
+
{
|
|
84
|
+
className: `bg-white rounded-lg border border-gray-200 hover:border-gray-300 p-6 transition-colors ${className}`,
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ jsx2("div", { className: "flex items-center justify-between mb-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
87
|
+
showCategory && blog.category && /* @__PURE__ */ jsx2("span", { className: "inline-flex items-center rounded-full border border-transparent bg-orange-100 px-2.5 py-0.5 text-xs font-semibold text-orange-500", children: blog.category }),
|
|
88
|
+
(showReadingTime || showDate) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [
|
|
89
|
+
showReadingTime && /* @__PURE__ */ jsx2("span", { children: blog.readingTime }),
|
|
90
|
+
showReadingTime && showDate && /* @__PURE__ */ jsx2("span", { children: "\u2022" }),
|
|
91
|
+
showDate && /* @__PURE__ */ jsx2("time", { dateTime: blog.date, children: new Date(blog.date).toLocaleDateString("en-US", {
|
|
92
|
+
year: "numeric",
|
|
93
|
+
month: "long",
|
|
94
|
+
day: "numeric"
|
|
95
|
+
}) })
|
|
96
|
+
] })
|
|
97
|
+
] }) }),
|
|
98
|
+
Link(
|
|
99
|
+
href,
|
|
100
|
+
/* @__PURE__ */ jsx2("h2", { className: "font-semibold text-xl text-gray-700 mb-2 hover:underline transition-colors", children: blog.title })
|
|
101
|
+
),
|
|
102
|
+
/* @__PURE__ */ jsx2("p", { className: "text-sm text-gray-500 leading-6 mb-4", children: blog.description }),
|
|
103
|
+
Link(
|
|
104
|
+
href,
|
|
105
|
+
/* @__PURE__ */ jsx2("span", { className: "inline-flex items-center text-blue-600 hover:text-blue-700 font-medium text-sm", children: "Read more \u2192" })
|
|
106
|
+
)
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/components/BlogList.tsx
|
|
113
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
114
|
+
function BlogList({
|
|
115
|
+
blogs,
|
|
116
|
+
basePath = "/blog",
|
|
117
|
+
renderLink,
|
|
118
|
+
className = "",
|
|
119
|
+
emptyMessage = "No blog posts found.",
|
|
120
|
+
cardProps
|
|
121
|
+
}) {
|
|
122
|
+
if (blogs.length === 0) {
|
|
123
|
+
return /* @__PURE__ */ jsx3("div", { className: `text-center text-gray-500 py-12 ${className}`, children: emptyMessage });
|
|
124
|
+
}
|
|
125
|
+
return /* @__PURE__ */ jsx3("div", { className: `space-y-6 ${className}`, children: blogs.map((blog) => /* @__PURE__ */ jsx3(
|
|
126
|
+
BlogCard,
|
|
127
|
+
{
|
|
128
|
+
blog,
|
|
129
|
+
basePath,
|
|
130
|
+
renderLink,
|
|
131
|
+
...cardProps
|
|
132
|
+
},
|
|
133
|
+
blog.slug
|
|
134
|
+
)) });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/components/BlogPlaceholder.tsx
|
|
138
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
139
|
+
function BlogPlaceholder({ count = 3, className = "" }) {
|
|
140
|
+
return /* @__PURE__ */ jsx4("div", { className: `space-y-6 ${className}`, children: Array.from({ length: count }).map((_, i) => /* @__PURE__ */ jsxs2("div", { className: "bg-white rounded-lg border border-gray-200 p-6 animate-pulse", children: [
|
|
141
|
+
/* @__PURE__ */ jsx4("div", { className: "h-4 bg-gray-200 rounded mb-3" }),
|
|
142
|
+
/* @__PURE__ */ jsx4("div", { className: "h-6 bg-gray-200 rounded mb-2" }),
|
|
143
|
+
/* @__PURE__ */ jsx4("div", { className: "h-4 bg-gray-200 rounded w-3/4" })
|
|
144
|
+
] }, i)) });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/hooks/useBlogs.ts
|
|
148
|
+
import { useState, useEffect } from "react";
|
|
149
|
+
function useBlogs(blogs) {
|
|
150
|
+
const [filteredBlogs, setFilteredBlogs] = useState(blogs);
|
|
151
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
152
|
+
const [selectedCategory, setSelectedCategory] = useState(null);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
let filtered = blogs;
|
|
155
|
+
if (searchTerm) {
|
|
156
|
+
filtered = filtered.filter(
|
|
157
|
+
(blog) => blog.title.toLowerCase().includes(searchTerm.toLowerCase()) || blog.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (selectedCategory) {
|
|
161
|
+
filtered = filtered.filter((blog) => blog.category === selectedCategory);
|
|
162
|
+
}
|
|
163
|
+
setFilteredBlogs(filtered);
|
|
164
|
+
}, [blogs, searchTerm, selectedCategory]);
|
|
165
|
+
const categories = Array.from(
|
|
166
|
+
new Set(blogs.map((blog) => blog.category).filter(Boolean))
|
|
167
|
+
);
|
|
168
|
+
return {
|
|
169
|
+
blogs: filteredBlogs,
|
|
170
|
+
searchTerm,
|
|
171
|
+
setSearchTerm,
|
|
172
|
+
selectedCategory,
|
|
173
|
+
setSelectedCategory,
|
|
174
|
+
categories
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export {
|
|
178
|
+
BlogCard,
|
|
179
|
+
BlogList,
|
|
180
|
+
BlogPlaceholder,
|
|
181
|
+
BlogRenderer as MarkdownRenderer,
|
|
182
|
+
useBlogs
|
|
183
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haroonwaves/blog-kit-react",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"react": ">=18",
|
|
13
|
+
"react-dom": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"react-markdown": "^10.1.0",
|
|
17
|
+
"remark-gfm": "^4.0.1",
|
|
18
|
+
"rehype-prism-plus": "^2.0.1",
|
|
19
|
+
"rehype-raw": "^7.0.0",
|
|
20
|
+
"prismjs": "^1.30.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^7.2.0",
|
|
24
|
+
"typescript": "^5.5.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
28
|
+
"type-check": "tsc --noEmit"
|
|
29
|
+
}
|
|
30
|
+
}
|