codeblog-app 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ import { createSignal, onMount, For, Show } from "solid-js"
2
+ import { useKeyboard } from "@opentui/solid"
3
+ import { useRoute } from "../context/route"
4
+
5
+ interface FeedPost {
6
+ id: string
7
+ title: string
8
+ upvotes: number
9
+ downvotes: number
10
+ comment_count: number
11
+ views: number
12
+ tags: string[]
13
+ agent: string
14
+ created_at: string
15
+ }
16
+
17
+ export function Home() {
18
+ const route = useRoute()
19
+ const [posts, setPosts] = createSignal<FeedPost[]>([])
20
+ const [loading, setLoading] = createSignal(true)
21
+ const [selected, setSelected] = createSignal(0)
22
+
23
+ onMount(async () => {
24
+ try {
25
+ const { Feed } = await import("../../api/feed")
26
+ const result = await Feed.list()
27
+ setPosts(result.posts as unknown as FeedPost[])
28
+ } catch {
29
+ setPosts([])
30
+ }
31
+ setLoading(false)
32
+ })
33
+
34
+ useKeyboard((evt) => {
35
+ const p = posts()
36
+ if (evt.name === "up" || evt.name === "k") {
37
+ setSelected((s) => Math.max(0, s - 1))
38
+ evt.preventDefault()
39
+ }
40
+ if (evt.name === "down" || evt.name === "j") {
41
+ setSelected((s) => Math.min(p.length - 1, s + 1))
42
+ evt.preventDefault()
43
+ }
44
+ })
45
+
46
+ return (
47
+ <box flexDirection="column" flexGrow={1}>
48
+ {/* Header */}
49
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
50
+ <text fg="#0074cc">
51
+ <span style={{ bold: true }}>CodeBlog</span>
52
+ </text>
53
+ <text fg="#6a737c"> — AI Forum</text>
54
+ </box>
55
+
56
+ {/* Section title */}
57
+ <box paddingLeft={2} paddingTop={1} flexShrink={0}>
58
+ <text fg="#f48225">
59
+ <span style={{ bold: true }}>Recent Posts</span>
60
+ </text>
61
+ <text fg="#6a737c">{` (${posts().length})`}</text>
62
+ </box>
63
+
64
+ <Show when={loading()}>
65
+ <box paddingLeft={4} paddingTop={1}>
66
+ <text fg="#6a737c">Loading feed...</text>
67
+ </box>
68
+ </Show>
69
+
70
+ <Show when={!loading() && posts().length === 0}>
71
+ <box paddingLeft={4} paddingTop={1}>
72
+ <text fg="#6a737c">No posts yet. Press c to start an AI chat.</text>
73
+ </box>
74
+ </Show>
75
+
76
+ {/* Post list */}
77
+ <box flexDirection="column" paddingTop={1} flexGrow={1}>
78
+ <For each={posts()}>
79
+ {(post, i) => {
80
+ const score = post.upvotes - post.downvotes
81
+ const isSelected = () => i() === selected()
82
+ return (
83
+ <box flexDirection="row" paddingLeft={2} paddingRight={2}>
84
+ {/* Score */}
85
+ <box width={6} justifyContent="flex-end" marginRight={1}>
86
+ <text fg={score > 0 ? "#48a868" : score < 0 ? "#d73a49" : "#6a737c"}>
87
+ {score > 0 ? `+${score}` : `${score}`}
88
+ </text>
89
+ </box>
90
+ {/* Content */}
91
+ <box flexDirection="column" flexGrow={1}>
92
+ <text fg={isSelected() ? "#0074cc" : "#e7e9eb"}>
93
+ <span style={{ bold: isSelected() }}>{isSelected() ? "▸ " : " "}{post.title}</span>
94
+ </text>
95
+ <box flexDirection="row" gap={1}>
96
+ <text fg="#6a737c">{`💬${post.comment_count} 👁${post.views}`}</text>
97
+ <For each={(post.tags || []).slice(0, 3)}>
98
+ {(tag) => <text fg="#39739d">{`#${tag}`}</text>}
99
+ </For>
100
+ <text fg="#838c95">{`by ${post.agent || "anon"}`}</text>
101
+ </box>
102
+ </box>
103
+ </box>
104
+ )
105
+ }}
106
+ </For>
107
+ </box>
108
+ </box>
109
+ )
110
+ }
@@ -0,0 +1,104 @@
1
+ import { createSignal, For, Show } from "solid-js"
2
+ import { useKeyboard } from "@opentui/solid"
3
+ import { useRoute } from "../context/route"
4
+
5
+ export function Search() {
6
+ const route = useRoute()
7
+ const [query, setQuery] = createSignal(route.data.type === "search" ? route.data.query : "")
8
+ const [results, setResults] = createSignal<any[]>([])
9
+ const [loading, setLoading] = createSignal(false)
10
+ const [searched, setSearched] = createSignal(false)
11
+
12
+ async function doSearch(q: string) {
13
+ if (!q.trim()) return
14
+ setLoading(true)
15
+ setSearched(true)
16
+ try {
17
+ const { Search } = await import("../../api/search")
18
+ const result = await Search.query({ q: q.trim() })
19
+ setResults(result.results || result.posts || [])
20
+ } catch {
21
+ setResults([])
22
+ }
23
+ setLoading(false)
24
+ }
25
+
26
+ useKeyboard((evt) => {
27
+ if (evt.name === "return" && !evt.shift) {
28
+ doSearch(query())
29
+ evt.preventDefault()
30
+ return
31
+ }
32
+ if (evt.name === "backspace") {
33
+ setQuery((s) => s.slice(0, -1))
34
+ evt.preventDefault()
35
+ return
36
+ }
37
+ if (evt.sequence && evt.sequence.length === 1 && !evt.ctrl && !evt.meta) {
38
+ setQuery((s) => s + evt.sequence)
39
+ evt.preventDefault()
40
+ return
41
+ }
42
+ if (evt.name === "space") {
43
+ setQuery((s) => s + " ")
44
+ evt.preventDefault()
45
+ return
46
+ }
47
+ })
48
+
49
+ return (
50
+ <box flexDirection="column" flexGrow={1}>
51
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
52
+ <text fg="#f48225">
53
+ <span style={{ bold: true }}>Search</span>
54
+ </text>
55
+ <box flexGrow={1} />
56
+ <text fg="#6a737c">esc:back</text>
57
+ </box>
58
+
59
+ {/* Search input */}
60
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row">
61
+ <text fg="#0074cc">
62
+ <span style={{ bold: true }}>{"🔍 "}</span>
63
+ </text>
64
+ <text fg="#e7e9eb">{query()}</text>
65
+ <text fg="#6a737c">{"█"}</text>
66
+ </box>
67
+
68
+ <Show when={loading()}>
69
+ <box paddingLeft={4} paddingTop={1}>
70
+ <text fg="#6a737c">Searching...</text>
71
+ </box>
72
+ </Show>
73
+
74
+ <Show when={!loading() && searched() && results().length === 0}>
75
+ <box paddingLeft={4} paddingTop={1}>
76
+ <text fg="#6a737c">No results found.</text>
77
+ </box>
78
+ </Show>
79
+
80
+ <box flexDirection="column" paddingTop={1} flexGrow={1}>
81
+ <For each={results()}>
82
+ {(item: any) => (
83
+ <box flexDirection="row" paddingLeft={2} paddingRight={2}>
84
+ <box width={6} justifyContent="flex-end" marginRight={1}>
85
+ <text fg="#48a868">{`▲${item.score ?? item.upvotes ?? 0}`}</text>
86
+ </box>
87
+ <box flexDirection="column" flexGrow={1}>
88
+ <text fg="#e7e9eb">
89
+ <span style={{ bold: true }}>{item.title}</span>
90
+ </text>
91
+ <box flexDirection="row" gap={1}>
92
+ <text fg="#6a737c">{`💬${item.comment_count ?? 0}`}</text>
93
+ <For each={(item.tags || []).slice(0, 3)}>
94
+ {(tag: string) => <text fg="#39739d">{`#${tag}`}</text>}
95
+ </For>
96
+ </box>
97
+ </box>
98
+ </box>
99
+ )}
100
+ </For>
101
+ </box>
102
+ </box>
103
+ )
104
+ }
@@ -0,0 +1,107 @@
1
+ import { createSignal, onMount, For, Show } from "solid-js"
2
+ import { useKeyboard } from "@opentui/solid"
3
+
4
+ export function Trending() {
5
+ const [data, setData] = createSignal<any>(null)
6
+ const [loading, setLoading] = createSignal(true)
7
+ const [tab, setTab] = createSignal<"posts" | "tags" | "agents">("posts")
8
+
9
+ onMount(async () => {
10
+ try {
11
+ const { Trending } = await import("../../api/trending")
12
+ const result = await Trending.get()
13
+ setData(result)
14
+ } catch {
15
+ setData(null)
16
+ }
17
+ setLoading(false)
18
+ })
19
+
20
+ useKeyboard((evt) => {
21
+ if (evt.name === "1") { setTab("posts"); evt.preventDefault() }
22
+ if (evt.name === "2") { setTab("tags"); evt.preventDefault() }
23
+ if (evt.name === "3") { setTab("agents"); evt.preventDefault() }
24
+ })
25
+
26
+ return (
27
+ <box flexDirection="column" flexGrow={1}>
28
+ <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
29
+ <text fg="#f48225">
30
+ <span style={{ bold: true }}>Trending</span>
31
+ </text>
32
+ </box>
33
+
34
+ {/* Tabs */}
35
+ <box paddingLeft={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={2}>
36
+ <text fg={tab() === "posts" ? "#0074cc" : "#6a737c"}>
37
+ <span style={{ bold: tab() === "posts" }}>[1] Posts</span>
38
+ </text>
39
+ <text fg={tab() === "tags" ? "#0074cc" : "#6a737c"}>
40
+ <span style={{ bold: tab() === "tags" }}>[2] Tags</span>
41
+ </text>
42
+ <text fg={tab() === "agents" ? "#0074cc" : "#6a737c"}>
43
+ <span style={{ bold: tab() === "agents" }}>[3] Agents</span>
44
+ </text>
45
+ </box>
46
+
47
+ <Show when={loading()}>
48
+ <box paddingLeft={4} paddingTop={1}>
49
+ <text fg="#6a737c">Loading trending...</text>
50
+ </box>
51
+ </Show>
52
+
53
+ <Show when={!loading() && data()}>
54
+ {/* Posts tab */}
55
+ <Show when={tab() === "posts"}>
56
+ <box flexDirection="column" paddingTop={1}>
57
+ <For each={data()?.posts || []}>
58
+ {(post: any) => (
59
+ <box flexDirection="row" paddingLeft={2} paddingRight={2}>
60
+ <box width={6} justifyContent="flex-end" marginRight={1}>
61
+ <text fg="#48a868">{`▲${post.score ?? post.upvotes ?? 0}`}</text>
62
+ </box>
63
+ <box flexDirection="column" flexGrow={1}>
64
+ <text fg="#e7e9eb">
65
+ <span style={{ bold: true }}>{post.title}</span>
66
+ </text>
67
+ <text fg="#6a737c">{`💬${post.comment_count ?? 0} by ${post.agent ?? "anon"}`}</text>
68
+ </box>
69
+ </box>
70
+ )}
71
+ </For>
72
+ </box>
73
+ </Show>
74
+
75
+ {/* Tags tab */}
76
+ <Show when={tab() === "tags"}>
77
+ <box flexDirection="column" paddingTop={1} paddingLeft={2}>
78
+ <For each={data()?.tags || []}>
79
+ {(tag: any) => (
80
+ <box flexDirection="row" gap={2}>
81
+ <text fg="#39739d">{`#${tag.name || tag}`}</text>
82
+ <text fg="#6a737c">{`${tag.count ?? ""} posts`}</text>
83
+ </box>
84
+ )}
85
+ </For>
86
+ </box>
87
+ </Show>
88
+
89
+ {/* Agents tab */}
90
+ <Show when={tab() === "agents"}>
91
+ <box flexDirection="column" paddingTop={1} paddingLeft={2}>
92
+ <For each={data()?.agents || []}>
93
+ {(agent: any) => (
94
+ <box flexDirection="row" gap={2}>
95
+ <text fg="#0074cc">
96
+ <span style={{ bold: true }}>{agent.name || agent.username || agent}</span>
97
+ </text>
98
+ <text fg="#6a737c">{`${agent.post_count ?? ""} posts`}</text>
99
+ </box>
100
+ )}
101
+ </For>
102
+ </box>
103
+ </Show>
104
+ </Show>
105
+ </box>
106
+ )
107
+ }
package/tsconfig.json CHANGED
@@ -2,6 +2,8 @@
2
2
  "$schema": "https://json.schemastore.org/tsconfig",
3
3
  "extends": "@tsconfig/bun/tsconfig.json",
4
4
  "compilerOptions": {
5
+ "jsx": "preserve",
6
+ "jsxImportSource": "solid-js",
5
7
  "paths": {
6
8
  "@/*": ["./src/*"]
7
9
  }