chore: refactored post selecting on mobile 24ebb2d0
Steve · 2025-11-02 00:12 1 file(s) · +186 −34
src/components/app-sidebar.tsx +186 −34
1 1
"use client";
2 2
3 3
import * as React from "react";
4 -
import { Plus, RotateCw, MoreVertical, Check, X } from "lucide-react";
4 +
import {
5 +
	Plus,
6 +
	RotateCw,
7 +
	MoreVertical,
8 +
	Check,
9 +
	X,
10 +
	ChevronLeft,
11 +
} from "lucide-react";
5 12
6 13
import { NavUser } from "@/components/nav-user";
7 14
import { NavFeeds } from "@/components/nav-feeds";
82 89
	const [searchQuery, setSearchQuery] = React.useState("");
83 90
	const [isAddingFeed, setIsAddingFeed] = React.useState(false);
84 91
	const [statusMessage, setStatusMessage] = React.useState("");
92 +
	// Mobile navigation state: 'feeds' or 'posts'
93 +
	const [mobileView, setMobileView] = React.useState<"feeds" | "posts">(
94 +
		"feeds",
95 +
	);
85 96
86 -
	const { hidden } = useSidebar();
97 +
	const { hidden, isMobile, setOpenMobile } = useSidebar();
87 98
	const { insert, update } = useEvolu();
88 99
	const allFeeds = useQuery(allFeedsQuery);
89 100
	const allReadStatuses = useQuery(allReadStatusesQuery);
120 131
		[allReadStatuses],
121 132
	);
122 133
134 +
	// Handle feed selection - on mobile, navigate to posts view
135 +
	const handleFeedSelect = React.useCallback(
136 +
		(feedId: string | null) => {
137 +
			onFeedSelect(feedId);
138 +
			// Navigate to posts view on mobile for any feed selection (including "All Feeds")
139 +
			if (isMobile) {
140 +
				setMobileView("posts");
141 +
			}
142 +
		},
143 +
		[onFeedSelect, isMobile],
144 +
	);
145 +
146 +
	// Handle back to feeds on mobile
147 +
	const handleBackToFeeds = React.useCallback(() => {
148 +
		setMobileView("feeds");
149 +
		onFeedSelect(null);
150 +
	}, [onFeedSelect]);
151 +
123 152
	// Handle post selection and mark as read
124 153
	const handlePostSelect = React.useCallback(
125 154
		(postId: string) => {
146 175
147 176
			// Call the original onPostSelect
148 177
			onPostSelect(postId);
178 +
179 +
			// On mobile, close the sidebar after selecting a post
180 +
			if (isMobile) {
181 +
				setOpenMobile(false);
182 +
				// Reset to feeds view for next time sidebar opens
183 +
				setTimeout(() => setMobileView("feeds"), 300);
184 +
			}
149 185
		},
150 -
		[allReadStatusesWithUnread, feedPosts, insert, update, onPostSelect],
186 +
		[
187 +
			allReadStatusesWithUnread,
188 +
			feedPosts,
189 +
			insert,
190 +
			update,
191 +
			onPostSelect,
192 +
			isMobile,
193 +
			setOpenMobile,
194 +
		],
151 195
	);
152 196
153 197
	// Mark all visible posts as read
371 415
					<SidebarHeader>
372 416
						<SidebarMenu>
373 417
							<SidebarMenuItem>
374 -
								<a href="#">
375 -
									<div className="grid flex-1 text-left text-xl px-2 pt-2">
376 -
										<span className="truncate font-bold">Alcove</span>
418 +
								{isMobile && mobileView === "posts" ? (
419 +
									<div className="flex items-center gap-2 px-2 pt-2">
420 +
										<Button
421 +
											variant="ghost"
422 +
											size="sm"
423 +
											onClick={handleBackToFeeds}
424 +
											className="h-8 w-8 p-0"
425 +
										>
426 +
											<ChevronLeft className="size-5" />
427 +
										</Button>
428 +
										<div className="flex-1 text-left text-xl">
429 +
											<span className="truncate font-bold">
430 +
												{selectedFeedId
431 +
													? allFeeds.find((f) => f.id === selectedFeedId)
432 +
															?.title || "Posts"
433 +
													: "All Posts"}
434 +
											</span>
435 +
										</div>
377 436
									</div>
378 -
								</a>
437 +
								) : (
438 +
									<a href="#">
439 +
										<div className="grid flex-1 text-left text-xl px-2 pt-2">
440 +
											<span className="truncate font-bold">Alcove</span>
441 +
										</div>
442 +
									</a>
443 +
								)}
379 444
							</SidebarMenuItem>
380 445
						</SidebarMenu>
381 446
					</SidebarHeader>
382 447
					<SidebarContent>
383 -
						<SidebarGroup>
384 -
							<SidebarGroupLabel>Actions</SidebarGroupLabel>
385 -
							<SidebarGroupContent>
386 -
								<SidebarMenu>
387 -
									<DialogTrigger asChild>
388 -
										<SidebarMenuItem>
389 -
											<SidebarMenuButton>
390 -
												<Plus className="size-4" />
391 -
												<span>Add Feed</span>
392 -
											</SidebarMenuButton>
393 -
										</SidebarMenuItem>
394 -
									</DialogTrigger>
395 -
									<SidebarMenuItem>
396 -
										<SidebarMenuButton onClick={reset}>
