Skip to content

Commit 9a82fcb

Browse files
committed
Exemplary version of littlex started
1 parent 9ff3da3 commit 9a82fcb

File tree

13 files changed

+3815
-0
lines changed

13 files changed

+3815
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.jac_mypy_cache/
2+
.jac/
15.7 KB
Loading
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Authentication form component for Little X."""
2+
3+
def:pub AuthForm(props: Any) -> Any {
4+
isSignup = props.isSignup;
5+
username = props.username;
6+
password = props.password;
7+
error = props.error;
8+
loading = props.loading;
9+
onUsernameChange = props.onUsernameChange;
10+
onPasswordChange = props.onPasswordChange;
11+
onSubmit = props.onSubmit;
12+
onToggleMode = props.onToggleMode;
13+
14+
return <div className="lx-auth-root">
15+
<div className="lx-auth-left">
16+
<img src="/static/assets/littlex-logo.png" className="lx-auth-logo-big" alt="little-x" />
17+
</div>
18+
<div className="lx-auth-right">
19+
<div className="lx-auth-box">
20+
<img src="/static/assets/littlex-logo.png" className="lx-auth-mobile-logo" alt="little-x" />
21+
<div className="lx-auth-heading">LITTLE-X</div>
22+
<div className="lx-auth-sub">
23+
{"Create your account and join the conversation" if isSignup else "Sign in to your account"}
24+
</div>
25+
{(
26+
<div className="lx-auth-error">{error}</div>
27+
) if error else None}
28+
<form onSubmit={onSubmit} className="lx-auth-form-fields">
29+
<input
30+
className="lx-input"
31+
type="text"
32+
placeholder="Username"
33+
value={username}
34+
onChange={onUsernameChange}
35+
autoComplete="username"
36+
/>
37+
<input
38+
className="lx-input"
39+
type="password"
40+
placeholder="Password"
41+
value={password}
42+
onChange={onPasswordChange}
43+
autoComplete={"new-password" if isSignup else "current-password"}
44+
/>
45+
<button
46+
className="lx-auth-btn"
47+
type="submit"
48+
disabled={loading}
49+
>
50+
{("Processing..." if loading else ("Sign Up" if isSignup else "Log In"))}
51+
</button>
52+
</form>
53+
<div className="lx-auth-toggle">
54+
{("Already have an account? " if isSignup else "New to Little-X? ")}
55+
<span className="lx-auth-toggle-link" onClick={onToggleMode}>
56+
{"Log In" if isSignup else "Sign Up"}
57+
</span>
58+
</div>
59+
</div>
60+
</div>
61+
</div>;
62+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Button component for the Jac client application."""
2+
3+
def:pub Button(label: str, onClick: any, variant: str = "primary", disabled: bool = False) -> any {
4+
base_styles = {
5+
"padding": "0.75rem 1.5rem",
6+
"fontSize": "1rem",
7+
"fontWeight": "600",
8+
"borderRadius": "0.5rem",
9+
"border": "none",
10+
"cursor": "not-allowed" if disabled else "pointer",
11+
"transition": "all 0.2s ease"
12+
};
13+
14+
variant_styles = {
15+
"primary": {
16+
"backgroundColor": "#9ca3af" if disabled else "#3b82f6",
17+
"color": "#ffffff"
18+
},
19+
"secondary": {
20+
"backgroundColor": "#e5e7eb" if disabled else "#6b7280",
21+
"color": "#ffffff"
22+
}
23+
};
24+
25+
return <button
26+
style={{**base_styles, **variant_styles[variant]}}
27+
onClick={onClick}
28+
disabled={disabled}
29+
>
30+
{label}
31+
</button>;
32+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""Tweet card component for Little X."""
2+
3+
import from "lucide-react" { Heart, MessageCircle, Trash2, Repeat2 }
4+
5+
def:pub TweetCard(props: Any) -> Any {
6+
tweet = props.tweet;
7+
myUsername = props.myUsername;
8+
onLike = props.onLike;
9+
onComment = props.onComment;
10+
onDelete = props.onDelete;
11+
onRepost = props.onRepost;
12+
13+
has showComments: bool = False,
14+
commentText: str = "";
15+
16+
likeFn = onLike;
17+
commentFn = onComment;
18+
deleteFn = onDelete;
19+
repostFn = onRepost;
20+
21+
is_liked = tweet["likes"].includes(myUsername);
22+
like_count = String(len(tweet["likes"]));
23+
comment_count = String(len(tweet["comments"]));
24+
author_avatar = "https://i.pravatar.cc/150?u=fake@" + tweet["author_username"];
25+
26+
raw_name = tweet["author_username"] if tweet["author_username"] else "unknown";
27+
display_name = raw_name.split("@")[0] if raw_name.includes("@") else raw_name;
28+
29+
diff_ms = Date.now() - Reflect.construct(Date, [tweet["created_at"]]).getTime() if tweet["created_at"] else 0;
30+
diff_min = Math.floor(diff_ms / 60000);
31+
diff_hr = Math.floor(diff_min / 60);
32+
diff_day = Math.floor(diff_hr / 24);
33+
date_str = (
34+
"now" if diff_min < 1 else (
35+
String(diff_min) + "m" if diff_hr < 1 else (
36+
String(diff_hr) + "h" if diff_day < 1 else (
37+
String(diff_day) + "d" if diff_day < 30 else tweet["created_at"].substring(0, 10)
38+
)
39+
)
40+
)
41+
) if tweet["created_at"] else "";
42+
43+
return <div className="lx-tweet">
44+
<img className="lx-avatar" src={author_avatar} alt={tweet["author_username"]} />
45+
<div className="lx-tweet-body">
46+
<div className="lx-tweet-header">
47+
<span className="lx-tweet-name">{display_name}</span>
48+
<span className="lx-tweet-handle">{"@" + display_name}</span>
49+
<span className="lx-tweet-dot">·</span>
50+
<span className="lx-tweet-time">{date_str}</span>
51+
</div>
52+
<div className="lx-tweet-text">{tweet["content"]}</div>
53+
<div className="lx-tweet-actions">
54+
<button
55+
className="lx-act-btn lx-act-comment"
56+
onClick={lambda -> None { showComments = not showComments; }}
57+
>
58+
<MessageCircle size={16} />
59+
<span>{comment_count}</span>
60+
</button>
61+
<button
62+
className="lx-act-btn lx-act-repost"
63+
onClick={lambda -> None { repostFn.call(None, tweet["id"], tweet["content"], tweet["author_username"]); }}
64+
title="Repost"
65+
>
66+
<Repeat2 size={16} />
67+
</button>
68+
<button
69+
className={"lx-act-btn lx-act-liked" if is_liked else "lx-act-btn lx-act-like"}
70+
onClick={lambda -> None { likeFn.call(None, tweet["id"]); }}
71+
>
72+
<Heart size={16} />
73+
<span>{like_count}</span>
74+
</button>
75+
{(
76+
<button
77+
className="lx-act-btn lx-act-delete"
78+
onClick={lambda -> None { deleteFn.call(None, tweet["id"]); }}
79+
>
80+
<Trash2 size={16} />
81+
</button>
82+
) if tweet["is_mine"] else None}
83+
</div>
84+
{(
85+
<div className="lx-comments-section">
86+
{[
87+
<div className="lx-comment-item" key={c["created_at"]}>
88+
<img className="lx-avatar-sm" src={"https://i.pravatar.cc/150?u=fake@" + c["username"]} alt={c["username"]} />
89+
<div>
90+
<div className="lx-comment-meta">
91+
<strong>{c["username"]}</strong>
92+
<span>{" · "}</span>
93+
</div>
94+
<div className="lx-comment-text">{c["content"]}</div>
95+
</div>
96+
</div>
97+
for c in tweet["comments"]
98+
]}
99+
<div className="lx-comment-form">
100+
<input
101+
className="lx-comment-input"
102+
placeholder="Reply..."
103+
value={commentText}
104+
onChange={lambda e: Any -> None { commentText = e.target.value; }}
105+
onKeyDown={lambda e: Any -> None {
106+
if e.key == "Enter" and commentText.trim() {
107+
commentFn.call(None, tweet["id"], commentText);
108+
commentText = "";
109+
}
110+
}}
111+
/>
112+
<button
113+
className="lx-comment-submit"
114+
onClick={lambda -> None {
115+
if commentText.trim() {
116+
commentFn.call(None, tweet["id"], commentText);
117+
commentText = "";
118+
}
119+
}}
120+
>
121+
Reply
122+
</button>
123+
</div>
124+
</div>
125+
) if showComments else None}
126+
</div>
127+
</div>;
128+
}

0 commit comments

Comments
 (0)