Add Recently Added notebooks section to homepage
Server-rendered section between Featured and Explore showing the 4 most recently modified notebooks, excluding featured and untitled ones. Compact card layout with engine badge, cell count, and relative date.
This commit is contained in:
parent
3e68a355c3
commit
365df157b4
92
frontend/src/components/RecentNotebooks.astro
Normal file
92
frontend/src/components/RecentNotebooks.astro
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import type { NotebookSummary } from '../lib/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
notebooks: NotebookSummary[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const FEATURED_IDS = new Set([
|
||||||
|
'rc-lowpass-filter',
|
||||||
|
'555-astable-blinker',
|
||||||
|
'common-emitter-amplifier',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { notebooks } = Astro.props;
|
||||||
|
|
||||||
|
const recent = notebooks
|
||||||
|
.filter((nb) => !FEATURED_IDS.has(nb.id) && nb.title !== 'Untitled Notebook')
|
||||||
|
.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime())
|
||||||
|
.slice(0, 4);
|
||||||
|
|
||||||
|
function formatDate(iso: string): string {
|
||||||
|
try {
|
||||||
|
return new Date(iso).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return iso;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const engineColor: Record<string, { border: string; bg: string; text: string }> = {
|
||||||
|
ngspice: { border: 'border-blue-500/30', bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
||||||
|
ltspice: { border: 'border-amber-500/30', bg: 'bg-amber-500/10', text: 'text-amber-400' },
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
{recent.length > 0 && (
|
||||||
|
<section class="max-w-6xl mx-auto px-6 py-12">
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<Icon name="lucide:clock" class="w-4 h-4 text-slate-500" />
|
||||||
|
<h2 class="text-lg font-semibold text-slate-200">Recently Added</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{recent.map((nb) => {
|
||||||
|
const colors = engineColor[nb.engine] ?? engineColor.ngspice;
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/notebook/${nb.id}`}
|
||||||
|
class:list={[
|
||||||
|
'group block rounded-lg border bg-slate-900/50 p-4',
|
||||||
|
'hover:border-blue-500/40 hover:bg-slate-800/70',
|
||||||
|
'hover:-translate-y-0.5 hover:shadow-md hover:shadow-black/20',
|
||||||
|
'transition-all duration-200',
|
||||||
|
'border-slate-800',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<h3 class="text-sm font-semibold text-slate-100 group-hover:text-blue-400 transition-colors line-clamp-1">
|
||||||
|
{nb.title}
|
||||||
|
</h3>
|
||||||
|
<span class:list={[
|
||||||
|
'shrink-0 ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium border',
|
||||||
|
colors.border, colors.bg, colors.text,
|
||||||
|
]}>
|
||||||
|
{nb.engine}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{nb.description && (
|
||||||
|
<p class="text-xs text-slate-400 leading-relaxed mb-2 line-clamp-2">
|
||||||
|
{nb.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-[11px] text-slate-500">
|
||||||
|
<span class="inline-flex items-center gap-1">
|
||||||
|
<Icon name="lucide:layers" class="w-3 h-3" />
|
||||||
|
{nb.cell_count} cell{nb.cell_count !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{formatDate(nb.modified)}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
@ -4,6 +4,7 @@ import NotebookLayout from '../layouts/NotebookLayout.astro';
|
|||||||
import NotebookGallery from '../components/NotebookGallery';
|
import NotebookGallery from '../components/NotebookGallery';
|
||||||
import PipelineStrip from '../components/PipelineStrip.astro';
|
import PipelineStrip from '../components/PipelineStrip.astro';
|
||||||
import FeaturedNotebooks from '../components/FeaturedNotebooks.astro';
|
import FeaturedNotebooks from '../components/FeaturedNotebooks.astro';
|
||||||
|
import RecentNotebooks from '../components/RecentNotebooks.astro';
|
||||||
import OscilloscopeDisplay from '../components/OscilloscopeDisplay.astro';
|
import OscilloscopeDisplay from '../components/OscilloscopeDisplay.astro';
|
||||||
import { fetchNotebookList } from '../lib/server-api';
|
import { fetchNotebookList } from '../lib/server-api';
|
||||||
import type { NotebookSummary } from '../lib/types';
|
import type { NotebookSummary } from '../lib/types';
|
||||||
@ -150,6 +151,11 @@ try {
|
|||||||
<FeaturedNotebooks />
|
<FeaturedNotebooks />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Recently Added -->
|
||||||
|
<section class="section-fade">
|
||||||
|
<RecentNotebooks notebooks={notebooks} />
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Notebook Gallery -->
|
<!-- Notebook Gallery -->
|
||||||
<section id="notebooks" class="max-w-6xl mx-auto px-6 py-20 scroll-mt-8">
|
<section id="notebooks" class="max-w-6xl mx-auto px-6 py-20 scroll-mt-8">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user