Update code

This commit is contained in:
UltraLionFr
2025-08-20 15:16:00 +02:00
parent 0c1a0d74d7
commit f6170fa581
9 changed files with 341 additions and 261 deletions
+22 -257
View File
@@ -1,114 +1,40 @@
'use client'; 'use client';
import {
CalendarDays,
Copy,
Download,
Eye,
FileText,
Plus,
} from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import Prism from 'prismjs'; import Prism from 'prismjs';
import { use, useEffect, useState } from 'react';
import 'prismjs/components/prism-bash'; import 'prismjs/components/prism-bash';
import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-json'; import 'prismjs/components/prism-json';
import 'prismjs/components/prism-python'; import 'prismjs/components/prism-python';
import 'prismjs/components/prism-typescript'; import 'prismjs/components/prism-typescript';
import 'prismjs/themes/prism-tomorrow.css';
import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
import 'prismjs/plugins/line-numbers/prism-line-numbers.js'; import 'prismjs/plugins/line-numbers/prism-line-numbers.js';
import 'prismjs/themes/prism-tomorrow.css';
/** ---------- Loader animé (spinner + skeletons) ---------- */ import AuthSection from "@/components/paste-form/AuthSection";
function LoadingPaste() { import CodeViewer from '@/components/paste/CodeViewer';
return ( import LoadingPaste from '@/components/paste/LoadingPaste';
<div className="relative w-screen h-screen bg-[#0e0f13] text-white flex overflow-hidden"> import PasswordForm from '@/components/paste/PasswordForm';
{/* Zone code (skeleton lignes) */} import PasteSidebar from '@/components/paste/PasteSidebar';
<div className="flex-1 h-full font-mono text-sm">
<div className="h-full overflow-auto pt-6 pb-6 px-6">
<div className="mb-4 flex items-center gap-2">
<div
className="h-5 w-5 rounded-full border-2 border-white/30 border-t-transparent animate-spin"
aria-hidden
/>
<span className="text-white/70 text-sm">Chargement du paste</span>
</div>
<div className="space-y-2">
{Array.from({ length: 28 }).map((_, i) => (
<div
key={i}
className={`h-3 rounded bg-white/10 animate-pulse ${i % 3 === 0 ? 'w-5/6' : i % 3 === 1 ? 'w-11/12' : 'w-3/4'}`}
/>
))}
</div>
</div>
</div>
{/* Sidebar (skeleton cartes + boutons) */} export default function PastePage({
<div params,
className=" }: {
absolute top-4 right-4 w-[300px] rounded-2xl p-5 text-sm params: Promise<{ id: string }>;
backdrop-blur-xl shadow-2xl }) {
bg-[linear-gradient(to_bottom_right,#1a2035,#101522)] const { id } = use(params);
ring-1 ring-white/10 mr-4 const router = useRouter();
"
>
<div className="mb-4">
<div className="flex items-center justify-between">
<div className="h-6 w-24 bg-white/10 rounded animate-pulse" />
<div className="h-7 w-20 rounded-full bg-green-600/60 animate-pulse" />
</div>
<div className="h-4 w-40 bg-white/10 rounded mt-2 animate-pulse" />
</div>
<div className="grid grid-cols-2 gap-3 text-neutral-300">
<div className="flex items-center gap-2 rounded-lg px-3 py-2 bg-[#151b2e] ring-1 ring-white/15">
<div className="h-4 w-4 rounded bg-white/10 animate-pulse" />
<div className="flex-1 space-y-1">
<div className="h-3 w-10 bg-white/10 rounded animate-pulse" />
<div className="h-3 w-12 bg-white/20 rounded animate-pulse" />
</div>
</div>
<div className="flex items-center gap-2 rounded-lg px-3 py-2 bg-[#151b2e] ring-1 ring-white/15">
<div className="h-4 w-4 rounded bg-white/10 animate-pulse" />
<div className="flex-1 space-y-1">
<div className="h-3 w-12 bg-white/10 rounded animate-pulse" />
<div className="h-3 w-16 bg-white/20 rounded animate-pulse" />
</div>
</div>
</div>
<div className="h-px bg-white/10 my-4" />
<div className="grid grid-cols-4 gap-2">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-9 rounded-lg bg-[#151b2e] ring-1 ring-white/10 animate-pulse" />
))}
</div>
<div className="mt-3 h-3 w-16 mx-auto bg-green-400/50 rounded animate-pulse" />
</div>
</div>
);
}
/** -------------------------------------------------------- */
export default function PastePage({ params }: { params: { id: string } }) {
const [paste, setPaste] = useState<any | null>(null); const [paste, setPaste] = useState<any | null>(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [language, setLanguage] = useState<string>('language-javascript'); const [language, setLanguage] = useState<string>('language-javascript');
const [copied, setCopied] = useState(false);
const codeRef = useRef<HTMLPreElement | null>(null);
const router = useRouter();
const fetchPaste = async (pwd?: string) => { const fetchPaste = async (pwd?: string) => {
setLoading(true); setLoading(true);
const res = await fetch(`/api/paste/${params.id}`, { const res = await fetch(`/api/paste/${id}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pwd || '' }), body: JSON.stringify({ password: pwd || '' }),
@@ -144,7 +70,7 @@ export default function PastePage({ params }: { params: { id: string } }) {
useEffect(() => { useEffect(() => {
fetchPaste(); fetchPaste();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [id]);
useEffect(() => { useEffect(() => {
if (paste?.content) { if (paste?.content) {
@@ -152,179 +78,18 @@ export default function PastePage({ params }: { params: { id: string } }) {
} }
}, [paste]); }, [paste]);
// 👉 Remplace l'ancien message "Loading..." par le loader animé
if (loading) return <LoadingPaste />; if (loading) return <LoadingPaste />;
if (error || (paste?.protected && !paste?.content)) { if (error || (paste?.protected && !paste?.content)) {
return ( return <PasswordForm error={error} onSubmit={(pwd) => fetchPaste(pwd)} />;
<div className="h-screen w-screen bg-[#0e0f13] flex items-center justify-center text-white">
<form
onSubmit={(e) => {
e.preventDefault();
fetchPaste((e.currentTarget as any).password.value);
}}
className="
w-full max-w-sm rounded-2xl p-6 space-y-4
backdrop-blur-xl shadow-2xl
bg-[linear-gradient(to_bottom_right,rgba(20,24,38,0.9),rgba(14,16,24,0.9))]
ring-1 ring-white/10
"
>
<h2 className="text-xl font-bold">
<span className="text-red-400">🔒 Protected Paste</span>
</h2>
{error && <p className="text-red-500 text-sm">{error}</p>}
<input
name="password"
type="password"
placeholder="Enter password"
className="
w-full rounded-xl bg-[#0f1320]/60 text-white placeholder:text-neutral-500
px-3 py-2 outline-none
ring-1 ring-white/10 focus:ring-2 focus:ring-blue-500/50
"
/>
<button
type="submit"
className="
w-full inline-flex items-center justify-center gap-2 rounded-xl py-2 text-sm font-medium
bg-gradient-to-b from-[#1b2135] to-[#141a2a] text-white ring-1 ring-white/10
hover:from-[#232a41] hover:to-[#161d2f]
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60
"
>
View Paste
</button>
</form>
</div>
);
} }
return ( return (
<div className="relative w-screen h-screen bg-[#0e0f13] text-white flex overflow-hidden"> <div className="relative w-screen h-screen bg-[#0e0f13] text-white flex overflow-hidden">
<div className="flex-1 h-full font-mono text-sm"> <CodeViewer paste={paste} language={language} />
<pre <PasteSidebar paste={paste} id={id} />
ref={codeRef} <AuthSection />
className="h-full overflow-auto pt-6 pb-6 px-6 line-numbers"
>
<code className={`${language} whitespace-pre-wrap break-words`}>
{paste.content}
</code>
</pre>
</div>
<div
className="
absolute top-4 right-4 w-[300px] rounded-2xl p-5 text-sm
backdrop-blur-xl shadow-2xl
bg-[linear-gradient(to_bottom_right,#1a2035,#101522)]
ring-1 ring-white/10 mr-4
"
>
<div className="mb-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold tracking-tight">
Alt<span className="text-blue-400">Bin</span>
</h2>
<button
onClick={() => router.push('/')}
className="
cursor-pointer inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold
bg-green-600 text-white shadow-sm
hover:bg-green-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-400/60
"
>
<Plus size={14} />
CREATE
</button>
</div>
{paste.title && (
<h1
className="text-sm font-semibold text-blue-300 truncate mt-1"
title={paste.title}
>
{paste.title}
</h1>
)}
</div>
<div className="grid grid-cols-2 gap-3 text-neutral-300">
<div className="flex items-center gap-2 rounded-lg px-3 py-2 bg-[#151b2e] ring-1 ring-white/15">
<Eye size={16} />
<div className="leading-tight">
<div className="text-xs text-neutral-400">Views</div>
<div className="text-sm font-medium text-white">{paste.views}</div>
</div>
</div>
<div className="flex items-center gap-2 rounded-lg px-3 py-2 bg-[#151b2e] ring-1 ring-white/15">
<CalendarDays size={16} />
<div className="leading-tight">
<div className="text-xs text-neutral-400">Created</div>
<div className="text-sm font-medium text-white">
{new Date(paste.createdAt).toLocaleDateString()}
</div>
</div>
</div>
</div>
<div className="h-px bg-white/10 my-4" />
<div className="grid grid-cols-4 gap-2">
<button
onClick={async () => {
await navigator.clipboard.writeText(paste.content);
setCopied(true);
setTimeout(() => setCopied(false), 1200);
}}
className="cursor-pointer inline-flex items-center justify-center rounded-lg p-2 bg-[#151b2e] ring-1 ring-white/10 hover:bg-[#1e253d] hover:ring-blue-400/30"
title="Copy"
>
<Copy size={18} />
</button>
<button
onClick={() => window.open(`/api/${params.id}?raw=1`, '_blank')}
className="cursor-pointer inline-flex items-center justify-center rounded-lg p-2 bg-[#151b2e] ring-1 ring-white/10 hover:bg-[#1e253d] hover:ring-blue-400/30"
title="Raw"
>
<FileText size={18} />
</button>
<button
onClick={() => {
const blob = new Blob([paste.content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${params.id}.txt`;
a.click();
URL.revokeObjectURL(url);
}}
className="cursor-pointer inline-flex items-center justify-center rounded-lg p-2 bg-[#151b2e] ring-1 ring-white/10 hover:bg-[#1e253d] hover:ring-blue-400/30"
title="Download"
>
<Download size={18} />
</button>
<button
onClick={() => router.push('/')}
className="cursor-pointer inline-flex items-center justify-center rounded-lg p-2 bg-[#151b2e] ring-1 ring-white/10 hover:bg-[#1e253d] hover:ring-blue-400/30"
title="New paste"
>
<Plus size={18} />
</button>
</div>
<div
className={`mt-3 text-center text-xs transition-opacity ${
copied ? 'opacity-100 text-green-400' : 'opacity-0'
}`}
>
Copied!
</div>
</div>
</div> </div>
); );
} }
+43 -4
View File
@@ -5,12 +5,51 @@ import { NextRequest, NextResponse } from 'next/server';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export const revalidate = 0; export const revalidate = 0;
// --- GET pour raw ou JSON ---
export async function GET(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const { id: pasteId } = await context.params; // ✅ await obligatoire
if (!pasteId) {
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
}
const paste = await prisma.paste.findUnique({ where: { id: pasteId } });
if (!paste) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
// Si paste protégé, on bloque le GET
if (paste.password) {
return NextResponse.json({ error: 'This paste is protected' }, { status: 403 });
}
const searchParams = request.nextUrl.searchParams;
if (searchParams.get('raw') === '1') {
return new NextResponse(paste.content, {
status: 200,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
return NextResponse.json({
id: paste.id,
title: paste.title,
content: paste.content,
createdAt: paste.createdAt,
views: paste.views,
protected: !!paste.password,
});
}
// --- POST pour récupérer en validant le password ---
export async function POST( export async function POST(
request: NextRequest, request: NextRequest,
context: { params: { id: string } } context: { params: Promise<{ id: string }> }
) { ) {
const { params } = context; const { id: pasteId } = await context.params; // ✅ ici aussi
const pasteId = params.id;
const { password = '' } = await request.json(); const { password = '' } = await request.json();
@@ -65,4 +104,4 @@ export async function POST(
return NextResponse.json(payload, { return NextResponse.json(payload, {
headers: { 'Cache-Control': 'no-store' }, headers: { 'Cache-Control': 'no-store' },
}); });
} }
+25
View File
@@ -0,0 +1,25 @@
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const { id } = params;
if (!id) {
return new NextResponse("Invalid ID", { status: 400 });
}
const paste = await prisma.paste.findUnique({ where: { id } });
if (!paste) {
return new NextResponse("Paste not found", { status: 404 });
}
return new NextResponse(paste.content, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-store",
},
});
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function RawIndexPage() {
redirect("/");
}
+38
View File
@@ -0,0 +1,38 @@
'use client';
import Prism from 'prismjs';
import 'prismjs/components/prism-bash';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-typescript';
import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
import 'prismjs/plugins/line-numbers/prism-line-numbers.js';
import 'prismjs/themes/prism-tomorrow.css';
import { useEffect, useRef } from 'react';
export default function CodeViewer({
paste,
language,
}: {
paste: any;
language: string;
}) {
const codeRef = useRef<HTMLPreElement | null>(null);
useEffect(() => {
if (paste?.content) {
Prism.highlightAll();
}
}, [paste, language]);
return (
<div className="flex-1 h-full font-mono text-sm">
<pre ref={codeRef} className="h-full overflow-auto pt-6 pb-6 px-6 line-numbers">
<code className={`${language} whitespace-pre-wrap break-words`}>
{paste.content}
</code>
</pre>
</div>
);
}
+62
View File
@@ -0,0 +1,62 @@
export default function LoadingPaste() {
return (
<div className="relative w-screen h-screen bg-[#0e0f13] text-white flex overflow-hidden">
<div className="flex-1 h-full font-mono text-sm">
<div className="h-full overflow-auto pt-6 pb-6 px-6">
<div className="mb-4 flex items-center gap-2">
<div className="h-5 w-5 rounded-full border-2 border-white/30 border-t-transparent animate-spin" />
<span className="text-white/70 text-sm">Loading paste</span>
</div>
<div className="space-y-2">
{Array.from({ length: 28 }).map((_, i) => (
<div
key={i}
className={`h-3 rounded bg-white/10 animate-pulse ${
i % 3 === 0 ? 'w-5/6' : i % 3 === 1 ? 'w-11/12' : 'w-3/4'
}`}
/>
))}
</div>
</div>
</div>
<div className="absolute top-4 right-4 w-[300px] rounded-2xl p-5 text-sm backdrop-blur-xl shadow-2xl bg-[linear-gradient(to_bottom_right,#1a2035,#101522)] ring-1 ring-white/10 mr-4">
<div className="mb-4">
<div className="flex items-center justify-between">
<div className="h-6 w-24 bg-white/10 rounded animate-pulse" />
<div className="h-7 w-20 rounded-full bg-green-600/60 animate-pulse" />
</div>
<div className="h-4 w-40 bg-white/10 rounded mt-2 animate-pulse" />
</div>
<div className="grid grid-cols-2 gap-3 text-neutral-300">
{[...Array(2)].map((_, i) => (
<div
key={i}
className="flex items-center gap-2 rounded-lg px-3 py-2 bg-[#151b2e] ring-1 ring-white/15"
>
<div className="h-4 w-4 rounded bg-white/10 animate-pulse" />
<div className="flex-1 space-y-1">
<div className="h-3 w-10 bg-white/10 rounded animate-pulse" />
<div className="h-3 w-12 bg-white/20 rounded animate-pulse" />
</div>
</div>
))}
</div>
<div className="h-px bg-white/10 my-4" />
<div className="grid grid-cols-4 gap-2">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="h-9 rounded-lg bg-[#151b2e] ring-1 ring-white/10 animate-pulse"
/>
))}
</div>
<div className="mt-3 h-3 w-16 mx-auto bg-green-400/50 rounded animate-pulse" />
</div>
</div>
);
}
+39
View File
@@ -0,0 +1,39 @@
export default function PasswordForm({
onSubmit,
error,
}: {
onSubmit: (pwd: string) => void;
error?: string;
}) {
return (
<div className="h-screen w-screen bg-[#0e0f13] flex items-center justify-center text-white">
<form
onSubmit={(e) => {
e.preventDefault();
const pwd = (e.currentTarget as any).password.value;
onSubmit(pwd);
}}
className="w-full max-w-sm rounded-2xl p-6 space-y-4 backdrop-blur-xl shadow-2xl bg-[linear-gradient(to_bottom_right,rgba(20,24,38,0.9),rgba(14,16,24,0.9))] ring-1 ring-white/10"
>
<h2 className="text-xl font-bold">
<span className="text-red-400">🔒 Protected Paste</span>
</h2>
{error && <p className="text-red-500 text-sm">{error}</p>}
<input
name="password"
type="password"
placeholder="Enter password"
className="w-full rounded-xl bg-[#0f1320]/60 text-white placeholder:text-neutral-500 px-3 py-2 outline-none ring-1 ring-white/10 focus:ring-2 focus:ring-blue-500/50"
/>
<button
type="submit"
className="w-full inline-flex items-center justify-center gap-2 rounded-xl py-2 text-sm font-medium bg-gradient-to-b from-[#1b2135] to-[#141a2a] text-white ring-1 ring-white/10 hover:from-[#232a41] hover:to-[#161d2f] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60"
>
View Paste
</button>
</form>
</div>
);
}
+107
View File
@@ -0,0 +1,107 @@
'use client';
import { CalendarDays, Copy, Download, Eye, FileText, Plus } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function PasteSidebar({ paste, id }: { paste: any; id: string }) {
const [copied, setCopied] = useState(false);
const router = useRouter();
return (
<div className="absolute top-4 right-4 w-[300px] rounded-2xl p-5 text-sm backdrop-blur-xl shadow-2xl bg-[linear-gradient(to_bottom_right,#1a2035,#101522)] ring-1 ring-white/10 mr-4">
<div className="mb-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold tracking-tight">
Alt<span className="text-blue-400">Bin</span>
</h2>
<button
onClick={() => router.push("/")}
className="cursor-pointer inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold bg-green-600 text-white shadow-sm hover:bg-green-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-400/60"
>
<Plus size={14} /> CREATE
</button>
</div>
{paste.title && (
<h1 className="text-sm font-semibold text-blue-300 truncate mt-1" title={paste.title}>
{paste.title}
</h1>
)}
</div>
{/* Infos */}
<div className="grid grid-cols-2 gap-3 text-neutral-300">
<div className="flex items-center gap-2 rounded-lg px-3 py-2 bg-[#151b2e] ring-1 ring-white/15">
<Eye size={16} />
<div className="leading-tight">
<div className="text-xs text-neutral-400">Views</div>
<div className="text-sm font-medium text-white">{paste.views}</div>
</div>
</div>
<div className="flex items-center gap-2 rounded-lg px-3 py-2 bg-[#151b2e] ring-1 ring-white/15">
<CalendarDays size={16} />
<div className="leading-tight">
<div className="text-xs text-neutral-400">Created</div>
<div className="text-sm font-medium text-white">
{new Date(paste.createdAt).toLocaleDateString()}
</div>
</div>
</div>
</div>
<div className="h-px bg-white/10 my-4" />
{/* Actions */}
<div className="grid grid-cols-4 gap-2">
<button
onClick={async () => {
await navigator.clipboard.writeText(paste.content);
setCopied(true);
setTimeout(() => setCopied(false), 1200);
}}
title="Copy"
className="cursor-pointer inline-flex items-center justify-center rounded-lg p-2 bg-[#151b2e] ring-1 ring-white/10 hover:bg-[#1e253d] hover:ring-blue-400/30"
>
<Copy size={18} />
</button>
<button
onClick={() => window.open(`/raw/${id}`, "_blank")}
title="Raw"
className="cursor-pointer inline-flex items-center justify-center rounded-lg p-2 bg-[#151b2e] ring-1 ring-white/10 hover:bg-[#1e253d] hover:ring-blue-400/30"
>
<FileText size={18} />
</button>
<button
onClick={() => {
const blob = new Blob([paste.content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${id}.txt`;
a.click();
URL.revokeObjectURL(url);
}}
title="Download"
className="cursor-pointer inline-flex items-center justify-center rounded-lg p-2 bg-[#151b2e] ring-1 ring-white/10 hover:bg-[#1e253d] hover:ring-blue-400/30"
>
<Download size={18} />
</button>
<button
onClick={() => router.push("/")}
title="New paste"
className="cursor-pointer inline-flex items-center justify-center rounded-lg p-2 bg-[#151b2e] ring-1 ring-white/10 hover:bg-[#1e253d] hover:ring-blue-400/30"
>
<Plus size={18} />
</button>
</div>
<div
className={`mt-3 text-center text-xs transition-opacity ${
copied ? "opacity-100 text-green-400" : "opacity-0"
}`}
>
Copied!
</div>
</div>
);
}
BIN
View File
Binary file not shown.