Files
2026-04-26 21:41:28 +02:00

195 lines
7.6 KiB
JavaScript

import db from '../../lib/db';
import Editor from "@monaco-editor/react";
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github-dark.css';
import { useState, useEffect } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { SITE_CONFIG } from '../../config';
export async function getServerSideProps({ params }) {
const paste = db.prepare('SELECT id, content, language, filename, created_at, expires_at, expiry_type, view_count, allow_discussions FROM pastes WHERE id = ?').get(params.id);
if (!paste) return { notFound: true };
if (paste.expires_at && new Date(paste.expires_at) < new Date()) {
db.prepare('DELETE FROM pastes WHERE id = ?').run(params.id);
return { notFound: true };
}
let showBurnWarning = false;
if (paste.expiry_type === 'burn') {
if (paste.view_count === 0) {
db.prepare('UPDATE pastes SET view_count = 1 WHERE id = ?').run(params.id);
} else {
showBurnWarning = true;
db.prepare('DELETE FROM pastes WHERE id = ?').run(params.id);
}
}
const comments = db.prepare('SELECT * FROM comments WHERE paste_id = ? ORDER BY created_at DESC').all(params.id);
return {
props: {
paste: JSON.parse(JSON.stringify(paste)),
initialComments: JSON.parse(JSON.stringify(comments)),
showBurnWarning
}
};
}
export default function ViewPaste({ paste, initialComments, showBurnWarning }) {
const [author, setAuthor] = useState('');
const [comment, setComment] = useState('');
const [comments, setComments] = useState(initialComments);
const [copied, setCopied] = useState(false);
const [mounted, setMounted] = useState(false);
const [canDelete, setCanDelete] = useState(false);
const router = useRouter();
useEffect(() => {
setMounted(true);
const token = localStorage.getItem(`delete_token_${paste.id}`);
if (token) setCanDelete(true);
}, [paste.id]);
const handleDelete = async () => {
if (!confirm("Delete this paste permanently?")) return;
const token = localStorage.getItem(`delete_token_${paste.id}`);
const res = await fetch(`/api/pastes?id=${paste.id}&token=${token}`, { method: 'DELETE' });
if (res.ok) {
localStorage.removeItem(`delete_token_${paste.id}`);
router.push('/');
}
};
const copyToClipboard = () => {
const url = window.location.href;
navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const addComment = async (e) => {
e.preventDefault();
await fetch('/api/pastes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'comment', pasteId: paste.id, author, commentContent: comment })
});
setComments([{ author, content: comment, created_at: new Date().toISOString() }, ...comments]);
setAuthor('');
setComment('');
};
if (!mounted) return <div className="min-h-screen bg-zinc-950" />;
return (
<div className="min-h-screen bg-zinc-950 text-zinc-200 p-8 font-sans" suppressHydrationWarning>
<Head>
<title>{`${paste.filename} // ${SITE_CONFIG.title}`}</title>
</Head>
<div className="max-w-6xl mx-auto space-y-6">
{showBurnWarning && (
<div className="bg-orange-900/20 border border-orange-800 text-orange-400 p-4 rounded-lg flex items-center gap-3 animate-in fade-in slide-in-from-top-2 duration-500">
<span className="text-xl">🔥</span>
<div>
<p className="font-bold">Final Viewing</p>
<p className="text-sm opacity-80">This paste has been deleted from the server.</p>
</div>
</div>
)}
<div className="flex justify-between items-center border-b border-zinc-800 pb-4">
<div>
<h1 className="text-xl font-bold text-white font-mono">{paste.filename}</h1>
<p className="text-xs text-zinc-500 uppercase tracking-widest mt-1">
{paste.language} {new Date(paste.created_at).toLocaleDateString()}
</p>
</div>
<div className="flex gap-2">
{canDelete && (
<button
onClick={handleDelete}
className="bg-red-900/20 border border-red-900/50 text-red-500 px-4 py-2 rounded text-sm hover:bg-red-900/40 transition"
>
Delete Paste
</button>
)}
<a
href={`/api/raw/${paste.id}`}
target="_blank"
rel="noreferrer"
className="bg-zinc-900 border border-zinc-800 text-zinc-400 px-4 py-2 rounded text-sm hover:bg-zinc-800 transition shadow-sm"
>
Raw
</a>
<button
className={`${copied ? 'bg-green-600 border-green-500 text-white' : 'bg-zinc-900 border-zinc-800 text-zinc-400'} border px-4 py-2 rounded text-sm hover:opacity-80 transition shadow-sm w-32`}
onClick={copyToClipboard}
>
{copied ? 'Copied!' : 'Copy URL'}
</button>
</div>
</div>
<div className="border border-zinc-800 rounded-lg overflow-hidden shadow-2xl">
<Editor
height="60vh"
theme="vs-dark"
language={paste.language}
value={paste.content}
options={{ readOnly: true, fontSize: 14, minimap: { enabled: false }, scrollBeyondLastLine: false }}
/>
</div>
{paste.allow_discussions === 1 && (
<div className="pt-8 border-t border-zinc-900">
<h2 className="text-lg font-bold mb-4 text-white">Discussion</h2>
{!showBurnWarning && (
<div className="space-y-8">
<form onSubmit={addComment} className="bg-zinc-900 p-4 rounded-lg border border-zinc-800 space-y-3 shadow-inner">
<input
className="bg-zinc-950 border border-zinc-800 p-2 rounded text-sm w-full focus:outline-none focus:border-blue-500 text-white"
placeholder="Your name"
value={author}
onChange={e => setAuthor(e.target.value)}
required
/>
<textarea
className="bg-zinc-950 border border-zinc-800 p-2 rounded text-sm w-full h-24 focus:outline-none focus:border-blue-500 text-white"
placeholder="Markdown supported..."
value={comment}
onChange={e => setComment(e.target.value)}
required
/>
<button className="bg-blue-600 px-4 py-2 rounded text-sm font-bold hover:bg-blue-700 transition text-white">Post Comment</button>
</form>
<div className="space-y-4">
{comments.map((c, i) => (
<div key={i} className="border-l-2 border-zinc-800 pl-4 py-2">
<p className="text-xs font-bold text-blue-400 mb-1">
{c.author}
<span className="text-zinc-600 font-normal ml-2">{new Date(c.created_at).toLocaleString()}</span>
</p>
<div className="prose prose-invert prose-sm max-w-none">
<ReactMarkdown rehypePlugins={[rehypeHighlight]}>{c.content}</ReactMarkdown>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}