Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // app/(site)/blog/[id]/page.tsx
- import Image from "next/image";
- import { notFound } from "next/navigation";
- import { Calendar, Clock } from "lucide-react";
- import { getBlogbySlug } from "@/server/blogs";
- import { getReadTime } from "@/lib/utils";
- import "@/components/tiptap-templates/simple/simple-editor.scss";
- import "@/components/tiptap-templates/simple/keyframe-animations.scss";
- import "@/components/tiptap-templates/simple/variables.scss";
- import BlogPageContent from "./_components/content";
- export const instant = false;
- async function BlogPage(props: PageProps<"/blog/[id]">) {
- const { id } = await props.params;
- const blogData = await getBlogbySlug(id);
- if (!blogData || blogData.status !== "published") {
- notFound();
- }
- return (
- <div className="min-h-screen">
- <div className="mx-auto max-w-5xl px-4 py-12 sm:px-6 lg:px-8 lg:py-20">
- <article className="space-y-10">
- {/* Meta */}
- <div className="flex flex-wrap items-center gap-3 text-sm text-stone-500">
- <div className="flex items-center gap-2 rounded-full border border-stone-200 bg-stone-50 px-3 py-1.5">
- <Calendar className="h-4 w-4" />
- <span>
- {blogData.createdAt.toLocaleDateString("en-US", {
- year: "numeric",
- month: "long",
- day: "numeric",
- })}
- </span>
- </div>
- <div className="flex items-center gap-2 rounded-full border border-stone-200 bg-stone-50 px-3 py-1.5">
- <Clock className="h-4 w-4" />
- <span>{getReadTime(blogData.content)}</span>
- </div>
- </div>
- {/* Header */}
- <div className="space-y-6">
- <h1 className="text-3xl font-semibold tracking-tight text-stone-900 sm:text-4xl lg:text-5xl">
- {blogData.title}
- </h1>
- <p className="max-w-3xl text-lg leading-8 text-stone-600">
- {blogData.excerpt}
- </p>
- <div className="flex flex-wrap gap-2">
- {blogData.tags.map((tag) => (
- <span
- key={tag}
- className="rounded-full border border-stone-200 bg-stone-50 px-3 py-1 text-sm text-stone-600"
- >
- {tag}
- </span>
- ))}
- </div>
- </div>
- {/* Cover */}
- <div className="relative aspect-video overflow-hidden rounded-2xl bg-stone-100">
- <Image
- src={blogData.image}
- alt={blogData.title}
- fill
- sizes="(max-width: 768px) 100vw, 75vw"
- className="object-cover"
- />
- </div>
- <BlogPageContent value={blogData.content} />
- </article>
- </div>
- </div>
- );
- }
- export default BlogPage;
- // app/(site)/blog/[id]/_components/content.tsx
- "use client";
- import { EditorContent, useEditor } from "@tiptap/react";
- // --- Tiptap Core Extensions ---
- import { StarterKit } from "@tiptap/starter-kit";
- import { Image } from "@tiptap/extension-image";
- import { TaskItem, TaskList } from "@tiptap/extension-list";
- import { TextAlign } from "@tiptap/extension-text-align";
- import { Typography } from "@tiptap/extension-typography";
- import { Highlight } from "@tiptap/extension-highlight";
- import { Subscript } from "@tiptap/extension-subscript";
- import { Superscript } from "@tiptap/extension-superscript";
- import { Selection } from "@tiptap/extensions";
- // --- Tiptap Node ---
- import { HorizontalRule } from "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension";
- import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
- import "@/components/tiptap-node/blockquote-node/blockquote-node.scss";
- import "@/components/tiptap-node/code-block-node/code-block-node.scss";
- import "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss";
- import "@/components/tiptap-node/list-node/list-node.scss";
- import "@/components/tiptap-node/image-node/image-node.scss";
- import "@/components/tiptap-node/heading-node/heading-node.scss";
- import "@/components/tiptap-node/paragraph-node/paragraph-node.scss";
- import "highlight.js/styles/xcode.css";
- // --- Lib ---
- import { EditorContentType } from "@/lib/tiptap-utils";
- // --- Styles ---
- import "@/components/tiptap-templates/simple/simple-editor.scss";
- import { lowlight } from "@/components/tiptap-node/code-block-lowlight/code-block-lowlight-extension";
- import { LoaderCircleIcon } from "lucide-react";
- function BlogPageContent({ value }: { value: EditorContentType }) {
- const editor = useEditor({
- immediatelyRender: false,
- editorProps: {
- attributes: {
- autocomplete: "off",
- autocorrect: "off",
- autocapitalize: "off",
- "aria-label": "Main content area, start typing to enter text.",
- class: "simple-editor",
- },
- },
- extensions: [
- StarterKit.configure({
- horizontalRule: false,
- link: {
- openOnClick: false,
- enableClickSelection: true,
- },
- codeBlock: false,
- }),
- CodeBlockLowlight.configure({
- lowlight: lowlight,
- }),
- HorizontalRule,
- TextAlign.configure({ types: ["heading", "paragraph"] }),
- TaskList,
- TaskItem.configure({ nested: true }),
- Highlight.configure({ multicolor: true }),
- Image,
- Typography,
- Superscript,
- Subscript,
- Selection,
- ],
- content: value,
- editable: false,
- });
- if (!editor) {
- return (
- <div className="simple-editor-content flex min-h-50 items-center justify-center rounded-md border">
- <LoaderCircleIcon className="h-6 w-6 animate-spin text-gray-500" />
- </div>
- );
- }
- return (
- <EditorContent
- editor={editor}
- role="presentation"
- className="simple-editor-content"
- id="simple-editor-content"
- />
- );
- }
- export default BlogPageContent;
Advertisement
Add Comment
Please, Sign In to add comment