Files
Aether/frontend/src/pages/SharedWithMePage.tsx
T
marcandClaude Opus 5 ea90f43e77 refactor(frontend): PdfViewer und FileBrowser aufteilen, Tests dazu
FileBrowser 1177 -> 712 Zeilen, PdfViewer 1193 -> 848. Herausgeloest
wurde, was fuer sich zusammenhaengt:

  hooks/useUploads            Warteschlange, Ordnerbaeume, Ablage
  components/files/           Werkzeugleiste, Dialoge, Menues, Reihenfolge
  components/pdf/             Werkzeugleiste, Zoomstufen, Seitenmasse, Suche
  src/format.ts               formatBytes -- lag in einer *Seite*

Dabei gefundene und behobene Fehler:

* Ordner-Upload haengte Fortschritt und Fehler an die falsche Zeile,
  sobald in einem Ordner Dateien *nach* einem Unterordner lagen: Die
  Zeilen standen in Fundreihenfolge, hochgeladen wurde nach Ordnern
  gruppiert, und ein mitlaufender Zaehler verband beides falsch.
* formatBytes gab es zweimal, zeichengleich (Dateibrowser + Admin).
* Beide Sortierungen liefen bei *jedem* Bild -- der Rahmen beim Ziehen
  ist eines je Mausbewegung. Jetzt gemerkt.
* Die Suche im PDF las bei jedem Tastendruck den Text aller Seiten neu.
  Jetzt 160 ms Wartezeit; "Searching..." bleibt auch bei einem Fehler
  nicht mehr stehen.
* Seitenmasse werden in Buendeln statt streng nacheinander geholt.

Pruefinfrastruktur: vitest stand auf 'jsdom', ohne dass jsdom je
installiert war -- "npm test" brach ab, bevor es etwas ausfuehrte. Jetzt
'node' und 32 Tests fuer die herausgeloesten reinen Funktionen. Das
Abbild baut nur noch mit gruenen Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 22:23:21 +02:00

129 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { aclApi, filesApi, type SharedResource } from '../api/client'
import { formatBytes } from '../format'
import PageHeader from '../components/PageHeader'
import { FolderGlyph } from '../components/icons'
interface Props {
/** Öffnet einen freigegebenen Ordner in der Dateiansicht. */
onOpenFolder: (folderId: string, folderName: string) => void
}
/** „Geteilt mit mir": alles, worauf man Zugriff bekommen hat, ohne es zu
* besitzen. Die Daten liegen weiterhin beim Eigentümer – hier wird nur
* darauf verwiesen. */
export default function SharedWithMePage({ onOpenFolder }: Props) {
const [items, setItems] = useState<SharedResource[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
aclApi.sharedWithMe()
.then(d => setItems(d ?? []))
.catch(() => {})
.finally(() => setLoading(false))
}, [])
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<PageHeader
icon={
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
<circle cx="12" cy="13" r="2" />
<path d="M8.5 18a3.5 3.5 0 017 0" />
</svg>
}
title="Shared with me"
meta="Files and folders other people gave you access to"
/>
<div className="aether-content" style={{ flex: 1, overflowY: 'auto' }}>
{loading ? (
<p style={{ fontSize: 13, color: 'var(--color-text-muted)' }}>Loading…</p>
) : items.length === 0 ? (
<div style={{
padding: 40, textAlign: 'center', borderRadius: 12,
border: '1px dashed var(--color-border)',
}}>
<p style={{ margin: 0, fontSize: 14, color: 'var(--color-text)' }}>Nothing shared with you yet</p>
<p style={{ margin: '6px 0 0', fontSize: 12, color: 'var(--color-text-muted)' }}>
When someone shares a file or folder, it shows up here.
</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, maxWidth: 760 }}>
{items.map(item => (
<div
key={`${item.type}-${item.id}`}
onClick={() => item.type === 'folder' && onOpenFolder(item.id, item.name)}
style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '13px 16px', borderRadius: 11,
border: '1px solid var(--color-border)',
background: 'var(--color-surface)',
cursor: item.type === 'folder' ? 'pointer' : 'default',
}}
onMouseEnter={e => { if (item.type === 'folder') e.currentTarget.style.borderColor = 'var(--color-primary)' }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)' }}
>
<span style={{ flexShrink: 0, display: 'flex' }}>
{item.type === 'folder' ? (
<FolderGlyph size={24} />
) : (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-muted)" strokeWidth="1.8">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
)}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: 14, fontWeight: 600, color: 'var(--color-text)',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{item.name}
</div>
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2 }}>
Shared by {item.owner_name}
{/* Ohne diesen Hinweis wundert man sich, woher eine Freigabe
kommt, die man nie persönlich bekommen hat. */}
{item.via_group && ` via ${item.via_group}`}
{item.type === 'file' && ` · ${formatBytes(item.size)}`}
</div>
</div>
<span style={{
flexShrink: 0, padding: '3px 10px', borderRadius: 999,
fontSize: 11, fontWeight: 600,
background: item.permission === 'write'
? 'rgba(var(--color-primary-rgb), 0.12)'
: 'var(--color-surface-2)',
color: item.permission === 'write' ? 'var(--color-primary)' : 'var(--color-text-muted)',
}}>
{item.permission === 'write' ? 'Can edit' : 'View only'}
</span>
{item.type === 'file' && (
<a
href={filesApi.download(item.id)}
download={item.name}
onClick={e => e.stopPropagation()}
style={{
flexShrink: 0, padding: '5px 12px', borderRadius: 7,
border: '1px solid var(--color-border)',
color: 'var(--color-text)', fontSize: 12, textDecoration: 'none',
}}
>
Download
</a>
)}
</div>
))}
</div>
)}
</div>
</div>
)
}