Skip to content

Commit 26109f6

Browse files
manzaro1claude
andcommitted
Add 8 major features: modes, templates, storyboard, undo, find/replace, drag-reorder, freemium gating, toast notifications
- Toast notification system with auto-dismiss for all operations - Undo/Redo with Ctrl+Z/Y and snapshot-based history (50 entries) - Script modes: Screenplay, YouTube, Podcast, TikTok with mode-specific block types, colors, AI prompts - 10 pre-built templates filtered by mode (Horror, Rom-Com, Thriller, etc.) - Find & Replace with Ctrl+F, prev/next navigation, replace all - Scene drag-to-reorder via HTML5 drag events in SceneNavigator - Storyboard mode: AI visual descriptions per scene, Canva page insertion - Freemium gating: free/pro tiers with FeatureGate wrapper and UpgradePrompt - Wire previously unused components: ExportPanel, CharacterTracker, ScriptStats - Both Canva and GitHub Pages builds compile successfully Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent aa2fdae commit 26109f6

20 files changed

Lines changed: 1773 additions & 144 deletions

canva-app/src/components/AIToolsPanel.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ const TOOLS: { key: AITool; label: string; description: string }[] = [
2323
interface AIToolsPanelProps {
2424
blocks: ScriptBlock[];
2525
apiKey?: string;
26+
addToast?: (text: string, type: "success" | "error" | "info") => void;
2627
}
2728

28-
export default function AIToolsPanel({ blocks, apiKey }: AIToolsPanelProps) {
29+
export default function AIToolsPanel({ blocks, apiKey, addToast }: AIToolsPanelProps) {
2930
const [activeTool, setActiveTool] = useState<AITool | null>(null);
3031
const [loading, setLoading] = useState(false);
3132
const [result, setResult] = useState<AIAnalysisResult | null>(null);
@@ -45,8 +46,10 @@ export default function AIToolsPanel({ blocks, apiKey }: AIToolsPanelProps) {
4546
try {
4647
const analysis = await analyzeScript(blocks, tool, apiKey);
4748
setResult(analysis);
49+
addToast?.("Analysis complete!", "success");
4850
} catch (err: any) {
4951
setError(err.message || "Analysis failed");
52+
addToast?.(err.message || "Analysis failed", "error");
5053
} finally {
5154
setLoading(false);
5255
}

canva-app/src/components/DocumentImport.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { parseDocumentToBlocks } from "../utils/ai";
55
interface DocumentImportProps {
66
apiKey?: string;
77
onImport: (blocks: ScriptBlock[]) => void;
8+
addToast?: (text: string, type: "success" | "error" | "info") => void;
89
}
910

1011
async function extractTextFromDocx(file: File): Promise<string> {
@@ -41,6 +42,7 @@ async function extractTextFromTxt(file: File): Promise<string> {
4142
export default function DocumentImport({
4243
apiKey,
4344
onImport,
45+
addToast,
4446
}: DocumentImportProps) {
4547
const [status, setStatus] = useState<"idle" | "reading" | "parsing" | "preview">("idle");
4648
const [rawText, setRawText] = useState("");
@@ -89,8 +91,10 @@ export default function DocumentImport({
8991
);
9092
setParsedBlocks(blocks);
9193
setStatus("preview");
94+
addToast?.("Document parsed successfully!", "success");
9295
} catch (err: any) {
9396
setError(err.message || "Failed to process document");
97+
addToast?.(err.message || "Failed to process document", "error");
9498
setStatus("idle");
9599
}
96100
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from "react";
2+
import type { Tier } from "../utils/pricing";
3+
import { canAccess } from "../utils/pricing";
4+
import UpgradePrompt from "./UpgradePrompt";
5+
6+
interface FeatureGateProps {
7+
featureId: string;
8+
tier: Tier;
9+
children: React.ReactNode;
10+
onUpgrade: () => void;
11+
}
12+
13+
export default function FeatureGate({
14+
featureId,
15+
tier,
16+
children,
17+
onUpgrade,
18+
}: FeatureGateProps) {
19+
if (canAccess(featureId, tier)) {
20+
return <>{children}</>;
21+
}
22+
return <UpgradePrompt featureId={featureId} onUpgrade={onUpgrade} />;
23+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import React, { useState, useMemo, useCallback, useEffect, useRef } from "react";
2+
import type { ScriptBlock } from "../types";
3+
4+
interface FindReplaceProps {
5+
blocks: ScriptBlock[];
6+
onBlocksChange: (blocks: ScriptBlock[]) => void;
7+
onHighlightBlock: (blockId: string | null) => void;
8+
visible: boolean;
9+
onClose: () => void;
10+
}
11+
12+
export default function FindReplace({
13+
blocks,
14+
onBlocksChange,
15+
onHighlightBlock,
16+
visible,
17+
onClose,
18+
}: FindReplaceProps) {
19+
const [searchTerm, setSearchTerm] = useState("");
20+
const [replaceTerm, setReplaceTerm] = useState("");
21+
const [currentIdx, setCurrentIdx] = useState(0);
22+
const inputRef = useRef<HTMLInputElement>(null);
23+
24+
const matchIndices = useMemo(() => {
25+
if (!searchTerm) return [];
26+
const lower = searchTerm.toLowerCase();
27+
return blocks
28+
.map((b, i) => (b.content.toLowerCase().includes(lower) ? i : -1))
29+
.filter((i) => i !== -1);
30+
}, [blocks, searchTerm]);
31+
32+
// Focus input when opened
33+
useEffect(() => {
34+
if (visible) inputRef.current?.focus();
35+
}, [visible]);
36+
37+
// Highlight current match
38+
useEffect(() => {
39+
if (matchIndices.length > 0 && currentIdx < matchIndices.length) {
40+
onHighlightBlock(blocks[matchIndices[currentIdx]]?.id ?? null);
41+
} else {
42+
onHighlightBlock(null);
43+
}
44+
}, [matchIndices, currentIdx, blocks, onHighlightBlock]);
45+
46+
// Reset index when search changes
47+
useEffect(() => {
48+
setCurrentIdx(0);
49+
}, [searchTerm]);
50+
51+
const goNext = useCallback(() => {
52+
if (matchIndices.length === 0) return;
53+
setCurrentIdx((prev) => (prev + 1) % matchIndices.length);
54+
}, [matchIndices]);
55+
56+
const goPrev = useCallback(() => {
57+
if (matchIndices.length === 0) return;
58+
setCurrentIdx((prev) =>
59+
prev === 0 ? matchIndices.length - 1 : prev - 1
60+
);
61+
}, [matchIndices]);
62+
63+
const handleReplace = useCallback(() => {
64+
if (matchIndices.length === 0 || !searchTerm) return;
65+
const idx = matchIndices[currentIdx];
66+
const block = blocks[idx];
67+
const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
68+
const newContent = block.content.replace(regex, replaceTerm);
69+
onBlocksChange(
70+
blocks.map((b, i) => (i === idx ? { ...b, content: newContent } : b))
71+
);
72+
}, [blocks, matchIndices, currentIdx, searchTerm, replaceTerm, onBlocksChange]);
73+
74+
const handleReplaceAll = useCallback(() => {
75+
if (!searchTerm) return;
76+
const regex = new RegExp(
77+
searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
78+
"gi"
79+
);
80+
onBlocksChange(
81+
blocks.map((b) => ({
82+
...b,
83+
content: b.content.replace(regex, replaceTerm),
84+
}))
85+
);
86+
}, [blocks, searchTerm, replaceTerm, onBlocksChange]);
87+
88+
const handleClose = useCallback(() => {
89+
setSearchTerm("");
90+
setReplaceTerm("");
91+
onHighlightBlock(null);
92+
onClose();
93+
}, [onClose, onHighlightBlock]);
94+
95+
if (!visible) return null;
96+
97+
return (
98+
<div style={containerStyle}>
99+
<div style={{ display: "flex", gap: 4, alignItems: "center", flex: 1 }}>
100+
<input
101+
ref={inputRef}
102+
value={searchTerm}
103+
onChange={(e) => setSearchTerm(e.target.value)}
104+
placeholder="Find..."
105+
style={inputStyle}
106+
/>
107+
<input
108+
value={replaceTerm}
109+
onChange={(e) => setReplaceTerm(e.target.value)}
110+
placeholder="Replace..."
111+
style={inputStyle}
112+
/>
113+
</div>
114+
<div style={{ display: "flex", gap: 3, alignItems: "center" }}>
115+
<span style={{ fontSize: 9, color: "#6b7280", minWidth: 30, textAlign: "center" }}>
116+
{matchIndices.length > 0
117+
? `${currentIdx + 1}/${matchIndices.length}`
118+
: searchTerm
119+
? "0"
120+
: ""}
121+
</span>
122+
<button onClick={goPrev} style={btnStyle} title="Previous">
123+
&#9650;
124+
</button>
125+
<button onClick={goNext} style={btnStyle} title="Next">
126+
&#9660;
127+
</button>
128+
<button onClick={handleReplace} style={btnStyle} title="Replace">
129+
R
130+
</button>
131+
<button onClick={handleReplaceAll} style={btnStyle} title="Replace All">
132+
RA
133+
</button>
134+
<button onClick={handleClose} style={btnStyle} title="Close">
135+
×
136+
</button>
137+
</div>
138+
</div>
139+
);
140+
}
141+
142+
const containerStyle: React.CSSProperties = {
143+
display: "flex",
144+
alignItems: "center",
145+
gap: 6,
146+
padding: "6px 8px",
147+
backgroundColor: "#f9fafb",
148+
border: "1px solid #e5e7eb",
149+
borderRadius: 6,
150+
};
151+
152+
const inputStyle: React.CSSProperties = {
153+
fontSize: 11,
154+
padding: "4px 6px",
155+
border: "1px solid #d1d5db",
156+
borderRadius: 4,
157+
outline: "none",
158+
flex: 1,
159+
minWidth: 0,
160+
};
161+
162+
const btnStyle: React.CSSProperties = {
163+
fontSize: 10,
164+
fontWeight: 700,
165+
color: "#374151",
166+
backgroundColor: "#e5e7eb",
167+
border: "none",
168+
borderRadius: 3,
169+
padding: "3px 6px",
170+
cursor: "pointer",
171+
lineHeight: 1,
172+
};

canva-app/src/components/SceneGenerator.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState } from "react";
2-
import type { ScriptBlock } from "../types";
2+
import type { ScriptBlock, ScriptMode } from "../types";
33
import { generateScene } from "../utils/ai";
44
import { extractCharacters } from "../utils/script-helpers";
55

@@ -26,12 +26,16 @@ interface SceneGeneratorProps {
2626
apiKey?: string;
2727
existingBlocks: ScriptBlock[];
2828
onInsert: (blocks: ScriptBlock[]) => void;
29+
addToast?: (text: string, type: "success" | "error" | "info") => void;
30+
mode?: ScriptMode;
2931
}
3032

3133
export default function SceneGenerator({
3234
apiKey,
3335
existingBlocks,
3436
onInsert,
37+
addToast,
38+
mode = "screenplay",
3539
}: SceneGeneratorProps) {
3640
const [premise, setPremise] = useState("");
3741
const [tone, setTone] = useState("Dramatic");
@@ -58,11 +62,14 @@ export default function SceneGenerator({
5862
tone,
5963
sceneLength,
6064
existingChars,
61-
apiKey
65+
apiKey,
66+
mode
6267
);
6368
setGeneratedBlocks(blocks);
69+
addToast?.("Scene generated!", "success");
6470
} catch (err: any) {
6571
setError(err.message || "Generation failed");
72+
addToast?.(err.message || "Generation failed", "error");
6673
} finally {
6774
setGenerating(false);
6875
}

0 commit comments

Comments
 (0)