195 lines
7.6 KiB
JavaScript
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>
|
|
);
|
|
}
|