fix: better handling of many posts

This commit is contained in:
2026-02-10 22:48:13 +01:00
parent 7e4457c15d
commit 6bbf13dd41
10 changed files with 285 additions and 24 deletions

View File

@@ -33,10 +33,11 @@ const App: React.FC = () => {
// First, get active project to set the correct context in backend engines
await window.electronAPI?.projects.getActive();
// Load posts (now with correct project context)
const posts = await window.electronAPI?.posts.getAll();
if (posts) {
setPosts(posts as PostData[]);
// Load posts (now with correct project context, limited to 500)
const postsResult = await window.electronAPI?.posts.getAll({ limit: 500, offset: 0 });
if (postsResult) {
const { items, hasMore, total } = postsResult as { items: PostData[]; hasMore: boolean; total: number };
setPosts(items, hasMore, total);
}
// Load media

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import MonacoEditor from '@monaco-editor/react';
import { useAppStore, PostData, EditorMode } from '../../store';
import { useAppStore, PostData, EditorMode, MediaData } from '../../store';
import { showToast } from '../Toast';
import { WysiwygEditor } from '../WysiwygEditor';
import { Lightbox, useMarkdownImages } from '../Lightbox';
@@ -9,6 +9,60 @@ import { ErrorModal } from '../ErrorModal';
import { SettingsView } from '../SettingsView';
import './Editor.css';
/**
* Resolves media references in markdown content to bds-media:// URLs
* Matches images by:
* 1. Media ID in the path (e.g., /media/2025/01/{id}.jpg)
* 2. Original filename (e.g., image.jpg)
* 3. Filename pattern (e.g., {id}.jpg)
*/
const resolveMediaUrls = (content: string, mediaList: MediaData[]): string => {
if (!content || mediaList.length === 0) return content;
// Build lookup maps for efficient matching
const byId = new Map<string, string>();
const byOriginalName = new Map<string, string>();
const byFilename = new Map<string, string>();
for (const m of mediaList) {
byId.set(m.id, m.id);
byOriginalName.set(m.originalName.toLowerCase(), m.id);
byFilename.set(m.filename.toLowerCase(), m.id);
}
// Replace image URLs in markdown
return content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
// Skip if already using bds-media protocol or external URLs
if (src.startsWith('bds-media://') || src.startsWith('http://') || src.startsWith('https://')) {
return match;
}
// Extract the filename from the path
const filename = src.split('/').pop() || '';
const filenameWithoutExt = filename.replace(/\.[^.]+$/, '');
const filenameLower = filename.toLowerCase();
// Try to match by:
// 1. UUID in path (the file is named by ID)
if (byId.has(filenameWithoutExt)) {
return `![${alt}](bds-media://${filenameWithoutExt})`;
}
// 2. Filename lookup
if (byFilename.has(filenameLower)) {
return `![${alt}](bds-media://${byFilename.get(filenameLower)})`;
}
// 3. Original name lookup
if (byOriginalName.has(filenameLower)) {
return `![${alt}](bds-media://${byOriginalName.get(filenameLower)})`;
}
// No match found, return original
return match;
});
};
// Simple markdown to HTML converter for preview
const markdownToHtml = (markdown: string): string => {
return markdown
@@ -52,6 +106,7 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
preferredEditorMode,
setPreferredEditorMode,
showErrorModal,
media,
} = useAppStore();
const [title, setTitle] = useState(post.title);
@@ -88,8 +143,11 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
}
}, []);
// Extract images from content for lightbox
const images = useMarkdownImages(content);
// Resolve media URLs in content for display
const resolvedContent = useMemo(() => resolveMediaUrls(content, media), [content, media]);
// Extract images from resolved content for lightbox
const images = useMarkdownImages(resolvedContent);
// Track latest values for auto-save on unmount/switch
const pendingChangesRef = useRef<{
@@ -505,7 +563,7 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
<div className="editor-preview markdown-body">
<div
className="preview-content"
dangerouslySetInnerHTML={{ __html: markdownToHtml(content) }}
dangerouslySetInnerHTML={{ __html: markdownToHtml(resolvedContent) }}
/>
</div>
)}

View File

