Skip to content
File

Blob: src/client/islands/blob-actions.tsx

typescript111 lines
1/// <reference lib="dom" />
2 
3import { useState } from "react";
4import { Clipboard, Download, FileText, Menu } from "lucide-react";
5 
6import { hydrateIsland } from "@/client/hydrate";
7import { Button, buttonClasses } from "@/client/components/ui/button";
8 
9export type BlobActionsProps = {
10 viewRawHref: string;
11 rawHref: string;
12 showCopy: boolean;
13 isImage?: boolean;
14 isPdf?: boolean;
15};
16 
17export function BlobActionsIsland({
18 viewRawHref,
19 rawHref,
20 showCopy,
21 isImage,
22 isPdf,
23}: BlobActionsProps) {
24 const [copyLabel, setCopyLabel] = useState("Copy");
25 
26 async function copyRawText() {
27 try {
28 const response = await fetch(viewRawHref);
29 const text = await response.text();
30 await navigator.clipboard.writeText(text);
31 setCopyLabel("Copied");
32 window.setTimeout(() => setCopyLabel("Copy"), 1200);
33 } catch (error) {
34 console.warn("Copy failed", error);
35 }
36 }
37 
38 return (
39 <div className="flex items-center gap-2">
40 <div className="hidden items-center gap-2 sm:flex">
41 <div className="inline-flex items-center overflow-hidden rounded-full border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900">
42 {showCopy ? (
43 <button
44 className="px-3 py-1.5 text-sm no-underline text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
45 type="button"
46 onClick={() => void copyRawText()}
47 >
48 {copyLabel}
49 </button>
50 ) : null}
51 {!isImage && !isPdf ? (
52 <a
53 className="border-l border-zinc-200 dark:border-zinc-700 px-3 py-1.5 text-sm no-underline text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
54 href={viewRawHref}
55 >
56 View
57 </a>
58 ) : null}
59 <span className="border-l border-zinc-200 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-800/50 px-3 py-1.5 text-sm text-zinc-500 dark:text-zinc-400">
60 Raw
61 </span>
62 </div>
63 <Button size="sm" href={rawHref}>
64 Download
65 </Button>
66 </div>
67 <div className="sm:hidden">
68 <details className="ref-menu relative">
69 <summary className={buttonClasses("secondary", "sm")}>
70 <Menu className="h-4 w-4" aria-hidden="true" />
71 </summary>
72 <div className="fixed inset-x-0 z-20 mx-3 mt-2 rounded-xl border border-zinc-200 bg-white p-2 shadow-xl dark:border-zinc-800/60 dark:bg-zinc-900">
73 <div className="flex flex-col">
74 {showCopy ? (
75 <button
76 type="button"
77 className="flex items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-zinc-100 dark:hover:bg-zinc-800"
78 onClick={() => void copyRawText()}
79 >
80 <Clipboard className="h-4 w-4" aria-hidden="true" />
81 <span>{copyLabel === "Copy" ? "Copy raw" : "Copied"}</span>
82 </button>
83 ) : null}
84 {!isImage && !isPdf ? (
85 <a
86 href={viewRawHref}
87 className="flex items-center gap-2 rounded-lg px-3 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-800"
88 >
89 <FileText className="h-4 w-4" aria-hidden="true" />
90 <span>View raw</span>
91 </a>
92 ) : null}
93 <a
94 href={rawHref}
95 className="flex items-center gap-2 rounded-lg px-3 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-800"
96 >
97 <Download className="h-4 w-4" aria-hidden="true" />
98 <span>Download</span>
99 </a>
100 </div>
101 </div>
102 </details>
103 </div>
104 </div>
105 );
106}
107 
108export function initBlobActions() {
109 hydrateIsland<BlobActionsProps>("blob-actions", BlobActionsIsland);
110}