Skip to content

Commit 9ff3da3

Browse files
committed
Add channel management features and enhance tweet functionality
- Introduced Channel node with attributes: name, description, creator_username, and created_at. - Implemented edge for Member and ChannelPost to manage channel memberships and posts. - Added walkers for creating, joining, leaving channels, and retrieving channel details. - Enhanced tweet functionalities to support comments and likes within channels. - Implemented create_channel_tweet to allow posting tweets in specific channels.
1 parent f47789e commit 9ff3da3

File tree

6 files changed

+705
-82
lines changed

6 files changed

+705
-82
lines changed

littleX_FULLSTACK/frontend.cl.jac

Lines changed: 205 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Little X - Frontend declarations."""
22

33
import from "@jac/runtime" { jacSignup, jacLogin, jacLogout, jacIsLoggedIn }
4-
sv import from .server { setup_profile, get_profile, get_all_profiles, follow_user, unfollow_user, create_tweet, delete_tweet, like_tweet, add_comment, load_feed, get_trending }
5-
import from "lucide-react" { Home, Compass, User, LogOut, Search, Feather }
4+
sv import from .server { setup_profile, get_profile, get_all_profiles, follow_user, unfollow_user, create_tweet, delete_tweet, like_tweet, add_comment, load_feed, get_trending, create_channel, join_channel, leave_channel, get_channels, get_channel_detail, create_channel_tweet }
5+
import from "lucide-react" { Home, Compass, User, LogOut, Search, Feather, Hash, Users, Plus, ArrowLeft }
66
import from .components.TweetCard { TweetCard }
77
import from .components.AuthForm { AuthForm }
88
import "./global.css";
@@ -26,7 +26,15 @@ def:pub app() -> Any {
2626
bioText: str = "",
2727
trendingTags: list = [],
2828
notifCount: int = 0,
29-
showWelcome: bool = False;
29+
showWelcome: bool = False,
30+
channels: list = [],
31+
channelsLoading: bool = False,
32+
selectedChannel: Any = None,
33+
channelPosts: list = [],
34+
channelComposerText: str = "",
35+
showCreateChannel: bool = False,
36+
newChannelName: str = "",
37+
newChannelDescription: str = "";
3038

3139
can with entry {
3240
isLoggedIn = jacIsLoggedIn();
@@ -54,6 +62,13 @@ def:pub app() -> Any {
5462
async def handleSearch(query: str) -> None;
5563
def handleRepost(tweetId: str, content: str, author: str) -> None;
5664
def handleDismissWelcome() -> None;
65+
async def loadChannels() -> None;
66+
async def handleCreateChannel() -> None;
67+
async def handleJoinChannel(channelId: str) -> None;
68+
async def handleLeaveChannel(channelId: str) -> None;
69+
async def handleSelectChannel(channelId: str) -> None;
70+
async def handleChannelPost() -> None;
71+
def handleBackToChannels() -> None;
5772

5873
if checkingAuth {
5974
return <div className="lx-loading">Loading...</div>;
@@ -106,6 +121,13 @@ def:pub app() -> Any {
106121
<Compass size={22} />
107122
<span>Explore</span>
108123
</div>
124+
<div
125+
className={"lx-nav-item" + (" lx-nav-item-active" if activeTab == "channels" else "")}
126+
onClick={lambda -> None { activeTab = "channels"; loadChannels(); }}
127+
>
128+
<Hash size={22} />
129+
<span>Channels</span>
130+
</div>
109131
<div
110132
className={"lx-nav-item" + (" lx-nav-item-active" if activeTab == "profile" else "")}
111133
onClick={lambda -> None { activeTab = "profile"; localStorage.setItem("lx_activity_total", String(current_total_activity)); notifCount = 0; }}
@@ -140,7 +162,7 @@ def:pub app() -> Any {
140162
<main className="lx-main">
141163
<div className="lx-feed-header">
142164
<span className="lx-feed-title">
143-
{("Home" if activeTab == "feed" else ("Explore" if activeTab == "explore" else "Profile"))}
165+
{("Home" if activeTab == "feed" else ("Explore" if activeTab == "explore" else ("Channels" if activeTab == "channels" else "Profile")))}
144166
</span>
145167
{(
146168
<button
@@ -272,6 +294,179 @@ def:pub app() -> Any {
272294
</div>
273295
) if activeTab == "explore" else None}
274296
297+
{(
298+
<div>
299+
{(
300+
<div>
301+
<div className="lx-channel-actions">
302+
<button
303+
className="lx-post-btn"
304+
onClick={lambda -> None { showCreateChannel = not showCreateChannel; }}
305+
>
306+
<Plus size={16} />
307+
{"Create Channel"}
308+
</button>
309+
</div>
310+
311+
{(
312+
<div className="lx-channel-create-form">
313+
<input
314+
className="lx-input"
315+
placeholder="Channel name"
316+
value={newChannelName}
317+
onChange={lambda e: Any -> None { newChannelName = e.target.value; }}
318+
/>
319+
<input
320+
className="lx-input"
321+
placeholder="Description (optional)"
322+
value={newChannelDescription}
323+
onChange={lambda e: Any -> None { newChannelDescription = e.target.value; }}
324+
/>
325+
<div style={{"display": "flex", "gap": "0.5rem"}}>
326+
<button
327+
className="lx-post-btn"
328+
disabled={not newChannelName.trim()}
329+
onClick={lambda -> None { handleCreateChannel(); }}
330+
>
331+
Create
332+
</button>
333+
<button
334+
className="lx-edit-profile-btn"
335+
onClick={lambda -> None { showCreateChannel = False; newChannelName = ""; newChannelDescription = ""; }}
336+
>
337+
Cancel
338+
</button>
339+
</div>
340+
</div>
341+
) if showCreateChannel else None}
342+
343+
{(
344+
<div className="lx-loading">Loading channels...</div>
345+
) if channelsLoading else None}
346+
347+
{(
348+
<div>
349+
{(
350+
<div className="lx-empty">
351+
<div className="lx-empty-title">No channels yet</div>
352+
<span>Create one to get started!</span>
353+
</div>
354+
) if channels.length == 0 else None}
355+
{[
356+
<div
357+
className="lx-channel-item"
358+
key={ch["id"]}
359+
onClick={lambda -> None { handleSelectChannel(ch["id"]); }}
360+
>
361+
<div className="lx-channel-icon">
362+
<Hash size={20} />
363+
</div>
364+
<div className="lx-channel-item-info">
365+
<div className="lx-channel-item-name">{ch["name"]}</div>
366+
{(
367+
<div className="lx-channel-item-desc">{ch["description"]}</div>
368+
) if ch["description"] else None}
369+
<div className="lx-channel-item-meta">
370+
<Users size={12} />
371+
<span>{String(ch["member_count"]) + " members"}</span>
372+
</div>
373+
</div>
374+
{(
375+
<span className="lx-channel-joined-badge">Joined</span>
376+
) if ch["is_member"] else None}
377+
</div>
378+
for ch in channels
379+
]}
380+
</div>
381+
) if not channelsLoading else None}
382+
</div>
383+
) if not selectedChannel else (
384+
<div>
385+
<div className="lx-channel-header">
386+
<button
387+
className="lx-edit-profile-btn"
388+
onClick={lambda -> None { handleBackToChannels(); }}
389+
style={{"display": "flex", "alignItems": "center", "gap": "0.3rem"}}
390+
>
391+
<ArrowLeft size={16} />
392+
Back
393+
</button>
394+
<div className="lx-channel-name">
395+
<Hash size={20} />
396+
{selectedChannel.name}
397+
</div>
398+
{(
399+
<div className="lx-channel-desc">{selectedChannel.description}</div>
400+
) if selectedChannel.description else None}
401+
<div className="lx-channel-meta">
402+
<Users size={14} />
403+
<span>{String(selectedChannel.member_count) + " members"}</span>
404+
{(
405+
<button
406+
className="lx-edit-profile-btn"
407+
onClick={lambda -> None { handleLeaveChannel(selectedChannel.id); }}
408+
>
409+
Leave
410+
</button>
411+
) if selectedChannel.is_member else (
412+
<button
413+
className="lx-follow-btn"
414+
onClick={lambda -> None { handleJoinChannel(selectedChannel.id); }}
415+
>
416+
Join
417+
</button>
418+
)}
419+
</div>
420+
</div>
421+
422+
{(
423+
<div className="lx-composer">
424+
<img className="lx-avatar" src={profileAvatar} alt={profileUsername} />
425+
<div className="lx-composer-body">
426+
<textarea
427+
className="lx-composer-textarea"
428+
placeholder={"Post in #" + selectedChannel.name}
429+
value={channelComposerText}
430+
onChange={lambda e: Any -> None { channelComposerText = e.target.value; }}
431+
rows={2}
432+
/>
433+
<div className="lx-composer-footer">
434+
<div />
435+
<button
436+
className="lx-post-btn"
437+
disabled={not channelComposerText.trim()}
438+
onClick={lambda -> None { handleChannelPost(); }}
439+
>
440+
Post
441+
</button>
442+
</div>
443+
</div>
444+
</div>
445+
) if selectedChannel.is_member else None}
446+
447+
{(
448+
<div className="lx-empty">
449+
<div className="lx-empty-title">No posts yet</div>
450+
<span>{("Be the first to post!" if selectedChannel.is_member else "Join this channel to post!")}</span>
451+
</div>
452+
) if channelPosts.length == 0 else None}
453+
454+
{[
455+
<TweetCard
456+
key={tweet["id"]}
457+
tweet={tweet}
458+
myUsername={profileUsername}
459+
onLike={handleLike}
460+
onComment={handleComment}
461+
onDelete={handleDelete}
462+
onRepost={handleRepost}
463+
/> for tweet in channelPosts
464+
]}
465+
</div>
466+
)}
467+
</div>
468+
) if activeTab == "channels" else None}
469+
275470
{(
276471
<div>
277472
<div className="lx-profile-banner" />
@@ -429,6 +624,12 @@ def:pub app() -> Any {
429624
>
430625
<Compass size={24} />
431626
</div>
627+
<div
628+
className={"lx-mobile-nav-item" + (" lx-mobile-nav-item-active" if activeTab == "channels" else "")}
629+
onClick={lambda -> None { activeTab = "channels"; loadChannels(); }}
630+
>
631+
<Hash size={24} />
632+
</div>
432633
<div
433634
className={"lx-mobile-nav-item" + (" lx-mobile-nav-item-active" if activeTab == "profile" else "")}
434635
onClick={lambda -> None { activeTab = "profile"; localStorage.setItem("lx_activity_total", String(current_total_activity)); notifCount = 0; }}

0 commit comments

Comments
 (0)