397 -
											<RotateCw className="size-4" />
398 -
											<span>Reset</span>
399 -
										</SidebarMenuButton>
400 -
									</SidebarMenuItem>
401 -
								</SidebarMenu>
402 -
							</SidebarGroupContent>
403 -
						</SidebarGroup>
404 -
						<NavFeeds
405 -
							feeds={allFeeds}
406 -
							selectedFeedId={selectedFeedId}
407 -
							onFeedSelect={onFeedSelect}
408 -
						/>
448 +
						{isMobile && mobileView === "posts" ? (
449 +
							// Mobile posts view
450 +
							<div className="flex flex-col h-full">
451 +
								<div className="gap-2 border-b p-3 flex flex-col">
452 +
									<div className="flex w-full items-center justify-between gap-2">
453 +
										<div className="text-foreground text-sm font-semibold truncate">
454 +
											{filteredPosts.length} post
455 +
											{filteredPosts.length !== 1 ? "s" : ""}
456 +
										</div>
457 +
										<DropdownMenu>
458 +
											<DropdownMenuTrigger asChild>
459 +
												<Button
460 +
													variant="ghost"
461 +
													size="sm"
462 +
													className="h-6 w-6 p-0"
463 +
												>
464 +
													<MoreVertical className="h-4 w-4" />
465 +
												</Button>
466 +
											</DropdownMenuTrigger>
467 +
											<DropdownMenuContent align="end">
468 +
												<DropdownMenuItem onClick={handleMarkAllAsRead}>
469 +
													<Check className="h-4 w-4 mr-2" />
470 +
													Mark all as read
471 +
												</DropdownMenuItem>
472 +
												<DropdownMenuItem onClick={handleMarkAllAsUnread}>
473 +
													<X className="h-4 w-4 mr-2" />
474 +
													Mark all as unread
475 +
												</DropdownMenuItem>
476 +
											</DropdownMenuContent>
477 +
										</DropdownMenu>
478 +
									</div>
479 +
									<Input
480 +
										placeholder="Search..."
481 +
										value={searchQuery}
482 +
										onChange={(e) => setSearchQuery(e.target.value)}
483 +
										className="h-8"
484 +
									/>
485 +
								</div>
486 +
								<div className="flex-1 overflow-y-auto">
487 +
									{filteredPosts.length === 0 ? (
488 +
										<div className="p-4 text-center text-sm text-muted-foreground">
489 +
											No posts found
490 +
										</div>
491 +
									) : (
492 +
										filteredPosts.map((post) => {
493 +
											const isRead = isPostRead(post.id);
494 +
											return (
495 +
												<button
496 +
													type="button"
497 +
													key={post.id}
498 +
													onClick={() => handlePostSelect(post.id)}
499 +
													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 ${
500 +
														selectedPostId === post.id
501 +
															? "bg-sidebar-accent"
502 +
															: ""
503 +
													}`}
504 +
												>
505 +
													{/* Unread indicator */}
506 +
													<div className="flex-shrink-0 pt-1">
507 +
														{!isRead && (
508 +
															<div className="size-2 rounded-full bg-primary" />
509 +
														)}
510 +
													</div>
511 +
													{/* Post content */}
512 +
													<div className="flex flex-col gap-1.5 flex-1 min-w-0">
513 +
														<span className="font-medium line-clamp-2 leading-snug">
514 +
															{post.title}
515 +
														</span>
516 +
														{post.author && (
517 +
															<span className="text-muted-foreground text-xs">
518 +
																{post.author}
519 +
															</span>
520 +
														)}
521 +
													</div>
522 +
												</button>
523 +
											);
524 +
										})
525 +
									)}
526 +
								</div>
527 +
							</div>
528 +
						) : (
529 +
							// Feeds view (desktop and mobile default)
530 +
							<>
531 +
								<SidebarGroup>
532 +
									<SidebarGroupLabel>Actions</SidebarGroupLabel>
533 +
									<SidebarGroupContent>
534 +
										<SidebarMenu>
535 +
											<DialogTrigger asChild>
536 +
												<SidebarMenuItem>
537 +
													<SidebarMenuButton>
538 +
														<Plus className="size-4" />
539 +
														<span>Add Feed</span>
540 +
													</SidebarMenuButton>
541 +
												</SidebarMenuItem>
542 +
											</DialogTrigger>
543 +
											<SidebarMenuItem>
544 +
												<SidebarMenuButton onClick={reset}>
545 +
													<RotateCw className="size-4" />
546 +
													<span>Reset</span>
547 +
												</SidebarMenuButton>
548 +
											</SidebarMenuItem>
549 +
										</SidebarMenu>
550 +
									</SidebarGroupContent>
551 +
								</SidebarGroup>
552 +
								<NavFeeds
553 +
									feeds={allFeeds}
554 +
									selectedFeedId={selectedFeedId}
555 +
									onFeedSelect={handleFeedSelect}
556 +
								/>
557 +
							</>
558 +
						)}
409 559
					</SidebarContent>
410 560
					<SidebarFooter>
411 -
						<NavUser user={data.user} />
561 +
						{!isMobile || mobileView === "feeds" ? (
562 +
							<NavUser user={data.user} />
563 +
						) : null}
412 564
					</SidebarFooter>
413 565
				</Sidebar>
414 566
				<DialogContent className="sm:max-w-[425px]">