mirror of
https://github.com/thegeneralist01/archivr
synced 2026-10-09 21:03:17 +02:00
feat(frontend): preview text-capture entries (.md / .txt)
Text captures land as `.md` (Markdown) or `.txt` (plain) blobs, but PreviewPanel only dispatched on video/audio/image/pdf/html extensions, so opening a text entry hit the 'No preview available' fallback with the raw artifact path exposed. - New `TextPreview` component fetches the primary artifact as text, renders it in a monospace `<pre>` with word-wrap, and shows the entry title on top and the MIME as a small trailing tag. Handles loading/error states. - `PreviewPanel` gains a `TEXT_EXTS` set + a branch that dispatches to `TextPreview` for `md` / `markdown` / `txt`. - CSS is padded and centered to ~780px so a text note reads like a document rather than an edge-to-edge terminal dump. v1 intentionally does NOT parse Markdown: keeping frontend deps at react+react-dom only. Bump to a real Markdown renderer if we start capturing Markdown-authored notes.
This commit is contained in:
parent
067d766819
commit
e8bb03e08f
6 changed files with 114 additions and 5 deletions
|
|
@ -2,10 +2,12 @@ import VideoPreview from './VideoPreview';
|
|||
import IframePreview from './IframePreview';
|
||||
import ImagePreview from './ImagePreview';
|
||||
import TweetPreview from './TweetPreview';
|
||||
import TextPreview from './TextPreview';
|
||||
|
||||
const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'mkv', 'avi', 'm4v', 'ogv']);
|
||||
const AUDIO_EXTS = new Set(['mp3', 'ogg', 'm4a', 'opus', 'wav', 'flac', 'aac']);
|
||||
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg', 'bmp']);
|
||||
const TEXT_EXTS = new Set(['md', 'markdown', 'txt']);
|
||||
const CONTENT_TYPES = {
|
||||
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',
|
||||
mkv: 'video/x-matroska', avi: 'video/x-msvideo', m4v: 'video/mp4', ogv: 'video/ogg',
|
||||
|
|
@ -131,7 +133,20 @@ export default function PreviewPanel({ archiveId, entry, detail, fullPage, onXAr
|
|||
);
|
||||
}
|
||||
|
||||
// 7. Image
|
||||
// 6b. Plain text / Markdown
|
||||
if (TEXT_EXTS.has(ext)) {
|
||||
return (
|
||||
<div className="preview-panel" style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
||||
<TextPreview
|
||||
src={primaryMediaUrl}
|
||||
mime={primaryArtifact.mime_type}
|
||||
title={summary.title}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 7. Image
|
||||
if (IMAGE_EXTS.has(ext)) {
|
||||
return (
|
||||
<div className="preview-panel" style={{ height: '100%' }}>
|
||||
|
|
|
|||
46
frontend/src/components/TextPreview.jsx
Normal file
46
frontend/src/components/TextPreview.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Renders the primary_media artifact of a text/document entry as plain text.
|
||||
*
|
||||
* v1 intentionally does NOT render Markdown — we don't want a Markdown parser
|
||||
* dep just to unblock the "no preview available" fallback, and monospace text
|
||||
* with visible fences reads fine for the note-length content this feature
|
||||
* captures. Bump to a real renderer if/when Markdown-authored entries grow.
|
||||
*/
|
||||
export default function TextPreview({ src, mime, title }) {
|
||||
const [text, setText] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setText(null)
|
||||
setError(null)
|
||||
fetch(src, { credentials: 'same-origin' })
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
return r.text()
|
||||
})
|
||||
.then(t => { if (!cancelled) setText(t) })
|
||||
.catch(e => { if (!cancelled) setError(e.message || String(e)) })
|
||||
return () => { cancelled = true }
|
||||
}, [src])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-preview text-preview--error">
|
||||
Failed to load text: {error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (text === null) {
|
||||
return <div className="text-preview text-preview--loading">Loading…</div>
|
||||
}
|
||||
return (
|
||||
<div className="text-preview">
|
||||
{title && <h1 className="text-preview__title">{title}</h1>}
|
||||
<pre className="text-preview__body">{text}</pre>
|
||||
{mime && <div className="text-preview__mime">{mime}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -3285,3 +3285,51 @@ body.has-audio-bar { padding-bottom: 56px; }
|
|||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ── Text / Markdown preview ────────────────────────────────────────────── */
|
||||
.text-preview {
|
||||
padding: 32px 40px 48px;
|
||||
max-width: 780px;
|
||||
margin: 0 auto;
|
||||
font-family: var(--sans);
|
||||
color: var(--ink);
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.text-preview--loading,
|
||||
.text-preview--error {
|
||||
padding: 24px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.text-preview--error { color: var(--accent); }
|
||||
.text-preview__title {
|
||||
font-family: var(--serif, var(--sans));
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 20px;
|
||||
line-height: 1.25;
|
||||
color: var(--ink);
|
||||
}
|
||||
.text-preview__body {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.65;
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r2);
|
||||
padding: 16px 18px;
|
||||
margin: 0;
|
||||
}
|
||||
.text-preview__mime {
|
||||
margin-top: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
letter-spacing: .04em;
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue