supermemory/apps/web/components/document-cards/notion-preview.tsx

219 lines
6 KiB
TypeScript

"use client"
import { useMemo } from "react"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { NotionDoc, SyncLogoIcon } from "@ui/assets/icons"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
// Extract structured blocks from markdown content for a visual preview
function parseContentBlocks(content: string) {
const lines = content.split("\n").filter((l) => l.trim())
const blocks: {
type: "heading" | "text" | "bullet" | "todo" | "divider"
text: string
checked?: boolean
}[] = []
for (const line of lines) {
if (blocks.length >= 6) break
const trimmed = line.trim()
if (trimmed.startsWith("# ")) {
blocks.push({ type: "heading", text: trimmed.replace(/^#+\s*/, "") })
} else if (trimmed.startsWith("- [x]") || trimmed.startsWith("- [ ]")) {
blocks.push({
type: "todo",
text: trimmed.replace(/^- \[.\]\s*/, ""),
checked: trimmed.includes("[x]"),
})
} else if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
blocks.push({ type: "bullet", text: trimmed.replace(/^[-*]\s*/, "") })
} else if (trimmed === "---") {
blocks.push({ type: "divider", text: "" })
} else if (trimmed.length > 0) {
blocks.push({
type: "text",
text: trimmed.replace(/\*\*/g, "").replace(/\*/g, "").replace(/`/g, ""),
})
}
}
return blocks
}
export function NotionPreview({
document,
}: {
document: DocumentWithMemories
}) {
const blocks = useMemo(
() => parseContentBlocks(document.content || document.summary || ""),
[document.content, document.summary],
)
return (
<div className="bg-[#0B1017] rounded-[18px] overflow-hidden">
{/* Header with Notion branding */}
<div className="px-3 pt-3 pb-2 flex items-center justify-between">
<div className="flex items-center gap-1.5">
<NotionDoc className="size-[14px]" />
<span
className={cn(
dmSansClassName(),
"text-[11px] tracking-wide text-[#929292] uppercase",
)}
>
Notion
</span>
</div>
{/* Decorative dots mimicking Notion's block handles */}
<div className="flex gap-[3px]">
<div className="size-[3px] rounded-full bg-[#333]" />
<div className="size-[3px] rounded-full bg-[#333]" />
<div className="size-[3px] rounded-full bg-[#333]" />
</div>
</div>
{/* Content area — structured block preview */}
<div className="px-3 pb-3 space-y-[5px]">
{document.title && (
<p
className={cn(
dmSansClassName(),
"text-[13px] font-semibold text-[#E5E5E5] line-clamp-2 leading-[140%]",
)}
>
{document.title}
</p>
)}
{blocks.length > 0 ? (
<div className="space-y-[3px]">
{blocks.map((block, i) => {
if (block.type === "divider") {
return <div key={i} className="h-px bg-[#1F2937] my-1" />
}
if (block.type === "heading") {
return (
<p
key={i}
className={cn(
dmSansClassName(),
"text-[11px] font-semibold text-[#B0B0B0] line-clamp-1",
)}
>
{block.text}
</p>
)
}
if (block.type === "todo") {
return (
<div key={i} className="flex items-start gap-1.5">
<div
className={cn(
"mt-[2px] size-[10px] rounded-[2px] border shrink-0",
block.checked
? "bg-[#2F81F7] border-[#2F81F7]"
: "border-[#3B3B3B] bg-transparent",
)}
>
{block.checked && (
<svg
viewBox="0 0 10 10"
className="size-full text-white"
aria-label="Checked"
role="img"
>
<path
d="M2.5 5L4.5 7L7.5 3.5"
stroke="currentColor"
strokeWidth="1.2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
<p
className={cn(
"text-[11px] line-clamp-1 leading-[140%]",
block.checked
? "text-[#555] line-through"
: "text-[#737373]",
)}
>
{block.text}
</p>
</div>
)
}
if (block.type === "bullet") {
return (
<div key={i} className="flex items-start gap-1.5">
<div className="mt-[5px] size-[4px] rounded-full bg-[#555] shrink-0" />
<p className="text-[11px] text-[#737373] line-clamp-1 leading-[140%]">
{block.text}
</p>
</div>
)
}
return (
<p
key={i}
className="text-[11px] text-[#737373] line-clamp-1 leading-[140%]"
>
{block.text}
</p>
)
})}
</div>
) : document.summary ? (
<p className="text-[11px] text-[#737373] line-clamp-4">
{document.summary}
</p>
) : null}
</div>
{/* Footer */}
{(document.memoryEntries.length > 0 || document.createdAt) && (
<div className="px-3 pb-2.5 flex items-center justify-between">
{document.memoryEntries.length > 0 && (
<p
className={cn(
dmSansClassName(),
"text-[11px] font-semibold flex items-center gap-1",
)}
style={{
background:
"linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}}
>
<SyncLogoIcon className="w-[12.33px] h-[10px]" />
{document.memoryEntries.length}
</p>
)}
<p className={cn(dmSansClassName(), "text-[11px] text-[#737373]")}>
{new Date(document.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})}
</p>
</div>
)}
</div>
)
}