Repository Public
1// Deposit portals of the account.2import { formatBytes } from '@spd/shared';3import { useQuery } from '@tanstack/react-query';4import { useState } from 'react';5import { Link, useLocation } from 'wouter';6import { get } from '../api.ts';7import { Glyph } from '../glyphs.tsx';8import { fmtDate, getLocale, useT } from '../i18n.ts';9import { FolderPicker, PortalActions, PortalState } from '../sharing/PortalParts.tsx';10import { RequestDialog } from '../sharing/RequestDialog.tsx';11import type { Portal } from '../types.ts';12import { Button, EmptyState, ErrorState, Loading, ProgressBar } from '../ui.tsx';1314export function Requests() {15 const t = useT();16 const loc = getLocale();17 const [, navigate] = useLocation();18 const q = useQuery({ queryKey: ['requests', 'list'], queryFn: () => get<Portal[]>('/requests'), refetchOnMount: 'always' });19 const [picking, setPicking] = useState(false);20 const [folder, setFolder] = useState<{ id: string; name: string } | null>(null);21 const create = <Button variant="primary" icon="plus" onClick={() => setPicking(true)}>{t('sharing.requests.new')}</Button>;22 return (23 <div className="pb-dock mx-auto w-full max-w-3xl px-4 pt-3 sm:px-6 sm:pt-7" data-testid="requests-screen">24 <header className="flex flex-col items-start justify-between gap-3 px-1 sm:flex-row">25 <div className="min-w-0 flex-1"><h1 className="text-[30px] leading-[1.1] font-bold tracking-[-0.03em] sm:text-[32px]">{t('sharing.requests.title')}</h1><p className="mt-1.5 text-[15px] text-ink-2">{t('sharing.requests.intro')}</p></div>26 {q.data?.length ? create : null}27 </header>28 {q.isLoading ? <Loading /> : q.error ? <ErrorState error={q.error} onRetry={() => void q.refetch()} /> : !q.data?.length ? <EmptyState icon="inbox" title={t('sharing.requests.empty')} text={t('sharing.requests.emptyText')} action={create} /> : (29 <ul className="stagger mt-5 space-y-3">30 {q.data.map((p) => (31 <li key={p.id} className="card p-5" data-testid="portal-card" data-state={p.state}>32 <div className="flex items-start gap-3">33 <span className="mt-0.5 h-9 w-1.5 shrink-0 rounded-full" style={{ background: p.brandColor }} aria-hidden="true" />34 <div className="min-w-0 flex-1">35 <p className="flex flex-wrap items-center gap-2"><Link href={`/requests/${p.id}`} className="min-w-0 truncate text-base font-semibold underline-offset-2 hover:underline">{p.title}</Link><PortalState state={p.state} /></p>36 <p className="mt-0.5 text-xs text-ink-3">{t(`sharing.pstateHint.${p.state}`)}</p>37 <p className="mt-2 flex min-w-0 items-center gap-1.5 text-sm text-ink-2"><Glyph name="folder" size={16} className="shrink-0 text-ink-3" />38 {p.state === 'target_missing' ? <span className="truncate">{p.folderName}</span> : <Link href={`/folder/${p.folderId}`} className="truncate text-accent underline-offset-2 hover:underline">{p.folderName}</Link>}</p>39 </div>40 </div>41 <dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 text-sm sm:grid-cols-3">42 <div><dt className="text-xs text-ink-3">{t('sharing.requests.deposits')}</dt><dd className="font-medium tabular-nums" data-testid="portal-deposits">{p.submissions}{p.maxSubmissions !== null ? ` / ${p.maxSubmissions}` : ''}</dd></div>43 <div><dt className="text-xs text-ink-3">{t('sharing.requests.volume')}</dt><dd className="font-medium tabular-nums">{formatBytes(p.bytesReceived, loc)} / {formatBytes(p.maxTotalBytes, loc)}</dd></div>44 <div className="col-span-2 sm:col-span-1"><dt className="text-xs text-ink-3">{t('sharing.requests.expiry')}</dt><dd className="font-medium">{p.expiresAt ? fmtDate(p.expiresAt) : t('sharing.item.noExpiry')}</dd></div>45 </dl>46 <div className="mt-2"><ProgressBar value={p.maxTotalBytes ? p.bytesReceived / p.maxTotalBytes : 0} label={t('sharing.requests.volume')} /></div>47 <div className="mt-3 flex flex-wrap items-center justify-end gap-2">48 <Button size="sm" icon="inbox" onClick={() => navigate(`/requests/${p.id}`)} className="mr-auto">{t('sharing.requests.view')}</Button>49 <PortalActions portal={p} />50 </div>51 </li>52 ))}53 </ul>54 )}55 {picking ? <FolderPicker title={t('sharing.picker.title')} confirmLabel={t('sharing.picker.confirm')} onClose={() => setPicking(false)} onPick={(f) => { setPicking(false); setFolder(f); }} /> : null}56 {folder ? <RequestDialog folder={folder} onClose={() => setFolder(null)} /> : null}57 </div>58 );59}60