Files
altbin.dev/prisma/altbin.db
T

1029 lines
56 KiB
Plaintext
Raw Normal View History

2025-08-20 14:14:33 +02:00
SQLite format 3@ 11.zp
ø k
Ö³ k
«=„YtablePastePasteCREATE TABLE "Paste" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT,
"content" TEXT NOT NULL,
"password" TEXT,
"maxViews" INTEGER,
"views" INTEGER NOT NULL DEFAULT 0,
"size" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdBy" TEXT
))=indexsqlite_autoindex_Paste_1PasteƒZ11†_table_prisma_migrations_prisma_migrationsCREATE TABLE "_prisma_migrations" (
"id" TEXT PRIMARY KEY NOT NULL,
"checksum" TEXT NOT NULL,
"finished_at" DATETIME,
"migration_name" TEXT NOT NULL,
"logs" TEXT,
"rolled_back_at" DATETIME,
"started_at" DATETIME NOT NULL DEFAULT current_timestamp,
"applied_steps_count" INTEGER UNSIGNED NOT NULL DEFAULT 0
)CW1indexsqlite_autoindex__prisma_migrations_1_prisma_migrations
pp

U
3 f873948d-8cca-42e0-b29f-47a4d55b0c03e5a55fd30fef2e082ffb3d4b286f515a65e3b4b139c55e2d74181d0d36d0d903˜Â‰Ú;20250819133430_init˜Â‰Ú4
ØØ'U f873948d-8cca-42e0-b29f-47a4d55b0c03
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal';
import LogoutButton from '@/components/LogoutButton';
import { Player } from "@lordicon/react";
import { motion } from 'framer-motion';
import { Search } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { forwardRef, useEffect, useRef, useState } from "react";
import { toast, Toaster } from 'react-hot-toast';
const PER_PAGE = 6;
const dashboardIcons = {
clipboard: "https://cdn.lordicon.com/gvtjlyjf.json",
views: "https://cdn.lordicon.com/dicvhxpz.json",
calendar: "https://cdn.lordicon.com/uphbloed.json",
code: "https://cdn.lordicon.com/xqdfobxg.json",
storage: "https://cdn.lordicon.com/kikjlzqr.json",
chart: "https://cdn.lordicon.com/abwrkdvl.json",
flame: "https://cdn.lordicon.com/thtrcqvk.json",
file: "https://cdn.lordicon.com/fikcyfpp.json",
};
// --- Loading screen (spinner + skeletons)
function LoadingDashboard() {
const StatSkeleton = () => (
<div className="rounded-2xl bg-[#11131c] p-5 border border-white/10">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-white/10 animate-pulse" />
9 ¤%1vwxmq49"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import Editor from "./paste-form/Editor";
import SidebarPanel from "./paste-form/SidebarPanel";
import AuthSection from "./paste-form/AuthSection";
export default function PasteForm() {
const router = useRouter();
const { data: session } = useSession();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [password, setPassword] = useState("");
const [maxViews, setMaxViews] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [advanced, setAdvanced] = useState(true);
const handleSave = useCallback(async () => {
if (!content.trim()) return;
setIsSaving(true);
const res = await fetch("/api/paste", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: title.trim() || null,
content,
password: password || null,
maxViews: maxViews ? parseInt(maxViews, 10) : null,
createdBy: session?.user?.id || null,
}),
});
if (res.ok) {
const data = await res.json();
router.push(`/${data.id}`);
}
setIsSaving(false);
}, [title, content, password, maxViews, session, router]);
// Raccourci clavier Ctrl+S
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
e.preventDefault();
if (!isSaving && content.trim()) handleSave();
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [handleSave, isSaving, content]);
return (
<div className="w-screen h-screen bg-[#0e0f13] text-white relative">
<Editor content={content} setContent={setContent} />
<SidebarPanel
title={title}
setTitle={setTitle}
maxViews={maxViews}
setMaxViews={setMaxViews}
password={password}
setPassword={setPassword}
content={content}
isSaving={isSaving}
handleSave={handleSave}
advanced={advanced}
setAdvanced={setAdvanced}
/>
<AuthSection />
</div>
);
}
 ˜Ãgàf2811134578336
ééõÝ z3hxfma qa65aao
 vwxmq49