@@ -145,6 +145,34 @@
margin-bottom: 16px;
}
/* Load More Button */
.sidebar-load-more {
padding: 12px 16px;
display: flex;
justify-content: center;
}
.load-more-button {
width: 100%;
padding: 8px 16px;
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.load-more-button:hover:not(:disabled) {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.load-more-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Media Grid */
.media-grid {
display: grid;

View File

@@ -222,7 +222,7 @@ const SearchBox: React.FC<SearchBoxProps> = ({ onSearch }) => {
};
const PostsList: React.FC = () => {
const { posts, selectedPostId, setSelectedPost } = useAppStore();
const { posts, selectedPostId, setSelectedPost, hasMorePosts, totalPosts, appendPosts } = useAppStore();
// Filter state
const [searchQuery, setSearchQuery] = useState('');
@@ -235,6 +235,7 @@ const PostsList: React.FC = () => {
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
const [showFilters, setShowFilters] = useState(false);
const [filteredPosts, setFilteredPosts] = useState<PostData[] | null>(null);
const [isLoadingMore, setIsLoadingMore] = useState(false);
// Load available tags and categories
useEffect(() => {
@@ -339,6 +340,24 @@ const PostsList: React.FC = () => {
}
};
const handleLoadMore = async () => {
if (isLoadingMore || !hasMorePosts) return;
setIsLoadingMore(true);
try {
const postsResult = await window.electronAPI?.posts.getAll({ limit: 500, offset: posts.length });
if (postsResult) {
const { items, hasMore } = postsResult as { items: PostData[]; hasMore: boolean };
appendPosts(items, hasMore);
}
} catch (error) {
console.error('Failed to load more posts:', error);
showToast.error('Failed to load more posts');
} finally {
setIsLoadingMore(false);
}
};
// Determine which posts to display
const displayPosts = searchResults ?? filteredPosts ?? posts;
const isFiltered = searchResults !== null || filteredPosts !== null;
@@ -510,6 +529,19 @@ const PostsList: React.FC = () => {
<button onClick={clearAllFilters}>Clear filters</button>
</div>
)}
{/* Load More button - only show when not filtering and has more posts */}
{!isFiltered && hasMorePosts && (
<div className="sidebar-load-more">
<button
onClick={handleLoadMore}
disabled={isLoadingMore}
className="load-more-button"
>
{isLoadingMore ? 'Loading...' : `Load more (${posts.length} of ${totalPosts})`}
</button>
</div>
)}
</div>
);
};

View File

@@ -82,6 +82,10 @@ interface AppState {
media: MediaData[];
tasks: TaskProgress[];
// Pagination
hasMorePosts: boolean;
totalPosts: number;
// Track which posts have unsaved changes (by post ID)
dirtyPosts: Set<string>;
@@ -112,7 +116,8 @@ interface AppState {
setSelectedMedia: (id: string | null) => void;
setPreferredEditorMode: (mode: EditorMode) => void;
setPosts: (posts: PostData[]) => void;
setPosts: (posts: PostData[], hasMore?: boolean, total?: number) => void;
appendPosts: (posts: PostData[], hasMore: boolean) => void;
addPost: (post: PostData) => void;
updatePost: (id: string, post: Partial<PostData>) => void;
removePost: (id: string) => void;
@@ -162,6 +167,10 @@ export const useAppStore = create<AppState>()(
media: [],
tasks: [],
// Pagination
hasMorePosts: false,
totalPosts: 0,
// Dirty posts tracking
dirtyPosts: new Set<string>(),
@@ -197,8 +206,12 @@ export const useAppStore = create<AppState>()(
setPreferredEditorMode: (mode) => set({ preferredEditorMode: mode }),
// Post Actions
setPosts: (posts) => set({ posts }),
addPost: (post) => set((state) => ({ posts: [...state.posts, post] })),
setPosts: (posts, hasMore = false, total = 0) => set({ posts, hasMorePosts: hasMore, totalPosts: total }),
appendPosts: (newPosts, hasMore) => set((state) => ({
posts: [...state.posts, ...newPosts],
hasMorePosts: hasMore,
})),
addPost: (post) => set((state) => ({ posts: [post, ...state.posts], totalPosts: state.totalPosts + 1 })),
updatePost: (id, updatedPost) => set((state) => ({
posts: state.posts.map((p) => (p.id === id ? { ...p, ...updatedPost } : p)),
})),