@victusvinceere/saas-blog 0.1.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.
@@ -0,0 +1,57 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface BlogPost {
4
+ slug: string;
5
+ title: string;
6
+ description: string;
7
+ date: string;
8
+ author: {
9
+ name: string;
10
+ image?: string;
11
+ };
12
+ image?: string;
13
+ tags?: string[];
14
+ content: string;
15
+ readingTime: string;
16
+ }
17
+ interface BlogPostMeta {
18
+ slug: string;
19
+ title: string;
20
+ description: string;
21
+ date: string;
22
+ author: {
23
+ name: string;
24
+ image?: string;
25
+ };
26
+ image?: string;
27
+ tags?: string[];
28
+ readingTime: string;
29
+ }
30
+ interface BlogConfig {
31
+ contentDirectory?: string;
32
+ defaultAuthor?: {
33
+ name: string;
34
+ image?: string;
35
+ };
36
+ }
37
+ declare function configureBlog(options: BlogConfig): void;
38
+ declare function getAllPosts(): BlogPostMeta[];
39
+ declare function getPostBySlug(slug: string): BlogPost | null;
40
+ declare function getAllPostSlugs(): string[];
41
+ declare function getPostsByTag(tag: string): BlogPostMeta[];
42
+ declare function getAllTags(): string[];
43
+
44
+ interface PostCardProps {
45
+ post: BlogPostMeta;
46
+ basePath?: string;
47
+ className?: string;
48
+ }
49
+ declare function PostCard({ post, basePath, className }: PostCardProps): react_jsx_runtime.JSX.Element;
50
+
51
+ interface PostHeaderProps {
52
+ post: BlogPost;
53
+ className?: string;
54
+ }
55
+ declare function PostHeader({ post, className }: PostHeaderProps): react_jsx_runtime.JSX.Element;
56
+
57
+ export { type BlogConfig as B, PostCard, type PostCardProps, PostHeader, type PostHeaderProps, type BlogPost as a, type BlogPostMeta as b, configureBlog as c, getAllPosts as d, getAllTags as e, getPostBySlug as f, getAllPostSlugs as g, getPostsByTag as h };
@@ -0,0 +1,57 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface BlogPost {
4
+ slug: string;
5
+ title: string;
6
+ description: string;
7
+ date: string;
8
+ author: {
9
+ name: string;
10
+ image?: string;
11
+ };
12
+ image?: string;
13
+ tags?: string[];
14
+ content: string;
15
+ readingTime: string;
16
+ }
17
+ interface BlogPostMeta {
18
+ slug: string;
19
+ title: string;
20
+ description: string;
21
+ date: string;
22
+ author: {
23
+ name: string;
24
+ image?: string;
25
+ };
26
+ image?: string;
27
+ tags?: string[];
28
+ readingTime: string;
29
+ }
30
+ interface BlogConfig {
31
+ contentDirectory?: string;
32
+ defaultAuthor?: {
33
+ name: string;
34
+ image?: string;
35
+ };
36
+ }
37
+ declare function configureBlog(options: BlogConfig): void;
38
+ declare function getAllPosts(): BlogPostMeta[];
39
+ declare function getPostBySlug(slug: string): BlogPost | null;
40
+ declare function getAllPostSlugs(): string[];
41
+ declare function getPostsByTag(tag: string): BlogPostMeta[];
42
+ declare function getAllTags(): string[];
43
+
44
+ interface PostCardProps {
45
+ post: BlogPostMeta;
46
+ basePath?: string;
47
+ className?: string;
48
+ }
49
+ declare function PostCard({ post, basePath, className }: PostCardProps): react_jsx_runtime.JSX.Element;
50
+
51
+ interface PostHeaderProps {
52
+ post: BlogPost;
53
+ className?: string;
54
+ }
55
+ declare function PostHeader({ post, className }: PostHeaderProps): react_jsx_runtime.JSX.Element;
56
+
57
+ export { type BlogConfig as B, PostCard, type PostCardProps, PostHeader, type PostHeaderProps, type BlogPost as a, type BlogPostMeta as b, configureBlog as c, getAllPosts as d, getAllTags as e, getPostBySlug as f, getAllPostSlugs as g, getPostsByTag as h };
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/components/index.ts
32
+ var components_exports = {};
33
+ __export(components_exports, {
34
+ PostCard: () => PostCard,
35
+ PostHeader: () => PostHeader
36
+ });
37
+ module.exports = __toCommonJS(components_exports);
38
+
39
+ // src/components/post-card.tsx
40
+ var import_link = __toESM(require("next/link"));
41
+ var import_image = __toESM(require("next/image"));
42
+ var import_date_fns = require("date-fns");
43
+ var import_jsx_runtime = require("react/jsx-runtime");
44
+ function PostCard({ post, basePath = "/blog", className }) {
45
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("article", { className: `group relative flex flex-col space-y-2 ${className}`, children: [
46
+ post.image && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative aspect-video overflow-hidden rounded-lg", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
47
+ import_image.default,
48
+ {
49
+ src: post.image,
50
+ alt: post.title,
51
+ fill: true,
52
+ className: "object-cover transition-transform group-hover:scale-105"
53
+ }
54
+ ) }),
55
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex items-center gap-2", children: post.tags?.slice(0, 2).map((tag) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
56
+ "span",
57
+ {
58
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
59
+ children: tag
60
+ },
61
+ tag
62
+ )) }),
63
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-2xl font-semibold tracking-tight", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href: `${basePath}/${post.slug}`, children: [
64
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute inset-0" }),
65
+ post.title
66
+ ] }) }),
67
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "line-clamp-2 text-muted-foreground", children: post.description }),
68
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-4 text-sm text-muted-foreground", children: [
69
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: (0, import_date_fns.format)(new Date(post.date), "MMM d, yyyy") }),
70
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: post.readingTime })
71
+ ] })
72
+ ] });
73
+ }
74
+
75
+ // src/components/post-header.tsx
76
+ var import_image2 = __toESM(require("next/image"));
77
+ var import_date_fns2 = require("date-fns");
78
+ var import_jsx_runtime2 = require("react/jsx-runtime");
79
+ function PostHeader({ post, className }) {
80
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: `space-y-4 ${className}`, children: [
81
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex items-center gap-2", children: post.tags?.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
82
+ "span",
83
+ {
84
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
85
+ children: tag
86
+ },
87
+ tag
88
+ )) }),
89
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { className: "text-4xl font-bold tracking-tight lg:text-5xl", children: post.title }),
90
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-xl text-muted-foreground", children: post.description }),
91
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-4", children: [
92
+ post.author.image && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
93
+ import_image2.default,
94
+ {
95
+ src: post.author.image,
96
+ alt: post.author.name,
97
+ width: 40,
98
+ height: 40,
99
+ className: "rounded-full"
100
+ }
101
+ ),
102
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
103
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-medium", children: post.author.name }),
104
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "text-sm text-muted-foreground", children: [
105
+ (0, import_date_fns2.format)(new Date(post.date), "MMMM d, yyyy"),
106
+ " \xB7 ",
107
+ post.readingTime
108
+ ] })
109
+ ] })
110
+ ] }),
111
+ post.image && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "relative aspect-video overflow-hidden rounded-lg", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
112
+ import_image2.default,
113
+ {
114
+ src: post.image,
115
+ alt: post.title,
116
+ fill: true,
117
+ className: "object-cover",
118
+ priority: true
119
+ }
120
+ ) })
121
+ ] });
122
+ }
123
+ // Annotate the CommonJS export names for ESM import in node:
124
+ 0 && (module.exports = {
125
+ PostCard,
126
+ PostHeader
127
+ });
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/index.ts","../../src/components/post-card.tsx","../../src/components/post-header.tsx"],"sourcesContent":["export { PostCard } from \"./post-card\";\nexport type { PostCardProps } from \"./post-card\";\n\nexport { PostHeader } from \"./post-header\";\nexport type { PostHeaderProps } from \"./post-header\";\n","\"use client\";\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport type { BlogPostMeta } from \"../lib/blog\";\n\nexport interface PostCardProps {\n post: BlogPostMeta;\n basePath?: string;\n className?: string;\n}\n\nexport function PostCard({ post, basePath = \"/blog\", className }: PostCardProps) {\n return (\n <article className={`group relative flex flex-col space-y-2 ${className}`}>\n {post.image && (\n <div className=\"relative aspect-video overflow-hidden rounded-lg\">\n <Image\n src={post.image}\n alt={post.title}\n fill\n className=\"object-cover transition-transform group-hover:scale-105\"\n />\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n {post.tags?.slice(0, 2).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h2 className=\"text-2xl font-semibold tracking-tight\">\n <Link href={`${basePath}/${post.slug}`}>\n <span className=\"absolute inset-0\" />\n {post.title}\n </Link>\n </h2>\n <p className=\"line-clamp-2 text-muted-foreground\">{post.description}</p>\n <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n <span>{format(new Date(post.date), \"MMM d, yyyy\")}</span>\n <span>{post.readingTime}</span>\n </div>\n </article>\n );\n}\n","\"use client\";\n\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport type { BlogPost } from \"../lib/blog\";\n\nexport interface PostHeaderProps {\n post: BlogPost;\n className?: string;\n}\n\nexport function PostHeader({ post, className }: PostHeaderProps) {\n return (\n <header className={`space-y-4 ${className}`}>\n <div className=\"flex items-center gap-2\">\n {post.tags?.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h1 className=\"text-4xl font-bold tracking-tight lg:text-5xl\">\n {post.title}\n </h1>\n <p className=\"text-xl text-muted-foreground\">{post.description}</p>\n <div className=\"flex items-center gap-4\">\n {post.author.image && (\n <Image\n src={post.author.image}\n alt={post.author.name}\n width={40}\n height={40}\n className=\"rounded-full\"\n />\n )}\n <div>\n <p className=\"font-medium\">{post.author.name}</p>\n <p className=\"text-sm text-muted-foreground\">\n {format(new Date(post.date), \"MMMM d, yyyy\")} · {post.readingTime}\n </p>\n </div>\n </div>\n {post.image && (\n <div className=\"relative aspect-video overflow-hidden rounded-lg\">\n <Image\n src={post.image}\n alt={post.title}\n fill\n className=\"object-cover\"\n priority\n />\n </div>\n )}\n </header>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,kBAAiB;AACjB,mBAAkB;AAClB,sBAAuB;AAcb;AALH,SAAS,SAAS,EAAE,MAAM,WAAW,SAAS,UAAU,GAAkB;AAC/E,SACE,6CAAC,aAAQ,WAAW,0CAA0C,SAAS,IACpE;AAAA,SAAK,SACJ,4CAAC,SAAI,WAAU,oDACb;AAAA,MAAC,aAAAA;AAAA,MAAA;AAAA,QACC,KAAK,KAAK;AAAA,QACV,KAAK,KAAK;AAAA,QACV,MAAI;AAAA,QACJ,WAAU;AAAA;AAAA,IACZ,GACF;AAAA,IAEF,4CAAC,SAAI,WAAU,2BACZ,eAAK,MAAM,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,QAC3B;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QAET;AAAA;AAAA,MAHI;AAAA,IAIP,CACD,GACH;AAAA,IACA,4CAAC,QAAG,WAAU,yCACZ,uDAAC,YAAAC,SAAA,EAAK,MAAM,GAAG,QAAQ,IAAI,KAAK,IAAI,IAClC;AAAA,kDAAC,UAAK,WAAU,oBAAmB;AAAA,MAClC,KAAK;AAAA,OACR,GACF;AAAA,IACA,4CAAC,OAAE,WAAU,sCAAsC,eAAK,aAAY;AAAA,IACpE,6CAAC,SAAI,WAAU,yDACb;AAAA,kDAAC,UAAM,sCAAO,IAAI,KAAK,KAAK,IAAI,GAAG,aAAa,GAAE;AAAA,MAClD,4CAAC,UAAM,eAAK,aAAY;AAAA,OAC1B;AAAA,KACF;AAEJ;;;AC/CA,IAAAC,gBAAkB;AAClB,IAAAC,mBAAuB;AAab,IAAAC,sBAAA;AALH,SAAS,WAAW,EAAE,MAAM,UAAU,GAAoB;AAC/D,SACE,8CAAC,YAAO,WAAW,aAAa,SAAS,IACvC;AAAA,iDAAC,SAAI,WAAU,2BACZ,eAAK,MAAM,IAAI,CAAC,QACf;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QAET;AAAA;AAAA,MAHI;AAAA,IAIP,CACD,GACH;AAAA,IACA,6CAAC,QAAG,WAAU,iDACX,eAAK,OACR;AAAA,IACA,6CAAC,OAAE,WAAU,iCAAiC,eAAK,aAAY;AAAA,IAC/D,8CAAC,SAAI,WAAU,2BACZ;AAAA,WAAK,OAAO,SACX;AAAA,QAAC,cAAAC;AAAA,QAAA;AAAA,UACC,KAAK,KAAK,OAAO;AAAA,UACjB,KAAK,KAAK,OAAO;AAAA,UACjB,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,WAAU;AAAA;AAAA,MACZ;AAAA,MAEF,8CAAC,SACC;AAAA,qDAAC,OAAE,WAAU,eAAe,eAAK,OAAO,MAAK;AAAA,QAC7C,8CAAC,OAAE,WAAU,iCACV;AAAA,uCAAO,IAAI,KAAK,KAAK,IAAI,GAAG,cAAc;AAAA,UAAE;AAAA,UAAI,KAAK;AAAA,WACxD;AAAA,SACF;AAAA,OACF;AAAA,IACC,KAAK,SACJ,6CAAC,SAAI,WAAU,oDACb;AAAA,MAAC,cAAAA;AAAA,MAAA;AAAA,QACC,KAAK,KAAK;AAAA,QACV,KAAK,KAAK;AAAA,QACV,MAAI;AAAA,QACJ,WAAU;AAAA,QACV,UAAQ;AAAA;AAAA,IACV,GACF;AAAA,KAEJ;AAEJ;","names":["Image","Link","import_image","import_date_fns","import_jsx_runtime","Image"]}
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ // src/components/post-card.tsx
4
+ import Link from "next/link";
5
+ import Image from "next/image";
6
+ import { format } from "date-fns";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function PostCard({ post, basePath = "/blog", className }) {
9
+ return /* @__PURE__ */ jsxs("article", { className: `group relative flex flex-col space-y-2 ${className}`, children: [
10
+ post.image && /* @__PURE__ */ jsx("div", { className: "relative aspect-video overflow-hidden rounded-lg", children: /* @__PURE__ */ jsx(
11
+ Image,
12
+ {
13
+ src: post.image,
14
+ alt: post.title,
15
+ fill: true,
16
+ className: "object-cover transition-transform group-hover:scale-105"
17
+ }
18
+ ) }),
19
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: post.tags?.slice(0, 2).map((tag) => /* @__PURE__ */ jsx(
20
+ "span",
21
+ {
22
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
23
+ children: tag
24
+ },
25
+ tag
26
+ )) }),
27
+ /* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: /* @__PURE__ */ jsxs(Link, { href: `${basePath}/${post.slug}`, children: [
28
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0" }),
29
+ post.title
30
+ ] }) }),
31
+ /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-muted-foreground", children: post.description }),
32
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 text-sm text-muted-foreground", children: [
33
+ /* @__PURE__ */ jsx("span", { children: format(new Date(post.date), "MMM d, yyyy") }),
34
+ /* @__PURE__ */ jsx("span", { children: post.readingTime })
35
+ ] })
36
+ ] });
37
+ }
38
+
39
+ // src/components/post-header.tsx
40
+ import Image2 from "next/image";
41
+ import { format as format2 } from "date-fns";
42
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
43
+ function PostHeader({ post, className }) {
44
+ return /* @__PURE__ */ jsxs2("header", { className: `space-y-4 ${className}`, children: [
45
+ /* @__PURE__ */ jsx2("div", { className: "flex items-center gap-2", children: post.tags?.map((tag) => /* @__PURE__ */ jsx2(
46
+ "span",
47
+ {
48
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
49
+ children: tag
50
+ },
51
+ tag
52
+ )) }),
53
+ /* @__PURE__ */ jsx2("h1", { className: "text-4xl font-bold tracking-tight lg:text-5xl", children: post.title }),
54
+ /* @__PURE__ */ jsx2("p", { className: "text-xl text-muted-foreground", children: post.description }),
55
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-4", children: [
56
+ post.author.image && /* @__PURE__ */ jsx2(
57
+ Image2,
58
+ {
59
+ src: post.author.image,
60
+ alt: post.author.name,
61
+ width: 40,
62
+ height: 40,
63
+ className: "rounded-full"
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsxs2("div", { children: [
67
+ /* @__PURE__ */ jsx2("p", { className: "font-medium", children: post.author.name }),
68
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm text-muted-foreground", children: [
69
+ format2(new Date(post.date), "MMMM d, yyyy"),
70
+ " \xB7 ",
71
+ post.readingTime
72
+ ] })
73
+ ] })
74
+ ] }),
75
+ post.image && /* @__PURE__ */ jsx2("div", { className: "relative aspect-video overflow-hidden rounded-lg", children: /* @__PURE__ */ jsx2(
76
+ Image2,
77
+ {
78
+ src: post.image,
79
+ alt: post.title,
80
+ fill: true,
81
+ className: "object-cover",
82
+ priority: true
83
+ }
84
+ ) })
85
+ ] });
86
+ }
87
+ export {
88
+ PostCard,
89
+ PostHeader
90
+ };
91
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/post-card.tsx","../../src/components/post-header.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport type { BlogPostMeta } from \"../lib/blog\";\n\nexport interface PostCardProps {\n post: BlogPostMeta;\n basePath?: string;\n className?: string;\n}\n\nexport function PostCard({ post, basePath = \"/blog\", className }: PostCardProps) {\n return (\n <article className={`group relative flex flex-col space-y-2 ${className}`}>\n {post.image && (\n <div className=\"relative aspect-video overflow-hidden rounded-lg\">\n <Image\n src={post.image}\n alt={post.title}\n fill\n className=\"object-cover transition-transform group-hover:scale-105\"\n />\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n {post.tags?.slice(0, 2).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h2 className=\"text-2xl font-semibold tracking-tight\">\n <Link href={`${basePath}/${post.slug}`}>\n <span className=\"absolute inset-0\" />\n {post.title}\n </Link>\n </h2>\n <p className=\"line-clamp-2 text-muted-foreground\">{post.description}</p>\n <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n <span>{format(new Date(post.date), \"MMM d, yyyy\")}</span>\n <span>{post.readingTime}</span>\n </div>\n </article>\n );\n}\n","\"use client\";\n\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport type { BlogPost } from \"../lib/blog\";\n\nexport interface PostHeaderProps {\n post: BlogPost;\n className?: string;\n}\n\nexport function PostHeader({ post, className }: PostHeaderProps) {\n return (\n <header className={`space-y-4 ${className}`}>\n <div className=\"flex items-center gap-2\">\n {post.tags?.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h1 className=\"text-4xl font-bold tracking-tight lg:text-5xl\">\n {post.title}\n </h1>\n <p className=\"text-xl text-muted-foreground\">{post.description}</p>\n <div className=\"flex items-center gap-4\">\n {post.author.image && (\n <Image\n src={post.author.image}\n alt={post.author.name}\n width={40}\n height={40}\n className=\"rounded-full\"\n />\n )}\n <div>\n <p className=\"font-medium\">{post.author.name}</p>\n <p className=\"text-sm text-muted-foreground\">\n {format(new Date(post.date), \"MMMM d, yyyy\")} · {post.readingTime}\n </p>\n </div>\n </div>\n {post.image && (\n <div className=\"relative aspect-video overflow-hidden rounded-lg\">\n <Image\n src={post.image}\n alt={post.title}\n fill\n className=\"object-cover\"\n priority\n />\n </div>\n )}\n </header>\n );\n}\n"],"mappings":";;;AAEA,OAAO,UAAU;AACjB,OAAO,WAAW;AAClB,SAAS,cAAc;AAcb,cAmBF,YAnBE;AALH,SAAS,SAAS,EAAE,MAAM,WAAW,SAAS,UAAU,GAAkB;AAC/E,SACE,qBAAC,aAAQ,WAAW,0CAA0C,SAAS,IACpE;AAAA,SAAK,SACJ,oBAAC,SAAI,WAAU,oDACb;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,KAAK;AAAA,QACV,KAAK,KAAK;AAAA,QACV,MAAI;AAAA,QACJ,WAAU;AAAA;AAAA,IACZ,GACF;AAAA,IAEF,oBAAC,SAAI,WAAU,2BACZ,eAAK,MAAM,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,QAC3B;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QAET;AAAA;AAAA,MAHI;AAAA,IAIP,CACD,GACH;AAAA,IACA,oBAAC,QAAG,WAAU,yCACZ,+BAAC,QAAK,MAAM,GAAG,QAAQ,IAAI,KAAK,IAAI,IAClC;AAAA,0BAAC,UAAK,WAAU,oBAAmB;AAAA,MAClC,KAAK;AAAA,OACR,GACF;AAAA,IACA,oBAAC,OAAE,WAAU,sCAAsC,eAAK,aAAY;AAAA,IACpE,qBAAC,SAAI,WAAU,yDACb;AAAA,0BAAC,UAAM,iBAAO,IAAI,KAAK,KAAK,IAAI,GAAG,aAAa,GAAE;AAAA,MAClD,oBAAC,UAAM,eAAK,aAAY;AAAA,OAC1B;AAAA,KACF;AAEJ;;;AC/CA,OAAOA,YAAW;AAClB,SAAS,UAAAC,eAAc;AAab,gBAAAC,MAwBA,QAAAC,aAxBA;AALH,SAAS,WAAW,EAAE,MAAM,UAAU,GAAoB;AAC/D,SACE,gBAAAA,MAAC,YAAO,WAAW,aAAa,SAAS,IACvC;AAAA,oBAAAD,KAAC,SAAI,WAAU,2BACZ,eAAK,MAAM,IAAI,CAAC,QACf,gBAAAA;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QAET;AAAA;AAAA,MAHI;AAAA,IAIP,CACD,GACH;AAAA,IACA,gBAAAA,KAAC,QAAG,WAAU,iDACX,eAAK,OACR;AAAA,IACA,gBAAAA,KAAC,OAAE,WAAU,iCAAiC,eAAK,aAAY;AAAA,IAC/D,gBAAAC,MAAC,SAAI,WAAU,2BACZ;AAAA,WAAK,OAAO,SACX,gBAAAD;AAAA,QAACF;AAAA,QAAA;AAAA,UACC,KAAK,KAAK,OAAO;AAAA,UACjB,KAAK,KAAK,OAAO;AAAA,UACjB,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,WAAU;AAAA;AAAA,MACZ;AAAA,MAEF,gBAAAG,MAAC,SACC;AAAA,wBAAAD,KAAC,OAAE,WAAU,eAAe,eAAK,OAAO,MAAK;AAAA,QAC7C,gBAAAC,MAAC,OAAE,WAAU,iCACV;AAAA,UAAAF,QAAO,IAAI,KAAK,KAAK,IAAI,GAAG,cAAc;AAAA,UAAE;AAAA,UAAI,KAAK;AAAA,WACxD;AAAA,SACF;AAAA,OACF;AAAA,IACC,KAAK,SACJ,gBAAAC,KAAC,SAAI,WAAU,oDACb,0BAAAA;AAAA,MAACF;AAAA,MAAA;AAAA,QACC,KAAK,KAAK;AAAA,QACV,KAAK,KAAK;AAAA,QACV,MAAI;AAAA,QACJ,WAAU;AAAA,QACV,UAAQ;AAAA;AAAA,IACV,GACF;AAAA,KAEJ;AAEJ;","names":["Image","format","jsx","jsxs"]}
@@ -0,0 +1,2 @@
1
+ export { B as BlogConfig, a as BlogPost, b as BlogPostMeta, PostCard, PostCardProps, PostHeader, PostHeaderProps, c as configureBlog, g as getAllPostSlugs, d as getAllPosts, e as getAllTags, f as getPostBySlug, h as getPostsByTag } from './components/index.mjs';
2
+ import 'react/jsx-runtime';
@@ -0,0 +1,2 @@
1
+ export { B as BlogConfig, a as BlogPost, b as BlogPostMeta, PostCard, PostCardProps, PostHeader, PostHeaderProps, c as configureBlog, g as getAllPostSlugs, d as getAllPosts, e as getAllTags, f as getPostBySlug, h as getPostsByTag } from './components/index.js';
2
+ import 'react/jsx-runtime';
package/dist/index.js ADDED
@@ -0,0 +1,224 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var src_exports = {};
33
+ __export(src_exports, {
34
+ PostCard: () => PostCard,
35
+ PostHeader: () => PostHeader,
36
+ configureBlog: () => configureBlog,
37
+ getAllPostSlugs: () => getAllPostSlugs,
38
+ getAllPosts: () => getAllPosts,
39
+ getAllTags: () => getAllTags,
40
+ getPostBySlug: () => getPostBySlug,
41
+ getPostsByTag: () => getPostsByTag
42
+ });
43
+ module.exports = __toCommonJS(src_exports);
44
+
45
+ // src/lib/blog.ts
46
+ var import_fs = __toESM(require("fs"));
47
+ var import_path = __toESM(require("path"));
48
+ var import_gray_matter = __toESM(require("gray-matter"));
49
+ var import_reading_time = __toESM(require("reading-time"));
50
+ var config = {
51
+ contentDirectory: import_path.default.join(process.cwd(), "content/blog"),
52
+ defaultAuthor: { name: "Anonymous" }
53
+ };
54
+ function configureBlog(options) {
55
+ config = { ...config, ...options };
56
+ }
57
+ function getContentDirectory() {
58
+ return config.contentDirectory || import_path.default.join(process.cwd(), "content/blog");
59
+ }
60
+ function getAllPosts() {
61
+ const contentDirectory = getContentDirectory();
62
+ if (!import_fs.default.existsSync(contentDirectory)) {
63
+ return [];
64
+ }
65
+ const files = import_fs.default.readdirSync(contentDirectory);
66
+ const posts = files.filter((file) => file.endsWith(".mdx") || file.endsWith(".md")).map((file) => {
67
+ const slug = file.replace(/\.mdx?$/, "");
68
+ const fullPath = import_path.default.join(contentDirectory, file);
69
+ const fileContents = import_fs.default.readFileSync(fullPath, "utf8");
70
+ const { data, content } = (0, import_gray_matter.default)(fileContents);
71
+ return {
72
+ slug,
73
+ title: data.title || slug,
74
+ description: data.description || "",
75
+ date: data.date || (/* @__PURE__ */ new Date()).toISOString(),
76
+ author: data.author || config.defaultAuthor || { name: "Anonymous" },
77
+ image: data.image,
78
+ tags: data.tags || [],
79
+ readingTime: (0, import_reading_time.default)(content).text
80
+ };
81
+ }).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
82
+ return posts;
83
+ }
84
+ function getPostBySlug(slug) {
85
+ const contentDirectory = getContentDirectory();
86
+ let fullPath = import_path.default.join(contentDirectory, `${slug}.mdx`);
87
+ if (!import_fs.default.existsSync(fullPath)) {
88
+ fullPath = import_path.default.join(contentDirectory, `${slug}.md`);
89
+ }
90
+ if (!import_fs.default.existsSync(fullPath)) {
91
+ return null;
92
+ }
93
+ const fileContents = import_fs.default.readFileSync(fullPath, "utf8");
94
+ const { data, content } = (0, import_gray_matter.default)(fileContents);
95
+ return {
96
+ slug,
97
+ title: data.title || slug,
98
+ description: data.description || "",
99
+ date: data.date || (/* @__PURE__ */ new Date()).toISOString(),
100
+ author: data.author || config.defaultAuthor || { name: "Anonymous" },
101
+ image: data.image,
102
+ tags: data.tags || [],
103
+ content,
104
+ readingTime: (0, import_reading_time.default)(content).text
105
+ };
106
+ }
107
+ function getAllPostSlugs() {
108
+ const contentDirectory = getContentDirectory();
109
+ if (!import_fs.default.existsSync(contentDirectory)) {
110
+ return [];
111
+ }
112
+ const files = import_fs.default.readdirSync(contentDirectory);
113
+ return files.filter((file) => file.endsWith(".mdx") || file.endsWith(".md")).map((file) => file.replace(/\.mdx?$/, ""));
114
+ }
115
+ function getPostsByTag(tag) {
116
+ return getAllPosts().filter(
117
+ (post) => post.tags?.map((t) => t.toLowerCase()).includes(tag.toLowerCase())
118
+ );
119
+ }
120
+ function getAllTags() {
121
+ const posts = getAllPosts();
122
+ const tags = /* @__PURE__ */ new Set();
123
+ posts.forEach((post) => {
124
+ post.tags?.forEach((tag) => tags.add(tag));
125
+ });
126
+ return Array.from(tags);
127
+ }
128
+
129
+ // src/components/post-card.tsx
130
+ var import_link = __toESM(require("next/link"));
131
+ var import_image = __toESM(require("next/image"));
132
+ var import_date_fns = require("date-fns");
133
+ var import_jsx_runtime = require("react/jsx-runtime");
134
+ function PostCard({ post, basePath = "/blog", className }) {
135
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("article", { className: `group relative flex flex-col space-y-2 ${className}`, children: [
136
+ post.image && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative aspect-video overflow-hidden rounded-lg", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
137
+ import_image.default,
138
+ {
139
+ src: post.image,
140
+ alt: post.title,
141
+ fill: true,
142
+ className: "object-cover transition-transform group-hover:scale-105"
143
+ }
144
+ ) }),
145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex items-center gap-2", children: post.tags?.slice(0, 2).map((tag) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
146
+ "span",
147
+ {
148
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
149
+ children: tag
150
+ },
151
+ tag
152
+ )) }),
153
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-2xl font-semibold tracking-tight", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href: `${basePath}/${post.slug}`, children: [
154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute inset-0" }),
155
+ post.title
156
+ ] }) }),
157
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "line-clamp-2 text-muted-foreground", children: post.description }),
158
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-4 text-sm text-muted-foreground", children: [
159
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: (0, import_date_fns.format)(new Date(post.date), "MMM d, yyyy") }),
160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: post.readingTime })
161
+ ] })
162
+ ] });
163
+ }
164
+
165
+ // src/components/post-header.tsx
166
+ var import_image2 = __toESM(require("next/image"));
167
+ var import_date_fns2 = require("date-fns");
168
+ var import_jsx_runtime2 = require("react/jsx-runtime");
169
+ function PostHeader({ post, className }) {
170
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: `space-y-4 ${className}`, children: [
171
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex items-center gap-2", children: post.tags?.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
172
+ "span",
173
+ {
174
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
175
+ children: tag
176
+ },
177
+ tag
178
+ )) }),
179
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { className: "text-4xl font-bold tracking-tight lg:text-5xl", children: post.title }),
180
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-xl text-muted-foreground", children: post.description }),
181
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-4", children: [
182
+ post.author.image && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
183
+ import_image2.default,
184
+ {
185
+ src: post.author.image,
186
+ alt: post.author.name,
187
+ width: 40,
188
+ height: 40,
189
+ className: "rounded-full"
190
+ }
191
+ ),
192
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
193
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-medium", children: post.author.name }),
194
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "text-sm text-muted-foreground", children: [
195
+ (0, import_date_fns2.format)(new Date(post.date), "MMMM d, yyyy"),
196
+ " \xB7 ",
197
+ post.readingTime
198
+ ] })
199
+ ] })
200
+ ] }),
201
+ post.image && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "relative aspect-video overflow-hidden rounded-lg", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
202
+ import_image2.default,
203
+ {
204
+ src: post.image,
205
+ alt: post.title,
206
+ fill: true,
207
+ className: "object-cover",
208
+ priority: true
209
+ }
210
+ ) })
211
+ ] });
212
+ }
213
+ // Annotate the CommonJS export names for ESM import in node:
214
+ 0 && (module.exports = {
215
+ PostCard,
216
+ PostHeader,
217
+ configureBlog,
218
+ getAllPostSlugs,
219
+ getAllPosts,
220
+ getAllTags,
221
+ getPostBySlug,
222
+ getPostsByTag
223
+ });
224
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/lib/blog.ts","../src/components/post-card.tsx","../src/components/post-header.tsx"],"sourcesContent":["// Blog utilities\nexport {\n getAllPosts,\n getPostBySlug,\n getAllPostSlugs,\n getPostsByTag,\n getAllTags,\n configureBlog,\n} from \"./lib/blog\";\nexport type { BlogPost, BlogPostMeta, BlogConfig } from \"./lib/blog\";\n\n// Components\nexport { PostCard, PostHeader } from \"./components\";\nexport type { PostCardProps, PostHeaderProps } from \"./components\";\n","import fs from \"fs\";\nimport path from \"path\";\nimport matter from \"gray-matter\";\nimport readingTime from \"reading-time\";\n\nexport interface BlogPost {\n slug: string;\n title: string;\n description: string;\n date: string;\n author: {\n name: string;\n image?: string;\n };\n image?: string;\n tags?: string[];\n content: string;\n readingTime: string;\n}\n\nexport interface BlogPostMeta {\n slug: string;\n title: string;\n description: string;\n date: string;\n author: {\n name: string;\n image?: string;\n };\n image?: string;\n tags?: string[];\n readingTime: string;\n}\n\nexport interface BlogConfig {\n contentDirectory?: string;\n defaultAuthor?: {\n name: string;\n image?: string;\n };\n}\n\nlet config: BlogConfig = {\n contentDirectory: path.join(process.cwd(), \"content/blog\"),\n defaultAuthor: { name: \"Anonymous\" },\n};\n\nexport function configureBlog(options: BlogConfig) {\n config = { ...config, ...options };\n}\n\nfunction getContentDirectory(): string {\n return config.contentDirectory || path.join(process.cwd(), \"content/blog\");\n}\n\nexport function getAllPosts(): BlogPostMeta[] {\n const contentDirectory = getContentDirectory();\n\n if (!fs.existsSync(contentDirectory)) {\n return [];\n }\n\n const files = fs.readdirSync(contentDirectory);\n const posts = files\n .filter((file) => file.endsWith(\".mdx\") || file.endsWith(\".md\"))\n .map((file) => {\n const slug = file.replace(/\\.mdx?$/, \"\");\n const fullPath = path.join(contentDirectory, file);\n const fileContents = fs.readFileSync(fullPath, \"utf8\");\n const { data, content } = matter(fileContents);\n\n return {\n slug,\n title: data.title || slug,\n description: data.description || \"\",\n date: data.date || new Date().toISOString(),\n author: data.author || config.defaultAuthor || { name: \"Anonymous\" },\n image: data.image,\n tags: data.tags || [],\n readingTime: readingTime(content).text,\n };\n })\n .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());\n\n return posts;\n}\n\nexport function getPostBySlug(slug: string): BlogPost | null {\n const contentDirectory = getContentDirectory();\n\n // Try both .mdx and .md extensions\n let fullPath = path.join(contentDirectory, `${slug}.mdx`);\n if (!fs.existsSync(fullPath)) {\n fullPath = path.join(contentDirectory, `${slug}.md`);\n }\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n const fileContents = fs.readFileSync(fullPath, \"utf8\");\n const { data, content } = matter(fileContents);\n\n return {\n slug,\n title: data.title || slug,\n description: data.description || \"\",\n date: data.date || new Date().toISOString(),\n author: data.author || config.defaultAuthor || { name: \"Anonymous\" },\n image: data.image,\n tags: data.tags || [],\n content,\n readingTime: readingTime(content).text,\n };\n}\n\nexport function getAllPostSlugs(): string[] {\n const contentDirectory = getContentDirectory();\n\n if (!fs.existsSync(contentDirectory)) {\n return [];\n }\n\n const files = fs.readdirSync(contentDirectory);\n return files\n .filter((file) => file.endsWith(\".mdx\") || file.endsWith(\".md\"))\n .map((file) => file.replace(/\\.mdx?$/, \"\"));\n}\n\nexport function getPostsByTag(tag: string): BlogPostMeta[] {\n return getAllPosts().filter((post) =>\n post.tags?.map((t) => t.toLowerCase()).includes(tag.toLowerCase())\n );\n}\n\nexport function getAllTags(): string[] {\n const posts = getAllPosts();\n const tags = new Set<string>();\n posts.forEach((post) => {\n post.tags?.forEach((tag) => tags.add(tag));\n });\n return Array.from(tags);\n}\n","\"use client\";\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport type { BlogPostMeta } from \"../lib/blog\";\n\nexport interface PostCardProps {\n post: BlogPostMeta;\n basePath?: string;\n className?: string;\n}\n\nexport function PostCard({ post, basePath = \"/blog\", className }: PostCardProps) {\n return (\n <article className={`group relative flex flex-col space-y-2 ${className}`}>\n {post.image && (\n <div className=\"relative aspect-video overflow-hidden rounded-lg\">\n <Image\n src={post.image}\n alt={post.title}\n fill\n className=\"object-cover transition-transform group-hover:scale-105\"\n />\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n {post.tags?.slice(0, 2).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h2 className=\"text-2xl font-semibold tracking-tight\">\n <Link href={`${basePath}/${post.slug}`}>\n <span className=\"absolute inset-0\" />\n {post.title}\n </Link>\n </h2>\n <p className=\"line-clamp-2 text-muted-foreground\">{post.description}</p>\n <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n <span>{format(new Date(post.date), \"MMM d, yyyy\")}</span>\n <span>{post.readingTime}</span>\n </div>\n </article>\n );\n}\n","\"use client\";\n\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport type { BlogPost } from \"../lib/blog\";\n\nexport interface PostHeaderProps {\n post: BlogPost;\n className?: string;\n}\n\nexport function PostHeader({ post, className }: PostHeaderProps) {\n return (\n <header className={`space-y-4 ${className}`}>\n <div className=\"flex items-center gap-2\">\n {post.tags?.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h1 className=\"text-4xl font-bold tracking-tight lg:text-5xl\">\n {post.title}\n </h1>\n <p className=\"text-xl text-muted-foreground\">{post.description}</p>\n <div className=\"flex items-center gap-4\">\n {post.author.image && (\n <Image\n src={post.author.image}\n alt={post.author.name}\n width={40}\n height={40}\n className=\"rounded-full\"\n />\n )}\n <div>\n <p className=\"font-medium\">{post.author.name}</p>\n <p className=\"text-sm text-muted-foreground\">\n {format(new Date(post.date), \"MMMM d, yyyy\")} · {post.readingTime}\n </p>\n </div>\n </div>\n {post.image && (\n <div className=\"relative aspect-video overflow-hidden rounded-lg\">\n <Image\n src={post.image}\n alt={post.title}\n fill\n className=\"object-cover\"\n priority\n />\n </div>\n )}\n </header>\n );\n}\n"],"mappings":"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","names":["path","fs","matter","readingTime","Image","Link","import_image","import_date_fns","import_jsx_runtime","Image"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,181 @@
1
+ "use client";
2
+
3
+ // src/lib/blog.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import matter from "gray-matter";
7
+ import readingTime from "reading-time";
8
+ var config = {
9
+ contentDirectory: path.join(process.cwd(), "content/blog"),
10
+ defaultAuthor: { name: "Anonymous" }
11
+ };
12
+ function configureBlog(options) {
13
+ config = { ...config, ...options };
14
+ }
15
+ function getContentDirectory() {
16
+ return config.contentDirectory || path.join(process.cwd(), "content/blog");
17
+ }
18
+ function getAllPosts() {
19
+ const contentDirectory = getContentDirectory();
20
+ if (!fs.existsSync(contentDirectory)) {
21
+ return [];
22
+ }
23
+ const files = fs.readdirSync(contentDirectory);
24
+ const posts = files.filter((file) => file.endsWith(".mdx") || file.endsWith(".md")).map((file) => {
25
+ const slug = file.replace(/\.mdx?$/, "");
26
+ const fullPath = path.join(contentDirectory, file);
27
+ const fileContents = fs.readFileSync(fullPath, "utf8");
28
+ const { data, content } = matter(fileContents);
29
+ return {
30
+ slug,
31
+ title: data.title || slug,
32
+ description: data.description || "",
33
+ date: data.date || (/* @__PURE__ */ new Date()).toISOString(),
34
+ author: data.author || config.defaultAuthor || { name: "Anonymous" },
35
+ image: data.image,
36
+ tags: data.tags || [],
37
+ readingTime: readingTime(content).text
38
+ };
39
+ }).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
40
+ return posts;
41
+ }
42
+ function getPostBySlug(slug) {
43
+ const contentDirectory = getContentDirectory();
44
+ let fullPath = path.join(contentDirectory, `${slug}.mdx`);
45
+ if (!fs.existsSync(fullPath)) {
46
+ fullPath = path.join(contentDirectory, `${slug}.md`);
47
+ }
48
+ if (!fs.existsSync(fullPath)) {
49
+ return null;
50
+ }
51
+ const fileContents = fs.readFileSync(fullPath, "utf8");
52
+ const { data, content } = matter(fileContents);
53
+ return {
54
+ slug,
55
+ title: data.title || slug,
56
+ description: data.description || "",
57
+ date: data.date || (/* @__PURE__ */ new Date()).toISOString(),
58
+ author: data.author || config.defaultAuthor || { name: "Anonymous" },
59
+ image: data.image,
60
+ tags: data.tags || [],
61
+ content,
62
+ readingTime: readingTime(content).text
63
+ };
64
+ }
65
+ function getAllPostSlugs() {
66
+ const contentDirectory = getContentDirectory();
67
+ if (!fs.existsSync(contentDirectory)) {
68
+ return [];
69
+ }
70
+ const files = fs.readdirSync(contentDirectory);
71
+ return files.filter((file) => file.endsWith(".mdx") || file.endsWith(".md")).map((file) => file.replace(/\.mdx?$/, ""));
72
+ }
73
+ function getPostsByTag(tag) {
74
+ return getAllPosts().filter(
75
+ (post) => post.tags?.map((t) => t.toLowerCase()).includes(tag.toLowerCase())
76
+ );
77
+ }
78
+ function getAllTags() {
79
+ const posts = getAllPosts();
80
+ const tags = /* @__PURE__ */ new Set();
81
+ posts.forEach((post) => {
82
+ post.tags?.forEach((tag) => tags.add(tag));
83
+ });
84
+ return Array.from(tags);
85
+ }
86
+
87
+ // src/components/post-card.tsx
88
+ import Link from "next/link";
89
+ import Image from "next/image";
90
+ import { format } from "date-fns";
91
+ import { jsx, jsxs } from "react/jsx-runtime";
92
+ function PostCard({ post, basePath = "/blog", className }) {
93
+ return /* @__PURE__ */ jsxs("article", { className: `group relative flex flex-col space-y-2 ${className}`, children: [
94
+ post.image && /* @__PURE__ */ jsx("div", { className: "relative aspect-video overflow-hidden rounded-lg", children: /* @__PURE__ */ jsx(
95
+ Image,
96
+ {
97
+ src: post.image,
98
+ alt: post.title,
99
+ fill: true,
100
+ className: "object-cover transition-transform group-hover:scale-105"
101
+ }
102
+ ) }),
103
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: post.tags?.slice(0, 2).map((tag) => /* @__PURE__ */ jsx(
104
+ "span",
105
+ {
106
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
107
+ children: tag
108
+ },
109
+ tag
110
+ )) }),
111
+ /* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold tracking-tight", children: /* @__PURE__ */ jsxs(Link, { href: `${basePath}/${post.slug}`, children: [
112
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0" }),
113
+ post.title
114
+ ] }) }),
115
+ /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-muted-foreground", children: post.description }),
116
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4 text-sm text-muted-foreground", children: [
117
+ /* @__PURE__ */ jsx("span", { children: format(new Date(post.date), "MMM d, yyyy") }),
118
+ /* @__PURE__ */ jsx("span", { children: post.readingTime })
119
+ ] })
120
+ ] });
121
+ }
122
+
123
+ // src/components/post-header.tsx
124
+ import Image2 from "next/image";
125
+ import { format as format2 } from "date-fns";
126
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
127
+ function PostHeader({ post, className }) {
128
+ return /* @__PURE__ */ jsxs2("header", { className: `space-y-4 ${className}`, children: [
129
+ /* @__PURE__ */ jsx2("div", { className: "flex items-center gap-2", children: post.tags?.map((tag) => /* @__PURE__ */ jsx2(
130
+ "span",
131
+ {
132
+ className: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground",
133
+ children: tag
134
+ },
135
+ tag
136
+ )) }),
137
+ /* @__PURE__ */ jsx2("h1", { className: "text-4xl font-bold tracking-tight lg:text-5xl", children: post.title }),
138
+ /* @__PURE__ */ jsx2("p", { className: "text-xl text-muted-foreground", children: post.description }),
139
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-4", children: [
140
+ post.author.image && /* @__PURE__ */ jsx2(
141
+ Image2,
142
+ {
143
+ src: post.author.image,
144
+ alt: post.author.name,
145
+ width: 40,
146
+ height: 40,
147
+ className: "rounded-full"
148
+ }
149
+ ),
150
+ /* @__PURE__ */ jsxs2("div", { children: [
151
+ /* @__PURE__ */ jsx2("p", { className: "font-medium", children: post.author.name }),
152
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm text-muted-foreground", children: [
153
+ format2(new Date(post.date), "MMMM d, yyyy"),
154
+ " \xB7 ",
155
+ post.readingTime
156
+ ] })
157
+ ] })
158
+ ] }),
159
+ post.image && /* @__PURE__ */ jsx2("div", { className: "relative aspect-video overflow-hidden rounded-lg", children: /* @__PURE__ */ jsx2(
160
+ Image2,
161
+ {
162
+ src: post.image,
163
+ alt: post.title,
164
+ fill: true,
165
+ className: "object-cover",
166
+ priority: true
167
+ }
168
+ ) })
169
+ ] });
170
+ }
171
+ export {
172
+ PostCard,
173
+ PostHeader,
174
+ configureBlog,
175
+ getAllPostSlugs,
176
+ getAllPosts,
177
+ getAllTags,
178
+ getPostBySlug,
179
+ getPostsByTag
180
+ };
181
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/blog.ts","../src/components/post-card.tsx","../src/components/post-header.tsx"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport matter from \"gray-matter\";\nimport readingTime from \"reading-time\";\n\nexport interface BlogPost {\n slug: string;\n title: string;\n description: string;\n date: string;\n author: {\n name: string;\n image?: string;\n };\n image?: string;\n tags?: string[];\n content: string;\n readingTime: string;\n}\n\nexport interface BlogPostMeta {\n slug: string;\n title: string;\n description: string;\n date: string;\n author: {\n name: string;\n image?: string;\n };\n image?: string;\n tags?: string[];\n readingTime: string;\n}\n\nexport interface BlogConfig {\n contentDirectory?: string;\n defaultAuthor?: {\n name: string;\n image?: string;\n };\n}\n\nlet config: BlogConfig = {\n contentDirectory: path.join(process.cwd(), \"content/blog\"),\n defaultAuthor: { name: \"Anonymous\" },\n};\n\nexport function configureBlog(options: BlogConfig) {\n config = { ...config, ...options };\n}\n\nfunction getContentDirectory(): string {\n return config.contentDirectory || path.join(process.cwd(), \"content/blog\");\n}\n\nexport function getAllPosts(): BlogPostMeta[] {\n const contentDirectory = getContentDirectory();\n\n if (!fs.existsSync(contentDirectory)) {\n return [];\n }\n\n const files = fs.readdirSync(contentDirectory);\n const posts = files\n .filter((file) => file.endsWith(\".mdx\") || file.endsWith(\".md\"))\n .map((file) => {\n const slug = file.replace(/\\.mdx?$/, \"\");\n const fullPath = path.join(contentDirectory, file);\n const fileContents = fs.readFileSync(fullPath, \"utf8\");\n const { data, content } = matter(fileContents);\n\n return {\n slug,\n title: data.title || slug,\n description: data.description || \"\",\n date: data.date || new Date().toISOString(),\n author: data.author || config.defaultAuthor || { name: \"Anonymous\" },\n image: data.image,\n tags: data.tags || [],\n readingTime: readingTime(content).text,\n };\n })\n .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());\n\n return posts;\n}\n\nexport function getPostBySlug(slug: string): BlogPost | null {\n const contentDirectory = getContentDirectory();\n\n // Try both .mdx and .md extensions\n let fullPath = path.join(contentDirectory, `${slug}.mdx`);\n if (!fs.existsSync(fullPath)) {\n fullPath = path.join(contentDirectory, `${slug}.md`);\n }\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n const fileContents = fs.readFileSync(fullPath, \"utf8\");\n const { data, content } = matter(fileContents);\n\n return {\n slug,\n title: data.title || slug,\n description: data.description || \"\",\n date: data.date || new Date().toISOString(),\n author: data.author || config.defaultAuthor || { name: \"Anonymous\" },\n image: data.image,\n tags: data.tags || [],\n content,\n readingTime: readingTime(content).text,\n };\n}\n\nexport function getAllPostSlugs(): string[] {\n const contentDirectory = getContentDirectory();\n\n if (!fs.existsSync(contentDirectory)) {\n return [];\n }\n\n const files = fs.readdirSync(contentDirectory);\n return files\n .filter((file) => file.endsWith(\".mdx\") || file.endsWith(\".md\"))\n .map((file) => file.replace(/\\.mdx?$/, \"\"));\n}\n\nexport function getPostsByTag(tag: string): BlogPostMeta[] {\n return getAllPosts().filter((post) =>\n post.tags?.map((t) => t.toLowerCase()).includes(tag.toLowerCase())\n );\n}\n\nexport function getAllTags(): string[] {\n const posts = getAllPosts();\n const tags = new Set<string>();\n posts.forEach((post) => {\n post.tags?.forEach((tag) => tags.add(tag));\n });\n return Array.from(tags);\n}\n","\"use client\";\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport type { BlogPostMeta } from \"../lib/blog\";\n\nexport interface PostCardProps {\n post: BlogPostMeta;\n basePath?: string;\n className?: string;\n}\n\nexport function PostCard({ post, basePath = \"/blog\", className }: PostCardProps) {\n return (\n <article className={`group relative flex flex-col space-y-2 ${className}`}>\n {post.image && (\n <div className=\"relative aspect-video overflow-hidden rounded-lg\">\n <Image\n src={post.image}\n alt={post.title}\n fill\n className=\"object-cover transition-transform group-hover:scale-105\"\n />\n </div>\n )}\n <div className=\"flex items-center gap-2\">\n {post.tags?.slice(0, 2).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h2 className=\"text-2xl font-semibold tracking-tight\">\n <Link href={`${basePath}/${post.slug}`}>\n <span className=\"absolute inset-0\" />\n {post.title}\n </Link>\n </h2>\n <p className=\"line-clamp-2 text-muted-foreground\">{post.description}</p>\n <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n <span>{format(new Date(post.date), \"MMM d, yyyy\")}</span>\n <span>{post.readingTime}</span>\n </div>\n </article>\n );\n}\n","\"use client\";\n\nimport Image from \"next/image\";\nimport { format } from \"date-fns\";\nimport type { BlogPost } from \"../lib/blog\";\n\nexport interface PostHeaderProps {\n post: BlogPost;\n className?: string;\n}\n\nexport function PostHeader({ post, className }: PostHeaderProps) {\n return (\n <header className={`space-y-4 ${className}`}>\n <div className=\"flex items-center gap-2\">\n {post.tags?.map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n >\n {tag}\n </span>\n ))}\n </div>\n <h1 className=\"text-4xl font-bold tracking-tight lg:text-5xl\">\n {post.title}\n </h1>\n <p className=\"text-xl text-muted-foreground\">{post.description}</p>\n <div className=\"flex items-center gap-4\">\n {post.author.image && (\n <Image\n src={post.author.image}\n alt={post.author.name}\n width={40}\n height={40}\n className=\"rounded-full\"\n />\n )}\n <div>\n <p className=\"font-medium\">{post.author.name}</p>\n <p className=\"text-sm text-muted-foreground\">\n {format(new Date(post.date), \"MMMM d, yyyy\")} · {post.readingTime}\n </p>\n </div>\n </div>\n {post.image && (\n <div className=\"relative aspect-video overflow-hidden rounded-lg\">\n <Image\n src={post.image}\n alt={post.title}\n fill\n className=\"object-cover\"\n priority\n />\n </div>\n )}\n </header>\n );\n}\n"],"mappings":"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","names":["Image","format","jsx","jsxs"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@victusvinceere/saas-blog",
3
+ "version": "0.1.0",
4
+ "description": "MDX blog module for SaaS Kit",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./components": {
15
+ "types": "./dist/components/index.d.ts",
16
+ "import": "./dist/components/index.mjs",
17
+ "require": "./dist/components/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "prisma"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "typecheck": "tsc --noEmit",
28
+ "clean": "rm -rf dist"
29
+ },
30
+ "peerDependencies": {
31
+ "@victusvinceere/saas-core": "workspace:*",
32
+ "next": ">=15.0.0",
33
+ "react": ">=19.0.0"
34
+ },
35
+ "dependencies": {
36
+ "date-fns": "^4.1.0",
37
+ "gray-matter": "^4.0.3",
38
+ "next-mdx-remote": "^5.0.0",
39
+ "reading-time": "^1.5.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20",
43
+ "@types/react": "^19",
44
+ "tsup": "^8.0.2",
45
+ "typescript": "^5"
46
+ },
47
+ "keywords": [
48
+ "saas",
49
+ "blog",
50
+ "mdx",
51
+ "react"
52
+ ],
53
+ "license": "MIT"
54
+ }
@@ -0,0 +1,70 @@
1
+ // Blog Prisma Schema Extension
2
+ // Adds blog-related models for database-backed blog system
3
+
4
+ // ==================== BLOG MODELS ====================
5
+
6
+ model Post {
7
+ id String @id @default(cuid())
8
+ slug String @unique
9
+ title String
10
+ description String?
11
+ content String @db.Text
12
+ image String?
13
+ published Boolean @default(false)
14
+ publishedAt DateTime? @map("published_at")
15
+ createdAt DateTime @default(now()) @map("created_at")
16
+ updatedAt DateTime @updatedAt @map("updated_at")
17
+
18
+ // SEO fields
19
+ metaTitle String? @map("meta_title")
20
+ metaDescription String? @map("meta_description")
21
+
22
+ // Relations
23
+ categories PostCategory[]
24
+ tags PostTag[]
25
+
26
+ @@map("posts")
27
+ }
28
+
29
+ model Category {
30
+ id String @id @default(cuid())
31
+ name String @unique
32
+ slug String @unique
33
+ description String?
34
+
35
+ posts PostCategory[]
36
+
37
+ @@map("categories")
38
+ }
39
+
40
+ model Tag {
41
+ id String @id @default(cuid())
42
+ name String @unique
43
+ slug String @unique
44
+
45
+ posts PostTag[]
46
+
47
+ @@map("tags")
48
+ }
49
+
50
+ model PostCategory {
51
+ postId String @map("post_id")
52
+ categoryId String @map("category_id")
53
+
54
+ post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
55
+ category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
56
+
57
+ @@id([postId, categoryId])
58
+ @@map("post_categories")
59
+ }
60
+
61
+ model PostTag {
62
+ postId String @map("post_id")
63
+ tagId String @map("tag_id")
64
+
65
+ post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
66
+ tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
67
+
68
+ @@id([postId, tagId])
69
+ @@map("post_tags")
70
+ }