plotlink-ows 1.2.94 → 1.2.96
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.
- package/app/lib/active-wallet.ts +260 -0
- package/app/lib/cartoon-coach.ts +1 -1
- package/app/lib/cartoon-readiness.ts +12 -10
- package/app/lib/cuts.ts +135 -18
- package/app/lib/lettering-status.ts +64 -6
- package/app/lib/story-progress.ts +2 -3
- package/app/routes/dashboard.ts +6 -4
- package/app/routes/publish.ts +56 -23
- package/app/routes/settings.ts +92 -37
- package/app/routes/wallet.ts +58 -30
- package/app/web/components/CartoonNextAction.tsx +145 -0
- package/app/web/components/CartoonPublishPage.tsx +1 -1
- package/app/web/components/CutListPanel.tsx +1198 -488
- package/app/web/components/Dashboard.tsx +15 -6
- package/app/web/components/FinishEpisodePanel.tsx +57 -46
- package/app/web/components/LetteringEditor.tsx +867 -366
- package/app/web/components/PreviewPanel.tsx +1459 -844
- package/app/web/components/StoriesPage.tsx +985 -475
- package/app/web/components/StoryProgressPanel.tsx +32 -102
- package/app/web/components/WalletCard.tsx +110 -8
- package/app/web/components/WorkflowCoach.tsx +63 -35
- package/app/web/dist/assets/{export-cut-nKQ_n2-J.js → export-cut-BqZI0-Rv.js} +1 -1
- package/app/web/dist/assets/index-C43toXVm.js +141 -0
- package/app/web/dist/assets/index-CcfChGEK.css +32 -0
- package/app/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/app/web/dist/assets/index-BAZGwVwj.js +0 -143
- package/app/web/dist/assets/index-DoXH2OlP.css +0 -32
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
1
|
+
import React, { useCallback, useState, useEffect } from "react";
|
|
2
2
|
|
|
3
3
|
const API_BASE = "http://localhost:7777";
|
|
4
4
|
|
|
5
5
|
interface WalletInfo {
|
|
6
|
+
walletId?: string;
|
|
7
|
+
name?: string;
|
|
6
8
|
address: string;
|
|
7
9
|
ethBalance: string;
|
|
8
10
|
ethFormatted: string;
|
|
@@ -48,16 +50,17 @@ interface DashboardData {
|
|
|
48
50
|
|
|
49
51
|
export function Dashboard({ token }: { token: string }) {
|
|
50
52
|
const [data, setData] = useState<DashboardData | null>(null);
|
|
51
|
-
const authFetch = (url: string, opts?: RequestInit) =>
|
|
52
|
-
fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } })
|
|
53
|
+
const authFetch = useCallback((url: string, opts?: RequestInit) =>
|
|
54
|
+
fetch(url, { ...opts, headers: { ...opts?.headers, Authorization: `Bearer ${token}`, "Content-Type": "application/json" } }),
|
|
55
|
+
[token]);
|
|
53
56
|
|
|
54
|
-
const loadDashboard = () => {
|
|
57
|
+
const loadDashboard = useCallback(() => {
|
|
55
58
|
authFetch(`${API_BASE}/api/dashboard`)
|
|
56
59
|
.then((r) => r.json())
|
|
57
60
|
.then(setData);
|
|
58
|
-
};
|
|
61
|
+
}, [authFetch]);
|
|
59
62
|
|
|
60
|
-
useEffect(() => { loadDashboard(); }, []);
|
|
63
|
+
useEffect(() => { loadDashboard(); }, [loadDashboard]);
|
|
61
64
|
|
|
62
65
|
const truncate = (addr: string) => `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
|
63
66
|
const formatDate = (d: string | undefined | null) => {
|
|
@@ -104,6 +107,12 @@ export function Dashboard({ token }: { token: string }) {
|
|
|
104
107
|
<div className="border-border rounded border p-4">
|
|
105
108
|
<h3 className="text-accent mb-3 text-xs font-bold uppercase tracking-wider">Wallet</h3>
|
|
106
109
|
<div className="space-y-1.5">
|
|
110
|
+
{data.wallet.name && (
|
|
111
|
+
<div className="flex justify-between text-xs">
|
|
112
|
+
<span className="text-muted">Active wallet</span>
|
|
113
|
+
<span className="text-foreground truncate pl-3 font-mono text-[10px]">{data.wallet.name}</span>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
107
116
|
<div className="flex justify-between text-xs">
|
|
108
117
|
<span className="text-muted">Address</span>
|
|
109
118
|
<code className="text-foreground font-mono text-[10px]">{truncate(data.wallet.address)}</code>
|
|
@@ -85,67 +85,78 @@ export function FinishEpisodePanel({
|
|
|
85
85
|
? "Episode ready to publish"
|
|
86
86
|
: "Finish episode";
|
|
87
87
|
|
|
88
|
+
const outstandingCount = steps.filter((s) => s.status !== "done").length;
|
|
89
|
+
const issuesCount = groups.reduce((sum, g) => sum + g.lines.length, 0);
|
|
90
|
+
|
|
88
91
|
return (
|
|
89
92
|
<div
|
|
90
|
-
className="px-3 py-
|
|
93
|
+
className="px-3 py-1.5 border-b border-border bg-surface/50 flex-shrink-0"
|
|
91
94
|
data-testid="finish-episode-panel"
|
|
92
95
|
>
|
|
93
|
-
<div className="flex items-center
|
|
96
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
94
97
|
<span className="text-[11px] font-medium text-foreground">Finish episode</span>
|
|
95
98
|
{checklist.nextStep && (
|
|
96
|
-
<span className="text-[10px] text-muted truncate" data-testid="finish-next-step">
|
|
99
|
+
<span className="min-w-0 flex-1 text-[10px] text-muted truncate" data-testid="finish-next-step">
|
|
97
100
|
Next: {checklist.nextStep}
|
|
98
101
|
</span>
|
|
99
102
|
)}
|
|
103
|
+
<button
|
|
104
|
+
onClick={onFinish}
|
|
105
|
+
disabled={finishing || !canFinish}
|
|
106
|
+
data-testid="finish-episode-btn"
|
|
107
|
+
title="Upload the exported final panels, then prepare the episode for publishing — picks up where it left off"
|
|
108
|
+
className="px-2.5 py-0.5 text-[11px] border border-accent/40 text-accent rounded hover:bg-accent/5 disabled:opacity-50"
|
|
109
|
+
>
|
|
110
|
+
{buttonLabel}
|
|
111
|
+
</button>
|
|
100
112
|
</div>
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
data-testid={`finish-step-${s.key}`}
|
|
108
|
-
data-status={s.status}
|
|
109
|
-
className={`flex items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] ${
|
|
110
|
-
s.status === "current"
|
|
111
|
-
? "border-accent/40 bg-accent/10 text-accent"
|
|
112
|
-
: s.status === "done"
|
|
113
|
-
? "border-border bg-background/70 text-foreground"
|
|
114
|
-
: "border-border/70 bg-background/40 text-muted"
|
|
115
|
-
}`}
|
|
116
|
-
>
|
|
117
|
-
<span aria-hidden>{STATUS_MARK[s.status]}</span>
|
|
118
|
-
<span>{s.label}</span>
|
|
119
|
-
{s.detail && <span className="text-muted">· {s.detail}</span>}
|
|
120
|
-
</li>
|
|
121
|
-
))}
|
|
122
|
-
</ol>
|
|
114
|
+
<details className="mt-1" data-testid="finish-episode-details">
|
|
115
|
+
<summary className="cursor-pointer select-none text-[10px] text-muted hover:text-foreground">
|
|
116
|
+
{outstandingCount === 0 ? "Progress details" : `${outstandingCount} step${outstandingCount === 1 ? "" : "s"} left`}
|
|
117
|
+
{issuesCount > 0 ? ` · ${issuesCount} blocker${issuesCount === 1 ? "" : "s"}` : ""}
|
|
118
|
+
</summary>
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
120
|
+
<div className="mt-1.5 space-y-1.5">
|
|
121
|
+
{/* Writer-language step status — the exact webtoon production sequence. */}
|
|
122
|
+
<ol className="flex flex-wrap gap-1.5">
|
|
123
|
+
{steps.map((s) => (
|
|
124
|
+
<li
|
|
125
|
+
key={s.key}
|
|
126
|
+
data-testid={`finish-step-${s.key}`}
|
|
127
|
+
data-status={s.status}
|
|
128
|
+
className={`flex items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] ${
|
|
129
|
+
s.status === "current"
|
|
130
|
+
? "border-accent/40 bg-accent/10 text-accent"
|
|
131
|
+
: s.status === "done"
|
|
132
|
+
? "border-border bg-background/70 text-foreground"
|
|
133
|
+
: "border-border/70 bg-background/40 text-muted"
|
|
134
|
+
}`}
|
|
135
|
+
>
|
|
136
|
+
<span aria-hidden>{STATUS_MARK[s.status]}</span>
|
|
137
|
+
<span>{s.label}</span>
|
|
138
|
+
{s.detail && <span className="text-muted">· {s.detail}</span>}
|
|
139
|
+
</li>
|
|
140
|
+
))}
|
|
141
|
+
</ol>
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
{/* Blockers grouped by the step that fixes them, not a flat red list. */}
|
|
144
|
+
{groups.length > 0 && (
|
|
145
|
+
<div className="space-y-1.5" data-testid="finish-issues">
|
|
146
|
+
{groups.map((g) => (
|
|
147
|
+
<div key={g.key} data-testid={`finish-issue-group-${g.key}`} className="text-[10px]">
|
|
148
|
+
<p className="font-medium text-amber-700">{g.title}</p>
|
|
149
|
+
<ul className="ml-3 list-disc text-muted">
|
|
150
|
+
{g.lines.map((line, i) => (
|
|
151
|
+
<li key={i}>{line}</li>
|
|
152
|
+
))}
|
|
153
|
+
</ul>
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
145
156
|
</div>
|
|
146
|
-
)
|
|
157
|
+
)}
|
|
147
158
|
</div>
|
|
148
|
-
|
|
159
|
+
</details>
|
|
149
160
|
</div>
|
|
150
161
|
);
|
|
151
162
|
}
|