zero-knowledge file sharing
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

upload link in circle

Juliet f0177a7d 2421d86f

+142 -124
+142 -124
web/src/pages/Upload.tsx
··· 12 12 const [resultUrl, setResultUrl] = createSignal(""); 13 13 const [dragging, setDragging] = createSignal(false); 14 14 const [burn, setBurn] = createSignal(false); 15 + const [copied, setCopied] = createSignal(false); 15 16 const [maxFileSize, setMaxFileSize] = createSignal(0); 16 17 17 18 let fileInput!: HTMLInputElement; 18 - let linkInput!: HTMLInputElement; 19 19 let expiryInput!: HTMLInputElement; 20 - let copyBtn!: HTMLButtonElement; 21 20 let activeXhr: XMLHttpRequest | null = null; 22 21 23 22 const RADIUS = 130; ··· 191 190 } 192 191 }; 193 192 194 - const copyLink = () => { 195 - navigator.clipboard.writeText(linkInput.value); 196 - copyBtn.textContent = "Copied!"; 197 - setTimeout(() => (copyBtn.textContent = "Copy"), 1500); 193 + const copyLink = (e: MouseEvent) => { 194 + e.stopPropagation(); 195 + navigator.clipboard.writeText(resultUrl()); 196 + setCopied(true); 197 + setTimeout(() => setCopied(false), 1500); 198 198 }; 199 199 200 200 return ( 201 201 <> 202 202 <div 203 203 class="group relative mx-auto flex aspect-square w-[80vw] max-w-[500px] items-center justify-center" 204 - onClick={() => fileInput.click()} 204 + onClick={() => !resultUrl() && fileInput.click()} 205 205 > 206 206 <svg 207 207 viewBox={`0 0 ${SIZE} ${SIZE}`} ··· 256 256 </svg> 257 257 258 258 <div class="z-10 flex flex-col items-center gap-2 text-center sm:gap-3"> 259 - <Show when={uploading()}> 259 + <Show when={resultUrl()}> 260 260 <span 261 - class="text-accent font-medium tabular-nums" 262 - style={{ "font-size": "clamp(1.5rem, 5vw, 2.5rem)" }} 261 + class="text-accent truncate px-4 font-mono" 262 + style={{ 263 + "font-size": "clamp(0.55rem, 1.5vw, 0.75rem)", 264 + "max-width": "clamp(120px, 50vw, 260px)", 265 + }} 263 266 > 264 - {progress()}% 267 + {resultUrl()} 265 268 </span> 266 - <span class="text-muted text-xs">{statusText()}</span> 269 + <button 270 + class="bg-accent hover:bg-accent-hover rounded-md border-none px-4 py-1.5 font-medium text-white transition-colors" 271 + style={{ "font-size": "clamp(0.875rem, 2.5vw, 1.125rem)" }} 272 + onClick={copyLink} 273 + > 274 + {copied() ? "copied!" : "copy link"} 275 + </button> 267 276 <button 268 277 class="text-muted hover:text-text mt-1 border-none bg-transparent p-0 text-[10px]" 269 278 onClick={(e) => { 270 279 e.stopPropagation(); 271 - cancelUpload(); 280 + setResultUrl(""); 281 + removeFile(); 272 282 }} 273 283 > 274 - cancel 284 + new drop 275 285 </button> 276 286 </Show> 277 - <Show when={!uploading()}> 278 - <Show 279 - when={!file()} 280 - fallback={ 281 - <> 282 - <span 283 - class="text-text truncate" 284 - style={{ 285 - "max-width": "clamp(120px, 40vw, 300px)", 286 - "font-size": "clamp(0.75rem, 2vw, 1rem)", 287 - }} 288 - > 289 - {file()!.name} 290 - </span> 291 - <span 292 - class={`font-medium ${tooLarge() ? "text-danger" : "text-muted"}`} 293 - style={{ "font-size": "clamp(0.625rem, 1.5vw, 0.875rem)" }} 294 - > 295 - {formatBytes(file()!.size)} 296 - {tooLarge() ? ` / ${formatBytes(maxFileSize())} limit` : ""} 297 - </span> 298 - <button 299 - class="bg-accent hover:bg-accent-hover mt-2 rounded-md border-none px-4 py-1.5 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-40" 300 - style={{ "font-size": "clamp(0.875rem, 2.5vw, 1.25rem)" }} 301 - disabled={tooLarge()} 302 - onClick={(e) => { 303 - e.stopPropagation(); 304 - handleUpload(); 305 - }} 306 - > 307 - upload 308 - </button> 309 - <button 310 - class="text-muted hover:text-text mt-1 border-none bg-transparent p-0 text-[10px]" 311 - onClick={(e) => { 312 - e.stopPropagation(); 313 - removeFile(); 314 - }} 315 - > 316 - remove 317 - </button> 318 - </> 319 - } 320 - > 321 - <p 322 - class="text-muted font-medium" 323 - style={{ "font-size": "clamp(0.625rem, 2vw, 0.875rem)" }} 287 + <Show when={!resultUrl()}> 288 + <Show when={uploading()}> 289 + <span 290 + class="text-accent font-medium tabular-nums" 291 + style={{ "font-size": "clamp(1.5rem, 5vw, 2.5rem)" }} 324 292 > 325 - drop a file, or 326 - </p> 293 + {progress()}% 294 + </span> 295 + <span class="text-muted text-xs">{statusText()}</span> 327 296 <button 328 - class="bg-accent hover:bg-accent-hover rounded-md border-none px-4 py-1.5 font-medium text-white transition-colors" 329 - style={{ "font-size": "clamp(0.875rem, 2.5vw, 1.25rem)" }} 297 + class="text-muted hover:text-text mt-1 border-none bg-transparent p-0 text-[10px]" 330 298 onClick={(e) => { 331 299 e.stopPropagation(); 332 - fileInput.click(); 300 + cancelUpload(); 333 301 }} 334 302 > 335 - browse 303 + cancel 336 304 </button> 337 - <Show when={maxFileSize()}> 338 - <span class="text-muted text-[10px]"> 339 - up to {formatBytes(maxFileSize())} 340 - </span> 305 + </Show> 306 + <Show when={!uploading()}> 307 + <Show 308 + when={!file()} 309 + fallback={ 310 + <> 311 + <span 312 + class="text-text truncate" 313 + style={{ 314 + "max-width": "clamp(120px, 40vw, 300px)", 315 + "font-size": "clamp(0.75rem, 2vw, 1rem)", 316 + }} 317 + > 318 + {file()!.name} 319 + </span> 320 + <span 321 + class={`font-medium ${tooLarge() ? "text-danger" : "text-muted"}`} 322 + style={{ 323 + "font-size": "clamp(0.625rem, 1.5vw, 0.875rem)", 324 + }} 325 + > 326 + {formatBytes(file()!.size)} 327 + {tooLarge() 328 + ? ` / ${formatBytes(maxFileSize())} limit` 329 + : ""} 330 + </span> 331 + <button 332 + class="bg-accent hover:bg-accent-hover mt-2 rounded-md border-none px-4 py-1.5 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-40" 333 + style={{ "font-size": "clamp(0.875rem, 2.5vw, 1.25rem)" }} 334 + disabled={tooLarge()} 335 + onClick={(e) => { 336 + e.stopPropagation(); 337 + handleUpload(); 338 + }} 339 + > 340 + upload 341 + </button> 342 + <button 343 + class="text-muted hover:text-text mt-1 border-none bg-transparent p-0 text-[10px]" 344 + onClick={(e) => { 345 + e.stopPropagation(); 346 + removeFile(); 347 + }} 348 + > 349 + remove 350 + </button> 351 + </> 352 + } 353 + > 354 + <p 355 + class="text-muted font-medium" 356 + style={{ "font-size": "clamp(0.625rem, 2vw, 0.875rem)" }} 357 + > 358 + drop a file, or 359 + </p> 360 + <button 361 + class="bg-accent hover:bg-accent-hover rounded-md border-none px-4 py-1.5 font-medium text-white transition-colors" 362 + style={{ "font-size": "clamp(0.875rem, 2.5vw, 1.25rem)" }} 363 + onClick={(e) => { 364 + e.stopPropagation(); 365 + fileInput.click(); 366 + }} 367 + > 368 + browse 369 + </button> 370 + <Show when={maxFileSize()}> 371 + <span class="text-muted text-[10px]"> 372 + up to {formatBytes(maxFileSize())} 373 + </span> 374 + </Show> 341 375 </Show> 342 376 </Show> 343 377 </Show> ··· 353 387 /> 354 388 </div> 355 389 356 - <div class="mx-auto mt-6 flex w-fit flex-col gap-4 text-sm"> 357 - <label class="text-muted flex items-center gap-3 select-none"> 358 - lifetime 359 - <input 360 - ref={expiryInput!} 361 - type="text" 362 - value="24h" 363 - placeholder="e.g. 30m, 24h, 7d" 364 - class="bg-surface border-border text-accent focus:border-accent w-24 rounded-md border px-2 py-1 text-center font-medium transition-colors outline-none" 365 - /> 366 - </label> 367 - <label 368 - class="text-muted flex items-center gap-3 select-none" 369 - onClick={() => setBurn((b) => !b)} 370 - > 371 - burn after read 372 - <div 373 - class={`flex size-5 items-center justify-center rounded border transition-colors ${burn() ? "bg-accent border-accent" : "bg-surface border-border"}`} 374 - > 375 - <Show when={burn()}> 376 - <svg class="text-bg h-3.5 w-3.5" viewBox="0 0 12 12" fill="none"> 377 - <path 378 - d="M2 6l3 3 5-5" 379 - stroke="currentColor" 380 - stroke-width="1.5" 381 - stroke-linecap="round" 382 - stroke-linejoin="round" 383 - /> 384 - </svg> 385 - </Show> 386 - </div> 387 - </label> 388 - </div> 389 - 390 - <Show when={error()}> 391 - <div class="text-danger mt-4 text-xs">{error()}</div> 392 - </Show> 393 - 394 - <Show when={resultUrl()}> 395 - <div class="bg-surface border-border mt-6 rounded-lg border p-4"> 396 - <label class="text-muted mb-1.5 block text-xs">drop link</label> 397 - <div class="flex gap-2"> 390 + <Show when={!resultUrl()}> 391 + <div class="mx-auto mt-6 flex w-fit flex-col gap-4 text-sm"> 392 + <label class="text-muted flex items-center gap-3 select-none"> 393 + lifetime 398 394 <input 399 - ref={linkInput!} 395 + ref={expiryInput!} 400 396 type="text" 401 - readonly 402 - value={resultUrl()} 403 - class="bg-bg border-border text-text min-w-0 flex-1 rounded-md border px-2.5 py-1.5 font-mono text-xs outline-none" 397 + value="24h" 398 + placeholder="30m, 24h, 7d" 399 + class="bg-surface border-border text-accent focus:border-accent w-28 rounded-md border px-2 py-1 text-center font-medium transition-colors outline-none" 404 400 /> 405 - <button 406 - ref={copyBtn!} 407 - class="bg-accent hover:bg-accent-hover shrink-0 rounded-md border-none px-3 py-1.5 text-xs font-medium text-white transition-colors" 408 - onClick={copyLink} 401 + </label> 402 + <label 403 + class="text-muted flex items-center gap-3 select-none" 404 + onClick={() => setBurn((b) => !b)} 405 + > 406 + burn after read 407 + <div 408 + class={`flex size-5 items-center justify-center rounded border transition-colors ${burn() ? "bg-accent border-accent" : "bg-surface border-border"}`} 409 409 > 410 - Copy 411 - </button> 412 - </div> 410 + <Show when={burn()}> 411 + <svg 412 + class="text-bg h-3.5 w-3.5" 413 + viewBox="0 0 12 12" 414 + fill="none" 415 + > 416 + <path 417 + d="M2 6l3 3 5-5" 418 + stroke="currentColor" 419 + stroke-width="1.5" 420 + stroke-linecap="round" 421 + stroke-linejoin="round" 422 + /> 423 + </svg> 424 + </Show> 425 + </div> 426 + </label> 413 427 </div> 428 + </Show> 429 + 430 + <Show when={error()}> 431 + <div class="text-danger mt-4 text-xs">{error()}</div> 414 432 </Show> 415 433 </> 416 434 );