A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve responsive media and following layouts

- Show action buttons and remove controls on mobile
- Track follow and unfollow loading per person
- Reorder following sidebar for mobile and close menu on navigation
- Surface cast, crew, and guest stars in mobile detail views

+104 -59
+1 -1
apps/web/src/components/FeedItemActions.tsx
··· 87 87 } 88 88 89 89 return ( 90 - <div className="flex items-center gap-2"> 90 + <div className="flex flex-wrap items-center gap-2"> 91 91 {/* Shelf Toggle */} 92 92 <button 93 93 type="button"
+2
apps/web/src/components/Header.tsx
··· 220 220 <Link 221 221 key={item.name} 222 222 to={item.href} 223 + onClick={() => setMobileMenuOpen(false)} 223 224 className={`flex items-center gap-3 rounded-md px-3 py-3 font-medium text-sm transition-colors ${ 224 225 isActive 225 226 ? "bg-(--accent-subtle) text-(--accent)" ··· 273 274 <div className="my-2 border-(--border) border-t" /> 274 275 <Link 275 276 to="/login" 277 + onClick={() => setMobileMenuOpen(false)} 276 278 className="flex items-center gap-3 rounded-md bg-(--accent) px-3 py-3 font-medium text-(--accent-foreground) text-sm" 277 279 > 278 280 Sign In
+2 -2
apps/web/src/components/MediaCard.tsx
··· 312 312 )} 313 313 </Link> 314 314 315 - {/* Actions menu — hover-only remove button for list pages */} 315 + {/* Actions menu — visible on mobile, hover-only on desktop */} 316 316 {onRemove && ( 317 - <div className="absolute top-2 right-2 flex flex-col gap-1.5 opacity-0 transition-opacity group-hover:opacity-100"> 317 + <div className="absolute top-2 right-2 flex flex-col gap-1.5 opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100"> 318 318 <button 319 319 type="button" 320 320 onClick={(e) => {
+1 -1
apps/web/src/components/following/ActivityCard.tsx
··· 163 163 )} 164 164 165 165 {/* Actions */} 166 - <div className="flex items-center gap-4 pt-1"> 166 + <div className="flex flex-wrap items-center gap-4 pt-1"> 167 167 {activity.type === "movie" ? ( 168 168 <FeedItemActions 169 169 type="movie"
+5 -5
apps/web/src/components/following/FollowingList.tsx
··· 7 7 isLoading: boolean; 8 8 error: Error | null; 9 9 onUnfollow: (did: string) => void; 10 - isUnfollowPending: boolean; 10 + pendingUnfollowDid?: string; 11 11 } 12 12 13 13 export function FollowingList({ ··· 15 15 isLoading, 16 16 error, 17 17 onUnfollow, 18 - isUnfollowPending, 18 + pendingUnfollowDid, 19 19 }: FollowingListProps) { 20 20 return ( 21 21 <section className="card p-5"> ··· 47 47 <p className="truncate font-medium text-sm"> 48 48 {String(friend.displayName) || friend.handle} 49 49 </p> 50 - <p className="text-(--foreground-muted) text-xs"> 50 + <p className="truncate text-(--foreground-muted) text-xs"> 51 51 @{friend.handle} 52 52 </p> 53 53 </div> ··· 55 55 type="button" 56 56 className="btn btn-secondary btn-sm h-8 px-2 text-xs" 57 57 onClick={() => onUnfollow(friend.did)} 58 - disabled={isUnfollowPending} 58 + disabled={pendingUnfollowDid === friend.did} 59 59 title="Unfollow" 60 60 > 61 - {isUnfollowPending ? ( 61 + {pendingUnfollowDid === friend.did ? ( 62 62 <Loader2 className="h-3 w-3 animate-spin" /> 63 63 ) : ( 64 64 <UserCheck className="h-3 w-3" />
+8 -8
apps/web/src/components/following/PeopleSearch.tsx
··· 12 12 isLoading: boolean; 13 13 onFollow: (did: string) => void; 14 14 onUnfollow: (did: string) => void; 15 - isFollowPending: boolean; 16 - isUnfollowPending: boolean; 15 + pendingFollowDid?: string; 16 + pendingUnfollowDid?: string; 17 17 } 18 18 19 19 export function PeopleSearch({ ··· 26 26 isLoading, 27 27 onFollow, 28 28 onUnfollow, 29 - isFollowPending, 30 - isUnfollowPending, 29 + pendingFollowDid, 30 + pendingUnfollowDid, 31 31 }: PeopleSearchProps) { 32 32 return ( 33 33 <div className="relative"> ··· 79 79 type="button" 80 80 className="btn btn-secondary btn-sm h-8 px-3 text-xs" 81 81 onClick={() => onUnfollow(person.did)} 82 - disabled={isUnfollowPending} 82 + disabled={pendingUnfollowDid === person.did} 83 83 > 84 - {isUnfollowPending ? ( 84 + {pendingUnfollowDid === person.did ? ( 85 85 <Loader2 className="h-3 w-3 animate-spin" /> 86 86 ) : ( 87 87 "Unfollow" ··· 92 92 type="button" 93 93 className="btn btn-primary btn-sm h-8 px-3 text-xs" 94 94 onClick={() => onFollow(person.did)} 95 - disabled={isFollowPending} 95 + disabled={pendingFollowDid === person.did} 96 96 > 97 - {isFollowPending ? ( 97 + {pendingFollowDid === person.did ? ( 98 98 <Loader2 className="h-3 w-3 animate-spin" /> 99 99 ) : ( 100 100 <>
+16 -16
apps/web/src/routes/following.tsx
··· 160 160 <FollowingHeader /> 161 161 162 162 <div className="grid gap-8 lg:grid-cols-3"> 163 - {/* Main Feed */} 164 - <div className="space-y-6 lg:col-span-2"> 163 + {/* Sidebar - shown above feed on mobile, right side on desktop */} 164 + <div className="order-1 space-y-6 lg:order-2"> 165 + <FollowingList 166 + following={following} 167 + isLoading={followingLoading} 168 + error={followingError} 169 + onUnfollow={handleUnfollow} 170 + pendingUnfollowDid={unfollowMutation.variables?.path?.targetDid} 171 + /> 172 + <NetworkStats following={following} /> 173 + </div> 174 + 175 + {/* Main Feed - shown below sidebar on mobile, left side on desktop */} 176 + <div className="order-2 space-y-6 lg:order-1 lg:col-span-2"> 165 177 <PeopleSearch 166 178 query={searchQuery} 167 179 onQueryChange={setSearchQuery} ··· 172 184 isLoading={searchLoading} 173 185 onFollow={handleFollow} 174 186 onUnfollow={handleUnfollow} 175 - isFollowPending={followMutation.isPending} 176 - isUnfollowPending={unfollowMutation.isPending} 187 + pendingFollowDid={followMutation.variables?.path?.targetDid} 188 + pendingUnfollowDid={unfollowMutation.variables?.path?.targetDid} 177 189 /> 178 190 179 191 <ActivityFeed ··· 198 210 </button> 199 211 </div> 200 212 )} 201 - </div> 202 - 203 - {/* Sidebar */} 204 - <div className="space-y-6"> 205 - <FollowingList 206 - following={following} 207 - isLoading={followingLoading} 208 - error={followingError} 209 - onUnfollow={handleUnfollow} 210 - isUnfollowPending={unfollowMutation.isPending} 211 - /> 212 - <NetworkStats following={following} /> 213 213 </div> 214 214 </div> 215 215 </div>
+1 -1
apps/web/src/routes/lists.tsx
··· 636 636 !listError && 637 637 filteredItems.length > 0 && 638 638 (viewMode === "grid" ? ( 639 - <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> 639 + <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"> 640 640 {filteredItems 641 641 // Deduplicate by ID to prevent React key warnings 642 642 .filter(
+7 -1
apps/web/src/routes/movies/$movieId/$movieName.tsx
··· 265 265 </p> 266 266 </section> 267 267 268 - <PersonGrid people={cast} /> 268 + <div className="hidden lg:block"> 269 + <PersonGrid people={cast} /> 270 + </div> 269 271 <SimilarMediaGrid items={similarMovies} title="Similar Movies" /> 270 272 </div> 271 273 ··· 377 379 378 380 <InYourLists mediaType="movie" mediaId={movieId} /> 379 381 </div> 382 + </div> 383 + 384 + <div className="mt-8 lg:hidden"> 385 + <PersonGrid people={cast} /> 380 386 </div> 381 387 </div> 382 388
+17 -6
apps/web/src/routes/shows/$showId/$showName/index.tsx
··· 397 397 </section> 398 398 )} 399 399 400 - <PersonGrid people={cast} /> 401 - <PersonGrid 402 - people={crew} 403 - title="Crew" 404 - emptyMessage="No crew information available." 405 - /> 400 + <div className="hidden space-y-8 lg:block"> 401 + <PersonGrid people={cast} /> 402 + <PersonGrid 403 + people={crew} 404 + title="Crew" 405 + emptyMessage="No crew information available." 406 + /> 407 + </div> 406 408 <SimilarMediaGrid items={similarShows} title="Similar Shows" /> 407 409 </div> 408 410 ··· 452 454 453 455 <InYourLists mediaType="show" mediaId={showId} /> 454 456 </div> 457 + </div> 458 + 459 + <div className="mt-8 space-y-8 lg:hidden"> 460 + <PersonGrid people={cast} /> 461 + <PersonGrid 462 + people={crew} 463 + title="Crew" 464 + emptyMessage="No crew information available." 465 + /> 455 466 </div> 456 467 </div> 457 468 </div>
+27 -12
apps/web/src/routes/shows/$showId/$showName/seasons.$seasonNumber.episodes.$episodeNumber.tsx
··· 330 330 </p> 331 331 </section> 332 332 333 - {/* Guest Stars */} 334 - <PersonGrid 335 - people={guestStars} 336 - title="Guest Stars" 337 - emptyMessage="No guest stars information available." 338 - /> 333 + <div className="hidden space-y-8 lg:block"> 334 + {/* Guest Stars */} 335 + <PersonGrid 336 + people={guestStars} 337 + title="Guest Stars" 338 + emptyMessage="No guest stars information available." 339 + /> 339 340 340 - {/* Crew */} 341 - <PersonGrid 342 - people={episodeCrew} 343 - title="Crew" 344 - emptyMessage="No crew information available." 345 - /> 341 + {/* Crew */} 342 + <PersonGrid 343 + people={episodeCrew} 344 + title="Crew" 345 + emptyMessage="No crew information available." 346 + /> 347 + </div> 346 348 </div> 347 349 348 350 {/* Right Column - Sidebar */} ··· 464 466 episodeNumber={episodeNum} 465 467 /> 466 468 </div> 469 + </div> 470 + 471 + <div className="mt-8 space-y-8 lg:hidden"> 472 + <PersonGrid 473 + people={guestStars} 474 + title="Guest Stars" 475 + emptyMessage="No guest stars information available." 476 + /> 477 + <PersonGrid 478 + people={episodeCrew} 479 + title="Crew" 480 + emptyMessage="No crew information available." 481 + /> 467 482 </div> 468 483 </div> 469 484
+17 -6
apps/web/src/routes/shows/$showId/$showName/seasons.$seasonNumber/index.tsx
··· 386 386 </section> 387 387 )} 388 388 389 - <PersonGrid people={cast} /> 390 - <PersonGrid 391 - people={crew} 392 - title="Crew" 393 - emptyMessage="No crew information available." 394 - /> 389 + <div className="hidden space-y-8 lg:block"> 390 + <PersonGrid people={cast} /> 391 + <PersonGrid 392 + people={crew} 393 + title="Crew" 394 + emptyMessage="No crew information available." 395 + /> 396 + </div> 395 397 </div> 396 398 397 399 {/* Right Column - Sidebar */} ··· 444 446 seasonNumber={seasonNum} 445 447 /> 446 448 </div> 449 + </div> 450 + 451 + <div className="mt-8 space-y-8 lg:hidden"> 452 + <PersonGrid people={cast} /> 453 + <PersonGrid 454 + people={crew} 455 + title="Crew" 456 + emptyMessage="No crew information available." 457 + /> 447 458 </div> 448 459 </div> 449 460 </div>