grain.social is a photo sharing platform built on atproto.
at main 118 lines 4.2 kB view raw
1import { type TimelineItem } from "../lib/timeline.ts"; 2import { Button } from "./Button.tsx"; 3import { Header } from "./Header.tsx"; 4import { TimelineItem as Item } from "./TimelineItem.tsx"; 5 6export function Timeline( 7 { isLoggedIn, selectedTab, items, actorProfiles, selectedGraph }: Readonly< 8 { 9 isLoggedIn: boolean; 10 selectedTab: string; 11 items: TimelineItem[]; 12 actorProfiles: string[]; 13 selectedGraph: string; 14 } 15 >, 16) { 17 return ( 18 <div class="px-4 mb-4" id="timeline-page"> 19 <div id="dialog-target" /> 20 {isLoggedIn 21 ? ( 22 <> 23 <div class="my-4 pb-4 border-b border-zinc-200 dark:border-zinc-800"> 24 <div class="flex sm:w-fit"> 25 <Button 26 variant="tab" 27 class="flex-1" 28 hx-get={`/?graph=${selectedGraph}`} 29 hx-target="#timeline-page" 30 hx-swap="outerHTML" 31 role="tab" 32 aria-selected={!selectedTab} 33 aria-controls="tab-content" 34 > 35 Timeline 36 </Button> 37 <Button 38 variant="tab" 39 class="flex-1" 40 hx-get={`/?tab=following&graph=${selectedGraph}`} 41 hx-target="#timeline-page" 42 hx-swap="outerHTML" 43 role="tab" 44 aria-selected={selectedTab === "following"} 45 aria-controls="tab-content" 46 _="on click js document.title = 'Following — Grain'; end" 47 > 48 Following 49 </Button> 50 </div> 51 </div> 52 <div id="tab-content" role="tabpanel"> 53 {actorProfiles.length > 1 && selectedTab === "following" 54 ? ( 55 <form 56 hx-get="/" 57 hx-target="#timeline-page" 58 hx-swap="outerHTML" 59 hx-trigger="change from:#graph-filter" 60 class="mb-4 flex flex-col border-b border-zinc-200 dark:border-zinc-800 pb-4" 61 > 62 <label 63 htmlFor="graph-filter" 64 class="mb-1 font-medium sr-only" 65 > 66 Filter by AT Protocol Social Network 67 </label> 68 69 <input type="hidden" name="tab" value={selectedTab || ""} /> 70 71 <select 72 id="graph-filter" 73 name="graph" 74 class="border rounded px-2 py-1 dark:bg-zinc-900 dark:border-zinc-700 max-w-md" 75 > 76 {actorProfiles.map((graph) => ( 77 <option 78 value={graph} 79 key={graph} 80 selected={graph === selectedGraph} 81 > 82 {formatGraphName(graph)} 83 </option> 84 ))} 85 </select> 86 </form> 87 ) 88 : null} 89 <ul class="space-y-4 relative divide-zinc-200 dark:divide-zinc-800 divide-y sm:w-fit"> 90 {items.length > 0 91 ? items.map((item) => <Item item={item} key={item.itemUri} />) 92 : ( 93 <li class="text-center"> 94 No galleries by people you follow on{" "} 95 {formatGraphName(selectedGraph)} yet. 96 </li> 97 )} 98 </ul> 99 </div> 100 </> 101 ) 102 : ( 103 <> 104 <div class="my-4"> 105 <Header>Timeline</Header> 106 </div> 107 <ul class="space-y-4 relative divide-zinc-200 dark:divide-zinc-800 divide-y w-fit"> 108 {items.map((item) => <Item item={item} key={item.itemUri} />)} 109 </ul> 110 </> 111 )} 112 </div> 113 ); 114} 115 116export function formatGraphName(graph: string): string { 117 return graph.charAt(0).toUpperCase() + graph.slice(1); 118}