@opinly/next 0.1.2 → 0.1.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/dist/index.cjs +24 -29
- package/dist/index.d.cts +17 -0
- package/dist/index.js +21 -26
- package/package.json +8 -2
- package/.turbo/turbo-lint.log +0 -14
- package/CHANGELOG.md +0 -21
- package/src/components/author-page/_helpers/json-ld.ts +0 -0
- package/src/components/author-page/index.tsx +0 -62
- package/src/components/authors-page/index.tsx +0 -90
- package/src/components/blog-post/_components/content.tsx +0 -141
- package/src/components/blog-post/_components/faq.tsx +0 -18
- package/src/components/blog-post/_components/share.tsx +0 -48
- package/src/components/blog-post/_components/table-of-contents.tsx +0 -63
- package/src/components/blog-post/_helpers/calc-reading-time.ts +0 -0
- package/src/components/blog-post/_helpers/extract-headings.ts +0 -25
- package/src/components/blog-post/_helpers/json-ld.ts +0 -176
- package/src/components/blog-post/_helpers/reading-time.ts +0 -28
- package/src/components/blog-post/_types/blog-post.ts +0 -6
- package/src/components/blog-post/_types/class-names.ts +0 -10
- package/src/components/blog-post/index.tsx +0 -126
- package/src/components/bread-crumbs.tsx +0 -48
- package/src/components/folder-page/index.tsx +0 -50
- package/src/components/home-page.tsx +0 -65
- package/src/components/not-found.tsx +0 -36
- package/src/components/opinly-blog.tsx +0 -37
- package/src/components/post-preview.tsx +0 -73
- package/src/index.ts +0 -2
- package/src/sdk-config.ts +0 -37
- package/src/utils/dates.ts +0 -6
- package/src/utils/generate-metadata.ts +0 -54
- package/src/utils/images.ts +0 -7
- package/src/utils/merge.ts +0 -6
- package/sst-env.d.ts +0 -9
- package/tsconfig.json +0 -19
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Facebook, Linkedin, Mail, Twitter } from "lucide-react";
|
|
2
|
-
import Link from "next/link";
|
|
3
|
-
import { opinlyConfig } from "../../../sdk-config";
|
|
4
|
-
import { BlogPostRes } from "../_types/blog-post";
|
|
5
|
-
|
|
6
|
-
export const Share = ({ post }: { post: BlogPostRes }) => {
|
|
7
|
-
const postUrl = encodeURIComponent(`${opinlyConfig.blogUrl}/${post.slug}`);
|
|
8
|
-
return (
|
|
9
|
-
<div className="flex gap-2 mb-6">
|
|
10
|
-
<Link
|
|
11
|
-
href={`https://linkedin.com/sharing/share-offsite?url=${postUrl}`}
|
|
12
|
-
target="_blank"
|
|
13
|
-
rel="noopener noreferrer"
|
|
14
|
-
className="border p-1 rounded flex items-center justify-center w-8 h-8"
|
|
15
|
-
>
|
|
16
|
-
<span className="sr-only">Share on LinkedIn</span>
|
|
17
|
-
<Linkedin className="w-4 h-4" />
|
|
18
|
-
</Link>
|
|
19
|
-
<Link
|
|
20
|
-
href={`https://twitter.com/intent/tweet?url=${postUrl}&text=${encodeURIComponent(
|
|
21
|
-
`Check out this article by ${post.author?.name}`
|
|
22
|
-
)}`}
|
|
23
|
-
target="_blank"
|
|
24
|
-
rel="noopener noreferrer"
|
|
25
|
-
className="border p-1 rounded flex items-center justify-center w-8 h-8"
|
|
26
|
-
>
|
|
27
|
-
<span className="sr-only">Share on Twitter</span>
|
|
28
|
-
<Twitter />
|
|
29
|
-
</Link>
|
|
30
|
-
<Link
|
|
31
|
-
href={`https://facebook.com/sharer/sharer.php?u=${postUrl}`}
|
|
32
|
-
target="_blank"
|
|
33
|
-
rel="noopener noreferrer"
|
|
34
|
-
className="border p-1 rounded flex items-center justify-center w-8 h-8"
|
|
35
|
-
>
|
|
36
|
-
<span className="sr-only">Share on Facebook</span>
|
|
37
|
-
<Facebook />
|
|
38
|
-
</Link>
|
|
39
|
-
<Link
|
|
40
|
-
href={`mailto:?body=${encodeURIComponent(`${post.title}\n${postUrl}`)}`}
|
|
41
|
-
className="border p-1 rounded flex items-center justify-center w-8 h-8"
|
|
42
|
-
>
|
|
43
|
-
<span className="sr-only">Share via Email</span>
|
|
44
|
-
<Mail className="w-4 h-4" />
|
|
45
|
-
</Link>
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { JSONContent } from "@tiptap/core";
|
|
2
|
-
import { BlogPostRes } from "../_types/blog-post";
|
|
3
|
-
import { cn } from "../../../utils/merge";
|
|
4
|
-
import Image from "next/image";
|
|
5
|
-
import { opinlyConfig } from "../../../sdk-config";
|
|
6
|
-
import { replaceImageNameSpace } from "../../../utils/images";
|
|
7
|
-
import { extractHeadings } from "../_helpers/extract-headings";
|
|
8
|
-
import { Share } from "./share";
|
|
9
|
-
import Link from "next/link";
|
|
10
|
-
|
|
11
|
-
type TableOfContentsProps = {
|
|
12
|
-
content: JSONContent;
|
|
13
|
-
author: BlogPostRes["author"] | null;
|
|
14
|
-
post: BlogPostRes;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const TableOfContents = ({
|
|
18
|
-
content,
|
|
19
|
-
author,
|
|
20
|
-
post,
|
|
21
|
-
}: TableOfContentsProps) => {
|
|
22
|
-
return (
|
|
23
|
-
<aside className={cn("w-full lg:w-1/3 lg:sticky lg:top-20 lg:self-start")}>
|
|
24
|
-
{author && (
|
|
25
|
-
<div className="w-full mb-6 border p-4 rounded-md text-sm">
|
|
26
|
-
<div className="flex items-center gap-3 mb-3">
|
|
27
|
-
{author.fileKey && (
|
|
28
|
-
<Image
|
|
29
|
-
src={`${opinlyConfig.imagesPrefix}/${replaceImageNameSpace(author.fileKey)}`}
|
|
30
|
-
alt={author.name}
|
|
31
|
-
width={48}
|
|
32
|
-
height={48}
|
|
33
|
-
className="rounded-full"
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
|
-
<div>
|
|
37
|
-
<Link
|
|
38
|
-
href={`${opinlyConfig.blogPrefix}/authors/${author.slug}`}
|
|
39
|
-
className="text-xs uppercase text-muted-foreground font-medium"
|
|
40
|
-
>
|
|
41
|
-
{author.name}
|
|
42
|
-
</Link>
|
|
43
|
-
<div className="text-muted-foreground text-sm">{author.bio}</div>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
)}
|
|
48
|
-
<Share post={post} />
|
|
49
|
-
<div className="border-l-4 border-opinlyAccent pl-4 mb-4">
|
|
50
|
-
<h2 className="text-lg font-semibold mb-2">TABLE OF CONTENTS</h2>
|
|
51
|
-
<ul className="text-sm text-gray-700 space-y-1">
|
|
52
|
-
{(extractHeadings(content) || []).map((heading) => (
|
|
53
|
-
<li key={heading.slug}>
|
|
54
|
-
<a href={`#${heading.slug}`} className="hover:underline">
|
|
55
|
-
{heading.text}
|
|
56
|
-
</a>
|
|
57
|
-
</li>
|
|
58
|
-
))}
|
|
59
|
-
</ul>
|
|
60
|
-
</div>
|
|
61
|
-
</aside>
|
|
62
|
-
);
|
|
63
|
-
};
|
|
File without changes
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { JSONContent } from "@tiptap/core";
|
|
2
|
-
|
|
3
|
-
export function extractHeadings(
|
|
4
|
-
content: JSONContent
|
|
5
|
-
): { text: string; slug: string }[] {
|
|
6
|
-
const headings: { text: string; slug: string }[] = [];
|
|
7
|
-
if (!content?.content) return headings;
|
|
8
|
-
|
|
9
|
-
const walk = (nodes: any[]) => {
|
|
10
|
-
for (const node of nodes) {
|
|
11
|
-
if (node.type === "heading" && node.content?.[0]?.text) {
|
|
12
|
-
const text = node.content[0].text;
|
|
13
|
-
const slug = text
|
|
14
|
-
.toLowerCase()
|
|
15
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
16
|
-
.replace(/^-+|-+$/g, "");
|
|
17
|
-
headings.push({ text, slug });
|
|
18
|
-
}
|
|
19
|
-
if (node.content) walk(node.content);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
walk(content.content);
|
|
24
|
-
return headings;
|
|
25
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BlogPosting,
|
|
3
|
-
BreadcrumbList,
|
|
4
|
-
FAQPage,
|
|
5
|
-
WithContext,
|
|
6
|
-
ListItem,
|
|
7
|
-
} from "schema-dts";
|
|
8
|
-
import { BlogPostRes } from "../_types/blog-post";
|
|
9
|
-
import { opinlyConfig } from "../../../sdk-config";
|
|
10
|
-
import { JSONContent } from "@tiptap/core";
|
|
11
|
-
import { replaceImageNameSpace } from "../../../utils/images";
|
|
12
|
-
|
|
13
|
-
// Add this function somewhere in your BlogPost component file
|
|
14
|
-
export function generateArticleBodyHtml(content: JSONContent): string {
|
|
15
|
-
if (!content?.content) return "";
|
|
16
|
-
|
|
17
|
-
let htmlString = "";
|
|
18
|
-
|
|
19
|
-
content.content.forEach((node) => {
|
|
20
|
-
switch (node.type) {
|
|
21
|
-
case "paragraph":
|
|
22
|
-
htmlString += `<p>${generateChildrenHtml(node.content)}</p>`;
|
|
23
|
-
break;
|
|
24
|
-
case "heading":
|
|
25
|
-
const headingLevel = node.attrs?.level || 1;
|
|
26
|
-
htmlString += `<h${headingLevel}>${generateChildrenHtml(node.content)}</h${headingLevel}>`;
|
|
27
|
-
break;
|
|
28
|
-
case "bulletList":
|
|
29
|
-
htmlString += `<ul>${generateChildrenHtml(node.content)}</ul>`;
|
|
30
|
-
break;
|
|
31
|
-
case "orderedList":
|
|
32
|
-
htmlString += `<ol>${generateChildrenHtml(node.content)}</ol>`;
|
|
33
|
-
break;
|
|
34
|
-
case "listItem":
|
|
35
|
-
htmlString += `<li>${generateChildrenHtml(node.content)}</li>`;
|
|
36
|
-
break;
|
|
37
|
-
// Add other node types if they contain text content relevant for articleBody
|
|
38
|
-
// For images, you might just skip them or include an <img> tag if semantic
|
|
39
|
-
// For example:
|
|
40
|
-
// case "image":
|
|
41
|
-
// if (node.attrs?.src) {
|
|
42
|
-
// htmlString += `<img src="${replaceImageHostname(node.attrs.src, opinlyConfig.imagesPrefix)}" alt="${node.attrs?.alt || ''}" />`;
|
|
43
|
-
// }
|
|
44
|
-
// break;
|
|
45
|
-
default:
|
|
46
|
-
// Handle other block types or just ignore them for articleBody
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return htmlString;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function generateChildrenHtml(
|
|
55
|
-
nodes: JSONContent["content"] | undefined
|
|
56
|
-
): string {
|
|
57
|
-
if (!nodes) return "";
|
|
58
|
-
|
|
59
|
-
let childrenHtml = "";
|
|
60
|
-
nodes.forEach((child) => {
|
|
61
|
-
if (child.type === "text") {
|
|
62
|
-
let text = child.text || "";
|
|
63
|
-
if (child.marks) {
|
|
64
|
-
child.marks.forEach((mark) => {
|
|
65
|
-
if (mark.type === "bold") text = `<strong>${text}</strong>`;
|
|
66
|
-
if (mark.type === "italic") text = `<em>${text}</em>`;
|
|
67
|
-
if (mark.type === "link" && mark.attrs?.href) {
|
|
68
|
-
text = `<a href="${mark.attrs.href}">${text}</a>`;
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
childrenHtml += text;
|
|
73
|
-
}
|
|
74
|
-
// If you have nested content within child nodes, you might need a recursive call here
|
|
75
|
-
// For example, if a list item can contain paragraphs
|
|
76
|
-
// if (child.content) {
|
|
77
|
-
// childrenHtml += generateChildrenHtml(child.content);
|
|
78
|
-
// }
|
|
79
|
-
});
|
|
80
|
-
return childrenHtml;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export const genFaqJSONLD = (
|
|
84
|
-
faqs: NonNullable<BlogPostRes["faqs"]>
|
|
85
|
-
): WithContext<FAQPage> => {
|
|
86
|
-
return {
|
|
87
|
-
"@context": "https://schema.org",
|
|
88
|
-
"@type": "FAQPage",
|
|
89
|
-
mainEntity: faqs.map((faq) => ({
|
|
90
|
-
"@type": "Question",
|
|
91
|
-
name: faq.question,
|
|
92
|
-
acceptedAnswer: {
|
|
93
|
-
"@type": "Answer",
|
|
94
|
-
text: faq.answer,
|
|
95
|
-
},
|
|
96
|
-
})),
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export const genBlogPostingJSONLD = (
|
|
101
|
-
post: BlogPostRes
|
|
102
|
-
): WithContext<BlogPosting> => {
|
|
103
|
-
return {
|
|
104
|
-
"@context": "https://schema.org",
|
|
105
|
-
"@type": "BlogPosting",
|
|
106
|
-
headline: post.title,
|
|
107
|
-
description: post.description || undefined,
|
|
108
|
-
url: `${opinlyConfig.blogUrl}/${post.folder ? `${post.folder.slug + "/"}${post.slug}` : post.slug}`,
|
|
109
|
-
datePublished: post.firstPublishedAt || undefined,
|
|
110
|
-
dateModified: post.modifiedAt || undefined,
|
|
111
|
-
image: post.titleFile?.fileKey
|
|
112
|
-
? {
|
|
113
|
-
"@type": "ImageObject",
|
|
114
|
-
url: `${opinlyConfig.imagesUrl}/${replaceImageNameSpace(post.titleFile.fileKey)}`,
|
|
115
|
-
}
|
|
116
|
-
: undefined,
|
|
117
|
-
author: post.author
|
|
118
|
-
? {
|
|
119
|
-
"@type": "Person",
|
|
120
|
-
name: post.author.name,
|
|
121
|
-
url: `${opinlyConfig.blogUrl}/author/${post.author.slug}`,
|
|
122
|
-
description: post.author.bio || undefined,
|
|
123
|
-
}
|
|
124
|
-
: undefined,
|
|
125
|
-
isAccessibleForFree: true,
|
|
126
|
-
articleBody: generateArticleBodyHtml(post.content as JSONContent),
|
|
127
|
-
};
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
export const genBreadcrumbJSONLD = (
|
|
131
|
-
post: BlogPostRes
|
|
132
|
-
): WithContext<BreadcrumbList> => {
|
|
133
|
-
const itemListElement = [
|
|
134
|
-
{
|
|
135
|
-
"@type": "ListItem",
|
|
136
|
-
position: 1,
|
|
137
|
-
name: "Company Name",
|
|
138
|
-
item: opinlyConfig.siteUrl,
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
"@type": "ListItem",
|
|
142
|
-
position: 2,
|
|
143
|
-
name: post.folder?.name || "Blog",
|
|
144
|
-
item: `${opinlyConfig.blogUrl}/blog`,
|
|
145
|
-
},
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
if (post.folder) {
|
|
149
|
-
itemListElement.push({
|
|
150
|
-
"@type": "ListItem",
|
|
151
|
-
position: 3,
|
|
152
|
-
name: post.folder.name,
|
|
153
|
-
item: `${opinlyConfig.blogUrl}/blog/${post.folder.slug}`,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
itemListElement.push({
|
|
157
|
-
"@type": "ListItem",
|
|
158
|
-
position: 4,
|
|
159
|
-
name: post.title,
|
|
160
|
-
item: `${opinlyConfig.blogUrl}/blog/${post.folder.slug}/${post.slug}`,
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
itemListElement.push({
|
|
164
|
-
"@type": "ListItem",
|
|
165
|
-
position: 3,
|
|
166
|
-
name: post.title,
|
|
167
|
-
item: `${opinlyConfig.blogUrl}/blog/${post.slug}`,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
"@context": "https://schema.org",
|
|
173
|
-
"@type": "BreadcrumbList",
|
|
174
|
-
itemListElement: itemListElement as ListItem[],
|
|
175
|
-
};
|
|
176
|
-
};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { JSONContent } from "@tiptap/core";
|
|
2
|
-
|
|
3
|
-
export const countWords = (content: JSONContent | undefined): number => {
|
|
4
|
-
if (!content?.content) return 0;
|
|
5
|
-
|
|
6
|
-
let wordCount = 0;
|
|
7
|
-
|
|
8
|
-
const countWords = (nodes: any[]) => {
|
|
9
|
-
for (const node of nodes) {
|
|
10
|
-
if (node.type === "text" && node.text) {
|
|
11
|
-
const words = node.text.trim().split(/\s+/).filter(Boolean);
|
|
12
|
-
wordCount += words.length;
|
|
13
|
-
}
|
|
14
|
-
if (node.content) countWords(node.content);
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
countWords(content.content);
|
|
19
|
-
|
|
20
|
-
return wordCount;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export function calculateReadingTime(content: JSONContent): number {
|
|
24
|
-
const wordCount = countWords(content.content);
|
|
25
|
-
|
|
26
|
-
const readingTimeMinutes = Math.ceil(wordCount / 200);
|
|
27
|
-
return readingTimeMinutes;
|
|
28
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { JSONContent } from "@tiptap/core";
|
|
3
|
-
import Link from "next/link";
|
|
4
|
-
import { cn } from "../../utils/merge";
|
|
5
|
-
import { opinlyConfig } from "../../sdk-config";
|
|
6
|
-
import { BlogPostRes } from "./_types/blog-post";
|
|
7
|
-
import {
|
|
8
|
-
genBlogPostingJSONLD,
|
|
9
|
-
genBreadcrumbJSONLD,
|
|
10
|
-
genFaqJSONLD,
|
|
11
|
-
} from "./_helpers/json-ld";
|
|
12
|
-
import { BlogComponentClassNames } from "./_types/class-names";
|
|
13
|
-
import { FAQS } from "./_components/faq";
|
|
14
|
-
import { calculateReadingTime } from "./_helpers/reading-time";
|
|
15
|
-
import { Content } from "./_components/content";
|
|
16
|
-
import { TableOfContents } from "./_components/table-of-contents";
|
|
17
|
-
import { BreadCrumbs } from "../bread-crumbs";
|
|
18
|
-
import { formatDate } from "../../utils/dates";
|
|
19
|
-
import Head from "next/head";
|
|
20
|
-
import Script from "next/script";
|
|
21
|
-
|
|
22
|
-
export const BlogPost = ({
|
|
23
|
-
post,
|
|
24
|
-
classNames = {},
|
|
25
|
-
}: {
|
|
26
|
-
post: BlogPostRes;
|
|
27
|
-
classNames?: BlogComponentClassNames;
|
|
28
|
-
notFoundComponent?: React.ReactNode;
|
|
29
|
-
}) => {
|
|
30
|
-
const content = post.content as JSONContent;
|
|
31
|
-
|
|
32
|
-
console.log("content", content);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<>
|
|
36
|
-
{/* <Head>
|
|
37
|
-
<> */}
|
|
38
|
-
{post.faqs && (
|
|
39
|
-
<Script
|
|
40
|
-
strategy="beforeInteractive"
|
|
41
|
-
type="application/ld+json"
|
|
42
|
-
dangerouslySetInnerHTML={{
|
|
43
|
-
__html: JSON.stringify(genFaqJSONLD(post.faqs)).replace(
|
|
44
|
-
/</g,
|
|
45
|
-
"\\u003c"
|
|
46
|
-
),
|
|
47
|
-
}}
|
|
48
|
-
/>
|
|
49
|
-
)}
|
|
50
|
-
<Script
|
|
51
|
-
strategy="beforeInteractive"
|
|
52
|
-
type="application/ld+json"
|
|
53
|
-
dangerouslySetInnerHTML={{
|
|
54
|
-
__html: JSON.stringify(genBlogPostingJSONLD(post)),
|
|
55
|
-
}}
|
|
56
|
-
/>
|
|
57
|
-
<Script
|
|
58
|
-
strategy="beforeInteractive"
|
|
59
|
-
type="application/ld+json"
|
|
60
|
-
dangerouslySetInnerHTML={{
|
|
61
|
-
__html: JSON.stringify(genBreadcrumbJSONLD(post)),
|
|
62
|
-
}}
|
|
63
|
-
/>
|
|
64
|
-
{/* </>
|
|
65
|
-
</Head> */}
|
|
66
|
-
|
|
67
|
-
<div className="max-w-7xl mx-auto px-4 py-24 flex items-start space-x-8">
|
|
68
|
-
<div className="flex flex-col gap-12 w-full">
|
|
69
|
-
<div className="bg-orange-600">
|
|
70
|
-
<BreadCrumbs
|
|
71
|
-
breadcrumbs={[
|
|
72
|
-
post.folder && {
|
|
73
|
-
url: `${opinlyConfig.blogPrefix}/${post.folder.slug}`,
|
|
74
|
-
name: post.folder.name,
|
|
75
|
-
},
|
|
76
|
-
]}
|
|
77
|
-
/>
|
|
78
|
-
|
|
79
|
-
<h1 className="text-4xl font-bold mb-2 leading-tight">
|
|
80
|
-
{post.title}
|
|
81
|
-
</h1>
|
|
82
|
-
<div className="text-sm mb-4">
|
|
83
|
-
Author:{" "}
|
|
84
|
-
<Link
|
|
85
|
-
href={`${opinlyConfig.blogPrefix}/authors/${post.author?.slug}`}
|
|
86
|
-
className="font-medium"
|
|
87
|
-
>
|
|
88
|
-
{post.author?.name}
|
|
89
|
-
</Link>{" "}
|
|
90
|
-
· {calculateReadingTime(content)} min read ·{" "}
|
|
91
|
-
{formatDate(post.firstPublishedAt as string) || "Today"}
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
<div
|
|
96
|
-
className={cn(
|
|
97
|
-
"flex flex-col-reverse md:flex-row mx-auto mt-10 gap-12 relative w-full ",
|
|
98
|
-
classNames.container
|
|
99
|
-
)}
|
|
100
|
-
>
|
|
101
|
-
<article className="w-full lg:w-2/3 order-2 lg:order-1 flex items-start space-x-4">
|
|
102
|
-
<div className="prose w-full">
|
|
103
|
-
<Content content={content} classNames={classNames} />
|
|
104
|
-
<FAQS faqs={post.faqs} />
|
|
105
|
-
</div>
|
|
106
|
-
</article>
|
|
107
|
-
<div className="block md:min-w-72 md:sticky md:top-20 md:self-start">
|
|
108
|
-
<TableOfContents
|
|
109
|
-
content={content}
|
|
110
|
-
author={post.author}
|
|
111
|
-
post={post}
|
|
112
|
-
/>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
{/* <div className="hidden md:block ">
|
|
117
|
-
<TableOfContents
|
|
118
|
-
content={content}
|
|
119
|
-
author={post.post.author}
|
|
120
|
-
post={post}
|
|
121
|
-
/>
|
|
122
|
-
</div> */}
|
|
123
|
-
</div>
|
|
124
|
-
</>
|
|
125
|
-
);
|
|
126
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { Fragment } from "react";
|
|
3
|
-
import { opinlyConfig } from "../sdk-config";
|
|
4
|
-
|
|
5
|
-
export const BreadCrumbs = ({
|
|
6
|
-
breadcrumbs,
|
|
7
|
-
}: {
|
|
8
|
-
breadcrumbs?: (
|
|
9
|
-
| {
|
|
10
|
-
name: string;
|
|
11
|
-
url?: string;
|
|
12
|
-
}
|
|
13
|
-
| null
|
|
14
|
-
| undefined
|
|
15
|
-
)[];
|
|
16
|
-
}) => {
|
|
17
|
-
return (
|
|
18
|
-
<nav className="text-sm text-orange-100 text-muted-foreground mb-4">
|
|
19
|
-
<ol className="list-reset flex space-x-2">
|
|
20
|
-
<Link href="/" className="font-bold">
|
|
21
|
-
{opinlyConfig.siteName}
|
|
22
|
-
</Link>
|
|
23
|
-
<span>/</span>
|
|
24
|
-
<Link href={opinlyConfig.blogPrefix} className="font-bold">
|
|
25
|
-
Blog
|
|
26
|
-
</Link>
|
|
27
|
-
{breadcrumbs?.map((breadcrumb, index) => {
|
|
28
|
-
if (!breadcrumb) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<Fragment key={index}>
|
|
34
|
-
<span>/</span>
|
|
35
|
-
{breadcrumb.url ? (
|
|
36
|
-
<Link href={breadcrumb.url} className="font-bold">
|
|
37
|
-
{breadcrumb.name}
|
|
38
|
-
</Link>
|
|
39
|
-
) : (
|
|
40
|
-
<span className="font-bold">{breadcrumb.name}</span>
|
|
41
|
-
)}
|
|
42
|
-
</Fragment>
|
|
43
|
-
);
|
|
44
|
-
})}
|
|
45
|
-
</ol>
|
|
46
|
-
</nav>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Opinly } from "@opinly/backend";
|
|
2
|
-
import { PostPreview } from "../post-preview";
|
|
3
|
-
import { BreadCrumbs } from "../bread-crumbs";
|
|
4
|
-
|
|
5
|
-
export const FolderPage = ({
|
|
6
|
-
folder,
|
|
7
|
-
}: {
|
|
8
|
-
folder: Extract<
|
|
9
|
-
Awaited<ReturnType<Opinly["content"]["blog"]>>,
|
|
10
|
-
{ type: "folder" }
|
|
11
|
-
>["data"];
|
|
12
|
-
}) => {
|
|
13
|
-
console.log("folder", JSON.stringify(folder, null, 2));
|
|
14
|
-
return (
|
|
15
|
-
<div className="max-w-6xl mx-auto px-4 py-10">
|
|
16
|
-
<BreadCrumbs />
|
|
17
|
-
|
|
18
|
-
<h1 className="text-4xl font-bold mb-2">{folder.name}</h1>
|
|
19
|
-
<p className="text-muted-foreground max-w-2xl mb-8">
|
|
20
|
-
{folder.description || `Explore articles about ${folder.name}.`}
|
|
21
|
-
</p>
|
|
22
|
-
|
|
23
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
24
|
-
{folder.posts.length === 0 && (
|
|
25
|
-
<div className="text-muted-foreground">No posts added yet</div>
|
|
26
|
-
)}
|
|
27
|
-
{folder.posts.map((post) => {
|
|
28
|
-
console.log("post.image", post.image);
|
|
29
|
-
return <PostPreview key={post.slug} post={post} />;
|
|
30
|
-
})}
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function formatDate(dateStr: string) {
|
|
37
|
-
return new Date(dateStr).toLocaleDateString("en-GB", {
|
|
38
|
-
day: "2-digit",
|
|
39
|
-
month: "short",
|
|
40
|
-
year: "numeric",
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function formatMinutes(description: string | null) {
|
|
45
|
-
// Fake estimate based on length of description
|
|
46
|
-
if (!description) return "5 min read";
|
|
47
|
-
const words = description.split(" ").length;
|
|
48
|
-
const mins = Math.ceil(words / 200);
|
|
49
|
-
return `${mins} min read`;
|
|
50
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
|
|
3
|
-
import { Opinly } from "@opinly/backend";
|
|
4
|
-
import { PostPreview } from "./post-preview";
|
|
5
|
-
|
|
6
|
-
type HomePage = Extract<
|
|
7
|
-
Awaited<ReturnType<Opinly["content"]["blog"]>>,
|
|
8
|
-
{ type: "home" }
|
|
9
|
-
>["data"];
|
|
10
|
-
|
|
11
|
-
export const HomePage = ({
|
|
12
|
-
homePage,
|
|
13
|
-
page,
|
|
14
|
-
}: {
|
|
15
|
-
homePage: HomePage;
|
|
16
|
-
page: number | null | undefined;
|
|
17
|
-
}) => {
|
|
18
|
-
const { posts, folders, count } = homePage;
|
|
19
|
-
|
|
20
|
-
console.log("posts", JSON.stringify(posts, null, 2));
|
|
21
|
-
|
|
22
|
-
const currentPage = page;
|
|
23
|
-
const totalPages = Math.ceil(count / 10);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="max-w-7xl mx-auto px-4 py-10">
|
|
27
|
-
{/* Posts Grid */}
|
|
28
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-10">
|
|
29
|
-
{posts.map((post) => (
|
|
30
|
-
<PostPreview key={post.slug} post={post} />
|
|
31
|
-
))}
|
|
32
|
-
</div>
|
|
33
|
-
|
|
34
|
-
{/* Pagination */}
|
|
35
|
-
<div className="flex justify-center items-center mt-10 gap-2">
|
|
36
|
-
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
|
|
37
|
-
<Link
|
|
38
|
-
key={p}
|
|
39
|
-
href={`?page=${p}`}
|
|
40
|
-
className={`px-3 py-1 border rounded ${
|
|
41
|
-
p === currentPage
|
|
42
|
-
? "bg-black text-white"
|
|
43
|
-
: "hover:bg-gray-100 text-gray-700"
|
|
44
|
-
}`}
|
|
45
|
-
>
|
|
46
|
-
{p}
|
|
47
|
-
</Link>
|
|
48
|
-
))}
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
{/* Folder tags */}
|
|
52
|
-
<div className="flex flex-wrap gap-2 mb-10">
|
|
53
|
-
{folders.map((folder) => (
|
|
54
|
-
<Link
|
|
55
|
-
key={folder.slug}
|
|
56
|
-
href={`/blog/${folder.slug}`}
|
|
57
|
-
className="px-4 py-1 bg-gray-100 text-sm rounded-full hover:bg-gray-200"
|
|
58
|
-
>
|
|
59
|
-
{folder.title}
|
|
60
|
-
</Link>
|
|
61
|
-
))}
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
};
|