ÄÄ9 ¤%1vwxmq49"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import Editor from "./paste-form/Editor";
import SidebarPanel from "./paste-form/SidebarPanel";
import AuthSection from "./paste-form/AuthSection";
export default function PasteForm() {
const router = useRouter();
const { data: session } = useSession();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [password, setPassword] = useState("");
const [maxViews, setMaxViews] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [advanced, setAdvanced] = useState(true);
const handleSave = useCallback(async () => {
if (!content.trim()) return;
setIsSaving(true);
const res = await fetch("/api/paste", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: title.trim() || null,
content,
password: password || null,
maxViews: maxViews ? parseInt(maxViews, 10) : null,
createdBy: session?.user?.id || null,
}),
});
if (res.ok) {
const data = await res.json();
router.push(`/${data.id}`);
}
setIsSaving(false);
}, [title, content, password, maxViews, session, router]);
// Raccourci clavier Ctrl+S
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
e.preventDefault();
if (!isSaving && content.trim()) handleSave();
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [handleSave, isSaving, content]);
return (
<div className="w-screen h-screen bg-[#0e0f13] text-white relative">
<Editor content={content} setContent={setContent} />
<SidebarPanel
title={title}
setTitle={setTitle}
maxViews={maxViews}
setMaxViews={setMaxViews}
password={password}
setPassword={setPassword}
content={content}
isSaving={isSaving}
handleSave={handleSave}
advanced={advanced}
setAdvanced={setAdvanced}
/>
<AuthSection />
</div>
);
}
 ˜Ãgàf281113457833672706

