Repository Public
1// Every share link of the account, whatever it targets.2import { useQuery } from '@tanstack/react-query';3import { useState } from 'react';4import { Link } from 'wouter';5import { get } from '../api.ts';6import { Glyph } from '../glyphs.tsx';7import { useT } from '../i18n.ts';8import { ShareDialog } from '../sharing/ShareDialog.tsx';9import { ShareItem } from '../sharing/ShareParts.tsx';10import type { Share } from '../types.ts';11import { cx, EmptyState, ErrorState, Loading } from '../ui.tsx';1213const STATUSES: (Share['status'] | 'all')[] = ['all', 'active', 'expired', 'exhausted', 'revoked', 'target_missing'];1415export function Shares() {16 const t = useT();17 const q = useQuery({ queryKey: ['shares', 'all'], queryFn: () => get<Share[]>('/shares'), refetchOnMount: 'always' });18 const [filter, setFilter] = useState<Share['status'] | 'all'>('all');19 const [manage, setManage] = useState<Share | null>(null);20 const all = q.data ?? [];21 const shown = filter === 'all' ? all : all.filter((s) => s.status === filter);22 const count = (s: Share['status'] | 'all') => (s === 'all' ? all.length : all.filter((x) => x.status === s).length);23 return (24 <div className="pb-dock mx-auto w-full max-w-3xl px-4 pt-3 sm:px-6 sm:pt-7" data-testid="shares-screen">25 <header className="px-1">26 <h1 className="text-[30px] leading-[1.1] font-bold tracking-[-0.03em] sm:text-[32px]">{t('sharing.shares.title')}</h1>27 <p className="mt-1.5 text-[15px] text-ink-2">{t('sharing.shares.intro')}</p>28 </header>29 {q.isLoading ? <Loading /> : q.error ? <ErrorState error={q.error} onRetry={() => void q.refetch()} /> : !all.length ? <EmptyState icon="link" title={t('sharing.shares.empty')} text={t('sharing.shares.emptyText')} /> : (30 <>31 <div role="group" aria-label={t('sharing.shares.filter')} className="mt-4 flex flex-wrap gap-1.5">32 {STATUSES.filter((s) => s === 'all' || count(s) > 0).map((s) => (33 <button key={s} type="button" aria-pressed={filter === s} onClick={() => setFilter(s)}34 className={cx('press h-10 rounded-full px-4 text-sm font-medium', filter === s ? 'grad shadow-sm' : 'bg-surface text-ink-2 ring-1 ring-inset ring-line hover:bg-hover')}>35 {s === 'all' ? t('sharing.shares.all') : t(`sharing.status.${s}`)} <span className="tabular-nums opacity-70">{count(s)}</span>36 </button>37 ))}38 </div>39 <div className="stagger mt-5 space-y-3">40 {shown.length ? shown.map((s) => (41 <ShareItem key={s.id} share={s} heading={42 <p className="mb-1.5 flex min-w-0 items-center gap-2 text-sm">43 <Glyph name={s.type} size={18} className={cx('shrink-0', s.type === 'folder' ? 'text-accent' : 'text-ink-3')} />44 {s.status === 'target_missing' || !s.targetName ? <span className="truncate text-ink-3">{s.targetName || t('sharing.shares.targetGone')}</span>45 : s.type === 'folder' ? <Link href={`/folder/${s.targetId}`} className="truncate font-medium text-accent underline-offset-2 hover:underline">{s.targetName}</Link>46 : <button type="button" onClick={() => setManage(s)} className="truncate text-left font-medium text-accent underline-offset-2 hover:underline" title={t('sharing.shares.manageFile')}>{s.targetName}</button>}47 </p>} />48 )) : <p className="py-10 text-center text-sm text-ink-3">{t('sharing.shares.noneForFilter')}</p>}49 </div>50 </>51 )}52 {manage ? <ShareDialog target={{ type: manage.type, id: manage.targetId, name: manage.targetName }} onClose={() => setManage(null)} /> : null}53 </div>54 );55}56