247 lines
7.4 KiB
TypeScript
247 lines
7.4 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Category, Feed, FilterState, Stats } from '../types';
|
|
import {
|
|
Inbox,
|
|
Star,
|
|
Layers,
|
|
Folder,
|
|
FolderOpen,
|
|
Rss,
|
|
ChevronRight,
|
|
ChevronDown,
|
|
} from 'lucide-react';
|
|
|
|
interface SidebarProps {
|
|
filter: FilterState;
|
|
onFilterChange: (newFilter: Partial<FilterState>) => void;
|
|
categories: Category[];
|
|
feeds: Feed[];
|
|
stats: Stats | null;
|
|
}
|
|
|
|
export const Sidebar: React.FC<SidebarProps> = ({
|
|
filter,
|
|
onFilterChange,
|
|
categories,
|
|
feeds,
|
|
stats,
|
|
}) => {
|
|
const [collapsedCats, setCollapsedCats] = useState<Record<string, boolean>>({});
|
|
|
|
const toggleCategoryCollapse = (catId: string, e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
setCollapsedCats((prev) => ({ ...prev, [catId]: !prev[catId] }));
|
|
};
|
|
|
|
// Group feeds by category
|
|
const categorizedFeeds: Record<string, Feed[]> = {};
|
|
const uncategorizedFeeds: Feed[] = [];
|
|
|
|
feeds.forEach((f) => {
|
|
if (f.category_id) {
|
|
if (!categorizedFeeds[f.category_id]) {
|
|
categorizedFeeds[f.category_id] = [];
|
|
}
|
|
categorizedFeeds[f.category_id].push(f);
|
|
} else {
|
|
uncategorizedFeeds.push(f);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<div className="sidebar-scroll-area">
|
|
{/* Core Navigation Items */}
|
|
<div
|
|
className={`sidebar-nav-item ${
|
|
filter.view === 'inbox' && !filter.feedId && !filter.categoryId
|
|
? 'active'
|
|
: ''
|
|
}`}
|
|
onClick={() =>
|
|
onFilterChange({
|
|
view: 'inbox',
|
|
feedId: undefined,
|
|
categoryId: undefined,
|
|
})
|
|
}
|
|
>
|
|
<div className="sidebar-nav-label">
|
|
<Inbox size={16} />
|
|
<span>Inbox</span>
|
|
</div>
|
|
{stats && stats.total_unread > 0 && (
|
|
<span className="badge unread">{stats.total_unread}</span>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className={`sidebar-nav-item ${
|
|
filter.view === 'starred' && !filter.feedId && !filter.categoryId
|
|
? 'active'
|
|
: ''
|
|
}`}
|
|
onClick={() =>
|
|
onFilterChange({
|
|
view: 'starred',
|
|
feedId: undefined,
|
|
categoryId: undefined,
|
|
})
|
|
}
|
|
>
|
|
<div className="sidebar-nav-label">
|
|
<Star size={16} style={{ color: 'var(--star)' }} />
|
|
<span>Starred</span>
|
|
</div>
|
|
{stats && stats.total_starred > 0 && (
|
|
<span className="badge">{stats.total_starred}</span>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className={`sidebar-nav-item ${
|
|
filter.view === 'all' && !filter.feedId && !filter.categoryId
|
|
? 'active'
|
|
: ''
|
|
}`}
|
|
onClick={() =>
|
|
onFilterChange({
|
|
view: 'all',
|
|
feedId: undefined,
|
|
categoryId: undefined,
|
|
})
|
|
}
|
|
>
|
|
<div className="sidebar-nav-label">
|
|
<Layers size={16} />
|
|
<span>All Articles</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Categories Section */}
|
|
{categories.length > 0 && (
|
|
<div style={{ marginTop: '0.6rem' }}>
|
|
<div className="sidebar-section-header">
|
|
<span>Categories</span>
|
|
</div>
|
|
|
|
{categories.map((cat) => {
|
|
const catFeeds = categorizedFeeds[cat.id] || [];
|
|
const isCollapsed = collapsedCats[cat.id];
|
|
const isCatActive = filter.categoryId === cat.id && !filter.feedId;
|
|
const catUnread = catFeeds.reduce((sum, f) => sum + f.unread_count, 0);
|
|
|
|
return (
|
|
<div key={cat.id}>
|
|
<div
|
|
className={`sidebar-nav-item ${isCatActive ? 'active' : ''}`}
|
|
onClick={() =>
|
|
onFilterChange({
|
|
categoryId: cat.id,
|
|
feedId: undefined,
|
|
})
|
|
}
|
|
>
|
|
<div className="sidebar-nav-label">
|
|
<button
|
|
style={{
|
|
background: 'none',
|
|
border: 'none',
|
|
color: 'inherit',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: 0,
|
|
marginRight: -3,
|
|
}}
|
|
onClick={(e) => toggleCategoryCollapse(cat.id, e)}
|
|
>
|
|
{isCollapsed ? <ChevronRight size={13} /> : <ChevronDown size={13} />}
|
|
</button>
|
|
{isCollapsed ? <Folder size={15} /> : <FolderOpen size={15} />}
|
|
<span>{cat.title}</span>
|
|
</div>
|
|
|
|
{catUnread > 0 && <span className="badge unread">{catUnread}</span>}
|
|
</div>
|
|
|
|
{/* Category feeds */}
|
|
{!isCollapsed &&
|
|
catFeeds.map((feed) => {
|
|
const isFeedActive = filter.feedId === feed.id;
|
|
return (
|
|
<div
|
|
key={feed.id}
|
|
className={`feed-item ${isFeedActive ? 'active' : ''}`}
|
|
onClick={() =>
|
|
onFilterChange({
|
|
feedId: feed.id,
|
|
categoryId: undefined,
|
|
})
|
|
}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<Rss size={12} style={{ flexShrink: 0, color: 'var(--text-muted)' }} />
|
|
<span className="feed-title-text" title={feed.title}>
|
|
{feed.title}
|
|
</span>
|
|
</div>
|
|
|
|
{feed.unread_count > 0 && (
|
|
<span className="badge unread">{feed.unread_count}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Uncategorized Feeds */}
|
|
{uncategorizedFeeds.length > 0 && (
|
|
<div style={{ marginTop: '0.6rem' }}>
|
|
<div className="sidebar-section-header">
|
|
<span>Feeds</span>
|
|
</div>
|
|
|
|
{uncategorizedFeeds.map((feed) => {
|
|
const isFeedActive = filter.feedId === feed.id;
|
|
return (
|
|
<div
|
|
key={feed.id}
|
|
className={`sidebar-nav-item ${isFeedActive ? 'active' : ''}`}
|
|
style={{ paddingLeft: '0.65rem' }}
|
|
onClick={() =>
|
|
onFilterChange({
|
|
feedId: feed.id,
|
|
categoryId: undefined,
|
|
})
|
|
}
|
|
>
|
|
<div className="sidebar-nav-label">
|
|
<Rss size={14} style={{ flexShrink: 0, color: 'var(--text-muted)' }} />
|
|
<span className="feed-title-text" title={feed.title}>
|
|
{feed.title}
|
|
</span>
|
|
</div>
|
|
|
|
{feed.unread_count > 0 && (
|
|
<span className="badge unread">{feed.unread_count}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|