46 lines
6.0 KiB
TypeScript
46 lines
6.0 KiB
TypeScript
import Head from 'next/head';
|
|
import Link from 'next/link';
|
|
import {useEffect,useRef,useState} from 'react';
|
|
import {useRouter} from 'next/router';
|
|
import {X,ChevronLeft,ChevronRight,Link as LinkIcon,FileText,Pencil} from 'lucide-react';
|
|
import type {ExploreEntry} from '@/lib/explore';
|
|
import {previewUrl,exploreUrl} from '@/content/page-links';
|
|
import {Shell} from './Shell';
|
|
import {ExploreFeed} from './ExploreFeed';
|
|
import {NoteMarkdown} from './NoteMarkdown';
|
|
import {NoteEditor} from './NoteEditor';
|
|
export function ExploreDetail({entry:initial,background}:{entry:ExploreEntry;background:ExploreEntry[]}){
|
|
const [entry,setEntry]=useState(initial),[index,setIndex]=useState(0),[copied,setCopied]=useState(''),[editing,setEditing]=useState(false),[copyError,setCopyError]=useState('');
|
|
const router=useRouter();const closeRef=useRef<HTMLButtonElement>(null),detailRef=useRef<HTMLDivElement>(null);const media=entry.media[index];
|
|
useEffect(()=>{setEntry(initial);setIndex(0);setCopied('');setEditing(false)},[initial]);
|
|
const close=()=>{if(editing){setEditing(false);return;}void router.push('/')};
|
|
useEffect(()=>{
|
|
const previous=document.activeElement as HTMLElement|null;if(!editing)closeRef.current?.focus();const old=document.body.style.overflow;document.body.style.overflow='hidden';
|
|
function key(e:KeyboardEvent){
|
|
if(e.key==='Escape'){e.preventDefault();close();return;}
|
|
const typing=e.target instanceof HTMLElement&&!!e.target.closest('input,textarea,select,[contenteditable="true"]');
|
|
if(!editing&&!typing&&e.key==='ArrowLeft')setIndex(n=>Math.max(0,n-1));
|
|
if(!editing&&!typing&&e.key==='ArrowRight')setIndex(n=>Math.max(0,Math.min(entry.media.length-1,n+1)));
|
|
if(e.key==='Tab'){const items=Array.from(detailRef.current?.querySelectorAll<HTMLElement>('a[href],button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled),summary,video,audio')||[]).filter(el=>el.getClientRects().length);if(!items.length)return;const first=items[0],last=items[items.length-1];if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus()}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus()}}
|
|
}
|
|
window.addEventListener('keydown',key);return()=>{document.body.style.overflow=old;window.removeEventListener('keydown',key);previous?.focus()};
|
|
},[entry.id,editing]);
|
|
async function copy(mode:'web'|'agent'){
|
|
try{const url=window.location.origin+exploreUrl(entry.id)+(mode==='agent'?'?agent_mode=True':'');
|
|
if(navigator.clipboard)await navigator.clipboard.writeText(url);else{const input=document.createElement('textarea');input.value=url;document.body.appendChild(input);input.select();const success=document.execCommand('copy');input.remove();if(!success)throw Error('Copy failed');}
|
|
setCopied(mode);setCopyError('');
|
|
}catch{setCopied('');setCopyError('复制失败,请重试。')}
|
|
}
|
|
return <Shell><Head><title>{entry.title} · waterfall-wiki</title><link rel="canonical" href={exploreUrl(entry.id)}/><link rel="alternate" type="text/plain" href={exploreUrl(entry.id)+'?agent_mode=True'}/></Head>
|
|
<div aria-hidden="true" inert><ExploreFeed key={entry.revision} initial={background.map(e=>e.id===entry.id?entry:e)}/></div>
|
|
<div className="note-overlay" onClick={()=>{if(!editing)close()}}><div className="note-dialog" ref={detailRef} role="dialog" aria-modal="true" aria-labelledby="note-title" onClick={e=>e.stopPropagation()}>
|
|
<button className="note-close" ref={closeRef} aria-label={editing?'取消编辑':'关闭详情'} onClick={close}><X/></button>
|
|
<section className={'note-stage '+(!media?'note-text-stage':'')}>
|
|
{media?.kind==='image'?<img key={media.url} src={previewUrl(media.url,1280)} alt={media.title} decoding="async"/>:media?.kind==='video'?<video controls playsInline preload="metadata" src={media.url} poster={media.poster?previewUrl(media.poster,960):undefined}/>:media?.kind==='audio'?<div className="note-audio"><span>♫</span><audio controls preload="metadata" src={media.url}/></div>:<div className="note-title-cover" style={{background:entry.color}}><small>{entry.namespace}</small><strong>{entry.title}</strong><span>灵感笔记</span></div>}
|
|
{entry.media.length>1&&<><button className="note-prev" aria-label="上一张" disabled={index===0} onClick={()=>setIndex(n=>n-1)}><ChevronLeft/></button><button className="note-next" aria-label="下一张" disabled={index===entry.media.length-1} onClick={()=>setIndex(n=>n+1)}><ChevronRight/></button><div className="note-dots">{entry.media.map((_,i)=><button key={i} className={i===index?'selected':''} aria-label={'查看第 '+(i+1)+' 张'} aria-pressed={i===index} onClick={()=>setIndex(i)}/>)}</div><span className="note-count">{index+1} / {entry.media.length}</span></>}
|
|
</section><section className="note-info"><div className="note-content">
|
|
{editing?<NoteEditor key={entry.id} entry={entry} onCancel={()=>setEditing(false)} onSaved={updated=>{setEntry(updated);setEditing(false);setCopied('')}}/>:<><h1 id="note-title">{entry.title}</h1><p className="note-description">{entry.description}</p><div className="note-tags">{entry.categories.map(c=><Link key={c} href={'/categories?category='+encodeURIComponent(c)}>#{c} </Link>)}</div><NoteMarkdown text={entry.markdown.replace(/^# [^\n]*(?:\n|$)\n?/,'')}/><section id="backlinks" className="note-backlinks"><h2>被 {entry.backlinkCount??0} 篇笔记引用</h2>{entry.backlinks?.length?<ul>{entry.backlinks.map(ref=><li key={ref.id}><Link href={exploreUrl(ref.id)}>{ref.title}</Link></li>)}</ul>:<p>还没有笔记引用本文。</p>}</section><div className="note-id">内容 ID · {entry.id}</div></>}
|
|
</div>{!editing&&<footer><button onClick={()=>void copy('web')}><LinkIcon size={18}/>{copied==='web'?'网页链接已复制':'复制网页链接'}</button><button onClick={()=>void copy('agent')}><FileText size={18}/>{copied==='agent'?'纯文本链接已复制':'复制纯文本链接'}</button><button onClick={()=>setEditing(true)}><Pencil size={17}/>编辑</button>{copyError&&<span role="status">{copyError}</span>}</footer>}</section>
|
|
</div></div></Shell>;
|
|
}
|