import { useEffect, useState, useCallback } from "react"; import Button from "../components/button"; import FileList from "./files"; import PaymentFlow from "../components/payment"; import ProgressBar from "../components/progress-bar"; import { openFile } from "../upload"; import { Blossom } from "../upload/blossom"; import useLogin from "../hooks/login"; import usePublisher from "../hooks/publisher"; import { Nip96, Nip96FileList } from "../upload/nip96"; import { AdminSelf, Route96 } from "../upload/admin"; import { FormatBytes } from "../const"; import { UploadProgress } from "../upload/progress"; export default function Upload() { const [type, setType] = useState<"blossom" | "nip96">("blossom"); const [noCompress, setNoCompress] = useState(false); const [toUpload, setToUpload] = useState(); const [self, setSelf] = useState(); const [error, setError] = useState(); const [results, setResults] = useState>([]); const [listedFiles, setListedFiles] = useState(); const [listedPage, setListedPage] = useState(0); const [showPaymentFlow, setShowPaymentFlow] = useState(false); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(); const login = useLogin(); const pub = usePublisher(); const url = import.meta.env.VITE_API_URL || `${location.protocol}//${location.host}`; async function doUpload() { if (!pub) return; if (!toUpload) return; if (isUploading) return; // Prevent multiple uploads try { setError(undefined); setIsUploading(true); setUploadProgress(undefined); const onProgress = (progress: UploadProgress) => { setUploadProgress(progress); }; if (type === "blossom") { const uploader = new Blossom(url, pub); const result = noCompress ? await uploader.upload(toUpload, onProgress) : await uploader.media(toUpload, onProgress); setResults((s) => [...s, result]); } if (type === "nip96") { const uploader = new Nip96(url, pub); await uploader.loadInfo(); const result = await uploader.upload(toUpload, onProgress); setResults((s) => [...s, result]); } } catch (e) { if (e instanceof Error) { setError(e.message || "Upload failed - no error details provided"); } else if (typeof e === "string") { setError(e); } else { setError("Upload failed"); } } finally { setIsUploading(false); setUploadProgress(undefined); } } const listUploads = useCallback( async (n: number) => { if (!pub) return; try { setError(undefined); const uploader = new Nip96(url, pub); await uploader.loadInfo(); const result = await uploader.listFiles(n, 50); setListedFiles(result); } catch (e) { if (e instanceof Error) { setError( e.message || "List files failed - no error details provided", ); } else if (typeof e === "string") { setError(e); } else { setError("List files failed"); } } }, [pub, url], ); async function deleteFile(id: string) { if (!pub) return; try { setError(undefined); const uploader = new Blossom(url, pub); await uploader.delete(id); } catch (e) { if (e instanceof Error) { setError(e.message || "Delete failed - no error details provided"); } else if (typeof e === "string") { setError(e); } else { setError("Delete failed"); } } } useEffect(() => { if (pub && !listedFiles) { listUploads(listedPage); } }, [listedPage, pub, listUploads, listedFiles]); useEffect(() => { if (pub && !self) { const r96 = new Route96(url, pub); r96.getSelf().then((v) => setSelf(v.data)); } }, [pub, self, url]); if (!login) { return (

Welcome to {window.location.hostname}

Please log in to start uploading files to your storage.

); } return (
{error && (
{error}
)}

Upload Settings

{toUpload && (
)} {/* Upload Progress */} {isUploading && uploadProgress && ( )}
{self && (

Storage Quota

{self.total_available_quota && self.total_available_quota > 0 && ( <> {/* File Count */}
Files: {self.file_count.toLocaleString()}
{/* Progress Bar */}
Used: {FormatBytes(self.total_size)} of{" "} {FormatBytes(self.total_available_quota)}
0.8 ? "bg-red-500" : self.total_size / self.total_available_quota > 0.6 ? "bg-yellow-500" : "bg-green-500" }`} style={{ width: `${Math.min(100, (self.total_size / self.total_available_quota) * 100)}%`, }} >
{( (self.total_size / self.total_available_quota) * 100 ).toFixed(1)} % used 0.8 ? "text-red-400" : self.total_size / self.total_available_quota > 0.6 ? "text-yellow-400" : "text-green-400" }`} > {FormatBytes( Math.max( 0, self.total_available_quota - self.total_size, ), )}{" "} remaining
{/* Quota Breakdown */}
{self.free_quota && self.free_quota > 0 && (
Free Quota: {FormatBytes(self.free_quota)}
)} {(self.quota ?? 0) > 0 && (
Paid Quota: {FormatBytes(self.quota!)}
)} {(self.paid_until ?? 0) > 0 && (
Expires:
{new Date( self.paid_until! * 1000, ).toLocaleDateString()}
{(() => { const now = Date.now() / 1000; const daysLeft = Math.max( 0, Math.ceil( (self.paid_until! - now) / (24 * 60 * 60), ), ); return daysLeft > 0 ? `${daysLeft} days left` : "Expired"; })()}
)}
)} {(!self.total_available_quota || self.total_available_quota === 0) && (

No quota information available

Contact administrator for storage access

)}
)} {showPaymentFlow && pub && (
{ console.log("Payment requested:", pr); }} userInfo={self} />
)}

Your Files

{!listedFiles && ( )}
{listedFiles && ( setListedPage(x)} onDelete={async (x) => { await deleteFile(x); await listUploads(listedPage); }} /> )}
{results.length > 0 && (

Upload Results

{results.map((result: any, index) => (

✅ Upload Successful

{new Date( (result.uploaded || Date.now() / 1000) * 1000, ).toLocaleString()}

{result.type || "Unknown type"}

File Size

{FormatBytes(result.size || 0)}

{result.nip94?.find((tag: any[]) => tag[0] === "dim") && (

Dimensions

{ result.nip94.find( (tag: any[]) => tag[0] === "dim", )?.[1] }

)}

File URL

{result.url}
{result.nip94?.find((tag: any[]) => tag[0] === "thumb") && (

Thumbnail URL

{ result.nip94.find( (tag: any[]) => tag[0] === "thumb", )?.[1] }
)}

File Hash (SHA256)

{result.sha256}
Show raw JSON data
                    {JSON.stringify(result, undefined, 2)}
                  
))}
)}
); }