From 89ac6173bef2aaba8f2b1b6c184d94e5c212a55e Mon Sep 17 00:00:00 2001 From: Musa Date: Wed, 28 Jan 2026 16:19:23 -0800 Subject: [PATCH] add code block and markdown support for blogs --- apps/www/package.json | 8 + apps/www/sanity.config.ts | 5 +- apps/www/schemaTypes/blogType.ts | 39 + apps/www/src/components/PortableText.tsx | 263 ++- package-lock.json | 1955 ++++++++++++++++++++++ 5 files changed, 2267 insertions(+), 3 deletions(-) diff --git a/apps/www/package.json b/apps/www/package.json index 37d70211..849ac112 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -21,9 +21,12 @@ "@portabletext/react": "^5.0.0", "@portabletext/types": "^3.0.0", "@sanity/client": "^7.13.0", + "@sanity/code-input": "^6.0.4", "@sanity/image-url": "^1.2.0", + "@sanity/table": "^2.0.1", "@vercel/analytics": "^1.5.0", "csv-parse": "^6.1.0", + "easymde": "^2.20.0", "framer-motion": "^12.23.24", "jsdom": "^27.2.0", "next": "^16.1.6", @@ -31,8 +34,12 @@ "papaparse": "^5.5.3", "react": "19.2.0", "react-dom": "19.2.0", + "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^16.1.0", + "remark-gfm": "^4.0.1", "resend": "^6.6.0", "sanity": "^4.18.0", + "sanity-plugin-markdown": "^7.0.4", "styled-components": "^6.1.19" }, "devDependencies": { @@ -44,6 +51,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5" diff --git a/apps/www/sanity.config.ts b/apps/www/sanity.config.ts index 4db703f5..812e1ee7 100644 --- a/apps/www/sanity.config.ts +++ b/apps/www/sanity.config.ts @@ -1,4 +1,7 @@ import { defineConfig } from "sanity"; +import { codeInput } from "@sanity/code-input"; +import { table } from "@sanity/table"; +import { markdownSchema } from "sanity-plugin-markdown"; import { structureTool } from "sanity/structure"; import { schemaTypes } from "./schemaTypes"; @@ -11,7 +14,7 @@ export default defineConfig({ basePath: "/studio", - plugins: [structureTool()], + plugins: [structureTool(), codeInput(), table(), markdownSchema()], schema: { types: schemaTypes, diff --git a/apps/www/schemaTypes/blogType.ts b/apps/www/schemaTypes/blogType.ts index 90a33870..286ed843 100644 --- a/apps/www/schemaTypes/blogType.ts +++ b/apps/www/schemaTypes/blogType.ts @@ -35,6 +35,45 @@ export const blogType = defineType({ { type: "block", }, + { + type: "code", + options: { + language: "typescript", + languageAlternatives: [ + { title: "TypeScript", value: "typescript" }, + { title: "JavaScript", value: "javascript" }, + { title: "HTML", value: "html" }, + { title: "CSS", value: "css" }, + { title: "Bash", value: "sh" }, + { title: "Python", value: "python" }, + { title: "Markdown", value: "markdown" }, + { title: "YAML", value: "yaml" }, + { title: "JSON", value: "json" }, + { title: "XML", value: "xml" }, + { title: "SQL", value: "sql" }, + { title: "Shell", value: "shell" }, + { title: "PowerShell", value: "powershell" }, + { title: "Batch", value: "batch" }, + ], + withFilename: true, + }, + }, + { + type: "object", + name: "markdownBlock", + title: "Markdown", + fields: [ + { + name: "markdown", + title: "Markdown", + type: "markdown", + description: "Markdown content with preview and image uploads", + }, + ], + }, + { + type: "table", + }, { type: "image", fields: [ diff --git a/apps/www/src/components/PortableText.tsx b/apps/www/src/components/PortableText.tsx index f45d1500..166c5d03 100644 --- a/apps/www/src/components/PortableText.tsx +++ b/apps/www/src/components/PortableText.tsx @@ -1,14 +1,273 @@ +"use client"; + import { PortableText as SanityPortableText } from "@portabletext/react"; import Image from "next/image"; import { urlFor } from "@/lib/sanity"; import type { PortableTextBlock } from "@portabletext/types"; +import { useState } from "react"; +import ReactMarkdown from "react-markdown"; +import type { Components } from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism"; interface PortableTextProps { content: PortableTextBlock[]; } +const codeTheme: any = oneLight; + +function CodeBlock({ + code, + language, + filename, + highlightedLines, +}: { + code: string; + language?: string; + filename?: string; + highlightedLines: Set; +}) { + const [copied, setCopied] = useState(false); + const displayLanguage = language || "text"; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(code); + setCopied(true); + window.setTimeout(() => setCopied(false), 1200); + } catch { + setCopied(false); + } + }; + + return ( +
+
+ {(filename || language) && ( +
+ {filename || "Code"} +
+ + {displayLanguage} + + +
+
+ )} + + highlightedLines.has(lineNumber) + ? { + style: { + backgroundColor: "rgba(251, 191, 36, 0.2)", + }, + } + : { style: {} } + } + wrapLines + codeTagProps={{ style: { fontFamily: "inherit" } }} + > + {code} + +
+
+ ); +} + +const markdownComponents: Components = { + h1: (props) => ( +

+ ), + h2: (props) => ( +

+ ), + h3: (props) => ( +

+ ), + p: (props) => ( +

+ ), + ul: (props) => ( +