| 1 |
1 |
|
"use client"; |
| 2 |
2 |
|
|
| 3 |
3 |
|
import * as React from "react"; |
| 4 |
|
- |
import { Plus, RotateCw } from "lucide-react"; |
|
4 |
+ |
import { Plus, RotateCw, MoreVertical, Check, X } from "lucide-react"; |
| 5 |
5 |
|
|
| 6 |
6 |
|
import { NavUser } from "@/components/nav-user"; |
| 7 |
7 |
|
import { NavFeeds } from "@/components/nav-feeds"; |
|
| 16 |
16 |
|
SidebarMenu, |
| 17 |
17 |
|
SidebarMenuButton, |
| 18 |
18 |
|
SidebarMenuItem, |
| 19 |
|
- |
useSidebar, |
| 20 |
19 |
|
} from "@/components/ui/sidebar"; |
| 21 |
20 |
|
|
| 22 |
21 |
|
import { Button } from "@/components/ui/button"; |
|
| 30 |
29 |
|
DialogTitle, |
| 31 |
30 |
|
DialogTrigger, |
| 32 |
31 |
|
} from "@/components/ui/dialog"; |
|
32 |
+ |
import { |
|
33 |
+ |
DropdownMenu, |
|
34 |
+ |
DropdownMenuContent, |
|
35 |
+ |
DropdownMenuItem, |
|
36 |
+ |
DropdownMenuTrigger, |
|
37 |
+ |
} from "@/components/ui/dropdown-menu"; |
| 33 |
38 |
|
import { Input } from "@/components/ui/input"; |
| 34 |
39 |
|
import { Label } from "@/components/ui/label"; |
| 35 |
40 |
|
import { toast } from "sonner"; |
|
| 38 |
43 |
|
allPostsQuery, |
| 39 |
44 |
|
postsByFeedQuery, |
| 40 |
45 |
|
allReadStatusesQuery, |
|
46 |
+ |
allReadStatusesWithUnreadQuery, |
| 41 |
47 |
|
useEvolu, |
| 42 |
48 |
|
reset, |
| 43 |
49 |
|
} from "@/lib/evolu"; |
|
| 76 |
82 |
|
const [isAddingFeed, setIsAddingFeed] = React.useState(false); |
| 77 |
83 |
|
const [statusMessage, setStatusMessage] = React.useState(""); |
| 78 |
84 |
|
|
| 79 |
|
- |
const { setOpen } = useSidebar(); |
| 80 |
|
- |
|
| 81 |
85 |
|
const { insert, update } = useEvolu(); |
| 82 |
86 |
|
const allFeeds = useQuery(allFeedsQuery); |
| 83 |
87 |
|
const allReadStatuses = useQuery(allReadStatusesQuery); |
|
88 |
+ |
const allReadStatusesWithUnread = useQuery(allReadStatusesWithUnreadQuery); |
| 84 |
89 |
|
|
| 85 |
90 |
|
// Get posts based on selected feed |
| 86 |
91 |
|
const allPosts = useQuery(allPostsQuery); |
|
| 116 |
121 |
|
// Handle post selection and mark as read |
| 117 |
122 |
|
const handlePostSelect = React.useCallback( |
| 118 |
123 |
|
(postId: string) => { |
| 119 |
|
- |
// Mark as read if not already read |
| 120 |
|
- |
if (!isPostRead(postId)) { |
| 121 |
|
- |
const post = feedPosts.find((p) => p.id === postId); |
| 122 |
|
- |
if (post) { |
| 123 |
|
- |
insert("readStatus", { |
| 124 |
|
- |
postId: postId as any, |
| 125 |
|
- |
feedId: post.feedId, |
| 126 |
|
- |
}); |
| 127 |
|
- |
} |
|
124 |
+ |
// Mark as read |
|
125 |
+ |
const existingStatus = allReadStatuses.find( |
|
126 |
+ |
(status) => status.postId === postId, |
|
127 |
+ |
); |
|
128 |
+ |
const post = feedPosts.find((p) => p.id === postId); |
|
129 |
+ |
|
|
130 |
+ |
if (existingStatus) { |
|
131 |
+ |
// Update existing status to read |
|
132 |
+ |
update("readStatus", { |
|
133 |
+ |
id: existingStatus.id, |
|
134 |
+ |
isRead: true, |
|
135 |
+ |
}); |
|
136 |
+ |
} else if (post && post.feedId) { |
|
137 |
+ |
// Create new read status |
|
138 |
+ |
insert("readStatus", { |
|
139 |
+ |
postId: postId as any, |
|
140 |
+ |
feedId: post.feedId, |
|
141 |
+ |
isRead: true, |
|
142 |
+ |
}); |
| 128 |
143 |
|
} |
|
144 |
+ |
|
| 129 |
145 |
|
// Call the original onPostSelect |
| 130 |
146 |
|
onPostSelect(postId); |
| 131 |
147 |
|
}, |
| 132 |
|
- |
[isPostRead, feedPosts, insert, onPostSelect], |
|
148 |
+ |
[allReadStatuses, feedPosts, insert, update, onPostSelect], |
| 133 |
149 |
|
); |
| 134 |
150 |
|
|
|
151 |
+ |
// Mark all visible posts as read |
|
152 |
+ |
const handleMarkAllAsRead = React.useCallback(() => { |
|
153 |
+ |
let markedCount = 0; |
|
154 |
+ |
filteredPosts.forEach((post) => { |
|
155 |
+ |
const existingStatus = allReadStatusesWithUnread.find( |
|
156 |
+ |
(status) => status.postId === post.id, |
|
157 |
+ |
); |
|
158 |
+ |
|
|
159 |
+ |
if (existingStatus && !existingStatus.isRead) { |
|
160 |
+ |
// Update existing status to read |
|
161 |
+ |
update("readStatus", { |
|
162 |
+ |
id: existingStatus.id, |
|
163 |
+ |
isRead: true, |
|
164 |
+ |
}); |
|
165 |
+ |
markedCount++; |
|
166 |
+ |
} else if (!existingStatus && post.feedId) { |
|
167 |
+ |
// Create new read status |
|
168 |
+ |
insert("readStatus", { |
|
169 |
+ |
postId: post.id as any, |
|
170 |
+ |
feedId: post.feedId, |
|
171 |
+ |
isRead: true, |
|
172 |
+ |
}); |
|
173 |
+ |
markedCount++; |
|
174 |
+ |
} |
|
175 |
+ |
}); |
|
176 |
+ |
toast.success( |
|
177 |
+ |
`Marked ${markedCount} post${markedCount !== 1 ? "s" : ""} as read`, |
|
178 |
+ |
); |
|
179 |
+ |
}, [filteredPosts, allReadStatusesWithUnread, insert, update]); |
|
180 |
+ |
|
|
181 |
+ |
// Mark all visible posts as unread |
|
182 |
+ |
const handleMarkAllAsUnread = React.useCallback(() => { |
|
183 |
+ |
let unmarkedCount = 0; |
|
184 |
+ |
filteredPosts.forEach((post) => { |
|
185 |
+ |
const existingStatus = allReadStatusesWithUnread.find( |
|
186 |
+ |
(status) => status.postId === post.id, |
|
187 |
+ |
); |
|
188 |
+ |
|
|
189 |
+ |
if (existingStatus && existingStatus.isRead) { |
|
190 |
+ |
// Update existing status to unread |
|
191 |
+ |
update("readStatus", { |
|
192 |
+ |
id: existingStatus.id, |
|
193 |
+ |
isRead: false, |
|
194 |
+ |
}); |
|
195 |
+ |
unmarkedCount++; |
|
196 |
+ |
} else if (!existingStatus && post.feedId) { |
|
197 |
+ |
// Create new unread status |
|
198 |
+ |
insert("readStatus", { |
|
199 |
+ |
postId: post.id as any, |
|
200 |
+ |
feedId: post.feedId, |
|
201 |
+ |
isRead: false, |
|
202 |
+ |
}); |
|
203 |
+ |
unmarkedCount++; |
|
204 |
+ |
} |
|
205 |
+ |
}); |
|
206 |
+ |
toast.success( |
|
207 |
+ |
`Marked ${unmarkedCount} post${unmarkedCount !== 1 ? "s" : ""} as unread`, |
|
208 |
+ |
); |
|
209 |
+ |
}, [filteredPosts, allReadStatusesWithUnread, insert, update]); |
|
210 |
+ |
|
| 135 |
211 |
|
async function addFeed() { |
| 136 |
212 |
|
if (!urlInput.trim()) { |
| 137 |
213 |
|
setStatusMessage("Please enter a URL"); |
|
| 242 |
318 |
|
category: categoryInput || "Uncategorized", |
| 243 |
319 |
|
dateUpdated: new Date().toISOString(), |
| 244 |
320 |
|
}); |
|
321 |
+ |
|
|
322 |
+ |
if (!result.ok) { |
|
323 |
+ |
throw new Error("Failed to insert feed"); |
|
324 |
+ |
} |
| 245 |
325 |
|
|
| 246 |
326 |
|
// Process posts/entries |
| 247 |
327 |
|
for (const post of posts) { |
|
| 382 |
462 |
|
{/* Posts List Panel - Separate from main sidebar */} |
| 383 |
463 |
|
<div className="bg-sidebar text-sidebar-foreground hidden md:flex overflow-y-scroll h-screen w-[320px] flex-col border-r"> |
| 384 |
464 |
|
<div className="gap-2 border-b p-3 flex flex-col"> |
| 385 |
|
- |
<div className="flex w-full items-center justify-between"> |
|
465 |
+ |
<div className="flex w-full items-center justify-between gap-2"> |
| 386 |
466 |
|
<div className="text-foreground text-sm font-semibold truncate"> |
| 387 |
467 |
|
{selectedFeedId |
| 388 |
468 |
|
? allFeeds.find((f) => f.id === selectedFeedId)?.title || |
| 389 |
469 |
|
"Posts" |
| 390 |
470 |
|
: "All Posts"} |
| 391 |
471 |
|
</div> |
| 392 |
|
- |
<span className="text-muted-foreground text-xs whitespace-nowrap ml-2"> |
| 393 |
|
- |
{filteredPosts.length} |
| 394 |
|
- |
</span> |
|
472 |
+ |
<div className="flex items-center gap-1"> |
|
473 |
+ |
<span className="text-muted-foreground text-xs whitespace-nowrap"> |
|
474 |
+ |
{filteredPosts.length} |
|
475 |
+ |
</span> |
|
476 |
+ |
<DropdownMenu> |
|
477 |
+ |
<DropdownMenuTrigger asChild> |
|
478 |
+ |
<Button variant="ghost" size="sm" className="h-6 w-6 p-0"> |
|
479 |
+ |
<MoreVertical className="h-4 w-4" /> |
|
480 |
+ |
</Button> |
|
481 |
+ |
</DropdownMenuTrigger> |
|
482 |
+ |
<DropdownMenuContent align="end"> |
|
483 |
+ |
<DropdownMenuItem onClick={handleMarkAllAsRead}> |
|
484 |
+ |
<Check className="h-4 w-4 mr-2" /> |
|
485 |
+ |
Mark all as read |
|
486 |
+ |
</DropdownMenuItem> |
|
487 |
+ |
<DropdownMenuItem onClick={handleMarkAllAsUnread}> |
|
488 |
+ |
<X className="h-4 w-4 mr-2" /> |
|
489 |
+ |
Mark all as unread |
|
490 |
+ |
</DropdownMenuItem> |
|
491 |
+ |
</DropdownMenuContent> |
|
492 |
+ |
</DropdownMenu> |
|
493 |
+ |
</div> |
| 395 |
494 |
|
</div> |
| 396 |
495 |
|
<Input |
| 397 |
496 |
|
placeholder="Search..." |
|
| 410 |
509 |
|
const isRead = isPostRead(post.id); |
| 411 |
510 |
|
return ( |
| 412 |
511 |
|
<button |
|
512 |
+ |
type="button" |
| 413 |
513 |
|
key={post.id} |
| 414 |
514 |
|
onClick={() => handlePostSelect(post.id)} |
| 415 |
515 |
|
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 ${ |