theboardtui 0.1.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.
Files changed (51) hide show
  1. package/README.md +156 -0
  2. package/dist/App.js +129 -0
  3. package/dist/components/BoardDetail.js +50 -0
  4. package/dist/components/BoardsList.js +93 -0
  5. package/dist/components/ChatInterface.js +115 -0
  6. package/dist/components/CompactList.js +22 -0
  7. package/dist/components/Dashboard.js +74 -0
  8. package/dist/components/Layout.js +6 -0
  9. package/dist/components/Login.js +62 -0
  10. package/dist/components/MembersList.js +117 -0
  11. package/dist/components/Navigation.js +132 -0
  12. package/dist/components/ProblemDetail.js +576 -0
  13. package/dist/components/ProblemForm.js +126 -0
  14. package/dist/components/ProblemsList.js +118 -0
  15. package/dist/components/QuickActions.js +173 -0
  16. package/dist/components/QuickCreate.js +159 -0
  17. package/dist/components/ReportViewer.js +7 -0
  18. package/dist/components/Settings.js +53 -0
  19. package/dist/components/StatusMonitor.js +47 -0
  20. package/dist/components/layout/Container.js +6 -0
  21. package/dist/components/layout/Grid.js +6 -0
  22. package/dist/components/layout/Header.js +8 -0
  23. package/dist/components/layout/Section.js +6 -0
  24. package/dist/components/layout/Stack.js +7 -0
  25. package/dist/components/ui/Badge.js +16 -0
  26. package/dist/components/ui/Button.js +19 -0
  27. package/dist/components/ui/Card.js +15 -0
  28. package/dist/components/ui/Divider.js +9 -0
  29. package/dist/components/ui/ErrorDisplay.js +10 -0
  30. package/dist/components/ui/Input.js +13 -0
  31. package/dist/components/ui/List.js +13 -0
  32. package/dist/components/ui/Panel.js +11 -0
  33. package/dist/components/ui/Spinner.js +16 -0
  34. package/dist/design/borders.js +51 -0
  35. package/dist/design/colors.js +45 -0
  36. package/dist/design/index.js +30 -0
  37. package/dist/design/spacing.js +41 -0
  38. package/dist/design/typography.js +62 -0
  39. package/dist/index.js +43 -0
  40. package/dist/lib/api.js +204 -0
  41. package/dist/lib/asciiArt.js +56 -0
  42. package/dist/lib/auth.js +55 -0
  43. package/dist/lib/browser.js +68 -0
  44. package/dist/lib/commands.js +53 -0
  45. package/dist/lib/config.js +100 -0
  46. package/dist/lib/scrollIndicators.js +29 -0
  47. package/dist/lib/theme.js +33 -0
  48. package/dist/lib/types.js +1 -0
  49. package/dist/lib/viewport.js +32 -0
  50. package/dist/utils/formatters.js +114 -0
  51. package/package.json +48 -0
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ import { apiGet, apiPost } from "../lib/api.js";
6
+ import { selectionIndicator, heading, muted, dim, primary } from "../lib/theme.js";
7
+ import Layout from "./Layout.js";
8
+ import ErrorDisplay from "./ui/ErrorDisplay.js";
9
+ export default function ProblemForm({ onNavigate, onBack }) {
10
+ const [step, setStep] = useState("board");
11
+ const [boards, setBoards] = useState([]);
12
+ const [selectedBoardIndex, setSelectedBoardIndex] = useState(0);
13
+ const [title, setTitle] = useState("");
14
+ const [description, setDescription] = useState("");
15
+ const [votingType, setVotingType] = useState("standard");
16
+ const [error, setError] = useState("");
17
+ const [loading, setLoading] = useState(true);
18
+ useEffect(() => {
19
+ loadBoards();
20
+ }, []);
21
+ const loadBoards = async () => {
22
+ try {
23
+ setLoading(true);
24
+ const data = await apiGet("/api/boards/list");
25
+ setBoards(data.boards || []);
26
+ if (data.boards && data.boards.length === 0) {
27
+ setError("No boards found. Please create a board first.");
28
+ }
29
+ }
30
+ catch (err) {
31
+ setError(err.message || "Failed to load boards");
32
+ }
33
+ finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+ const handleSubmit = async () => {
38
+ if (!title.trim() || !description.trim()) {
39
+ setError("Title and description are required");
40
+ return;
41
+ }
42
+ if (boards.length === 0 || selectedBoardIndex >= boards.length) {
43
+ setError("Please select a board");
44
+ return;
45
+ }
46
+ try {
47
+ setStep("submitting");
48
+ setError("");
49
+ const result = await apiPost("/api/problems/create", {
50
+ title: title.trim(),
51
+ description: description.trim(),
52
+ boardId: boards[selectedBoardIndex].id,
53
+ votingType,
54
+ });
55
+ onNavigate("problem-detail", result.id);
56
+ }
57
+ catch (err) {
58
+ setError(err.message || "Failed to create problem");
59
+ setStep("description");
60
+ }
61
+ };
62
+ useInput((input, key) => {
63
+ if (key.leftArrow || key.escape || (key.ctrl && input === "c")) {
64
+ if (step === "board") {
65
+ onBack();
66
+ }
67
+ else if (step === "title") {
68
+ setStep("board");
69
+ }
70
+ else if (step === "description") {
71
+ setStep("title");
72
+ }
73
+ else if (step === "voting") {
74
+ setStep("description");
75
+ }
76
+ return;
77
+ }
78
+ if (step === "board") {
79
+ if (key.upArrow) {
80
+ setSelectedBoardIndex((prev) => Math.max(0, prev - 1));
81
+ }
82
+ else if (key.downArrow) {
83
+ setSelectedBoardIndex((prev) => Math.min(boards.length - 1, prev + 1));
84
+ }
85
+ else if (key.return) {
86
+ if (boards[selectedBoardIndex]) {
87
+ setStep("title");
88
+ }
89
+ }
90
+ }
91
+ else if (step === "voting") {
92
+ if (key.upArrow || key.downArrow) {
93
+ setVotingType((prev) => (prev === "standard" ? "weighted" : "standard"));
94
+ }
95
+ else if (key.return) {
96
+ handleSubmit();
97
+ }
98
+ }
99
+ });
100
+ if (loading) {
101
+ return (_jsx(Layout, { title: "Create Problem", children: _jsx(Text, { children: "Loading boards..." }) }));
102
+ }
103
+ if (error && boards.length === 0) {
104
+ return (_jsx(Layout, { title: "Create Problem", children: _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4), showHelpText: true, helpText: "Press \u2190/ESC to go back" }) }));
105
+ }
106
+ if (step === "submitting") {
107
+ return (_jsx(Layout, { title: "Create Problem", children: _jsx(Text, { children: "Creating problem..." }) }));
108
+ }
109
+ return (_jsx(Layout, { title: "Create Problem", children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [step === "board" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: heading("Select Board") }), boards.map((board, index) => (_jsxs(Text, { children: [selectedBoardIndex === index ? selectionIndicator() : " ", heading(board.name), board.description && dim(` - ${board.description}`)] }, board.id))), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("Use ↑↓ to navigate, Enter to select, ←/ESC to go back") })] })), step === "title" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: heading("Problem Title") }), _jsx(TextInput, { value: title, onChange: setTitle, onSubmit: () => {
110
+ if (title.trim()) {
111
+ setStep("description");
112
+ setError("");
113
+ }
114
+ else {
115
+ setError("Title is required");
116
+ }
117
+ }, placeholder: "Enter problem title" }), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("Press Enter to continue, ←/ESC to go back") })] })), step === "description" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: heading("Problem Description") }), _jsx(TextInput, { value: description, onChange: setDescription, onSubmit: () => {
118
+ if (description.trim()) {
119
+ setStep("voting");
120
+ setError("");
121
+ }
122
+ else {
123
+ setError("Description is required");
124
+ }
125
+ }, placeholder: "Enter problem description" }), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("Press Enter to continue, ←/ESC to go back") })] })), step === "voting" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: heading("Voting Type") }), _jsxs(Text, { children: [votingType === "standard" ? selectionIndicator() : " ", "Standard (one vote per member)"] }), _jsxs(Text, { children: [votingType === "weighted" ? selectionIndicator() : " ", "Weighted (votes weighted by reputation)"] }), _jsx(Box, { height: 1 }), _jsx(Text, { bold: true, children: heading("Review") }), _jsxs(Text, { children: ["Board: ", primary(boards[selectedBoardIndex]?.name || "")] }), _jsxs(Text, { children: ["Title: ", primary(title)] }), _jsxs(Text, { children: ["Description: ", primary(description.substring(0, 50)), description.length > 50 ? "..." : ""] }), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("Use ↑↓ to change voting type, Enter to create, ←/ESC to go back") })] })), error && (_jsxs(_Fragment, { children: [_jsx(Box, { height: 1 }), _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4) })] }))] }) }));
126
+ }
@@ -0,0 +1,118 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { apiGet } from "../lib/api.js";
5
+ import { formatStatus, formatDate, truncate, wrapWithIndent } from "../utils/formatters.js";
6
+ import { getItemPrefix, getSecondLinePrefix, heading, muted, dim, primary } from "../lib/theme.js";
7
+ import Layout from "./Layout.js";
8
+ import CompactList from "./CompactList.js";
9
+ import ErrorDisplay from "./ui/ErrorDisplay.js";
10
+ export default function ProblemsList({ onNavigate }) {
11
+ const [loading, setLoading] = useState(true);
12
+ const [problems, setProblems] = useState([]);
13
+ const [selectedIndex, setSelectedIndex] = useState(0);
14
+ const [currentPage, setCurrentPage] = useState(1);
15
+ const [pageSize] = useState(10);
16
+ const [error, setError] = useState("");
17
+ useEffect(() => {
18
+ loadProblems();
19
+ setSelectedIndex(0);
20
+ }, []);
21
+ useEffect(() => {
22
+ setSelectedIndex(0);
23
+ }, [currentPage]);
24
+ const loadProblems = async () => {
25
+ try {
26
+ setLoading(true);
27
+ setError("");
28
+ const data = await apiGet("/api/problems/list");
29
+ setProblems(data.problems || []);
30
+ }
31
+ catch (err) {
32
+ const errorMessage = err.data?.error || err.message || "Failed to load problems";
33
+ setError(errorMessage);
34
+ }
35
+ finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+ const hasCreateOption = currentPage === 1;
40
+ const problemsPerPage = hasCreateOption ? pageSize - 1 : pageSize;
41
+ const pageStartIndex = hasCreateOption ? 0 : (currentPage - 2) * pageSize + (pageSize - 1);
42
+ const pageEndIndex = pageStartIndex + problemsPerPage;
43
+ const pageProblems = problems.slice(pageStartIndex, pageEndIndex);
44
+ const totalPages = Math.ceil((problems.length + 1) / pageSize);
45
+ const listItems = [
46
+ ...(hasCreateOption ? [{ type: "create", data: null, id: "create" }] : []),
47
+ ...pageProblems.map(p => ({ type: "problem", data: p, id: p.id }))
48
+ ];
49
+ const totalPageItems = listItems.length;
50
+ useInput((input, key) => {
51
+ if (key.leftArrow || key.escape || (key.ctrl && input === "c")) {
52
+ onNavigate("quick-actions");
53
+ return;
54
+ }
55
+ if (key.pageUp) {
56
+ setCurrentPage((prev) => Math.max(1, prev - 1));
57
+ setSelectedIndex(0);
58
+ return;
59
+ }
60
+ if (key.pageDown) {
61
+ setCurrentPage((prev) => Math.min(totalPages, prev + 1));
62
+ setSelectedIndex(0);
63
+ return;
64
+ }
65
+ if (key.upArrow) {
66
+ setSelectedIndex((prev) => {
67
+ if (prev === 0 && currentPage > 1) {
68
+ const newPage = currentPage - 1;
69
+ setCurrentPage(newPage);
70
+ const prevPageHasCreate = newPage === 1;
71
+ const prevPageItems = prevPageHasCreate ? pageSize - 1 : pageSize - 1;
72
+ return prevPageItems;
73
+ }
74
+ return Math.max(0, prev - 1);
75
+ });
76
+ }
77
+ else if (key.downArrow) {
78
+ setSelectedIndex((prev) => {
79
+ const maxIndex = totalPageItems - 1;
80
+ const newIndex = Math.min(maxIndex, prev + 1);
81
+ if (newIndex === maxIndex && currentPage < totalPages) {
82
+ setCurrentPage((p) => p + 1);
83
+ return 0;
84
+ }
85
+ return newIndex;
86
+ });
87
+ }
88
+ else if (key.return) {
89
+ if (hasCreateOption && selectedIndex === 0) {
90
+ onNavigate("problem-create");
91
+ }
92
+ else {
93
+ const problemIndex = hasCreateOption
94
+ ? pageStartIndex + selectedIndex - 1
95
+ : pageStartIndex + selectedIndex;
96
+ if (problemIndex >= 0 && problemIndex < problems.length) {
97
+ onNavigate("problem-detail", problems[problemIndex].id);
98
+ }
99
+ }
100
+ }
101
+ });
102
+ if (loading) {
103
+ return (_jsx(Layout, { title: "Problems", children: _jsx(Text, { children: "Loading problems..." }) }));
104
+ }
105
+ if (error) {
106
+ return (_jsx(Layout, { title: "Problems", children: _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4), showHelpText: true, helpText: "Press ESC to go back" }) }));
107
+ }
108
+ if (problems.length === 0) {
109
+ return (_jsxs(Layout, { title: "Problems", children: [_jsx(Text, { children: "No problems found." }), _jsx(Text, { children: muted("Press ESC to go back") })] }));
110
+ }
111
+ return (_jsx(Layout, { title: "Problems", children: _jsx(CompactList, { title: "Problems", items: listItems, selectedIndex: selectedIndex, currentPage: currentPage, totalPages: totalPages, totalItems: problems.length + 1, pageSize: pageSize, viewportSize: 6, getItemId: (item) => item.id, renderItem: (item, index, isSelected) => {
112
+ if (item.type === "create") {
113
+ return (_jsxs(Text, { children: [getItemPrefix(isSelected), primary("Create New Problem")] }));
114
+ }
115
+ const problem = item.data;
116
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsxs(Text, { children: [getItemPrefix(isSelected), formatStatus(problem.status), " ", heading(problem.title)] }), wrapWithIndent(truncate(problem.description, 60) + " " + dim(`- ${formatDate(problem.createdAt)}`), 60, getSecondLinePrefix()).map((line, idx) => (_jsx(Text, { children: line }, idx)))] }));
117
+ }, navigationHint: "\u2191\u2193 navigate | Enter to select | PgUp/PgDn change page | \u2190/ESC back" }) }));
118
+ }
@@ -0,0 +1,173 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { apiGet } from "../lib/api.js";
5
+ import { formatCredits } from "../utils/formatters.js";
6
+ import { getItemPrefix, primary, muted, dim, divider, secondary } from "../lib/theme.js";
7
+ import ErrorDisplay from "./ui/ErrorDisplay.js";
8
+ import { getDashboardUrl, openInBrowser } from "../lib/browser.js";
9
+ import { getBoardHeader } from "../lib/asciiArt.js";
10
+ export default function QuickActions({ onNavigate, onExit }) {
11
+ const [loading, setLoading] = useState(true);
12
+ const [stats, setStats] = useState(null);
13
+ const [credits, setCredits] = useState(null);
14
+ const [selectedIndex, setSelectedIndex] = useState(0);
15
+ const [error, setError] = useState("");
16
+ const [terminalWidth, setTerminalWidth] = useState(() => process.stdout.columns || 80);
17
+ useEffect(() => {
18
+ loadData();
19
+ setSelectedIndex(0);
20
+ }, []);
21
+ useEffect(() => {
22
+ let resizeTimeout;
23
+ const handleResize = () => {
24
+ clearTimeout(resizeTimeout);
25
+ resizeTimeout = setTimeout(() => {
26
+ setTerminalWidth(process.stdout.columns || 80);
27
+ }, 100);
28
+ };
29
+ if (process.stdout.isTTY) {
30
+ process.stdout.on("resize", handleResize);
31
+ return () => {
32
+ clearTimeout(resizeTimeout);
33
+ process.stdout.off("resize", handleResize);
34
+ };
35
+ }
36
+ }, []);
37
+ const loadData = async () => {
38
+ try {
39
+ setLoading(true);
40
+ setError("");
41
+ const [statsData, creditsData, problemsData] = await Promise.all([
42
+ apiGet("/api/dashboard/stats").catch(() => (null)),
43
+ apiGet("/api/billing/credits").catch(() => ({ credits: null })),
44
+ apiGet("/api/problems/list").catch(() => ({ problems: [] })),
45
+ ]);
46
+ if (statsData) {
47
+ const stats = {
48
+ boardCount: statsData.totalBoards ?? statsData.boards?.length ?? 0,
49
+ problemCount: problemsData.problems?.length ?? 0,
50
+ recentProblems: statsData.recentProblems || [],
51
+ totalBoards: statsData.totalBoards,
52
+ totalMembers: statsData.totalMembers,
53
+ };
54
+ setStats(stats);
55
+ }
56
+ if (creditsData.credits !== null) {
57
+ setCredits(creditsData.credits);
58
+ }
59
+ }
60
+ catch (err) {
61
+ setError(err.message || "Failed to load dashboard");
62
+ }
63
+ finally {
64
+ setLoading(false);
65
+ }
66
+ };
67
+ const quickActions = [
68
+ { label: "Create Problem", shortcut: "C", action: "create", view: "problem-create" },
69
+ { label: "View Problems", shortcut: "P", action: "problems", view: "problems" },
70
+ { label: "View Boards", shortcut: "B", action: "boards", view: "boards" },
71
+ { label: "View Members", shortcut: "M", action: "members", view: "members" },
72
+ { label: "Settings", shortcut: "S", action: "settings", view: "settings" },
73
+ { label: "Open in Browser", shortcut: "O", action: "browser", view: null },
74
+ { label: "Exit", shortcut: "E", action: "exit", view: null, isExit: true },
75
+ ];
76
+ useInput((input, key) => {
77
+ if (key.upArrow) {
78
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
79
+ }
80
+ else if (key.downArrow) {
81
+ const maxIndex = quickActions.length - 1;
82
+ setSelectedIndex((prev) => Math.min(maxIndex, prev + 1));
83
+ }
84
+ else if (key.return) {
85
+ const action = quickActions[selectedIndex];
86
+ if (action) {
87
+ if (action.isExit) {
88
+ onExit();
89
+ process.exit(0);
90
+ }
91
+ else if (action.view === null) {
92
+ openInBrowser(getDashboardUrl()).catch(console.error);
93
+ }
94
+ else if (action.id) {
95
+ onNavigate(action.view, action.id);
96
+ }
97
+ else {
98
+ onNavigate(action.view);
99
+ }
100
+ }
101
+ }
102
+ else if (input === "e" || input === "E") {
103
+ onExit();
104
+ process.exit(0);
105
+ }
106
+ else if (input === "c" || input === "C") {
107
+ onNavigate("problem-create");
108
+ }
109
+ else if (input === "p" || input === "P") {
110
+ onNavigate("problems");
111
+ }
112
+ else if (input === "b" || input === "B") {
113
+ onNavigate("boards");
114
+ }
115
+ else if (input === "m" || input === "M") {
116
+ onNavigate("members");
117
+ }
118
+ else if (input === "s" || input === "S") {
119
+ onNavigate("settings");
120
+ }
121
+ else if (input === "o" || input === "O") {
122
+ openInBrowser(getDashboardUrl()).catch(console.error);
123
+ }
124
+ });
125
+ const renderHeader = () => {
126
+ const headerLines = getBoardHeader(terminalWidth);
127
+ return (_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 0, children: headerLines.map((line, index) => {
128
+ if (line.includes("╔") || line.includes("╚") || (line.includes("║") && !line.includes("█"))) {
129
+ return _jsx(Text, { children: secondary(line) }, `${terminalWidth}-${index}`);
130
+ }
131
+ else if (line.includes("█")) {
132
+ const parts = [];
133
+ let currentBlock = "";
134
+ let currentBorder = "";
135
+ for (let i = 0; i < line.length; i++) {
136
+ const char = line[i];
137
+ if (char === "█") {
138
+ if (currentBorder) {
139
+ parts.push(secondary(currentBorder));
140
+ currentBorder = "";
141
+ }
142
+ currentBlock += char;
143
+ }
144
+ else {
145
+ if (currentBlock) {
146
+ parts.push(primary(currentBlock));
147
+ currentBlock = "";
148
+ }
149
+ currentBorder += char;
150
+ }
151
+ }
152
+ if (currentBlock)
153
+ parts.push(primary(currentBlock));
154
+ if (currentBorder)
155
+ parts.push(secondary(currentBorder));
156
+ return _jsx(Text, { children: parts }, `${terminalWidth}-${index}`);
157
+ }
158
+ else {
159
+ return _jsx(Text, { children: secondary(line) }, `${terminalWidth}-${index}`);
160
+ }
161
+ }) }, `header-${terminalWidth}`));
162
+ };
163
+ if (loading) {
164
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [renderHeader(), _jsx(Box, { paddingX: 1, paddingY: 1, children: _jsx(Text, { children: "Loading..." }) })] }));
165
+ }
166
+ if (error) {
167
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [renderHeader(), _jsx(Box, { flexGrow: 1, paddingX: 1, paddingY: 1, children: _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, terminalWidth - 4) }) })] }));
168
+ }
169
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [renderHeader(), _jsx(Box, { paddingX: 1, paddingY: 0, children: _jsx(Text, { children: divider(75) }) }), _jsx(Box, { flexGrow: 1, paddingX: 1, paddingY: 1, children: _jsxs(Box, { flexDirection: "column", gap: 0, children: [quickActions.map((action, index) => {
170
+ const isSelected = selectedIndex === index;
171
+ return (_jsx(Box, { paddingY: 0, children: _jsxs(Text, { children: [getItemPrefix(isSelected), primary(action.label), " ", dim(`(${action.shortcut})`)] }) }, action.action));
172
+ }), _jsx(Box, { height: 1 }), _jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsxs(Text, { children: ["Credits: ", credits !== null ? formatCredits(credits) : muted("N/A")] }), _jsxs(Text, { children: ["| Boards: ", primary(String(stats?.boardCount || 0)), " | Problems: ", primary(String(stats?.problemCount || 0))] })] }), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("↑↓ to navigate, Enter to select, or press shortcut. O to open web app.") })] }) })] }));
173
+ }
@@ -0,0 +1,159 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { apiGet, apiPost } from "../lib/api.js";
5
+ import { getItemPrefix, getSecondLinePrefix, heading, muted, dim } from "../lib/theme.js";
6
+ import Input from "./ui/Input.js";
7
+ import Layout from "./Layout.js";
8
+ import CompactList from "./CompactList.js";
9
+ import { truncate, wrapWithIndent } from "../utils/formatters.js";
10
+ import ErrorDisplay from "./ui/ErrorDisplay.js";
11
+ export default function QuickCreate({ onNavigate, onBack }) {
12
+ const [step, setStep] = useState("board");
13
+ const [boards, setBoards] = useState([]);
14
+ const [selectedBoardIndex, setSelectedBoardIndex] = useState(0);
15
+ const [boardPage, setBoardPage] = useState(1);
16
+ const [boardPageSize] = useState(10);
17
+ const [title, setTitle] = useState("");
18
+ const [description, setDescription] = useState("");
19
+ const [error, setError] = useState("");
20
+ const [loading, setLoading] = useState(true);
21
+ const [createdProblemId, setCreatedProblemId] = useState(null);
22
+ useEffect(() => {
23
+ loadBoards();
24
+ }, []);
25
+ const loadBoards = async () => {
26
+ try {
27
+ setLoading(true);
28
+ const data = await apiGet("/api/boards/list");
29
+ setBoards(data.boards || []);
30
+ if (data.boards && data.boards.length === 0) {
31
+ setError("No boards found. Please create a board first in the web app.");
32
+ }
33
+ }
34
+ catch (err) {
35
+ setError(err.message || "Failed to load boards");
36
+ }
37
+ finally {
38
+ setLoading(false);
39
+ }
40
+ };
41
+ const handleSubmit = async () => {
42
+ if (!title.trim() || !description.trim()) {
43
+ setError("Title and description are required");
44
+ return;
45
+ }
46
+ if (boards.length === 0 || selectedBoardIndex < 0 || selectedBoardIndex >= boards.length) {
47
+ setError("Please select a board");
48
+ return;
49
+ }
50
+ try {
51
+ setStep("submitting");
52
+ setError("");
53
+ const result = await apiPost("/api/problems/create", {
54
+ title: title.trim(),
55
+ description: description.trim(),
56
+ boardId: boards[selectedBoardIndex].id,
57
+ votingType: "standard",
58
+ });
59
+ setCreatedProblemId(result.id);
60
+ onNavigate("problem-detail", result.id, true);
61
+ }
62
+ catch (err) {
63
+ setError(err.message || "Failed to create problem");
64
+ setStep("description");
65
+ }
66
+ };
67
+ useInput((input, key) => {
68
+ if (key.escape) {
69
+ onBack();
70
+ return;
71
+ }
72
+ if (step === "board") {
73
+ const boardsTotalPages = Math.ceil(boards.length / boardPageSize);
74
+ const pageStartIndex = (boardPage - 1) * boardPageSize;
75
+ const pageEndIndex = pageStartIndex + boardPageSize;
76
+ const pageBoards = boards.slice(pageStartIndex, pageEndIndex);
77
+ const totalPageItems = pageBoards.length;
78
+ if (key.pageUp) {
79
+ setBoardPage((prev) => Math.max(1, prev - 1));
80
+ setSelectedBoardIndex(0);
81
+ }
82
+ else if (key.pageDown) {
83
+ setBoardPage((prev) => Math.min(boardsTotalPages, prev + 1));
84
+ setSelectedBoardIndex(0);
85
+ }
86
+ else if (key.upArrow) {
87
+ setSelectedBoardIndex((prev) => {
88
+ if (prev === 0 && boardPage > 1) {
89
+ const newPage = boardPage - 1;
90
+ setBoardPage(newPage);
91
+ const newPageBoards = boards.slice((newPage - 1) * boardPageSize, newPage * boardPageSize);
92
+ return newPageBoards.length - 1;
93
+ }
94
+ return Math.max(0, prev - 1);
95
+ });
96
+ }
97
+ else if (key.downArrow) {
98
+ setSelectedBoardIndex((prev) => {
99
+ if (prev === totalPageItems - 1 && boardPage < boardsTotalPages) {
100
+ setBoardPage((p) => p + 1);
101
+ return 0;
102
+ }
103
+ return Math.min(totalPageItems - 1, prev + 1);
104
+ });
105
+ }
106
+ else if (key.return) {
107
+ if (pageBoards[selectedBoardIndex]) {
108
+ const actualIndex = pageStartIndex + selectedBoardIndex;
109
+ setSelectedBoardIndex(actualIndex);
110
+ setStep("title");
111
+ }
112
+ }
113
+ }
114
+ else if (step === "title") {
115
+ if (key.return && title.trim()) {
116
+ setStep("description");
117
+ setError("");
118
+ }
119
+ }
120
+ else if (step === "description") {
121
+ if (key.return && description.trim()) {
122
+ handleSubmit();
123
+ }
124
+ }
125
+ });
126
+ if (loading) {
127
+ return (_jsx(Layout, { title: "Quick Create", children: _jsx(Text, { children: "Loading boards..." }) }));
128
+ }
129
+ if (error && boards.length === 0) {
130
+ return (_jsx(Layout, { title: "Quick Create", children: _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4), showHelpText: true, helpText: "Press ESC to go back" }) }));
131
+ }
132
+ if (step === "submitting") {
133
+ return (_jsx(Layout, { title: "Quick Create", children: _jsx(Text, { children: "Creating problem..." }) }));
134
+ }
135
+ if (step === "board") {
136
+ const boardsTotalPages = Math.ceil(boards.length / boardPageSize);
137
+ const pageStartIndex = (boardPage - 1) * boardPageSize;
138
+ const pageEndIndex = pageStartIndex + boardPageSize;
139
+ const pageBoards = boards.slice(pageStartIndex, pageEndIndex);
140
+ return (_jsx(Layout, { title: "Create Problem - Select Board", children: _jsx(CompactList, { title: "Boards", items: pageBoards, selectedIndex: selectedBoardIndex, currentPage: boardPage, totalPages: boardsTotalPages, totalItems: boards.length, pageSize: boardPageSize, viewportSize: 8, getItemId: (board) => board.id, renderItem: (board, index, isSelected) => {
141
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsxs(Text, { children: [getItemPrefix(isSelected), heading(board.name), " ", dim(`(${board.memberCount || 0} members)`)] }), board.description && (_jsx(_Fragment, { children: wrapWithIndent(truncate(board.description, 60), 60, getSecondLinePrefix()).map((line, idx) => (_jsx(Text, { children: line }, idx))) }))] }));
142
+ }, navigationHint: "\u2191\u2193 navigate | Enter select | PgUp/PgDn change page | ESC cancel" }) }));
143
+ }
144
+ if (step === "title") {
145
+ return (_jsx(Layout, { title: "Create Problem - Title", children: _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { children: dim("Enter problem title:") }), _jsx(Box, { height: 0 }), _jsx(Input, { value: title, onChange: setTitle, onSubmit: () => {
146
+ if (title.trim()) {
147
+ setStep("description");
148
+ setError("");
149
+ }
150
+ else {
151
+ setError("Title is required");
152
+ }
153
+ }, placeholder: "Problem title", label: "Title", focused: true }), error && (_jsxs(_Fragment, { children: [_jsx(Box, { height: 0 }), _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4) })] })), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("Enter title and press Enter, ESC to go back") })] }) }));
154
+ }
155
+ if (step === "description") {
156
+ return (_jsx(Layout, { title: "Create Problem - Description", children: _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { children: dim("Enter problem description:") }), _jsx(Box, { height: 0 }), _jsx(Input, { value: description, onChange: setDescription, onSubmit: handleSubmit, placeholder: "Problem description", label: "Description", focused: true }), error && (_jsxs(_Fragment, { children: [_jsx(Box, { height: 0 }), _jsx(ErrorDisplay, { error: error, maxWidth: Math.min(70, (process.stdout.columns || 80) - 4) })] })), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("Enter description and press Enter to create, ESC to go back") })] }) }));
157
+ }
158
+ return null;
159
+ }
@@ -0,0 +1,7 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { formatDate, truncate } from "../utils/formatters.js";
4
+ import { primary, muted, approve, warning } from "../lib/theme.js";
5
+ export default function ReportViewer({ report }) {
6
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, borderStyle: "round", borderColor: "gray", padding: 1, children: [_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Text, { bold: true, children: [primary(report.memberName), " - ", muted(formatDate(report.createdAt))] }), _jsx(Text, { children: muted(`Votes: ${primary(String(report.voteCount))}`) })] }), report.research && (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: primary("Research") }), _jsx(Text, { children: truncate(report.research, 200) })] })), report.analysis && (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: warning("Analysis") }), _jsx(Text, { children: truncate(report.analysis, 200) })] })), report.solution && (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: approve("Solution") }), _jsx(Text, { children: truncate(report.solution, 200) })] }))] }));
7
+ }
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { getUser, clearSession } from "../lib/config.js";
5
+ import { formatCredits } from "../utils/formatters.js";
6
+ import { apiGet } from "../lib/api.js";
7
+ import { getItemPrefix, heading, muted, dim, primary } from "../lib/theme.js";
8
+ import Layout from "./Layout.js";
9
+ export default function Settings({ onBack, onLogout }) {
10
+ const [user, setUser] = useState(getUser());
11
+ const [credits, setCredits] = useState(null);
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ useEffect(() => {
14
+ loadCredits();
15
+ }, []);
16
+ const loadCredits = async () => {
17
+ try {
18
+ const data = await apiGet("/api/billing/credits");
19
+ setCredits(data.credits);
20
+ }
21
+ catch (err) {
22
+ console.error("Failed to load credits:", err);
23
+ }
24
+ };
25
+ useInput((input, key) => {
26
+ if (key.leftArrow || key.escape || (key.ctrl && input === "c")) {
27
+ onBack();
28
+ return;
29
+ }
30
+ if (key.upArrow) {
31
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
32
+ }
33
+ else if (key.downArrow) {
34
+ setSelectedIndex((prev) => Math.min(1, prev + 1));
35
+ }
36
+ else if (key.return) {
37
+ switch (selectedIndex) {
38
+ case 0:
39
+ loadCredits();
40
+ break;
41
+ case 1:
42
+ clearSession();
43
+ onLogout();
44
+ break;
45
+ }
46
+ }
47
+ });
48
+ const menuItems = [
49
+ { label: "Refresh Credits", action: "refresh" },
50
+ { label: "Logout", action: "logout" },
51
+ ];
52
+ return (_jsx(Layout, { title: "Settings", children: _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: heading("User") }), _jsxs(Text, { children: [dim("Email:"), " ", primary(user.email || "N/A")] }), _jsxs(Text, { children: [dim("User ID:"), " ", dim(user.userId || "N/A")] })] }), _jsx(Box, { height: 1 }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: heading("Credits") }), _jsx(Text, { children: credits !== null ? formatCredits(credits) : muted("Loading...") })] }), _jsx(Box, { height: 1 }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, children: heading("Actions") }), menuItems.map((item, index) => (_jsxs(Text, { children: [getItemPrefix(selectedIndex === index), primary(item.label)] }, item.action)))] }), _jsx(Box, { height: 1 }), _jsx(Text, { children: muted("↑↓ navigate | Enter select | ←/ESC back") })] }) }));
53
+ }