chore: added mark individual posts as read or unread
12e23777
1 file(s) · +77 −13
| 17 | 17 | SidebarTrigger, |
|
| 18 | 18 | } from "@/components/ui/sidebar"; |
|
| 19 | 19 | import { Button } from "@/components/ui/button"; |
|
| 20 | - | import { ExternalLink } from "lucide-react"; |
|
| 20 | + | import { ExternalLink, Circle, CircleCheckBig } from "lucide-react"; |
|
| 21 | 21 | import { useQuery } from "@evolu/react"; |
|
| 22 | - | import { allFeedsQuery, allPostsQuery } from "@/lib/evolu"; |
|
| 22 | + | import { |
|
| 23 | + | allFeedsQuery, |
|
| 24 | + | allPostsQuery, |
|
| 25 | + | allReadStatusesQuery, |
|
| 26 | + | allReadStatusesWithUnreadQuery, |
|
| 27 | + | useEvolu, |
|
| 28 | + | } from "@/lib/evolu"; |
|
| 23 | 29 | import ReactMarkdown from "react-markdown"; |
|
| 24 | 30 | import rehypeSanitize from "rehype-sanitize"; |
|
| 25 | 31 | import rehypeRaw from "rehype-raw"; |
|
| 26 | 32 | import remarkGfm from "remark-gfm"; |
|
| 33 | + | import { toast } from "sonner"; |
|
| 27 | 34 | ||
| 28 | 35 | function Dashboard() { |
|
| 29 | 36 | const [selectedFeedId, setSelectedFeedId] = React.useState<string | null>( |
|
| 34 | 41 | ); |
|
| 35 | 42 | const mainContentRef = React.useRef<HTMLDivElement>(null); |
|
| 36 | 43 | ||
| 44 | + | const evolu = useEvolu(); |
|
| 37 | 45 | const allFeeds = useQuery(allFeedsQuery); |
|
| 38 | 46 | const allPosts = useQuery(allPostsQuery); |
|
| 47 | + | const allReadStatuses = useQuery(allReadStatusesQuery); |
|
| 48 | + | const allReadStatusesWithUnread = useQuery(allReadStatusesWithUnreadQuery); |
|
| 39 | 49 | console.log(allPosts); |
|
| 40 | 50 | ||
| 41 | 51 | const selectedFeed = selectedFeedId |
|
| 46 | 56 | ? allPosts.find((p) => p.id === selectedPostId) |
|
| 47 | 57 | : null; |
|
| 48 | 58 | ||
| 59 | + | // Check if current post is read |
|
| 60 | + | const isCurrentPostRead = React.useMemo(() => { |
|
| 61 | + | if (!selectedPostId) return false; |
|
| 62 | + | return allReadStatuses.some((status) => status.postId === selectedPostId); |
|
| 63 | + | }, [selectedPostId, allReadStatuses]); |
|
| 64 | + | ||
| 65 | + | // Toggle read/unread status for current post |
|
| 66 | + | const toggleReadStatus = React.useCallback(() => { |
|
| 67 | + | if (!selectedPostId || !selectedPost) return; |
|
| 68 | + | ||
| 69 | + | const existingStatus = allReadStatusesWithUnread.find( |
|
| 70 | + | (status) => status.postId === selectedPostId, |
|
| 71 | + | ); |
|
| 72 | + | ||
| 73 | + | if (existingStatus) { |
|
| 74 | + | // Update existing status |
|
| 75 | + | const newReadStatus = existingStatus.isRead ? 0 : 1; |
|
| 76 | + | evolu.update("readStatus", { |
|
| 77 | + | id: existingStatus.id as any, |
|
| 78 | + | isRead: newReadStatus, |
|
| 79 | + | }); |
|
| 80 | + | toast.success(newReadStatus ? "Marked as read" : "Marked as unread"); |
|
| 81 | + | } else if (selectedPost.feedId) { |
|
| 82 | + | // Create new read status (mark as read) |
|
| 83 | + | evolu.insert("readStatus", { |
|
| 84 | + | postId: selectedPostId, |
|
| 85 | + | feedId: selectedPost.feedId, |
|
| 86 | + | isRead: 1, |
|
| 87 | + | }); |
|
| 88 | + | toast.success("Marked as read"); |
|
| 89 | + | } |
|
| 90 | + | }, [selectedPostId, selectedPost, allReadStatusesWithUnread, evolu]); |
|
| 91 | + | ||
| 49 | 92 | // Scroll to top when a new post is selected |
|
| 50 | 93 | React.useEffect(() => { |
|
| 51 | 94 | if (selectedPostId && mainContentRef.current) { |
|
| 138 | 181 | {selectedPost ? ( |
|
| 139 | 182 | <div className="flex flex-col gap-6 max-w-4xl mx-auto w-full pb-8"> |
|
| 140 | 183 | <div className="flex flex-col gap-3"> |
|
| 141 | - | <h1 className="text-3xl font-bold tracking-tight"> |
|
| 184 | + | <a |
|
| 185 | + | href={selectedPost.link ? selectedPost.link : ""} |
|
| 186 | + | target="_blank" |
|
| 187 | + | rel="noopener noreferrer" |
|
| 188 | + | className="text-3xl font-bold tracking-tight hover:underline" |
|
| 189 | + | > |
|
| 142 | 190 | {selectedPost.title} |
|
| 143 | - | </h1> |
|
| 191 | + | </a> |
|
| 144 | 192 | <div className="flex items-center gap-4 text-sm text-muted-foreground"> |
|
| 145 | 193 | {selectedPost.author && ( |
|
| 146 | - | <span>By {selectedPost.author}</span> |
|
| 147 | - | )} |
|
| 148 | - | {selectedPost.link && ( |
|
| 149 | - | <Button variant="outline" size="sm" asChild> |
|
| 194 | + | <> |
|
| 195 | + | {" "} |
|
| 196 | + | by |
|
| 150 | 197 | <a |
|
| 151 | - | href={selectedPost.link} |
|
| 152 | 198 | target="_blank" |
|
| 153 | 199 | rel="noopener noreferrer" |
|
| 154 | - | className="flex items-center gap-2" |
|
| 200 | + | className="hover:underline" |
|
| 201 | + | href={ |
|
| 202 | + | selectedPost.author && selectedPost.link |
|
| 203 | + | ? getBaseUrl(selectedPost.link) |
|
| 204 | + | : "" |
|
| 205 | + | } |
|
| 155 | 206 | > |
|
| 156 | - | <ExternalLink className="h-3 w-3" /> |
|
| 157 | - | Open Original |
|
| 207 | + | {selectedPost.author} |
|
| 158 | 208 | </a> |
|
| 209 | + | </> |
|
| 210 | + | )} |
|
| 211 | + | <div className="flex items-center gap-2"> |
|
| 212 | + | <Button |
|
| 213 | + | variant="outline" |
|
| 214 | + | size="sm" |
|
| 215 | + | onClick={toggleReadStatus} |
|
| 216 | + | className="flex items-center gap-2" |
|
| 217 | + | > |
|
| 218 | + | {isCurrentPostRead ? ( |
|
| 219 | + | <Circle className="h-3 w-3" /> |
|
| 220 | + | ) : ( |
|
| 221 | + | <CircleCheckBig className="h-3 w-3" /> |
|
| 222 | + | )} |
|
| 159 | 223 | </Button> |
|
| 160 | - | )} |
|
| 224 | + | </div> |
|
| 161 | 225 | </div> |
|
| 162 | 226 | <span className="text-xs text-muted-foreground"> |
|
| 163 | 227 | {selectedPost.publishedDate |
|