*/
export default function PastePage({ params }: { params: { id: string } }) {
const [paste, setPaste] = useState<any | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
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) => {
setLoading(true);
const res = await fetch(`/api/paste/${params.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pwd || '' }),
});
if (res.status === 403) {
setError('Invalid password.');
setLoading(false);
return;
}
if (res.status === 404) {
setError('Paste not found or expired.');
setLoading(false);
return;
}
const data = await res.json();
setPaste(data);
if (data.content.includes('function') || data.content.includes('const')) {
setLanguage('language-javascript');
} else if (data.content.includes('import') || data.content.includes('export')) {
setLanguage('language-typescript');
} else if (data.content.includes('{') && data.content.includes('}')) {
setLanguage('language-json');
} else {
setLanguage('language-bash');
}
setLoading(false);
};
useEffect(() => {
fetchPaste();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (paste?.content) {
Prism.highlightAll();
}
}, [paste]);
// 👉 Remplace l'ancien message "Loading..." par le loader animé
if (loading) return <LoadingPaste />;
if (error || (paste?.protected && !paste?.content)) {
return (
<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 (
<div className="relative w-screen h-screen bg-[#0e0f13] text-white flex overflow-hidden">
<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>
<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)]
ri
 G G6 š1p8p7k5v<motion.div
key={paste.id}
className="rounded-xl border border-white/10 bg-[#11131c] p-5 flex flex-col shadow-sm hover:shadow-lg hover:border-blue-500/30 transition-all"
variants={{ hidden: { opacity: 0, scale: 0.95 }, visible: { opacity: 1, scale: 1 } }}
onMouseEnter={() => pasteIconRef.current?.playFromBeginning()}
onMouseLeave={() => pasteIconRef.current?.goToFirstFrame()}
>
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-blue-500/20 text-blue-400">
<LordIcon
ref={pasteIconRef}
url={dashboardIcons.paste}
size={28}
colorize="#3b82f6"
/>
</div>
<div className="font-mono text-white text-sm truncate">
{paste.title || 'Untitled'}
</div>
</div>
<div className="text-xs text-neutral-400 font-mono mb-3 line-clamp-3">
{paste.content || 'No content'}
</div>
<div className="flex justify-between items-center mt-auto text-xs text-neutral-500">
<span>{new Date(paste.createdAt).toLocaleString()}</span>
<span className="px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">
{paste.views} views
</span>
</div>
<div className="flex justify-end gap-2 mt-4">
<a
href={`/${paste.id}`}
className="cursor-pointer px-3 py-1.5 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-500 transition-colors"
target="_blank"
rel="noopener noreferrer"
>
View
</a>
<button
onClick={() => setDeleteId(paste.id)}
className="cursor-pointer px-3 py-1.5 rounded-lg bg-red-600 text-white text-sm hover:bg-red-500 transition-colors"
>
Delete
</button>
</div>
</motion.div>
˜ÃD×+281113457833672706
(_, i) => (
<StatSkeleton key={i} />
))}
</div>
{/* Search skeleton */}
<div className="relative w-full md:w-1/3 mb-8">
<div className="h-10 w-full rounded-xl bg-white/10 animate-pulse" />
</div>
{/* Pastes skeleton grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<PasteSkeleton key={i} />
))}
</div>
</main>
</div>
);
}
const LordIcon = forwardRef(function LordIcon(
{ url, size = 28, colorize = "#3b82f6" }: { url: string; size?: number; colorize?: string },
ref: any
) {
const [iconData, setIconData] = useState<any>(null);
useEffect(() => {
fetch(url).then((res) => res.json()).then(setIconData);
}, [url]);
if (!iconData) return null;
return <Player ref={ref} icon={iconData} size={size} colorize={colorize} />;
});
function StatCard({ label, value, icon }: { label: string; value: any; icon: string }) {
const iconRef = useRef<any>(null);
return (
<motion.div
className="rounded-2xl bg-[#11131c] p-5 border border-white/10 hover:border-blue-500/40 transition-colors shadow-md"
whileHover={{ scale: 1.03 }}
onMouseEnter={() => iconRef.current?.playFromBeginning()}
onMouseLeave={() => iconRef.current?.goToFirstFrame()}
>
<div className="flex items-center gap-3">
<div className="p-3 rounded-xl bg-blue-500/20 text-blue-400">
<LordIcon ref={iconRef} url={icon} size={28} colorize="#3b82f6" />
</div>
<div>
<div className="text-sm text-neutral-400">{label}</div>
<div className="text-xl font-bold text-white">{value}</div>
</div>
</div>
</motion.div>
);
}
export default function DashboardPage() {
const plusIconRef = useRef<any>(null);
const { data: session, status } = useSession();
const router = useRouter();
const [searchTerm, setSearchTerm] = useState('');
const [stats, setStats] = useState<any>(null);
const [pastes, setPastes] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [totalPastes, setTotalPastes] = useState(0);
const [deleteId, setDeleteId] = useState<string | null>(null);
const handleDelete = async (id: string) => {
const res = await fetch(`/api/paste?id=${id}`, { method: "DELETE" });
if (res.ok) {
setPastes((prev) => prev.filter((p) => p.id !== id));
setTotalPastes((prev) => prev - 1);
toast.success("Paste deleted successfully 🚀");
} else {
toast.error("Failed to delete paste âŒ");
}
};
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/');
}
}, [status, router]);
useEffect(() => {
if (status === 'authenticated') {
const fetchStats = async () => {
const res = await fetch('/api/stats');
if (!res.ok) return;
const data = await res.json();
setStats(data);
};
const fetchPastes = async () => {
const res = await fetch(
`/api/user-pastes?page=${page}&perPage=${PER_PAGE}&q=${encodeURIComponent(searchTerm)}`
);
if (!res.ok) {
setPastes([]);
setTotalPastes(0);
return;
}
const text = await res.text();
if (!text) {
setPastes([]);
setTotalPastes(0);
return;
}
try {
const data = JSON.parse(text);
setPastes(data.pastes ?? []);
setTotalPastes(data.total ?? 0);
} catch {
setPastes([]);
setTotalPastes(0);
}
};
fetchStats();
fetchPastes();
}
}, [status, page, searchTerm]);
if (status === 'loading' || !stats) {
return <LoadingDashboard />;
}
const totalPages = Math.ceil(totalPastes / PER_PAGE);
return (
<div className="flex h-screen bg-[#0d1117]">
<Toaster position="top-right" />
{/* Sidebar */}
<aside className="hidden md:flex flex-col w-64 bg-[#11131c] border-r bord er-white/10 p-6">
<h2 className="text-xl font-bold text-white mb-8">AltBin Dashboard</h2>
<nav className="flex flex-col gap-4">
<button className="flex items-center gap-2 text-blue-400 hover:text-blue-300 transition-colors cursor-pointer">
📋 My Pastes
</button>
</nav>
<div className="mt-auto">
<LogoutButton />
</div>
</aside>
<main className="flex-1 p-8 overflow-y-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold text-white">
Welcome back, <span className="text-blue-400">{session?.user?.name || 'User'}</span>
</h1>
{/* Bouton Create New Paste avec animation Lordicon au hover */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.97 }}
onClick={() => router.push('/')}
className="cursor-pointer flex items-center gap-2 px-6 py-3 rounded-xl text-base font-medium
text-white bg-gradient-to-r from-blue-600 to-blue-500 shadow-lg shadow-blue-500/30
hover:shadow-blue-500/50 transition-all duration-300"
onMouseEnter={() => plusIconRef.current?.playFromBeginning()}
onMouseLeave={() => plusIconRef.current?.goToFirstFrame()}
>
<div className="w-6 h-6 flex items-center justify-center">
<LordIcon
ref={plusIconRef}
url="https://cdn.lordicon.com/vjgknpfx.json"
size={28}
colorize="#ffffff"
/>
</div>
Create New Paste
</motion.button>
</div>
{/* Statistiques avec Lordicon */}
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-10"
initial="hidden"
animate="visible"
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { staggerChildren: 0.1 } },
}}
>
{[
{ label: 'Total Pastes', value: stats.totalPastes, icon: dashboardIcons.clipboard },
{ label: 'Total Views', value: stats.totalViews, icon: dashboardIcons.views },
{ label: 'Last 30 Days', value: stats.recentPastes, icon: dashboardIcons.calendar },
{ label: 'API Usage', value: stats.apiUsage, icon: dashboardIcons.code },
{ label: 'Storage Used', value: formatBytes(stats.storageUsed), icon: dashboardIcons.storage },
{ label: 'Avg Views', value: stats.avgViews, icon: dashboardIcons.chart },
{ label: 'Most Viewed', value: stats.mostViewed, icon: dashboardIcons.flame },
{ label: 'Avg Paste Size', value: formatBytes(stats.avgSize), icon: dashboardIcons.file },
].map((stat, i) => (
<StatCard key={i} label={stat.label} value={stat.value} icon={stat.icon} />
))}
</motion.div>
{/* Search */}
<div className="relative w-full md:w-1/3 mb-8">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-500" size={18}/>
<input
type="text"
placeholder="Search by title..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setPage(1);
}}
className="cursor-text w-full pl-10 pr-4 py-2 rounded-xl bg-[#0f1320]/70 text-white placeholder:text-neutral-500 outline-none ring-1 ring-white/10 focus:ring-2 focus:ring-blue-500/50 transition-all"
/>
</div>
{/* Pastes */}
<h2 className="text-lg font-semibold text-white mb-4">Your Pastes</h2>
{pastes.length === 0 ? (
<div className="text-sm text-neutral-400">No pastes found.</div>
) : (
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
initial="hidden"
animate="visible"
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { staggerChildren: 0.08 } },
}}
>
{pastes.map((paste) => (
<motion.div
key={paste.id}
className="rounded-xl border border-white/10 bg-[#11131c] p-5 flex flex-col shadow-sm hover:shadow-lg hover:border-blue-500/30 transition-all"
variants={{ hidden: { opacity: 0, scale: 0.95 }, visible: { opacity: 1, scale: 1 } }}
>
<div className="flex items-center gap-3 mb-3">
<div className="p-2 rounded-lg bg-blue-500/20 text-blue-400">📄</div>
<div className="font-mono text-white text-sm truncate">
{paste.title || 'Untitled'}
</div>
</div>
<div className="text-xs text-neutral-400 font-mono mb-3 line-clamp-3">
{paste.content || 'No content'}
</div>
<div className="flex justify-between items-center mt-auto text-xs text-neutral-500">
<span>{new Date(paste.createdAt).toLocaleString()}</span>
<span className="px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">
{paste.views} views
</span>
</div>
<div className="flex justify-end gap-2 mt-4">
<a
href={`/${paste.id}`}
className="cursor-pointer px-3 py-1.5 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-500 transition-colors"
target="_blank"
rel="noopener noreferrer"
>
View
</a>
<button
onClick={() => setDeleteId(paste.id)}
className="cursor-pointer px-3 py-1.5 rounded-lg bg-red-600 text-white text-sm hover:bg-red-500 transition-colors"
>
Delete
</button>
</div>
</motion.div>
))}
</motion.div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center mt-10 gap-2">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className={`px-4 py-2 rounded-full text-sm transition-colors ${
page === 1
? 'bg-neutral-800 text-white/50 cursor-not-allowed'
: 'bg-neutral-800 text-white hover:bg-neutral-700 cursor-pointer'
}`}
>
Previous
</button>
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i}
onClick={() => setPage(i + 1)}
className={`px-4 py-2 rounded-full text-sm transition-colors cursor-pointer ${
page === i + 1
? 'bg-blue-600 text-white shadow-md'
: 'bg-neutral-800 text-white hover:bg-neutral-700'
}`}
>
{i + 1}
</button>
))}
<button
onClick={() => setPage((p) => Math.min(p + 1, totalPages))}
disabled={page === totalPages}
className={`px-4 py-2 rounded-full text-sm transition-colors ${
page === totalPages
? 'bg-neutral-800 text-white/50 cursor-not-allowed'
: 'bg-neutral-800 text-white hover:bg-neutral-700 cursor-pointer'
}`}
>
Next
</button>
</div>
)}
<ConfirmDeleteModal
open={!!deleteId}
onOpenChange={(open) => !open && setDeleteId(null)}
onConfirm={() => {
if (deleteId) {
handleDelete(deleteId);
setDeleteId(null);
}
}}
/>
</main>
</div>
);
} ˜Ã43Ÿ281113457833672706
ýý,ƒN †O1z3hxfmaexport default function PasteViewer({ paste }: { paste: any }) {
return (
<article className="space-y-4">
<h2 className="text-xl font-bold">{paste.title || '(Untitled)'}</h2>
<pre className="bg-neutral-900 p-4 rounded whitespace-pre-wrap break-words">{paste.content}</pre>
<p className="text-sm text-neutral-400">Created: {new Date(paste.createdAt).toLocaleString()}</p>
</article>
);
}
¡˜Ãm
¶281113457833672706˜ ¯31qa65aao'use client';
import { Eye, Lock, Settings } from 'lucide-react';
import { useState } from 'react';
export default function PasteOptionsPanel({
password,
setPassword,
maxViews,
setMaxViews,
onCreate,
disabled,
}: {
password: string;
setPassword: (val: string) => void;
maxViews: string;
setMaxViews: (val: string) => void;
onCreate: () => void;
disabled: boolean;
}) {
const [open, setOpen] = useState(true);
return (
<aside className="w-80 rounded-2xl border border-[#1b1d2a] bg-[#0f111a]/60 shadow-xl p-5 flex flex-col justify-between text-sm backdrop-blur-md">
<div>
<div className="flex justify-between items-center mb-6">
<h2 className="text-lg font-bold text-white">Alt<span className="text-blue-400">Bin</span></h2>
<button
className="bg-green-600 hover:bg-green-500 text-xs font-semibold px-3 py-1 rounded-full"
onClick={onCreate}
disabled={disabled}
>
+ CREATE
</button>
</div>
<button
className="flex items-center gap-2 text-neutral-400 text-xs mb-3"
onClick={() => setOpen(!open)}
>
<Settings size={14} />
Advanced {open ? 'â–¾' : 'â–¸'}
</button>
{open && (
<div className="space-y-6">
<div className="space-y-2">
<label className="flex items-center gap-2 text-neutral-400 text-xs uppercase tracking-wide">
<Eye size={14} /> Max Views
</label>
<input
type="number"
value={maxViews}
onChange={e => setMaxViews(e.target.value)}
placeholder="∞"
className="w-full bg-[#1a1d29] text-white p-2 rounded outline-none"
/>
<p className="text-[11px] text-neutral-500">Delete after X views</p>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2 text-neutral-400 text-xs uppercase tracking-wide">
<Lock size={14} /> Password
</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="Optional password"
className="w-full bg-[#1a1d29] text-white p-2 rounded outline-none"
/>
<p className="text-[11px] text-neutral-500">Require password to view</p>
</div>
</div>
)}
</div>
<button
disabled={disabled}
onClick={onCreate}
className={`mt-8 py-2 rounded-md text-sm tracking-wide font-medium flex items-center justify-center ${
disabled
? 'bg-[#1a1d29] text-neutral-500 opacity-50 cursor-not-allowed'
: 'bg-[#1a1d29] text-white hover:bg-neutral-800'
}`}
>
<span className="text-lg">🔒</span> SAVE PASTE
</button>
</aside>
);
} Ó˜Ãl­å281113457833672706rdicon.com/hmpomorl.json",
};
export default function DashboardPage() {
const { data: session, status } = useSession();
const router = useRouter();
const [searchTerm, setSearchTerm] = useState("");
const [stats, setStats] = useState<any>(null);
const [pastes, setPastes] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [totalPastes, setTotalPastes] = useState(0);
const [deleteId, setDeleteId] = useState<string | null>(null);
const handleDelete = async (id: string) => {
const res = await fetch(`/api/paste?id=${id}`, { method: "DELETE" });
if (res.ok) {
setPastes((prev) => prev.filter((p) => p.id !== id));
setTotalPastes((prev) => prev - 1);
toast.success("Paste deleted successfully 🚀");
} else {
toast.error("Failed to delete paste âŒ");
}
};
useEffect(() => {
if (status === "unauthenticated") router.push("/");
}, [status, router]);
useEffect(() => {
if (status === "authenticated") {
const fetchStats = async () => {
const res = await fetch("/api/stats");
if (res.ok) setStats(await res.json());
};
const fetchPastes = async () => {
const res = await fetch(
`/api/user-pastes?page=${page}&perPage=${PER_PAGE}&q=${encodeURIComponent(searchTerm)}`
);
if (!res.ok) return setPastes([]);
try {
const data = await res.json();
setPastes(data.pastes ?? []);
setTotalPastes(data.total ?? 0);
} catch {
setPastes([]);
setTotalPastes(0);
}
};
fetchStats();
fetchPastes();
}
}, [status, page, searchTerm]);
if (status === "loading" || !stats) return <LoadingDashboard />;
const totalPages = Math.ceil(totalPastes / PER_PAGE);
return (
<div className="flex h-screen bg-[#0d1117]">
<Toaster position="top-right" />
<Sidebar />
<main className="flex-1 p-8 overflow-y-auto">
<Header user={session?.user?.name} />
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
{[
{ label: "Total Pastes", value: stats.totalPastes, icon: dashboardIcons.clipboard },
{ label: "Total Views", value: stats.totalViews, icon: dashboardIcons.views },
{ label: "Last 30 Days", value: stats.recentPastes, icon: dashboardIcons.calendar },
{ label: "API Usage", value: stats.apiUsage, icon: dashboardIcons.code },
{ label: "Storage Used", value: formatBytes(stats.storageUsed), icon: dashboardIcons.storage },
{ label: "Avg Views", value: stats.avgViews, icon: dashboardIcons.chart },
{ label: "Most Viewed", value: stats.mostViewed, icon: dashboardIcons.flame },
{ label: "Avg Paste Size", value: formatBytes(stats.avgSize), icon: dashboardIcons.file },
].map((stat, i) => (
<StatCard key={i} label={stat.label} value={stat.value} icon={stat.icon} />
))}
</div>
<SearchBar value={searchTerm} onChange={(v) => { setSearchTerm(v); setPage(1); }} />
{/* Pastes */}
<h2 className="text-lg font-semibold text-white mb-4">Your Pastes</h2>
{pastes.length === 0 ? (
<div className="text-sm text-neutral-400">No pastes found.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{pastes.map((paste) => (
<PasteCard
key={paste.id}
paste={paste}
onDelete={setDeleteId}
pasteIconUrl={dashboardIcons.paste}
/>
))}
</div>
)}
<Pagination totalPages={totalPages} currentPage={page} onPageChange={setPage} />
<ConfirmDeleteModal
open={!!deleteId}
onOpenChange={(open) => !open && setDeleteId(null)}
onConfirm={() => { if (deleteId) { handleDelete(deleteId); setDeleteId(null); } }}
/>
</main>
</div>
);
}
¹˜Ã[X
281113457833672706g-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>