feat: pages shortcut feature
This commit is contained in:
@@ -10,6 +10,12 @@ const PostsIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const PagesIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M4 4h10v4h6v12H4V4zm10 1.5V9h4.5L14 5.5zM7 12h10v1.5H7V12zm0 3h10v1.5H7V15z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const MediaIcon = () => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||
@@ -62,7 +68,7 @@ export const ActivityBar: React.FC = () => {
|
||||
const isImportActive = activeView === 'import' && sidebarVisible;
|
||||
|
||||
// Handle view click - toggle sidebar if clicking on active view, otherwise switch view
|
||||
const handleViewClick = (view: 'posts' | 'media' | 'chat') => {
|
||||
const handleViewClick = (view: 'posts' | 'pages' | 'media' | 'chat') => {
|
||||
if (activeView === view && sidebarVisible) {
|
||||
// Clicking on active view toggles sidebar off
|
||||
toggleSidebar();
|
||||
@@ -118,6 +124,13 @@ export const ActivityBar: React.FC = () => {
|
||||
>
|
||||
<PostsIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`activity-bar-item ${activeView === 'pages' && sidebarVisible ? 'active' : ''}`}
|
||||
onClick={() => handleViewClick('pages')}
|
||||
title="Pages (click again to toggle sidebar)"
|
||||
>
|
||||
<PagesIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`activity-bar-item ${activeView === 'media' && sidebarVisible ? 'active' : ''}`}
|
||||
onClick={() => handleViewClick('media')}
|
||||
|
||||
@@ -57,6 +57,27 @@ const getPostTypeIcon = (categories: string[]): { icon: string; type: string } =
|
||||
|
||||
const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
const PAGE_CATEGORY = 'page';
|
||||
|
||||
const hasPageCategory = (post: PostData): boolean =>
|
||||
post.categories.some((category) => category.toLowerCase() === PAGE_CATEGORY);
|
||||
|
||||
const applyPageFilter = (posts: PostData[], isPagesMode: boolean): PostData[] =>
|
||||
isPagesMode ? posts.filter(hasPageCategory) : posts;
|
||||
|
||||
const mergeWithPageCategory = (categories: string[], isPagesMode: boolean): string[] => {
|
||||
if (!isPagesMode) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
const normalized = new Set(categories.map((category) => category.toLowerCase()));
|
||||
if (normalized.has(PAGE_CATEGORY)) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
return [...categories, PAGE_CATEGORY];
|
||||
};
|
||||
|
||||
interface CalendarViewProps {
|
||||
onDateSelect: (year: number, month?: number) => void;
|
||||
selectedYear?: number;
|
||||
@@ -458,8 +479,16 @@ const SearchBox: React.FC<SearchBoxProps> = ({ onSearch }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PostsList: React.FC = () => {
|
||||
type PostsListMode = 'posts' | 'pages';
|
||||
|
||||
interface PostsListProps {
|
||||
mode: PostsListMode;
|
||||
}
|
||||
|
||||
const PostsList: React.FC<PostsListProps> = ({ mode }) => {
|
||||
const { posts, hasMorePosts, totalPosts, appendPosts, openTab, activeTabId } = useAppStore();
|
||||
const isPagesMode = mode === 'pages';
|
||||
const postSubset = useMemo(() => applyPageFilter(posts, isPagesMode), [posts, isPagesMode]);
|
||||
|
||||
// Filter state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -484,7 +513,14 @@ const PostsList: React.FC = () => {
|
||||
window.electronAPI?.tags.getAll(),
|
||||
]);
|
||||
if (tags) setAvailableTags(tags as string[]);
|
||||
if (categories) setAvailableCategories(categories as string[]);
|
||||
if (categories) {
|
||||
const allCategories = categories as string[];
|
||||
setAvailableCategories(
|
||||
isPagesMode
|
||||
? allCategories.filter((category) => category.toLowerCase() !== PAGE_CATEGORY)
|
||||
: allCategories
|
||||
);
|
||||
}
|
||||
if (allTagsData) {
|
||||
const colorMap = new Map<string, string>();
|
||||
for (const tag of allTagsData as TagData[]) {
|
||||
@@ -516,7 +552,7 @@ const PostsList: React.FC = () => {
|
||||
fullPosts.push(post as PostData);
|
||||
}
|
||||
}
|
||||
setSearchResults(fullPosts);
|
||||
setSearchResults(applyPageFilter(fullPosts, isPagesMode));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
@@ -535,16 +571,18 @@ const PostsList: React.FC = () => {
|
||||
}
|
||||
setSelectedYear(year);
|
||||
setSelectedMonth(month);
|
||||
|
||||
const mergedCategories = mergeWithPageCategory(selectedCategories, isPagesMode);
|
||||
|
||||
try {
|
||||
const results = await window.electronAPI?.posts.filter({
|
||||
year,
|
||||
month,
|
||||
tags: selectedTags.length > 0 ? selectedTags : undefined,
|
||||
categories: selectedCategories.length > 0 ? selectedCategories : undefined,
|
||||
categories: mergedCategories.length > 0 ? mergedCategories : undefined,
|
||||
});
|
||||
if (results) {
|
||||
setFilteredPosts(results as PostData[]);
|
||||
setFilteredPosts(applyPageFilter(results as PostData[], isPagesMode));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Filter failed:', error);
|
||||
@@ -558,23 +596,25 @@ const PostsList: React.FC = () => {
|
||||
setFilteredPosts(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedCategories = mergeWithPageCategory(selectedCategories, isPagesMode);
|
||||
|
||||
try {
|
||||
const results = await window.electronAPI?.posts.filter({
|
||||
year: selectedYear,
|
||||
month: selectedMonth,
|
||||
tags: selectedTags.length > 0 ? selectedTags : undefined,
|
||||
categories: selectedCategories.length > 0 ? selectedCategories : undefined,
|
||||
categories: mergedCategories.length > 0 ? mergedCategories : undefined,
|
||||
});
|
||||
if (results) {
|
||||
setFilteredPosts(results as PostData[]);
|
||||
setFilteredPosts(applyPageFilter(results as PostData[], isPagesMode));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Filter failed:', error);
|
||||
}
|
||||
};
|
||||
applyFilters();
|
||||
}, [selectedTags, selectedCategories]);
|
||||
}, [selectedTags, selectedCategories, selectedYear, selectedMonth, isPagesMode]);
|
||||
|
||||
// Track previous post statuses to detect changes
|
||||
const prevPostStatusMapRef = useRef<Map<string, string>>(new Map());
|
||||
@@ -613,7 +653,7 @@ const PostsList: React.FC = () => {
|
||||
fullPosts.push(post as PostData);
|
||||
}
|
||||
}
|
||||
setSearchResults(fullPosts);
|
||||
setSearchResults(applyPageFilter(fullPosts, isPagesMode));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search refresh failed:', error);
|
||||
@@ -623,15 +663,16 @@ const PostsList: React.FC = () => {
|
||||
} else if (selectedYear || selectedTags.length > 0 || selectedCategories.length > 0) {
|
||||
// Re-run filter
|
||||
const refetchFilters = async () => {
|
||||
const mergedCategories = mergeWithPageCategory(selectedCategories, isPagesMode);
|
||||
try {
|
||||
const results = await window.electronAPI?.posts.filter({
|
||||
year: selectedYear,
|
||||
month: selectedMonth,
|
||||
tags: selectedTags.length > 0 ? selectedTags : undefined,
|
||||
categories: selectedCategories.length > 0 ? selectedCategories : undefined,
|
||||
categories: mergedCategories.length > 0 ? mergedCategories : undefined,
|
||||
});
|
||||
if (results) {
|
||||
setFilteredPosts(results as PostData[]);
|
||||
setFilteredPosts(applyPageFilter(results as PostData[], isPagesMode));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Filter refresh failed:', error);
|
||||
@@ -644,7 +685,7 @@ const PostsList: React.FC = () => {
|
||||
setFilteredPosts(null);
|
||||
}
|
||||
}
|
||||
}, [posts, searchQuery, selectedYear, selectedMonth, selectedTags, selectedCategories]);
|
||||
}, [posts, searchQuery, selectedYear, selectedMonth, selectedTags, selectedCategories, isPagesMode]);
|
||||
|
||||
const handleCreatePost = async () => {
|
||||
// Create a real post immediately in the database with default empty content
|
||||
@@ -691,8 +732,8 @@ const PostsList: React.FC = () => {
|
||||
// Memoized grouping that freshens cached filter results with current store data
|
||||
// This ensures status changes are reflected even when filters are active
|
||||
const groupedPosts = useMemo(
|
||||
() => groupPostsByStatus(posts, filteredDisplayPosts),
|
||||
[posts, filteredDisplayPosts]
|
||||
() => groupPostsByStatus(postSubset, filteredDisplayPosts),
|
||||
[postSubset, filteredDisplayPosts]
|
||||
);
|
||||
|
||||
const clearAllFilters = () => {
|
||||
@@ -718,7 +759,7 @@ const PostsList: React.FC = () => {
|
||||
<div className="sidebar-content">
|
||||
<div className="sidebar-section">
|
||||
<div className="sidebar-section-header">
|
||||
<span>POSTS</span>
|
||||
<span>{isPagesMode ? 'PAGES' : 'POSTS'}</span>
|
||||
<div className="sidebar-actions">
|
||||
<button
|
||||
className={`sidebar-action ${showFilters ? 'active' : ''}`}
|
||||
@@ -855,9 +896,9 @@ const PostsList: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{posts.length === 0 && !isFiltered && (
|
||||
{postSubset.length === 0 && !isFiltered && (
|
||||
<div className="sidebar-empty">
|
||||
<p>No posts yet</p>
|
||||
<p>{isPagesMode ? 'No pages yet' : 'No posts yet'}</p>
|
||||
<button onClick={handleCreatePost}>Create your first post</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -1541,7 +1582,12 @@ export const Sidebar: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
{activeView === 'posts' && <PostsList />}
|
||||
<div style={{ display: activeView === 'posts' ? 'block' : 'none' }}>
|
||||
<PostsList mode="posts" />
|
||||
</div>
|
||||
<div style={{ display: activeView === 'pages' ? 'block' : 'none' }}>
|
||||
<PostsList mode="pages" />
|
||||
</div>
|
||||
{activeView === 'media' && <MediaList />}
|
||||
{activeView === 'settings' && <SettingsNav />}
|
||||
{activeView === 'tags' && <TagsNav />}
|
||||
|
||||
@@ -50,7 +50,7 @@ interface AppState {
|
||||
activeTabId: string | null;
|
||||
|
||||
// UI State
|
||||
activeView: 'posts' | 'media' | 'settings' | 'tags' | 'chat' | 'import';
|
||||
activeView: 'posts' | 'pages' | 'media' | 'settings' | 'tags' | 'chat' | 'import';
|
||||
sidebarVisible: boolean;
|
||||
panelVisible: boolean;
|
||||
selectedPostId: string | null;
|
||||
@@ -96,7 +96,7 @@ interface AppState {
|
||||
restoreTabState: (state: TabState) => void;
|
||||
|
||||
// Actions
|
||||
setActiveView: (view: 'posts' | 'media' | 'settings' | 'tags' | 'chat' | 'import') => void;
|
||||
setActiveView: (view: 'posts' | 'pages' | 'media' | 'settings' | 'tags' | 'chat' | 'import') => void;
|
||||
toggleSidebar: () => void;
|
||||
togglePanel: () => void;
|
||||
setSelectedPost: (id: string | null) => void;
|
||||
|
||||
Reference in New Issue
Block a user