tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
material 3 initial design
rimar1337
4 months ago
ff2894e7
bfa92e50
+655
-231
12 changed files
expand all
collapse all
unified
split
package-lock.json
package.json
src
auto-imports.d.ts
components
Header.tsx
InfiniteCustomFeed.tsx
Login.tsx
routes
__root.tsx
index.tsx
profile.$did
index.tsx
post.$rkey.tsx
styles
app.css
vite.config.ts
+12
package-lock.json
···
32
32
"@iconify-icon/react": "^3.0.1",
33
33
"@iconify-json/material-symbols": "^1.2.42",
34
34
"@iconify-json/mdi": "^1.2.3",
35
35
+
"@iconify/json": "^2.2.396",
35
36
"@svgr/core": "^8.1.0",
36
37
"@svgr/plugin-jsx": "^8.1.0",
37
38
"@testing-library/dom": "^10.4.0",
···
1681
1682
"license": "Apache-2.0",
1682
1683
"dependencies": {
1683
1684
"@iconify/types": "*"
1685
1685
+
}
1686
1686
+
},
1687
1687
+
"node_modules/@iconify/json": {
1688
1688
+
"version": "2.2.396",
1689
1689
+
"resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.396.tgz",
1690
1690
+
"integrity": "sha512-tijg77JFuYIt32S9N8p7La8C0zp9zKZsX6UP8ip5GVB1F6Mp3pZA5Vc5eAquTY50NoDJX58U6z4Qn3d6Wyossg==",
1691
1691
+
"dev": true,
1692
1692
+
"license": "MIT",
1693
1693
+
"dependencies": {
1694
1694
+
"@iconify/types": "*",
1695
1695
+
"pathe": "^2.0.0"
1684
1696
}
1685
1697
},
1686
1698
"node_modules/@iconify/types": {
+1
package.json
···
36
36
"@iconify-icon/react": "^3.0.1",
37
37
"@iconify-json/material-symbols": "^1.2.42",
38
38
"@iconify-json/mdi": "^1.2.3",
39
39
+
"@iconify/json": "^2.2.396",
39
40
"@svgr/core": "^8.1.0",
40
41
"@svgr/plugin-jsx": "^8.1.0",
41
42
"@testing-library/dom": "^10.4.0",
+1
src/auto-imports.d.ts
···
8
8
declare global {
9
9
const IconMaterialSymbolsAccountCircle: typeof import('~icons/material-symbols/account-circle.jsx').default
10
10
const IconMaterialSymbolsAccountCircleOutline: typeof import('~icons/material-symbols/account-circle-outline.jsx').default
11
11
+
const IconMaterialSymbolsArrowBack: typeof import('~icons/material-symbols/arrow-back.jsx').default
11
12
const IconMaterialSymbolsHome: typeof import('~icons/material-symbols/home.jsx').default
12
13
const IconMaterialSymbolsHomeOutline: typeof import('~icons/material-symbols/home-outline.jsx').default
13
14
const IconMaterialSymbolsNotifications: typeof import('~icons/material-symbols/notifications.jsx').default
+29
src/components/Header.tsx
···
1
1
+
import { Link, useRouter } from "@tanstack/react-router";
2
2
+
3
3
+
export function Header({
4
4
+
backButtonCallback,
5
5
+
title
6
6
+
}: {
7
7
+
backButtonCallback?: () => void;
8
8
+
title?: string;
9
9
+
}) {
10
10
+
const router = useRouter();
11
11
+
//const what = router.history.
12
12
+
return (
13
13
+
<div className="flex items-center gap-4 px-4 py-3 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
14
14
+
{backButtonCallback ? (<Link
15
15
+
to=".."
16
16
+
//className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
17
17
+
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
18
18
+
onClick={(e) => {
19
19
+
e.preventDefault();
20
20
+
backButtonCallback();
21
21
+
}}
22
22
+
aria-label="Go back"
23
23
+
>
24
24
+
<IconMaterialSymbolsArrowBack className="w-6 h-6" />
25
25
+
</Link>) : (<div className="w-[0px]" />)}
26
26
+
<span className="text-[21px] font-roboto">{title}</span>
27
27
+
</div>
28
28
+
);
29
29
+
}
+5
-4
src/components/InfiniteCustomFeed.tsx
···
1
1
import * as React from "react";
2
2
+
2
3
//import { useInView } from "react-intersection-observer";
3
4
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
4
5
import { useAuth } from "~/providers/UnifiedAuthProvider";
5
6
import {
6
6
-
useQueryArbitrary,
7
7
-
useQueryIdentity,
8
7
useInfiniteQueryFeedSkeleton,
8
8
+
// useQueryArbitrary,
9
9
+
// useQueryIdentity,
9
10
} from "~/utils/useQuery";
10
11
11
12
interface InfiniteCustomFeedProps {
···
112
113
<button
113
114
onClick={handleRefresh}
114
115
disabled={isRefetching}
115
115
-
className="sticky lg:bottom-6 bottom-24 ml-4 w-[42px] h-[42px] z-10 bg-gray-500 hover:bg-gray-600 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed"
116
116
+
className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed"
116
117
aria-label="Refresh feed"
117
118
>
118
118
-
{isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />}
119
119
+
{isRefetching ? <RefreshIcon className="h-6 w-6 text-gray-600 dark:text-gray-400 animate-spin" /> : <RefreshIcon className="h-6 w-6 text-gray-600 dark:text-gray-400" />}
119
120
</button>
120
121
</>
121
122
);
+143
-57
src/components/Login.tsx
···
1
1
// src/components/Login.tsx
2
2
-
import React, { useEffect, useState, useRef } from "react";
2
2
+
import { Agent } from "@atproto/api";
3
3
+
import React, { useEffect, useRef, useState } from "react";
4
4
+
3
5
import { useAuth } from "~/providers/UnifiedAuthProvider";
4
4
-
import { Agent } from "@atproto/api";
5
6
6
7
// --- 1. The Main Component (Orchestrator with `compact` prop) ---
7
7
-
export default function Login({ compact = false }: { compact?: boolean }) {
8
8
+
export default function Login({
9
9
+
compact = false,
10
10
+
popup = false,
11
11
+
}: {
12
12
+
compact?: boolean;
13
13
+
popup?: boolean;
14
14
+
}) {
8
15
const { status, agent, logout } = useAuth();
9
16
10
17
// Loading state can be styled differently based on the prop
···
33
40
// Large view
34
41
if (!compact) {
35
42
return (
36
36
-
<div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
43
43
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4">
37
44
<div className="flex flex-col items-center justify-center text-center">
38
45
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
39
46
You are logged in!
···
41
48
<ProfileThing agent={agent} large />
42
49
<button
43
50
onClick={logout}
44
44
-
className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors"
51
51
+
className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded-full px-6 py-2 font-semibold text-base transition-colors"
45
52
>
46
53
Log out
47
54
</button>
···
67
74
if (!compact) {
68
75
// Large view renders the form directly in the card
69
76
return (
70
70
-
<div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4">
77
77
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4">
71
78
<UnifiedLoginForm />
72
79
</div>
73
80
);
74
81
}
75
82
76
83
// Compact view renders a button that toggles the form in a dropdown
77
77
-
return <CompactLoginButton />;
84
84
+
return <CompactLoginButton popup={popup} />;
78
85
}
79
86
80
87
// --- 2. The Reusable, Self-Contained Login Form Component ---
···
83
90
84
91
return (
85
92
<div>
86
86
-
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-4">
93
93
+
<div className="flex bg-gray-300 rounded-full dark:bg-gray-700 mb-4">
87
94
<TabButton
88
95
label="OAuth"
89
96
active={mode === "oauth"}
···
103
110
// --- 3. Helper components for layouts, forms, and UI ---
104
111
105
112
// A new component to contain the logic for the compact dropdown
106
106
-
const CompactLoginButton = () => {
113
113
+
const CompactLoginButton = ({popup}:{popup?: boolean}) => {
107
114
const [showForm, setShowForm] = useState(false);
108
115
const formRef = useRef<HTMLDivElement>(null);
109
116
···
125
132
<div className="relative" ref={formRef}>
126
133
<button
127
134
onClick={() => setShowForm(!showForm)}
128
128
-
className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors"
135
135
+
className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-full px-3 py-1 font-medium transition-colors"
129
136
>
130
137
Log in
131
138
</button>
132
139
{showForm && (
133
133
-
<div className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50">
140
140
+
<div className={`absolute ${popup ? `bottom-[calc(100%)]` :`top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`}>
134
141
<UnifiedLoginForm />
135
142
</div>
136
143
)}
···
138
145
);
139
146
};
140
147
141
141
-
const TabButton = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; }) => (
148
148
+
const TabButton = ({
149
149
+
label,
150
150
+
active,
151
151
+
onClick,
152
152
+
}: {
153
153
+
label: string;
154
154
+
active: boolean;
155
155
+
onClick: () => void;
156
156
+
}) => (
142
157
<button
143
158
onClick={onClick}
144
144
-
className={`px-4 py-2 text-sm font-medium transition-colors ${
159
159
+
className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${
145
160
active
146
146
-
? "text-gray-600 dark:text-gray-200 border-b-2 border-gray-500"
147
147
-
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
161
161
+
? "text-gray-950 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500"
162
162
+
: "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200"
148
163
}`}
149
164
>
150
165
{label}
···
169
184
};
170
185
return (
171
186
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
172
172
-
<p className="text-xs text-gray-500 dark:text-gray-400">Sign in with AT. Your password is never shared.</p>
173
173
-
<input type="text" placeholder="handle.bsky.social" value={handle} onChange={(e) => setHandle(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" />
174
174
-
<button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button>
187
187
+
<p className="text-xs text-gray-500 dark:text-gray-400">
188
188
+
Sign in with AT. Your password is never shared.
189
189
+
</p>
190
190
+
<input
191
191
+
type="text"
192
192
+
placeholder="handle.bsky.social"
193
193
+
value={handle}
194
194
+
onChange={(e) => setHandle(e.target.value)}
195
195
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
196
196
+
/>
197
197
+
<button
198
198
+
type="submit"
199
199
+
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
200
200
+
>
201
201
+
Log in
202
202
+
</button>
175
203
</form>
176
204
);
177
205
};
···
201
229
202
230
return (
203
231
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
204
204
-
<p className="text-xs text-red-500 dark:text-red-400">Warning: Less secure. Use an App Password.</p>
205
205
-
<input type="text" placeholder="handle.bsky.social" value={user} onChange={(e) => setUser(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="username" />
206
206
-
<input type="password" placeholder="App Password" value={password} onChange={(e) => setPassword(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="current-password" />
207
207
-
<input type="text" placeholder="PDS (e.g., bsky.social)" value={serviceURL} onChange={(e) => setServiceURL(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" />
232
232
+
<p className="text-xs text-red-500 dark:text-red-400">
233
233
+
Warning: Less secure. Use an App Password.
234
234
+
</p>
235
235
+
<input
236
236
+
type="text"
237
237
+
placeholder="handle.bsky.social"
238
238
+
value={user}
239
239
+
onChange={(e) => setUser(e.target.value)}
240
240
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
241
241
+
autoComplete="username"
242
242
+
/>
243
243
+
<input
244
244
+
type="password"
245
245
+
placeholder="App Password"
246
246
+
value={password}
247
247
+
onChange={(e) => setPassword(e.target.value)}
248
248
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
249
249
+
autoComplete="current-password"
250
250
+
/>
251
251
+
<input
252
252
+
type="text"
253
253
+
placeholder="PDS (e.g., bsky.social)"
254
254
+
value={serviceURL}
255
255
+
onChange={(e) => setServiceURL(e.target.value)}
256
256
+
className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500"
257
257
+
/>
208
258
{error && <p className="text-xs text-red-500">{error}</p>}
209
209
-
<button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button>
259
259
+
<button
260
260
+
type="submit"
261
261
+
className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors"
262
262
+
>
263
263
+
Log in
264
264
+
</button>
210
265
</form>
211
266
);
212
267
};
213
268
214
269
// --- Profile Component (now supports a `large` prop for styling) ---
215
215
-
export const ProfileThing = ({ agent, large = false }: { agent: Agent | null; large?: boolean }) => {
216
216
-
const [profile, setProfile] = useState<any>(null);
270
270
+
export const ProfileThing = ({
271
271
+
agent,
272
272
+
large = false,
273
273
+
}: {
274
274
+
agent: Agent | null;
275
275
+
large?: boolean;
276
276
+
}) => {
277
277
+
const [profile, setProfile] = useState<any>(null);
217
278
218
218
-
useEffect(() => {
219
219
-
const fetchUser = async () => {
220
220
-
const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid;
221
221
-
if (!did) return;
222
222
-
try {
223
223
-
const res = await agent!.getProfile({ actor: did });
224
224
-
setProfile(res.data);
225
225
-
} catch (e) { console.error("Failed to fetch profile", e); }
226
226
-
};
227
227
-
if (agent) fetchUser();
228
228
-
}, [agent]);
229
229
-
230
230
-
if (!profile) {
231
231
-
return ( // Skeleton loader
232
232
-
<div className={`flex items-center gap-2.5 animate-pulse ${large ? 'mb-1' : ''}`}>
233
233
-
<div className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} />
234
234
-
<div className="flex flex-col gap-2">
235
235
-
<div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-28' : 'h-3 w-20'}`} />
236
236
-
<div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-20' : 'h-3 w-16'}`} />
237
237
-
</div>
238
238
-
</div>
239
239
-
);
279
279
+
useEffect(() => {
280
280
+
const fetchUser = async () => {
281
281
+
const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid;
282
282
+
if (!did) return;
283
283
+
try {
284
284
+
const res = await agent!.getProfile({ actor: did });
285
285
+
setProfile(res.data);
286
286
+
} catch (e) {
287
287
+
console.error("Failed to fetch profile", e);
240
288
}
241
241
-
242
242
-
return (
243
243
-
<div className={`flex flex-row items-center gap-2.5 ${large ? 'mb-1' : ''}`}>
244
244
-
<img src={profile?.avatar} alt="avatar" className={`object-cover rounded-full ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} />
245
245
-
<div className="flex flex-col items-start text-left">
246
246
-
<div className={`font-medium ${large ? 'text-gray-800 dark:text-gray-100 text-md' : 'text-gray-800 dark:text-gray-100 text-sm'}`}>{profile?.displayName}</div>
247
247
-
<div className={` ${large ? 'text-gray-500 dark:text-gray-400 text-sm' : 'text-gray-500 dark:text-gray-400 text-xs'}`}>@{profile?.handle}</div>
248
248
-
</div>
289
289
+
};
290
290
+
if (agent) fetchUser();
291
291
+
}, [agent]);
292
292
+
293
293
+
if (!profile) {
294
294
+
return (
295
295
+
// Skeleton loader
296
296
+
<div
297
297
+
className={`flex items-center gap-2.5 animate-pulse ${large ? "mb-1" : ""}`}
298
298
+
>
299
299
+
<div
300
300
+
className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
301
301
+
/>
302
302
+
<div className="flex flex-col gap-2">
303
303
+
<div
304
304
+
className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-28" : "h-3 w-20"}`}
305
305
+
/>
306
306
+
<div
307
307
+
className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-20" : "h-3 w-16"}`}
308
308
+
/>
249
309
</div>
250
250
-
);
251
251
-
};
310
310
+
</div>
311
311
+
);
312
312
+
}
313
313
+
314
314
+
return (
315
315
+
<div
316
316
+
className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`}
317
317
+
>
318
318
+
<img
319
319
+
src={profile?.avatar}
320
320
+
alt="avatar"
321
321
+
className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`}
322
322
+
/>
323
323
+
<div className="flex flex-col items-start text-left">
324
324
+
<div
325
325
+
className={`font-medium ${large ? "text-gray-800 dark:text-gray-100 text-md" : "text-gray-800 dark:text-gray-100 text-sm"}`}
326
326
+
>
327
327
+
{profile?.displayName}
328
328
+
</div>
329
329
+
<div
330
330
+
className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`}
331
331
+
>
332
332
+
@{profile?.handle}
333
333
+
</div>
334
334
+
</div>
335
335
+
</div>
336
336
+
);
337
337
+
};
+403
-122
src/routes/__root.tsx
···
5
5
import type { QueryClient } from "@tanstack/react-query";
6
6
import {
7
7
createRootRouteWithContext,
8
8
-
Link,
8
8
+
// Link,
9
9
// Outlet,
10
10
Scripts,
11
11
useLocation,
···
176
176
)}
177
177
178
178
<div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950">
179
179
-
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start">
179
179
+
<nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start">
180
180
<div className="flex items-center gap-3 mb-4">
181
181
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" />
182
182
<span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100">
···
186
186
</span> */}
187
187
</span>
188
188
</div>
189
189
-
<Link
189
189
+
<MaterialNavItem
190
190
+
InactiveIcon={
191
191
+
<IconMaterialSymbolsHomeOutline className="w-6 h-6" />
192
192
+
}
193
193
+
ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
194
194
+
active={isHome}
195
195
+
onClickCallbback={() =>
196
196
+
navigate({
197
197
+
to: "/",
198
198
+
//params: { did: agent.assertDid },
199
199
+
})
200
200
+
}
201
201
+
text="Home"
202
202
+
/>
203
203
+
204
204
+
<MaterialNavItem
205
205
+
InactiveIcon={
206
206
+
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
207
207
+
}
208
208
+
ActiveIcon={
209
209
+
<IconMaterialSymbolsNotifications className="w-6 h-6" />
210
210
+
}
211
211
+
active={isNotifications}
212
212
+
onClickCallbback={() =>
213
213
+
navigate({
214
214
+
to: "/notifications",
215
215
+
//params: { did: agent.assertDid },
216
216
+
})
217
217
+
}
218
218
+
text="Notifications"
219
219
+
/>
220
220
+
<MaterialNavItem
221
221
+
InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
222
222
+
ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />}
223
223
+
active={location.pathname.startsWith("/feeds")}
224
224
+
onClickCallbback={() =>
225
225
+
navigate({
226
226
+
to: "/feeds",
227
227
+
//params: { did: agent.assertDid },
228
228
+
})
229
229
+
}
230
230
+
text="Feeds"
231
231
+
/>
232
232
+
<MaterialNavItem
233
233
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
234
234
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
235
235
+
active={location.pathname.startsWith("/search")}
236
236
+
onClickCallbback={() =>
237
237
+
navigate({
238
238
+
to: "/search",
239
239
+
//params: { did: agent.assertDid },
240
240
+
})
241
241
+
}
242
242
+
text="Search"
243
243
+
/>
244
244
+
<MaterialNavItem
245
245
+
InactiveIcon={
246
246
+
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
247
247
+
}
248
248
+
ActiveIcon={
249
249
+
<IconMaterialSymbolsAccountCircle className="w-6 h-6" />
250
250
+
}
251
251
+
active={isProfile ?? false}
252
252
+
onClickCallbback={() => {
253
253
+
if (authed && agent && agent.assertDid) {
254
254
+
//window.location.href = `/profile/${agent.assertDid}`;
255
255
+
navigate({
256
256
+
to: "/profile/$did",
257
257
+
params: { did: agent.assertDid },
258
258
+
});
259
259
+
}
260
260
+
}}
261
261
+
text="Profile"
262
262
+
/>
263
263
+
<MaterialNavItem
264
264
+
InactiveIcon={
265
265
+
<IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
266
266
+
}
267
267
+
ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
268
268
+
active={location.pathname.startsWith("/settings")}
269
269
+
onClickCallbback={() =>
270
270
+
navigate({
271
271
+
to: "/settings",
272
272
+
//params: { did: agent.assertDid },
273
273
+
})
274
274
+
}
275
275
+
text="Settings"
276
276
+
/>
277
277
+
<div className="flex flex-row items-center justify-center mt-3">
278
278
+
<MaterialPillButton
279
279
+
InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
280
280
+
ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />}
281
281
+
//active={true}
282
282
+
onClickCallbback={() => setPostOpen(true)}
283
283
+
text="Post"
284
284
+
/>
285
285
+
</div>
286
286
+
{/* <Link
190
287
to="/"
191
288
className={
192
289
`py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` +
···
260
357
<IconMaterialSymbolsAccountCircleOutline width={28} height={28} />
261
358
) : (
262
359
<IconMaterialSymbolsAccountCircle width={28} height={28} />
263
263
-
)
264
264
-
}
360
360
+
)}
265
361
<span>Profile</span>
266
362
</button>
267
363
<Link
···
276
372
<IconMaterialSymbolsSettings width={28} height={28} />
277
373
)}
278
374
<span>Settings</span>
279
279
-
</Link>
280
280
-
<button
375
375
+
</Link> */}
376
376
+
{/* <button
281
377
className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow"
282
378
onClick={() => setPostOpen(true)}
283
379
type="button"
···
288
384
className="text-gray-600 dark:text-gray-400"
289
385
/>
290
386
<span>Post</span>
291
291
-
</button>
387
387
+
</button> */}
292
388
<div className="flex-1"></div>
293
389
<a
294
390
href="https://tangled.sh/@whey.party/red-dwarf"
···
319
415
</div>
320
416
</nav>
321
417
322
322
-
<button
323
323
-
className="lg:hidden fixed bottom-20 right-6 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-blue-600 dark:text-blue-400 rounded-full shadow-lg w-16 h-16 flex items-center justify-center border-4 border-white dark:border-gray-950 transition-all"
324
324
-
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
325
325
-
onClick={() => setPostOpen(true)}
326
326
-
type="button"
327
327
-
aria-label="Create Post"
328
328
-
>
329
329
-
<IconMdiPencilOutline
330
330
-
width={24}
331
331
-
height={24}
332
332
-
className="text-gray-600 dark:text-gray-400"
333
333
-
/>
334
334
-
</button>
418
418
+
{agent?.did && (
419
419
+
<button
420
420
+
className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all"
421
421
+
style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }}
422
422
+
onClick={() => setPostOpen(true)}
423
423
+
type="button"
424
424
+
aria-label="Create Post"
425
425
+
>
426
426
+
<IconMdiPencilOutline
427
427
+
width={24}
428
428
+
height={24}
429
429
+
className="text-gray-600 dark:text-gray-400"
430
430
+
/>
431
431
+
</button>
432
432
+
)}
335
433
336
434
<main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 pb-16 lg:pb-0">
337
337
-
<div className="lg:hidden flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950">
338
338
-
<div className="flex items-center gap-2">
339
339
-
<img
340
340
-
src="/redstar.png"
341
341
-
alt="Red Dwarf Logo"
342
342
-
className="w-6 h-6"
343
343
-
/>
344
344
-
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
345
345
-
Red Dwarf{" "}
346
346
-
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
347
347
-
lite
348
348
-
</span> */}
349
349
-
</span>
350
350
-
</div>
351
351
-
<div className="flex items-center gap-2">
352
352
-
<Login compact={true} />
353
353
-
</div>
354
354
-
</div>
355
355
-
356
435
{children}
357
436
</main>
358
437
···
368
447
</aside>
369
448
</div>
370
449
371
371
-
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40">
372
372
-
<div className="flex justify-around items-center py-2">
373
373
-
<Link
374
374
-
to="/"
375
375
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
376
376
-
isHome
377
377
-
? "text-gray-900 dark:text-gray-100"
378
378
-
: "text-gray-600 dark:text-gray-400"
379
379
-
}`}
380
380
-
>
381
381
-
{!isHome ? (
382
382
-
<IconMaterialSymbolsHomeOutline width={24} height={24} />
383
383
-
) : (
384
384
-
<IconMaterialSymbolsHome width={24} height={24} />
385
385
-
)}
386
386
-
<span className="text-xs mt-1">Home</span>
387
387
-
</Link>
388
388
-
<Link
389
389
-
to="/search"
390
390
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
391
391
-
location.pathname.startsWith("/search")
392
392
-
? "text-gray-900 dark:text-gray-100"
393
393
-
: "text-gray-600 dark:text-gray-400"
394
394
-
}`}
395
395
-
>
396
396
-
{!location.pathname.startsWith("/search") ? (
397
397
-
<IconMaterialSymbolsSearch width={24} height={24} />
398
398
-
) : (
399
399
-
<IconMaterialSymbolsSearch width={24} height={24} />
400
400
-
)}
401
401
-
<span className="text-xs mt-1">Search</span>
402
402
-
</Link>
403
403
-
<Link
404
404
-
to="/notifications"
405
405
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
406
406
-
isNotifications
407
407
-
? "text-gray-900 dark:text-gray-100"
408
408
-
: "text-gray-600 dark:text-gray-400"
409
409
-
}`}
410
410
-
>
411
411
-
{!isNotifications ? (
412
412
-
<IconMaterialSymbolsNotificationsOutline width={24} height={24} />
413
413
-
) : (
414
414
-
<IconMaterialSymbolsNotifications width={24} height={24} />
415
415
-
)}
416
416
-
<span className="text-xs mt-1">Notifications</span>
417
417
-
</Link>
418
418
-
<button
419
419
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
420
420
-
isProfile
421
421
-
? "text-gray-900 dark:text-gray-100"
422
422
-
: "text-gray-600 dark:text-gray-400"
423
423
-
}`}
424
424
-
onClick={() => {
425
425
-
if (authed && agent && agent.assertDid) {
426
426
-
//window.location.href = `/profile/${agent.assertDid}`;
450
450
+
{agent?.did ? (
451
451
+
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40">
452
452
+
<div className="flex justify-around items-center p-2">
453
453
+
<MaterialNavItem
454
454
+
small
455
455
+
InactiveIcon={
456
456
+
<IconMaterialSymbolsHomeOutline className="w-6 h-6" />
457
457
+
}
458
458
+
ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />}
459
459
+
active={isHome}
460
460
+
onClickCallbback={() =>
461
461
+
navigate({
462
462
+
to: "/",
463
463
+
//params: { did: agent.assertDid },
464
464
+
})
465
465
+
}
466
466
+
text="Home"
467
467
+
/>
468
468
+
{/* <Link
469
469
+
to="/"
470
470
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
471
471
+
isHome
472
472
+
? "text-gray-900 dark:text-gray-100"
473
473
+
: "text-gray-600 dark:text-gray-400"
474
474
+
}`}
475
475
+
>
476
476
+
{!isHome ? (
477
477
+
<IconMaterialSymbolsHomeOutline width={24} height={24} />
478
478
+
) : (
479
479
+
<IconMaterialSymbolsHome width={24} height={24} />
480
480
+
)}
481
481
+
<span className="text-xs mt-1">Home</span>
482
482
+
</Link> */}
483
483
+
<MaterialNavItem
484
484
+
small
485
485
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
486
486
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
487
487
+
active={location.pathname.startsWith("/search")}
488
488
+
onClickCallbback={() =>
489
489
+
navigate({
490
490
+
to: "/search",
491
491
+
//params: { did: agent.assertDid },
492
492
+
})
493
493
+
}
494
494
+
text="Search"
495
495
+
/>
496
496
+
{/* <Link
497
497
+
to="/search"
498
498
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
499
499
+
location.pathname.startsWith("/search")
500
500
+
? "text-gray-900 dark:text-gray-100"
501
501
+
: "text-gray-600 dark:text-gray-400"
502
502
+
}`}
503
503
+
>
504
504
+
{!location.pathname.startsWith("/search") ? (
505
505
+
<IconMaterialSymbolsSearch width={24} height={24} />
506
506
+
) : (
507
507
+
<IconMaterialSymbolsSearch width={24} height={24} />
508
508
+
)}
509
509
+
<span className="text-xs mt-1">Search</span>
510
510
+
</Link> */}
511
511
+
<MaterialNavItem
512
512
+
small
513
513
+
InactiveIcon={
514
514
+
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
515
515
+
}
516
516
+
ActiveIcon={
517
517
+
<IconMaterialSymbolsNotifications className="w-6 h-6" />
518
518
+
}
519
519
+
active={isNotifications}
520
520
+
onClickCallbback={() =>
521
521
+
navigate({
522
522
+
to: "/notifications",
523
523
+
//params: { did: agent.assertDid },
524
524
+
})
525
525
+
}
526
526
+
text="Notifications"
527
527
+
/>
528
528
+
{/* <Link
529
529
+
to="/notifications"
530
530
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
531
531
+
isNotifications
532
532
+
? "text-gray-900 dark:text-gray-100"
533
533
+
: "text-gray-600 dark:text-gray-400"
534
534
+
}`}
535
535
+
>
536
536
+
{!isNotifications ? (
537
537
+
<IconMaterialSymbolsNotificationsOutline
538
538
+
width={24}
539
539
+
height={24}
540
540
+
/>
541
541
+
) : (
542
542
+
<IconMaterialSymbolsNotifications width={24} height={24} />
543
543
+
)}
544
544
+
<span className="text-xs mt-1">Notifications</span>
545
545
+
</Link> */}
546
546
+
<MaterialNavItem
547
547
+
small
548
548
+
InactiveIcon={
549
549
+
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
550
550
+
}
551
551
+
ActiveIcon={
552
552
+
<IconMaterialSymbolsAccountCircle className="w-6 h-6" />
553
553
+
}
554
554
+
active={isProfile ?? false}
555
555
+
onClickCallbback={() => {
556
556
+
if (authed && agent && agent.assertDid) {
557
557
+
//window.location.href = `/profile/${agent.assertDid}`;
558
558
+
navigate({
559
559
+
to: "/profile/$did",
560
560
+
params: { did: agent.assertDid },
561
561
+
});
562
562
+
}
563
563
+
}}
564
564
+
text="Profile"
565
565
+
/>
566
566
+
{/* <button
567
567
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
568
568
+
isProfile
569
569
+
? "text-gray-900 dark:text-gray-100"
570
570
+
: "text-gray-600 dark:text-gray-400"
571
571
+
}`}
572
572
+
onClick={() => {
573
573
+
if (authed && agent && agent.assertDid) {
574
574
+
//window.location.href = `/profile/${agent.assertDid}`;
575
575
+
navigate({
576
576
+
to: "/profile/$did",
577
577
+
params: { did: agent.assertDid },
578
578
+
});
579
579
+
}
580
580
+
}}
581
581
+
type="button"
582
582
+
>
583
583
+
<IconMaterialSymbolsAccountCircleOutline width={24} height={24} />
584
584
+
<span className="text-xs mt-1">Profile</span>
585
585
+
</button> */}
586
586
+
<MaterialNavItem
587
587
+
small
588
588
+
InactiveIcon={
589
589
+
<IconMaterialSymbolsSettingsOutline className="w-6 h-6" />
590
590
+
}
591
591
+
ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />}
592
592
+
active={location.pathname.startsWith("/settings")}
593
593
+
onClickCallbback={() =>
427
594
navigate({
428
428
-
to: "/profile/$did",
429
429
-
params: { did: agent.assertDid },
430
430
-
});
595
595
+
to: "/settings",
596
596
+
//params: { did: agent.assertDid },
597
597
+
})
431
598
}
432
432
-
}}
433
433
-
type="button"
434
434
-
>
435
435
-
<IconMaterialSymbolsAccountCircleOutline width={24} height={24} />
436
436
-
<span className="text-xs mt-1">Profile</span>
437
437
-
</button>
438
438
-
<Link
439
439
-
to="/settings"
440
440
-
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
441
441
-
location.pathname.startsWith("/settings")
442
442
-
? "text-gray-900 dark:text-gray-100"
443
443
-
: "text-gray-600 dark:text-gray-400"
444
444
-
}`}
445
445
-
>
446
446
-
{!location.pathname.startsWith("/settings") ? (
447
447
-
<IconMaterialSymbolsSettingsOutline width={24} height={24} />
448
448
-
) : (
449
449
-
<IconMaterialSymbolsSettings width={24} height={24} />
450
450
-
)}
451
451
-
<span className="text-xs mt-1">Settings</span>
452
452
-
</Link>
599
599
+
text="Settings"
600
600
+
/>
601
601
+
{/* <Link
602
602
+
to="/settings"
603
603
+
className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${
604
604
+
location.pathname.startsWith("/settings")
605
605
+
? "text-gray-900 dark:text-gray-100"
606
606
+
: "text-gray-600 dark:text-gray-400"
607
607
+
}`}
608
608
+
>
609
609
+
{!location.pathname.startsWith("/settings") ? (
610
610
+
<IconMaterialSymbolsSettingsOutline width={24} height={24} />
611
611
+
) : (
612
612
+
<IconMaterialSymbolsSettings width={24} height={24} />
613
613
+
)}
614
614
+
<span className="text-xs mt-1">Settings</span>
615
615
+
</Link> */}
616
616
+
</div>
617
617
+
</nav>
618
618
+
) : (
619
619
+
<div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 z-10">
620
620
+
<div className="flex items-center gap-2">
621
621
+
<img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" />
622
622
+
<span className="font-bold text-lg text-gray-900 dark:text-gray-100">
623
623
+
Red Dwarf{" "}
624
624
+
{/* <span className="text-gray-500 dark:text-gray-400 text-sm">
625
625
+
lite
626
626
+
</span> */}
627
627
+
</span>
628
628
+
</div>
629
629
+
<div className="flex items-center gap-2">
630
630
+
<Login compact={true} popup={true} />
631
631
+
</div>
453
632
</div>
454
454
-
</nav>
633
633
+
)}
455
634
456
456
-
<TanStackRouterDevtools position="bottom-right" />
635
635
+
<TanStackRouterDevtools position="bottom-left" />
457
636
<Scripts />
458
637
</>
459
638
);
460
639
}
640
640
+
641
641
+
function MaterialNavItem({
642
642
+
InactiveIcon,
643
643
+
ActiveIcon,
644
644
+
text,
645
645
+
active,
646
646
+
onClickCallbback,
647
647
+
small,
648
648
+
}: {
649
649
+
InactiveIcon: React.ReactElement;
650
650
+
ActiveIcon: React.ReactElement;
651
651
+
text: string;
652
652
+
active: boolean;
653
653
+
onClickCallbback: () => void;
654
654
+
small?: boolean;
655
655
+
}) {
656
656
+
if (small)
657
657
+
return (
658
658
+
<button
659
659
+
className={`flex flex-col items-center rounded-lg transition-colors flex-1 gap-1 ${
660
660
+
active
661
661
+
? "text-gray-900 dark:text-gray-100"
662
662
+
: "text-gray-600 dark:text-gray-400"
663
663
+
}`}
664
664
+
onClick={() => {
665
665
+
onClickCallbback();
666
666
+
}}
667
667
+
>
668
668
+
<div
669
669
+
className={`px-4 py-1 rounded-full flex items-center justify-center ${active ? " bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 hover:dark:bg-gray-700" : "hover:bg-gray-50 hover:dark:bg-gray-900"}`}
670
670
+
>
671
671
+
{active ? ActiveIcon : InactiveIcon}
672
672
+
</div>
673
673
+
<span
674
674
+
className={`text-[12.8px] text-roboto ${active ? "font-medium" : ""}`}
675
675
+
>
676
676
+
{text}
677
677
+
</span>
678
678
+
</button>
679
679
+
);
680
680
+
681
681
+
return (
682
682
+
<button
683
683
+
className={`flex flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 w-full items-center rounded-full transition-colors flex-1 gap-1 ${
684
684
+
active
685
685
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
686
686
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
687
687
+
}`}
688
688
+
onClick={() => {
689
689
+
onClickCallbback();
690
690
+
}}
691
691
+
>
692
692
+
<div className={`mr-4 ${active ? " " : " "}`}>
693
693
+
{active ? ActiveIcon : InactiveIcon}
694
694
+
</div>
695
695
+
<span
696
696
+
className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`}
697
697
+
>
698
698
+
{text}
699
699
+
</span>
700
700
+
</button>
701
701
+
);
702
702
+
}
703
703
+
704
704
+
function MaterialPillButton({
705
705
+
InactiveIcon,
706
706
+
ActiveIcon,
707
707
+
text,
708
708
+
//active,
709
709
+
onClickCallbback,
710
710
+
small,
711
711
+
}: {
712
712
+
InactiveIcon: React.ReactElement;
713
713
+
ActiveIcon: React.ReactElement;
714
714
+
text: string;
715
715
+
//active: boolean;
716
716
+
onClickCallbback: () => void;
717
717
+
small?: boolean;
718
718
+
}) {
719
719
+
const active = false;
720
720
+
return (
721
721
+
<button
722
722
+
className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 items-center rounded-full transition-colors gap-1 ${
723
723
+
active
724
724
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
725
725
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
726
726
+
}`}
727
727
+
onClick={() => {
728
728
+
onClickCallbback();
729
729
+
}}
730
730
+
>
731
731
+
<div className={`mr-2 ${active ? " " : " "}`}>
732
732
+
{active ? ActiveIcon : InactiveIcon}
733
733
+
</div>
734
734
+
<span
735
735
+
className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`}
736
736
+
>
737
737
+
{text}
738
738
+
</span>
739
739
+
</button>
740
740
+
);
741
741
+
}
+23
-13
src/routes/index.tsx
···
3
3
import * as React from "react";
4
4
import { useEffect, useLayoutEffect } from "react";
5
5
6
6
+
import { Header } from "~/components/Header";
6
7
import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed";
7
8
import { useAuth } from "~/providers/UnifiedAuthProvider";
8
9
import {
···
353
354
<div
354
355
className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`}
355
356
>
356
356
-
<div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin">
357
357
-
{savedFeeds.length > 0 ? (
358
358
-
savedFeeds.map((item: any, idx: number) => {
357
357
+
{savedFeeds.length > 0 ? (
358
358
+
<div className="flex items-center px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin">
359
359
+
{savedFeeds.map((item: any, idx: number) => {
359
360
const label = item.value.split("/").pop() || item.value;
360
361
const isActive = selectedFeed === item.value;
361
362
return (
···
363
364
key={item.value || idx}
364
365
className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${
365
366
isActive
366
366
-
? "bg-gray-500 text-white"
367
367
-
: item.pinned
368
368
-
? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
369
369
-
: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
367
367
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
368
368
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
369
369
+
// ? "bg-gray-500 text-white"
370
370
+
// : item.pinned
371
371
+
// ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200"
372
372
+
// : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200"
370
373
}`}
371
374
onClick={() => setSelectedFeed(item.value)}
372
375
title={item.value}
373
376
>
374
377
{label}
375
378
{item.pinned && (
376
376
-
<span className="ml-1 text-xs text-gray-700 dark:text-gray-200">
379
379
+
<span
380
380
+
className={`ml-1 text-xs ${
381
381
+
isActive
382
382
+
? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600"
383
383
+
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800"
384
384
+
}`}
385
385
+
>
377
386
★
378
387
</span>
379
388
)}
380
389
</button>
381
390
);
382
382
-
})
383
383
-
) : (
384
384
-
<span className="text-xl font-bold ml-2">Home</span>
385
385
-
)}
386
386
-
</div>
391
391
+
})}
392
392
+
</div>
393
393
+
) : (
394
394
+
// <span className="text-xl font-bold ml-2">Home</span>
395
395
+
<Header title="Home" />
396
396
+
)}
387
397
{/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>}
388
398
{feedError && <div className="p-4 text-red-500">{feedError.message}</div>}
389
399
{!isFeedLoading && !feedError && feed.length === 0 && (
+14
-3
src/routes/profile.$did/index.tsx
···
1
1
import { useQueryClient } from "@tanstack/react-query";
2
2
-
import { createFileRoute, Link } from "@tanstack/react-router";
2
2
+
import { createFileRoute } from "@tanstack/react-router";
3
3
import React from "react";
4
4
5
5
+
import { Header } from "~/components/Header";
5
6
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
6
7
import { useAuth } from "~/providers/UnifiedAuthProvider";
7
8
import { toggleFollow, useGetFollowState } from "~/utils/followState";
···
104
105
105
106
return (
106
107
<>
107
107
-
<div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
108
108
+
<Header
109
109
+
title={`Profile`}
110
110
+
backButtonCallback={() => {
111
111
+
if (window.history.length > 1) {
112
112
+
window.history.back();
113
113
+
} else {
114
114
+
window.location.assign("/");
115
115
+
}
116
116
+
}}
117
117
+
/>
118
118
+
{/* <div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
108
119
<Link
109
120
to=".."
110
121
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
···
121
132
←
122
133
</Link>
123
134
<span className="text-xl font-bold ml-2">Profile</span>
124
124
-
</div>
135
135
+
</div> */}
125
136
126
137
{/* Profile Header */}
127
138
<div className="w-full max-w-2xl mx-auto overflow-hidden relative bg-gray-100 dark:bg-gray-900">
+14
-31
src/routes/profile.$did/post.$rkey.tsx
···
2
2
import { createFileRoute } from "@tanstack/react-router";
3
3
import React, { useLayoutEffect } from "react";
4
4
5
5
+
import { Header } from "~/components/Header";
5
6
import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer";
6
7
//import { usePersistentStore } from '~/providers/PersistentStoreProvider';
7
8
import {
···
296
297
297
298
return (
298
299
<>
299
299
-
<div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700">
300
300
-
{!nopics ? (
301
301
-
<button
302
302
-
//to=".."
303
303
-
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
304
304
-
onClick={(e) => {
305
305
-
e.preventDefault();
306
306
-
if (window.history.length > 1) {
307
307
-
window.history.back();
308
308
-
} else {
309
309
-
window.location.assign("/");
300
300
+
<Header
301
301
+
title={`Post`}
302
302
+
backButtonCallback={
303
303
+
nopics
304
304
+
? nopics
305
305
+
: () => {
306
306
+
if (window.history.length > 1) {
307
307
+
window.history.back();
308
308
+
} else {
309
309
+
window.location.assign("/");
310
310
+
}
310
311
}
311
311
-
}}
312
312
-
aria-label="Go back"
313
313
-
>
314
314
-
←
315
315
-
</button>
316
316
-
) : (
317
317
-
<button
318
318
-
//to=".."
319
319
-
className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg"
320
320
-
onClick={(e) => {
321
321
-
e.preventDefault();
322
322
-
nopics();
323
323
-
}}
324
324
-
aria-label="Go back"
325
325
-
>
326
326
-
←
327
327
-
</button>
328
328
-
)}
329
329
-
<span className="text-xl font-bold ml-2">Post</span>
330
330
-
</div>
312
312
+
}
313
313
+
/>
331
314
332
315
{parentsLoading && (
333
316
<div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
+8
src/styles/app.css
···
1
1
+
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Spectral+SC:wght@500&display=swap');
1
2
@import "tailwindcss";
2
3
3
4
/* @theme {
···
78
79
color: rgb(29, 122, 242);
79
80
word-break: break-all;
80
81
}
82
82
+
}
83
83
+
84
84
+
.font-inter {
85
85
+
font-family: "Inter", sans-serif;
86
86
+
}
87
87
+
.font-roboto {
88
88
+
font-family: "Roboto", sans-serif;
81
89
}
+2
-1
vite.config.ts
···
39
39
IconsResolver({
40
40
prefix: 'Icon',
41
41
extension: 'jsx',
42
42
+
enabledCollections: ['mdi','material-symbols'],
42
43
}),
43
44
],
44
45
dts: 'src/auto-imports.d.ts',
45
46
}),
46
47
Icons({
47
47
-
autoInstall: true,
48
48
+
//autoInstall: true,
48
49
compiler: 'jsx',
49
50
jsx: 'react'
50
51
}),