import React, { useState, useEffect } from 'react'; import { useAppStore, PostData } from '../../store'; import { showToast } from '../Toast'; import './Sidebar.css'; const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }; const formatFileSize = (bytes: number) => { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; }; const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; interface CalendarViewProps { onDateSelect: (year: number, month?: number) => void; selectedYear?: number; selectedMonth?: number; } const CalendarView: React.FC = ({ onDateSelect, selectedYear, selectedMonth }) => { const [yearMonthData, setYearMonthData] = useState<{ year: number; month: number; count: number }[]>([]); const [expandedYear, setExpandedYear] = useState(null); useEffect(() => { const loadData = async () => { const data = await window.electronAPI?.posts.getByYearMonth(); if (data) { setYearMonthData(data as { year: number; month: number; count: number }[]); } }; loadData(); }, []); // Group by year const years = [...new Set(yearMonthData.map(d => d.year))].sort((a, b) => b - a); const getYearCount = (year: number) => { return yearMonthData.filter(d => d.year === year).reduce((sum, d) => sum + d.count, 0); }; const getMonthsForYear = (year: number) => { return yearMonthData.filter(d => d.year === year).sort((a, b) => b.month - a.month); }; return (
ARCHIVE {(selectedYear || selectedMonth !== undefined) && ( )}
{years.map(year => (
{ setExpandedYear(expandedYear === year ? null : year); onDateSelect(year); }} > {expandedYear === year ? '▼' : '▶'} {year} {getYearCount(year)}
{expandedYear === year && (
{getMonthsForYear(year).map(({ month, count }) => (
{ e.stopPropagation(); onDateSelect(year, month); }} > {MONTH_NAMES[month]} {count}
))}
)}
))} {years.length === 0 && (
No posts yet
)}
); }; interface FilterPanelProps { tags: string[]; categories: string[]; selectedTags: string[]; selectedCategories: string[]; onTagSelect: (tags: string[]) => void; onCategorySelect: (categories: string[]) => void; } const FilterPanel: React.FC = ({ tags, categories, selectedTags, selectedCategories, onTagSelect, onCategorySelect, }) => { return (
{tags.length > 0 && (
TAGS
{tags.map(tag => ( ))}
)} {categories.length > 0 && (
CATEGORIES
{categories.map(cat => ( ))}
)}
); }; interface SearchBoxProps { onSearch: (query: string) => void; } const SearchBox: React.FC = ({ onSearch }) => { const [query, setQuery] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSearch(query); }; return (
setQuery(e.target.value)} /> {query && ( )}
); }; const PostsList: React.FC = () => { const { posts, selectedPostId, setSelectedPost } = useAppStore(); // Filter state const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState(null); const [selectedYear, setSelectedYear] = useState(); const [selectedMonth, setSelectedMonth] = useState(); const [selectedTags, setSelectedTags] = useState([]); const [selectedCategories, setSelectedCategories] = useState([]); const [availableTags, setAvailableTags] = useState([]); const [availableCategories, setAvailableCategories] = useState([]); const [showFilters, setShowFilters] = useState(false); const [filteredPosts, setFilteredPosts] = useState(null); // Load available tags and categories useEffect(() => { const loadFilters = async () => { const [tags, categories] = await Promise.all([ window.electronAPI?.posts.getTags(), window.electronAPI?.posts.getCategories(), ]); if (tags) setAvailableTags(tags as string[]); if (categories) setAvailableCategories(categories as string[]); }; loadFilters(); }, [posts]); // Handle search const handleSearch = async (query: string) => { setSearchQuery(query); if (!query.trim()) { setSearchResults(null); return; } try { const results = await window.electronAPI?.posts.search(query); if (results) { // Map search results to PostData (search returns SearchResult with score) const postIds = (results as { id: string }[]).map(r => r.id); setSearchResults(posts.filter(p => postIds.includes(p.id))); } } catch (error) { console.error('Search failed:', error); showToast.error('Search failed'); } }; // Handle date selection const handleDateSelect = async (year: number, month?: number) => { if (year === 0) { // Clear filter setSelectedYear(undefined); setSelectedMonth(undefined); setFilteredPosts(null); return; } setSelectedYear(year); setSelectedMonth(month); try { const results = await window.electronAPI?.posts.filter({ year, month, tags: selectedTags.length > 0 ? selectedTags : undefined, categories: selectedCategories.length > 0 ? selectedCategories : undefined, }); if (results) { setFilteredPosts(results as PostData[]); } } catch (error) { console.error('Filter failed:', error); } }; // Handle tag/category filter changes useEffect(() => { const applyFilters = async () => { if (!selectedYear && selectedTags.length === 0 && selectedCategories.length === 0) { setFilteredPosts(null); return; } try { const results = await window.electronAPI?.posts.filter({ year: selectedYear, month: selectedMonth, tags: selectedTags.length > 0 ? selectedTags : undefined, categories: selectedCategories.length > 0 ? selectedCategories : undefined, }); if (results) { setFilteredPosts(results as PostData[]); } } catch (error) { console.error('Filter failed:', error); } }; applyFilters(); }, [selectedTags, selectedCategories]); const handleCreatePost = async () => { try { const newPost = await window.electronAPI?.posts.create({ title: 'Untitled Post', content: '# New Post\n\nStart writing your content here...', }); if (newPost) { setSelectedPost((newPost as PostData).id); showToast.success('Post created'); } } catch (error) { console.error('Failed to create post:', error); showToast.error('Failed to create post'); } }; // Determine which posts to display const displayPosts = searchResults ?? filteredPosts ?? posts; const isFiltered = searchResults !== null || filteredPosts !== null; const hasActiveFilters = searchQuery || selectedYear || selectedTags.length > 0 || selectedCategories.length > 0; const groupedPosts = { draft: displayPosts.filter(p => p.status === 'draft'), published: displayPosts.filter(p => p.status === 'published'), archived: displayPosts.filter(p => p.status === 'archived'), }; const clearAllFilters = () => { setSearchQuery(''); setSearchResults(null); setSelectedYear(undefined); setSelectedMonth(undefined); setSelectedTags([]); setSelectedCategories([]); setFilteredPosts(null); }; return (
POSTS
{showFilters && ( <> )} {hasActiveFilters && (
{displayPosts.length} result{displayPosts.length !== 1 ? 's' : ''} {searchQuery && ` for "${searchQuery}"`}
)} {groupedPosts.draft.length > 0 && (
Drafts ({groupedPosts.draft.length})
{groupedPosts.draft.map(post => (
setSelectedPost(post.id)} >
{post.title}
{formatDate(post.updatedAt)}
))}
)} {groupedPosts.published.length > 0 && (
Published ({groupedPosts.published.length})
{groupedPosts.published.map(post => (
setSelectedPost(post.id)} >
{post.title}
{formatDate(post.publishedAt || post.updatedAt)}
))}
)} {groupedPosts.archived.length > 0 && (
Archived ({groupedPosts.archived.length})
{groupedPosts.archived.map(post => (
setSelectedPost(post.id)} >
{post.title}
{formatDate(post.updatedAt)}
))}
)} {displayPosts.length === 0 && !isFiltered && (

No posts yet

)} {displayPosts.length === 0 && isFiltered && (

No matching posts

)}
); }; const MediaList: React.FC = () => { const { media, selectedMediaId, setSelectedMedia } = useAppStore(); const handleImportMedia = async () => { try { await window.electronAPI?.media.importDialog(); } catch (error) { console.error('Failed to import media:', error); } }; return (
MEDIA
{media.map(item => (
setSelectedMedia(item.id)} title={item.originalName} > {item.mimeType.startsWith('image/') ? (
{/* Would load actual image in production */}
) : (
)}
{item.originalName}
{formatFileSize(item.size)}
))}
{media.length === 0 && (

No media files

)}
); }; const SettingsPanel: React.FC = () => { const { syncConfigured } = useAppStore(); const [tursoUrl, setTursoUrl] = React.useState(''); const [tursoToken, setTursoToken] = React.useState(''); const handleSaveSync = async () => { try { await window.electronAPI?.sync.configure({ tursoUrl, tursoAuthToken: tursoToken, autoSync: true, syncInterval: 5, }); } catch (error) { console.error('Failed to configure sync:', error); } }; return (
SETTINGS

Cloud Sync (Turso/LibSQL)

setTursoUrl(e.target.value)} />
setTursoToken(e.target.value)} />
{syncConfigured && (

✓ Sync is configured

)}

Data Management

); }; export const Sidebar: React.FC = () => { const { activeView, sidebarVisible } = useAppStore(); if (!sidebarVisible) { return null; } return (
{activeView === 'posts' && } {activeView === 'media' && } {activeView === 'settings' && }
); };