ham7a311

Upload Stack

Component 1

About

A drag-and-drop multi-file upload queue that sorts files by what needs attention — failed uploads rise to the top, active transfers stay in view, queued files wait behind them, and finished files settle into a collapsible ledger below.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Upload Stack to your project and fits it to your theme.

Code

How Upload Stack is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import { UploadStack, type Uploader } from "@/components/ui/upload-stack";
import { useMemo, useState } from "react";

const SAMPLES: [string, number][] = [
  ["campus-wayfinding-v3.png", 4.2e6],
  ["thesis-draft-v4.pdf", 2.1e6],
  ["interview-recording.mov", 38e6],
  ["site-photos.zip", 18e6],
  ["survey-results.csv", 640e3],
  ["budget-2026.xlsx", 412e3],
  ["q3-roadmap.pdf", 1.4e6],
];

function fakeFile(name: string, size: number) {
  const f = new File([""], name);
  Object.defineProperty(f, "size", { value: size });
  return f;
}

/** Simulated network: ~5 MB/s with jitter; site-photos.zip drops once at 62%. */
function makeUploader(): Uploader {
  const failedOnce = new Set<string>();
  const resumeAt = new Map<string, number>(); // bytes already delivered before a drop
  return (file, report, signal) =>
    new Promise((resolve, reject) => {
      let sent = resumeAt.get(file.name) ?? 0;
      const rate = 3.5e6 + Math.random() * 3e6;
      const t = window.setInterval(() => {
        sent += (rate / 8) * (0.6 + Math.random() * 0.8);
        const p = Math.min(1, sent / Math.max(1, file.size));
        if (
          file.name === "site-photos.zip" &&
          !failedOnce.has(file.name) &&
          p > 0.62
        ) {
          failedOnce.add(file.name);
          resumeAt.set(file.name, sent);
          window.clearInterval(t);
          return reject(
            new Error("Connection dropped at 62% — retry to resume"),
          );
        }
        report(p);
        if (p >= 1) {
          window.clearInterval(t);
          resolve();
        }
      }, 125);
      signal.addEventListener("abort", () => {
        window.clearInterval(t);
        reject(new Error("Cancelled"));
      });
    });
}

export default function Demo({ variant = "paper" }: { variant?: string }) {
  const night = variant === "night";
  const [run, setRun] = useState(1);
  const uploader = useMemo(() => makeUploader(), [run]); // eslint-disable-line react-hooks/exhaustive-deps
  const files = useMemo(() => SAMPLES.map(([n, s]) => fakeFile(n, s)), [run]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div
      className={`flex h-full min-h-[620px] w-full items-start justify-center overflow-auto px-4 py-10 ${night ? "bg-[#0f1012] text-[#ececea]" : "bg-[#f3f1ec] text-[#1b1a17]"}`}
      style={{ fontFamily: "Geist, ui-sans-serif, system-ui" }}
    >
      <div className="flex w-full flex-col items-center gap-3">
        <UploadStack
          key={run}
          theme={night ? "night" : "paper"}
          uploader={uploader}
          initialFiles={files}
          maxSize={25e6}
          concurrency={2}
        />
        <button
          type="button"
          onClick={() => setRun((r) => r + 1)}
          className={`h-8 rounded-lg px-3 text-[12.5px] ring-1 ${night ? "ring-white/10 text-[#8b8d93]" : "ring-black/10 text-[#77736b]"}`}
        >
          Restart with sample files
        </button>
      </div>
    </div>
  );
}

More by ham7a311

View all
  • Wave Mesh

    Wave Mesh

    137
  • Route Globe

    Route Globe

    125
  • Glass Tiles

    Glass Tiles

    108
  • Glyph Swell

    Glyph Swell

    50
  • Orbit Ring Stats

    Orbit Ring Stats

    47
  • Ask Bar

    Ask Bar

    37
  • Glass Ribbons

    Glass Ribbons

    30
  • Intent Label

    Intent Label

    29