Files
probably-rendering-one-more…/apps/wiki/src/components/ExploreDetail.tsx
T
codex-bot 7f37aa4c93
wiki CI and deployment / verify-and-deploy (push) Failing after 25m11s
Add full-content discovery search and backlink ranking to waterfall-wiki
2026-10-26 20:20:55 +08:00

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>;
}