+19
-1
frontend/src/App.tsx
+19
-1
frontend/src/App.tsx
···
1
-
import React, { useState } from 'react';
1
+
import React, { useState, useEffect } from 'react';
2
2
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
3
3
import { FollowingFeed } from './components/FollowingFeed';
4
4
import { ProfilePage } from './components/ProfilePage';
5
5
import { PostView } from './components/PostView';
6
6
import { ThreadView } from './components/ThreadView';
7
7
import { PostComposer } from './components/PostComposer';
8
+
import { ApiClient } from './api';
8
9
import './App.css';
9
10
10
11
function Navigation() {
11
12
const location = useLocation();
13
+
const [myHandle, setMyHandle] = useState<string | null>(null);
14
+
15
+
useEffect(() => {
16
+
ApiClient.getMe().then(data => {
17
+
setMyHandle(data.handle);
18
+
}).catch(err => {
19
+
console.error('Failed to fetch current user:', err);
20
+
});
21
+
}, []);
12
22
13
23
return (
14
24
<nav className="app-nav">
···
23
33
>
24
34
Following
25
35
</Link>
36
+
{myHandle && (
37
+
<Link
38
+
to={`/profile/${myHandle}`}
39
+
className={`nav-link ${location.pathname.includes('/profile/') ? 'active' : ''}`}
40
+
>
41
+
Profile
42
+
</Link>
43
+
)}
26
44
</div>
27
45
</div>
28
46
</nav>
+8
frontend/src/api.ts
+8
frontend/src/api.ts
···
3
3
const API_BASE_URL = 'http://localhost:4444/api';
4
4
5
5
export class ApiClient {
6
+
static async getMe(): Promise<{did: string, handle: string}> {
7
+
const response = await fetch(`${API_BASE_URL}/me`);
8
+
if (!response.ok) {
9
+
throw new Error(`Failed to fetch current user: ${response.statusText}`);
10
+
}
11
+
return response.json();
12
+
}
13
+
6
14
static async getFollowingFeed(cursor?: string): Promise<FeedResponse> {
7
15
const url = cursor
8
16
? `${API_BASE_URL}/followingfeed?cursor=${encodeURIComponent(cursor)}`
+28
frontend/src/components/ProfilePage.css
+28
frontend/src/components/ProfilePage.css
···
147
147
padding: 0 20px;
148
148
}
149
149
150
+
.profile-tabs {
151
+
display: flex;
152
+
border-bottom: 1px solid #e1e8ed;
153
+
margin-bottom: 16px;
154
+
}
155
+
156
+
.profile-tab {
157
+
flex: 1;
158
+
padding: 16px;
159
+
background: none;
160
+
border: none;
161
+
border-bottom: 2px solid transparent;
162
+
font-size: 15px;
163
+
font-weight: 600;
164
+
color: #536471;
165
+
cursor: pointer;
166
+
transition: all 0.2s;
167
+
}
168
+
169
+
.profile-tab:hover {
170
+
background-color: #f7f9fa;
171
+
}
172
+
173
+
.profile-tab--active {
174
+
color: #1da1f2;
175
+
border-bottom-color: #1da1f2;
176
+
}
177
+
150
178
.posts-header {
151
179
padding: 16px 0;
152
180
border-bottom: 1px solid #e1e8ed;
+21
-7
frontend/src/components/ProfilePage.tsx
+21
-7
frontend/src/components/ProfilePage.tsx
···
16
16
const [userDid, setUserDid] = useState<string | null>(null);
17
17
const [cursor, setCursor] = useState<string | null>(null);
18
18
const [hasMore, setHasMore] = useState(true);
19
+
const [activeTab, setActiveTab] = useState<'posts' | 'replies'>('posts');
19
20
const observerTarget = useRef<HTMLDivElement>(null);
20
21
21
22
useEffect(() => {
···
189
190
</div>
190
191
191
192
<div className="profile-content">
192
-
<div className="posts-header">
193
-
<h2>Posts ({posts.length})</h2>
193
+
<div className="profile-tabs">
194
+
<button
195
+
className={`profile-tab ${activeTab === 'posts' ? 'profile-tab--active' : ''}`}
196
+
onClick={() => setActiveTab('posts')}
197
+
>
198
+
Posts
199
+
</button>
200
+
<button
201
+
className={`profile-tab ${activeTab === 'replies' ? 'profile-tab--active' : ''}`}
202
+
onClick={() => setActiveTab('replies')}
203
+
>
204
+
Replies
205
+
</button>
194
206
</div>
195
207
196
208
<div className="posts-list">
197
-
{posts.map((post, index) => (
198
-
<PostCard key={post.uri || index} postResponse={post} />
199
-
))}
200
-
{posts.length === 0 && !loading && (
209
+
{posts
210
+
.filter(post => activeTab === 'posts' ? !post.replyTo : !!post.replyTo)
211
+
.map((post, index) => (
212
+
<PostCard key={post.uri || index} postResponse={post} />
213
+
))}
214
+
{posts.filter(post => activeTab === 'posts' ? !post.replyTo : !!post.replyTo).length === 0 && !loading && (
201
215
<div className="empty-posts">
202
-
<p>No posts yet</p>
216
+
<p>{activeTab === 'posts' ? 'No posts yet' : 'No replies yet'}</p>
203
217
</div>
204
218
)}
205
219
{hasMore && <div ref={observerTarget} style={{ height: '20px' }} />}
+17
handlers.go
+17
handlers.go
···
25
25
e.GET("/debug", s.handleGetDebugInfo)
26
26
27
27
views := e.Group("/api")
28
+
views.GET("/me", s.handleGetMe)
28
29
views.GET("/profile/:account/post/:rkey", s.handleGetPost)
29
30
views.GET("/profile/:account", s.handleGetProfileView)
30
31
views.GET("/profile/:account/posts", s.handleGetProfilePosts)
···
45
46
46
47
return e.JSON(200, map[string]any{
47
48
"seq": seq,
49
+
})
50
+
}
51
+
52
+
func (s *Server) handleGetMe(e echo.Context) error {
53
+
ctx := e.Request().Context()
54
+
55
+
resp, err := s.dir.LookupDID(ctx, syntax.DID(s.mydid))
56
+
if err != nil {
57
+
return e.JSON(500, map[string]any{
58
+
"error": "failed to lookup handle",
59
+
})
60
+
}
61
+
62
+
return e.JSON(200, map[string]any{
63
+
"did": s.mydid,
64
+
"handle": resp.Handle.String(),
48
65
})
49
66
}
50
67