|
|
|
@ -17,8 +17,23 @@ import type { Synthesis, NewsItem as NewsItemType, ArticleHistoryEntry } from '~
|
|
|
|
import { extractWeekNumber, formatDateLong } from '~/utils/dates';
|
|
|
|
import { extractWeekNumber, formatDateLong } from '~/utils/dates';
|
|
|
|
import LoadingSpinner from '~/components/ui/LoadingSpinner';
|
|
|
|
import LoadingSpinner from '~/components/ui/LoadingSpinner';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Truncate text to approximately N lines (assuming ~80 chars per line). */
|
|
|
|
|
|
|
|
function truncateSummary(text: string, level: number): string {
|
|
|
|
|
|
|
|
if (level >= 4 || !text) return text;
|
|
|
|
|
|
|
|
if (level === 1) return '';
|
|
|
|
|
|
|
|
if (level === 2) {
|
|
|
|
|
|
|
|
// ~2 lines ≈ 160 chars
|
|
|
|
|
|
|
|
if (text.length <= 160) return text;
|
|
|
|
|
|
|
|
return text.slice(0, 157) + '...';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// level 3: half
|
|
|
|
|
|
|
|
const half = Math.ceil(text.length / 2);
|
|
|
|
|
|
|
|
if (text.length <= half + 3) return text;
|
|
|
|
|
|
|
|
return text.slice(0, half) + '...';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Renders a single news article with its title (linked) and summary. */
|
|
|
|
/** Renders a single news article with its title (linked) and summary. */
|
|
|
|
const NewsItemCard: Component<{ item: NewsItemType }> = (props) => {
|
|
|
|
const NewsItemCard: Component<{ item: NewsItemType; displayLevel: number }> = (props) => {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-100 p-6 hover:shadow-md transition-shadow">
|
|
|
|
<div class="bg-white rounded-lg shadow-sm border border-gray-100 p-6 hover:shadow-md transition-shadow">
|
|
|
|
<h3 class="text-lg font-semibold text-indigo-700 mb-2">
|
|
|
|
<h3 class="text-lg font-semibold text-indigo-700 mb-2">
|
|
|
|
@ -35,24 +50,26 @@ const NewsItemCard: Component<{ item: NewsItemType }> = (props) => {
|
|
|
|
<Show when={props.item.date}>
|
|
|
|
<Show when={props.item.date}>
|
|
|
|
<p class="text-xs text-gray-400 mb-1">{props.item.date}</p>
|
|
|
|
<p class="text-xs text-gray-400 mb-1">{props.item.date}</p>
|
|
|
|
</Show>
|
|
|
|
</Show>
|
|
|
|
<p class="text-gray-700 leading-relaxed text-sm">
|
|
|
|
<Show when={props.displayLevel > 1 && props.item.summary}>
|
|
|
|
{props.item.summary}
|
|
|
|
<p class={`text-gray-700 leading-relaxed text-sm ${props.displayLevel < 4 ? 'text-gray-500' : ''}`}>
|
|
|
|
|
|
|
|
{truncateSummary(props.item.summary, props.displayLevel)}
|
|
|
|
</p>
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** Renders a titled category section containing a list of {@link NewsItemCard}s. */
|
|
|
|
/** Renders a titled category section containing a list of {@link NewsItemCard}s. */
|
|
|
|
const Section: Component<{ title: string; items: NewsItemType[] }> = (props) => {
|
|
|
|
const Section: Component<{ title: string; items: NewsItemType[]; displayLevel: number }> = (props) => {
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<Show when={props.items && props.items.length > 0}>
|
|
|
|
<Show when={props.items && props.items.length > 0}>
|
|
|
|
<div class="mb-10">
|
|
|
|
<div class="mb-10">
|
|
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-6 border-b pb-2">
|
|
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-6 border-b pb-2">
|
|
|
|
{props.title}
|
|
|
|
{props.title}
|
|
|
|
</h2>
|
|
|
|
</h2>
|
|
|
|
<div class="space-y-6">
|
|
|
|
<div class={props.displayLevel === 1 ? 'space-y-2' : 'space-y-6'}>
|
|
|
|
<For each={props.items}>
|
|
|
|
<For each={props.items}>
|
|
|
|
{(item) => <NewsItemCard item={item} />}
|
|
|
|
{(item) => <NewsItemCard item={item} displayLevel={props.displayLevel} />}
|
|
|
|
</For>
|
|
|
|
</For>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@ -94,6 +111,9 @@ const SynthesisDetail: Component = () => {
|
|
|
|
if (t) clearTimeout(t);
|
|
|
|
if (t) clearTimeout(t);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Display level for article cards (1=title only, 2=+2 lines, 3=+half, 4=full)
|
|
|
|
|
|
|
|
const [displayLevel, setDisplayLevel] = createSignal(4);
|
|
|
|
|
|
|
|
|
|
|
|
// Export state
|
|
|
|
// Export state
|
|
|
|
const [exportingMarkdown, setExportingMarkdown] = createSignal(false);
|
|
|
|
const [exportingMarkdown, setExportingMarkdown] = createSignal(false);
|
|
|
|
const [exportingPdf, setExportingPdf] = createSignal(false);
|
|
|
|
const [exportingPdf, setExportingPdf] = createSignal(false);
|
|
|
|
@ -402,6 +422,21 @@ const SynthesisDetail: Component = () => {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Sections */}
|
|
|
|
{/* Sections */}
|
|
|
|
|
|
|
|
{/* Display level slider */}
|
|
|
|
|
|
|
|
<div class="mb-8 flex items-center gap-4 bg-gray-50 rounded-lg p-4 border border-gray-200">
|
|
|
|
|
|
|
|
<span class="text-xs text-gray-500 whitespace-nowrap">{t('synthesis.displayCompact')}</span>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="range"
|
|
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
|
|
max="4"
|
|
|
|
|
|
|
|
step="1"
|
|
|
|
|
|
|
|
class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"
|
|
|
|
|
|
|
|
value={displayLevel()}
|
|
|
|
|
|
|
|
onInput={(e) => setDisplayLevel(parseInt(e.currentTarget.value) || 4)}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<span class="text-xs text-gray-500 whitespace-nowrap">{t('synthesis.displayFull')}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-12">
|
|
|
|
<div class="space-y-12">
|
|
|
|
<Show
|
|
|
|
<Show
|
|
|
|
when={synth().sections && synth().sections.length > 0}
|
|
|
|
when={synth().sections && synth().sections.length > 0}
|
|
|
|
@ -412,7 +447,7 @@ const SynthesisDetail: Component = () => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<For each={synth().sections}>
|
|
|
|
<For each={synth().sections}>
|
|
|
|
{(section) => <Section title={section.title} items={section.items} />}
|
|
|
|
{(section) => <Section title={section.title} items={section.items} displayLevel={displayLevel()} />}
|
|
|
|
</For>
|
|
|
|
</For>
|
|
|
|
</Show>
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|