grain.social is a photo sharing platform built on atproto.
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}