diff --git a/frontend/src/components/PostCard.astro b/frontend/src/components/PostCard.astro deleted file mode 100644 index 03fe162..0000000 --- a/frontend/src/components/PostCard.astro +++ /dev/null @@ -1,55 +0,0 @@ ---- -interface Props { - slug: string; - date: string; - title?: string; - excerpt?: string; - tags?: string[]; - draft?: boolean; - readingTime: number; - formatSlug: (slug: string) => string; -} - -const { slug, date, title, excerpt, tags = [], draft = false, readingTime, formatSlug } = Astro.props; -const displayTitle = title || formatSlug(slug); - -const formattedDate = new Date(date).toLocaleDateString('en-US', { - year: 'numeric', month: 'short', day: 'numeric' -}); ---- - - -
-
-
- - · - {readingTime} min read - {draft && ( - <> - · - Draft - - )} -
-

- {displayTitle} -

-

- {excerpt || `Read more about ${displayTitle}...`} -

- {tags.length > 0 && ( -
- {tags.map(tag => ( - - {tag} - - ))} -
- )} -
- -
-
diff --git a/frontend/src/components/react/PostList.tsx b/frontend/src/components/react/PostList.tsx new file mode 100644 index 0000000..7374491 --- /dev/null +++ b/frontend/src/components/react/PostList.tsx @@ -0,0 +1,207 @@ +import { useMemo, useState } from 'react'; + +interface Post { + slug: string; + date: string; + title?: string; + excerpt?: string; + tags: string[]; + draft: boolean; + reading_time: number; +} + +interface Props { + posts: Post[]; +} + +function formatSlug(slug: string) { + if (!slug) return ''; + return slug + .split('-') + .map(w => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +function formatDate(date: string) { + return new Date(date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +} + +export default function PostList({ posts }: Props) { + const [query, setQuery] = useState(''); + const [activeTags, setActiveTags] = useState>(new Set()); + + const allTags = useMemo(() => { + const set = new Set(); + for (const p of posts) for (const t of p.tags || []) set.add(t); + return Array.from(set).sort((a, b) => a.localeCompare(b)); + }, [posts]); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + return posts.filter(p => { + if (activeTags.size > 0) { + for (const t of activeTags) if (!p.tags?.includes(t)) return false; + } + if (!q) return true; + const hay = `${p.title || formatSlug(p.slug)} ${p.excerpt || ''}`.toLowerCase(); + return hay.includes(q); + }); + }, [posts, query, activeTags]); + + function toggleTag(tag: string) { + setActiveTags(prev => { + const next = new Set(prev); + if (next.has(tag)) next.delete(tag); + else next.add(tag); + return next; + }); + } + + return ( +
+
+
+ + setQuery(e.target.value)} + placeholder="Search posts…" + aria-label="Search posts" + className="w-full bg-crust border border-surface1 rounded-lg pl-9 pr-3 py-2 text-sm md:text-base text-text placeholder:text-subtext0 focus:outline-none focus:border-mauve transition-colors" + /> +
+ {allTags.length > 0 && ( +
+ {allTags.map(tag => { + const active = activeTags.has(tag); + return ( + + ); + })} + {activeTags.size > 0 && ( + + )} +
+ )} +
+ + {filtered.length === 0 ? ( +
+ No posts match. +
+ ) : ( +
+ {filtered.map(post => { + const displayTitle = post.title || formatSlug(post.slug); + return ( +
+ +
+
+
+ + · + {post.reading_time} min read + {post.draft && ( + <> + · + + Draft + + + )} +
+

+ {displayTitle} +

+

+ {post.excerpt || `Read more about ${displayTitle}...`} +

+
+
+ + + + +
+
+
+ {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map(tag => { + const active = activeTags.has(tag); + return ( + + ); + })} +
+ )} +
+ ); + })} +
+ )} +
+ ); +} diff --git a/frontend/src/components/react/admin/Editor.tsx b/frontend/src/components/react/admin/Editor.tsx index d328e61..7b65b02 100644 --- a/frontend/src/components/react/admin/Editor.tsx +++ b/frontend/src/components/react/admin/Editor.tsx @@ -86,6 +86,7 @@ export default function Editor({ editSlug }: Props) { const [alert, setAlert] = useState<{ msg: string; type: 'success' | 'error' } | null>(null); const [showModal, setShowModal] = useState(false); const [showPreview, setShowPreview] = useState(false); + const [mobileView, setMobileView] = useState<'edit' | 'preview'>('edit'); const [vimEnabled, setVimEnabled] = useState(() => typeof window !== 'undefined' && window.innerWidth > 768 ); @@ -423,10 +424,46 @@ export default function Editor({ editSlug }: Props) { - {/* Editor + Preview — both columns stretch to the taller one */} -
-
-
+ {/* Mobile-only Edit | Preview tab bar */} + {showPreview && ( +
+ + +
+ )} + + {/* Editor + Preview — desktop side-by-side, mobile single-pane via tabs */} +
+
+
{/* Autocomplete dropdown */} {showAutocomplete && autocompleteAssets.length > 0 && ( @@ -461,9 +498,13 @@ export default function Editor({ editSlug }: Props) { )}
- {/* Live Preview — stretches to match editor height */} + {/* Live Preview — stretches to match editor height on desktop, full-pane via tabs on mobile */} {showPreview && ( -
+
Preview
diff --git a/frontend/src/layouts/Layout.astro b/frontend/src/layouts/Layout.astro index 82db0af..10cdc06 100644 --- a/frontend/src/layouts/Layout.astro +++ b/frontend/src/layouts/Layout.astro @@ -84,7 +84,6 @@ const fullTitle = `${title} | ${siteConfig.title}`;
diff --git a/frontend/src/pages/index.astro b/frontend/src/pages/index.astro index 7c42e84..92946ae 100644 --- a/frontend/src/pages/index.astro +++ b/frontend/src/pages/index.astro @@ -1,6 +1,6 @@ --- import Layout from '../layouts/Layout.astro'; -import PostCard from '../components/PostCard.astro'; +import PostList from '../components/react/PostList'; const API_URL = process.env.PUBLIC_API_URL || 'http://localhost:3000'; @@ -41,14 +41,6 @@ try { error = `Could not connect to backend at ${API_URL}: ${e instanceof Error ? e.message : String(e)}${cause ? ' (Cause: ' + (cause.message || cause.code || JSON.stringify(cause)) + ')' : ''}`; console.error(error); } - -function formatSlug(slug: string) { - if (!slug) return ''; - return slug - .split('-') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); -} --- @@ -75,18 +67,7 @@ function formatSlug(slug: string) {
)} - {posts.map((post) => ( - - ))} + {posts.length > 0 && }