blob: 3d9b85dfd18cfa89828c98a99535027d721f3866 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
import { useEffect, useState } from "react";
import type { DeckAccount } from "../types/app";
import { twitterClient } from "@/lib/client/twitterClient";
export interface NewAccountInput {
cookie: string;
}
interface SidebarProps {
accounts: DeckAccount[];
activeAccountId?: string;
onActivate: (id: string) => void;
onAddAccount: (payload: NewAccountInput) => void;
onRemoveAccount: (id: string) => void;
onAddColumn: () => void;
}
export function Sidebar({
accounts,
activeAccountId,
onActivate,
onAddAccount,
onRemoveAccount,
onAddColumn,
}: SidebarProps) {
const [isAdding, setIsAdding] = useState(!accounts.length);
const [cookie, setCookie] = useState("");
const [showCookie, setShowCookie] = useState(false);
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
if (!cookie.trim()) return;
onAddAccount({ cookie: decodeURIComponent(cookie.trim()) });
setCookie("");
setIsAdding(false);
};
return (
<aside className="sidebar">
<div>
<div className="brand">
<div className="brand-glow" />
<div>
<p className="eyebrow">Project Starling</p>
<h1>Open TweetDeck</h1>
<p className="tagline">
Multi-account Twitter cockpit powered by Bun.
</p>
</div>
</div>
<section className="sidebar-section">
<header>
<p className="eyebrow">Accounts</p>
<button className="ghost" onClick={() => setIsAdding((v) => !v)}>
{isAdding ? "Close" : "Add"}
</button>
</header>
{!accounts.length && !isAdding && (
<p className="muted">
Add a Twitter session cookie to start streaming timelines. You can
rename the account later once data loads.
</p>
)}
{accounts.map((account) => (
<div
role="button"
tabIndex={0}
key={account.id}
className={`account-chip ${account.id === activeAccountId ? "active" : ""}`}
onClick={() => onActivate(account.id)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onActivate(account.id);
}
}}
>
<span
className="chip-accent"
style={{ background: account.accent }}
/>
<span>
<strong>{account.label}</strong>
{account.handle ? <small>@{account.handle}</small> : null}
</span>
<span className="chip-actions">
<button
type="button"
className="ghost"
onClick={(event) => {
event.stopPropagation();
onRemoveAccount(account.id);
}}
aria-label={`Remove ${account.label}`}
>
×
</button>
</span>
</div>
))}
{isAdding && (
<form className="account-form" onSubmit={handleSubmit}>
<label>
Twitter session cookie
<textarea
className={!showCookie ? "masked" : undefined}
placeholder="Paste the entire Cookie header"
value={cookie}
onChange={(e) => setCookie(e.target.value)}
rows={4}
/>
</label>
<label className="checkbox">
<input
type="checkbox"
checked={showCookie}
onChange={(e) => setShowCookie(e.target.checked)}
/>
Reveal cookie contents
</label>
<small className="muted">
Cookie stays in your browser via localStorage. It is only sent
to your Bun server when fetching timelines.
</small>
<button
className="primary"
type="submit"
disabled={!cookie.trim()}
>
Save account
</button>
</form>
)}
</section>
</div>
<div className="sidebar-footer">
<button
className="primary wide"
onClick={onAddColumn}
disabled={!accounts.length}
>
+ Add column
</button>
<p className="muted tiny">
Need a cookie? Open x.com, inspect network requests and copy the
request `Cookie` header. Keep it secret.
</p>
</div>
</aside>
);
}
|