feat: add markdown rendering support and enhance file preview component

This commit is contained in:
2025-05-14 06:02:22 +02:00
parent 036f23b777
commit 60925ee6ac
9 changed files with 373 additions and 16 deletions
+1 -1
View File
@@ -105,7 +105,7 @@ export default function FileGrid({ session }: FileGridProps) {
key={file.id}
className="flex place-content-end max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
>
{<div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={file.extension} /></div>}
{<div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={file.extension} share={false} /></div>}
<button onClick={() => router.push(pageUrl + file.url)}>
<h3 className="text-2xl font-bold">{file.name}</h3>
+82 -12
View File
@@ -1,16 +1,22 @@
"use client";
import { useEffect, useState } from "react";
import { getFileType } from "~/utils/fileType"; // Adjust the import path as necessary
import { remark } from 'remark';
import html from 'remark-html';
import matter from 'gray-matter';
import "github-markdown-css/github-markdown.css";
import "../styles/custom.css"; // Adjust the path as necessary
import { MarkdownRenderer } from "../../components/MarkdownRenderer";
interface FilePreviewProps {
fileId: string;
fileType: string; // Pass the file type as a prop
}
export function FilePreview({ fileId, fileType }: FilePreviewProps) {
export function FilePreview({ fileId, fileType, share }: FilePreviewProps & { share: boolean }) {
const [mediaSrc, setMediaSrc] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [markdownContent, setMarkdownContent] = useState<string | null>(null);
console.log("File Type:", fileType);
@@ -47,20 +53,53 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
};
}, [fileId]);
useEffect(() => {
if (fileType.startsWith("markdown")) {
const fetchMarkdown = async () => {
try {
const result = await renderMarkdown({ id: fileId });
setMarkdownContent(result.props.postData.contentHtml);
} catch (err) {
console.error("Failed to fetch markdown content:", err);
}
};
fetchMarkdown();
}
}, [fileId, fileType]);
if (error) {
return <div className="text-red-500">{error}</div>;
}
if (!mediaSrc) {
if (!mediaSrc && !markdownContent) {
return <div>Loading...</div>;
}
if (fileType.startsWith("markdown")) {
if (share) {
return (
<div className="overflow-y-auto max-h-96 rounded-lg shadow-md">
{markdownContent ? (
<MarkdownRenderer markdownContent={markdownContent} />
) : (
<div>Loading markdown...</div>
)}
</div>
);
}
return (
<img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" />
);
}
if (fileType.startsWith("video")) {
return (
<video
controls
className="max-w-full max-h-96 rounded-lg shadow-md"
src={mediaSrc}
src={mediaSrc || ""}
>
Your browser does not support the video tag.
</video>
@@ -71,14 +110,14 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
<audio
controls
className="max-w-full max-h-96 rounded-lg shadow-md"
src={mediaSrc}
src={mediaSrc || ""}
>
Your browser does not support the audio tag.
</audio>
);
}
if (fileType.startsWith("image")) {
return <img src={mediaSrc} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />;
return <img src={mediaSrc || ""} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />;
}
if (fileType.startsWith("text")) {
@@ -91,17 +130,48 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
<img src="/icons/files/archive.svg" alt="Archive file preview" className="max-w-full max-h-96 rounded-lg invert" />
);
}
if (fileType.startsWith("code") || fileType.startsWith("markdown")) {
if (fileType.startsWith("code")) {
return (
<img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" />
);
}
// if (fileType.startsWith("markdown")) {
// return;
// }
// log file type
console.log("Unsupported file type:", fileType);
return;
return null;
}
export async function rendererMarkdown(id: string) {
const fileContents = await fetch(`/api/files/serv?id=${encodeURIComponent(id)}`)
.then((res) => res.text())
.catch((err) => {
console.error("Failed to fetch file contents:", err);
return null;
});
if (!fileContents) {
throw new Error("File contents could not be fetched.");
}
const matterResult = matter(fileContents);
const processedContent = await remark()
.use(html)
.process(matterResult.content);
const contentHtml = processedContent.toString();
return {
id,
contentHtml,
...matterResult.data,
};
}
export async function renderMarkdown({ id }: { id: string }) {
const postData = await rendererMarkdown(id);
return {
props: {
postData,
},
};
}
+2 -1
View File
@@ -5,6 +5,7 @@ import crypto from "crypto";
import { db } from "~/server/db";
import { auth } from "~/server/auth";
import { minioClient, ensureBucketExists } from "~/utils/minioClient";
import { getFileType } from "~/utils/fileType";
export const config = {
api: {
@@ -52,7 +53,7 @@ export async function POST(req: Request) {
url: `/share?id=${fileId}`,
name: fileName,
size: fileBuffer.length,
extension: info.mimeType,
extension: getFileType(fileName),
uploadedById: session.user.id,
},
});
+1 -1
View File
@@ -91,7 +91,7 @@ export default function SearchFile() {
className="flex place-content-end w-xxs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
>
<div className="self-center max-w-100 sm:max-w-50">
<FilePreview fileId={file.id} fileType={file.extension} />
<FilePreview fileId={file.id} fileType={file.extension} share={false} />
</div>
<button onClick={() => router.push(pageUrl + file.url)}>
+1 -1
View File
@@ -129,7 +129,7 @@ export default async function FilePreviewContainer({
</h1>
<div className="mt-6">
{fileDetails.type !== "unknown" && (
<FilePreview fileId={fileDetails.id} fileType={fileDetails.type} />
<FilePreview fileId={fileDetails.id} fileType={fileDetails.type} share={true} />
)}
</div>
<div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md">
+9
View File
@@ -0,0 +1,9 @@
.markdown-body ul,
.markdown-body ol {
list-style: initial; /* Ensures bullets or numbers are displayed */
margin-left: 1.5em; /* Adds proper indentation */
}
.markdown-body li {
margin-bottom: 0.5em; /* Adds spacing between list items */
}
+78
View File
@@ -0,0 +1,78 @@
import { useEffect } from "react";
import "github-markdown-css/github-markdown.css";
interface MarkdownRendererProps {
markdownContent: string;
}
export function MarkdownRenderer({ markdownContent }: MarkdownRendererProps) {
useEffect(() => {
if (markdownContent) {
const markdownContainer = document.querySelector("#markdown-preview");
if (!markdownContainer) return;
const codeBlocks = markdownContainer.querySelectorAll("code");
codeBlocks.forEach((block) => {
// Check if the block is already wrapped
if (block.parentElement?.classList.contains("code-wrapper")) return;
// Check if the code block is multiline
const isMultiline = block.textContent?.includes("\n");
if (!isMultiline) return;
// Create a wrapper only if it doesn't already exist
const wrapper = document.createElement("div");
wrapper.className = "code-wrapper"; // Add a class to identify the wrapper
wrapper.style.display = "flex";
wrapper.style.alignItems = "flex-start";
wrapper.style.justifyContent = "space-between";
wrapper.style.gap = "8px";
wrapper.style.width = "100%";
wrapper.style.position = "relative";
const codeContainer = document.createElement("div");
codeContainer.style.flex = "1";
codeContainer.appendChild(block.cloneNode(true));
const button = document.createElement("button");
button.innerHTML = `
<img src="/icons/copy.svg" alt="Copy" class="h-4 w-4" style="filter: invert(1) sepia(1) saturate(5) hue-rotate(180deg);"/>
`;
button.className =
"copy-button inline-flex items-center justify-center bg-gray-200 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition";
button.style.marginLeft = "8px";
button.style.width = "1.5rem";
button.style.height = "1.5rem";
button.addEventListener("click", () => {
navigator.clipboard.writeText(block.textContent || "").then(() => {
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
`;
setTimeout(() => {
button.innerHTML = `
<img src="/icons/copy.svg" alt="Copy" class="h-4 w-4" style="filter: invert(1) sepia(1) saturate(5) hue-rotate(180deg);"/>
`;
}, 2000);
});
});
wrapper.appendChild(codeContainer);
wrapper.appendChild(button);
// Replace the original block with the wrapper
block.replaceWith(wrapper);
});
}
}, [markdownContent]);
return (
<div className="markdown-body max-w-full p-4 pt-0 pb-0" id="markdown-preview">
<div dangerouslySetInnerHTML={{ __html: markdownContent }} />
</div>
);
}