src/components/posts-list.tsx 4.9 K raw
1
import {
2
	MoreVertical,
3
	Check,
4
	X,
5
	Trash2,
6
	FolderX,
7
	FolderEdit,
8
} from "lucide-react";
9
import { Button } from "@/components/ui/button";
10
import {
11
	DropdownMenu,
12
	DropdownMenuContent,
13
	DropdownMenuItem,
14
	DropdownMenuTrigger,
15
} from "@/components/ui/dropdown-menu";
16
import { Input } from "@/components/ui/input";
17
18
interface Post {
19
	id: string;
20
	title: string | null;
21
	author: string | null;
22
	feedTitle: string | null;
23
	publishedDate: string | null;
24
	link: string | null;
25
	feedId: string | null;
26
	content: string | null;
27
}
28
29
interface PostsListProps {
30
	posts: Post[];
31
	selectedPostId: string | null;
32
	onPostSelect: (postId: string) => void;
33
	searchQuery: string;
34
	onSearchChange: (query: string) => void;
35
	feedTitle?: string;
36
	isPostRead: (postId: string) => boolean;
37
	onMarkAllAsRead: () => void;
38
	onMarkAllAsUnread: () => void;
39
	onDeleteFeed?: () => void;
40
	onDeleteCategory?: () => void;
41
	onChangeCategory?: () => void;
42
	selectedFeedId?: string | null;
43
	selectedFeedCategory?: string | null;
44
	className?: string;
45
}
46
47
export function PostsList({
48
	posts,
49
	selectedPostId,
50
	onPostSelect,
51
	searchQuery,
52
	onSearchChange,
53
	feedTitle = "All Posts",
54
	isPostRead,
55
	onMarkAllAsRead,
56
	onMarkAllAsUnread,
57
	onDeleteFeed,
58
	onDeleteCategory,
59
	onChangeCategory,
60
	selectedFeedId,
61
	selectedFeedCategory,
62
	className = "",
63
}: PostsListProps) {
64
	return (
65
		<div className={`flex h-screen flex-col border-r ${className}`}>
66
			<div className="gap-2 border-b p-3 flex flex-col">
67
				<div className="flex w-full items-center justify-between gap-2">
68
					<div className="text-foreground text-sm font-semibold truncate">
69
						{feedTitle}
70
					</div>
71
					<div className="flex items-center gap-1">
72
						<span className="text-muted-foreground text-xs whitespace-nowrap">
73
							{posts.length}
74
						</span>
75
						<DropdownMenu>
76
							<DropdownMenuTrigger asChild>
77
								<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
78
									<MoreVertical className="h-4 w-4" />
79
								</Button>
80
							</DropdownMenuTrigger>
81
							<DropdownMenuContent align="end">
82
								<DropdownMenuItem onClick={onMarkAllAsRead}>
83
									<Check className="h-4 w-4 mr-2" />
84
									Mark all as read
85
								</DropdownMenuItem>
86
								<DropdownMenuItem onClick={onMarkAllAsUnread}>
87
									<X className="h-4 w-4 mr-2" />
88
									Mark all as unread
89
								</DropdownMenuItem>
90
								{selectedFeedId &&
91
									selectedFeedId !== "unread" &&
92
									onChangeCategory && (
93
										<DropdownMenuItem onClick={onChangeCategory}>
94
											<FolderEdit className="h-4 w-4 mr-2" />
95
											Change category
96
										</DropdownMenuItem>
97
									)}
98
								{selectedFeedId &&
99
									selectedFeedId !== "unread" &&
100
									onDeleteFeed && (
101
										<DropdownMenuItem
102
											onClick={onDeleteFeed}
103
											className="text-destructive focus:text-destructive"
104
										>
105
											<Trash2 className="h-4 w-4 mr-2" />
106
											Delete feed
107
										</DropdownMenuItem>
108
									)}
109
								{selectedFeedCategory && onDeleteCategory && (
110
									<DropdownMenuItem
111
										onClick={onDeleteCategory}
112
										className="text-destructive focus:text-destructive"
113
									>
114
										<FolderX className="h-4 w-4 mr-2" />
115
										Delete category
116
									</DropdownMenuItem>
117
								)}
118
							</DropdownMenuContent>
119
						</DropdownMenu>
120
					</div>
121
				</div>
122
				<Input
123
					placeholder="Search..."
124
					value={searchQuery}
125
					onChange={(e) => onSearchChange(e.target.value)}
126
					className="h-8"
127
				/>
128
			</div>
129
			<div className="flex-1 overflow-y-auto">
130
				{posts.length === 0 ? (
131
					<div className="p-4 text-center text-sm text-muted-foreground">
132
						No posts found
133
					</div>
134
				) : (
135
					posts.map((post) => {
136
						const isRead = isPostRead(post.id);
137
						return (
138
							<button
139
								type="button"
140
								key={post.id}
141
								onClick={() => onPostSelect(post.id)}
142
								className={`hover:bg-sidebar-accent flex items-start gap-2 border-b px-3 py-3 text-sm text-left w-full last:border-b-0 transition-colors ${
143
									selectedPostId === post.id ? "bg-sidebar-accent" : ""
144
								}`}
145
							>
146
								{/* Unread indicator */}
147
								<div className="flex-shrink-0 pt-1">
148
									{!isRead && (
149
										<div className="size-2 rounded-full bg-primary" />
150
									)}
151
								</div>
152
								{/* Post content */}
153
								<div className="flex flex-col gap-1.5 flex-1 min-w-0">
154
									<span className="font-medium line-clamp-2 leading-snug">
155
										{post.title}
156
									</span>
157
									<div className="flex items-center justify-between">
158
										{(post.feedTitle || post.author) && (
159
											<span className="text-muted-foreground truncate text-xs">
160
												{post.feedTitle || post.author}
161
											</span>
162
										)}
163
										{post.publishedDate && (
164
											<span className="text-muted-foreground text-xs">
165
												{new Date(post.publishedDate).toLocaleDateString()}
166
											</span>
167
										)}
168
									</div>
169
								</div>
170
							</button>
171
						);
172
					})
173
				)}
174
			</div>
175
		</div>
176
	);
177
}