diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 21ba5e45..d3044170 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -103,10 +103,21 @@ jobs: with: platform: windows + - name: Compute Windows Store version + id: msix-version + shell: bash + run: | + SHORT="${{ needs.prepare.outputs.short_version }}" + MAJOR=$(echo "$SHORT" | cut -d. -f1) + MINOR=$(echo "$SHORT" | cut -d. -f2) + PATCH=$(echo "$SHORT" | cut -d. -f3) + BUILD=$(( PATCH * 1000 + ${{ needs.prepare.outputs.revision }} )) + echo "version=${MAJOR}.${MINOR}.${BUILD}.0" >> $GITHUB_OUTPUT + - name: Apply app version uses: ./.github/actions/apply-version with: - version: ${{ needs.prepare.outputs.version }} + version: ${{ steps.msix-version.outputs.version }} staging: "true" - name: Build MSIX bundle diff --git a/components/analytics/stats/CharactersStats.tsx b/components/analytics/stats/CharactersStats.tsx index 62a6b473..e5f13eca 100644 --- a/components/analytics/stats/CharactersStats.tsx +++ b/components/analytics/stats/CharactersStats.tsx @@ -63,18 +63,25 @@ const barOpts = { // ── Dialogue counting ───────────────────────────────────────────────────────── -function countDialoguePerCharacter(screenplay: any[]): Record { +type ScreenplayNode = { + type?: string; + attrs?: Record; + content?: ScreenplayNode[]; + text?: string; +}; + +function countDialoguePerCharacter(screenplay: ScreenplayNode[]): Record { const counts: Record = {}; let currentChar: string | null = null; for (const node of screenplay) { - const nodeType: string = node.attrs?.["class"] ?? node.type ?? ""; + const nodeType: string = (node.attrs?.["class"] as string) ?? node.type ?? ""; if (nodeType === ScreenplayElement.Character) { // Extract plain text from the node's content array const text = (node.content ?? []) - .flatMap((c: any) => c.content ?? [c]) - .map((c: any) => c.text ?? "") + .flatMap((c) => c.content ?? [c]) + .map((c) => c.text ?? "") .join("") .toUpperCase() .replace(/\(.*?\)/g, "") // strip parentheticals like (V.O.) diff --git a/components/analytics/stats/LocationsStats.tsx b/components/analytics/stats/LocationsStats.tsx index 00c5e475..1023ef4b 100644 --- a/components/analytics/stats/LocationsStats.tsx +++ b/components/analytics/stats/LocationsStats.tsx @@ -123,14 +123,13 @@ export default function LocationsStats() { } } - const totalUnique = topLocations.length; // distinct named locations in top 10 const totalAll = Object.keys(locationCount).filter((k) => k !== "UNKNOWN").length; return { topLocations, overallSettings, totalAll }; }, [scenes]); if (stats.totalAll === 0) { - return

No locations found. Scene headings like "INT. OFFICE - DAY" will appear here.

; + return

{'No locations found. Scene headings like "INT. OFFICE - DAY" will appear here.'}

; } // ── Bar chart — top locations ─────────────────────────────────────────────── diff --git a/components/board/BoardCanvas.tsx b/components/board/BoardCanvas.tsx index d2ce4dee..91bfcd93 100644 --- a/components/board/BoardCanvas.tsx +++ b/components/board/BoardCanvas.tsx @@ -50,6 +50,15 @@ const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => { const [isSnapping, setIsSnapping] = useState(true); const [cardContextMenu, setCardContextMenu] = useState(null); const [arrowContextMenu, setArrowContextMenu] = useState(null); + const [prevIsVisible, setPrevIsVisible] = useState(isVisible); + if (prevIsVisible !== isVisible) { + setPrevIsVisible(isVisible); + if (!isVisible) { + setIsSnapping(true); + if (cardContextMenu) setCardContextMenu(null); + if (arrowContextMenu) setArrowContextMenu(null); + } + } const [isCameraReady, setIsCameraReady] = useState(false); const [connectingFrom, setConnectingFrom] = useState<{ cardId: string; side: string } | null>(null); const [connectingLine, setConnectingLine] = useState<{ x: number; y: number } | null>(null); @@ -191,10 +200,7 @@ const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => { // Handle keyboard events for snapping useEffect(() => { - if (!isVisible) { - setIsSnapping(true); // Reset snap state when hidden - return; - } + if (!isVisible) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Shift") { @@ -222,11 +228,7 @@ const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => { // Close context menus on click anywhere useEffect(() => { - if (!isVisible) { - if (cardContextMenu) setCardContextMenu(null); - if (arrowContextMenu) setArrowContextMenu(null); - return; - } + if (!isVisible) return; const handleClick = () => { if (cardContextMenu) setCardContextMenu(null); diff --git a/components/board/BoardCard.tsx b/components/board/BoardCard.tsx index e571dd7a..d8de0bb4 100644 --- a/components/board/BoardCard.tsx +++ b/components/board/BoardCard.tsx @@ -38,13 +38,19 @@ const BoardCard = ({ const [isEditingTitle, setIsEditingTitle] = useState(false); const [localTitle, setLocalTitle] = useState(card.title); const [localDescription, setLocalDescription] = useState(card.description); + const [prevTitle, setPrevTitle] = useState(card.title); + const [prevDescription, setPrevDescription] = useState(card.description); const dragOffset = useRef({ x: 0, y: 0 }); const resizeStart = useRef({ x: 0, y: 0, width: 0, height: 0 }); - useEffect(() => { + if (prevTitle !== card.title) { + setPrevTitle(card.title); setLocalTitle(card.title); + } + if (prevDescription !== card.description) { + setPrevDescription(card.description); setLocalDescription(card.description); - }, [card.title, card.description]); + } const snapToGrid = useCallback( (value: number) => { diff --git a/components/dashboard/DashboardModal.tsx b/components/dashboard/DashboardModal.tsx index f61af026..b42496de 100644 --- a/components/dashboard/DashboardModal.tsx +++ b/components/dashboard/DashboardModal.tsx @@ -5,15 +5,13 @@ import { DashboardContext } from "@src/context/DashboardContext"; import { ProjectContext } from "@src/context/ProjectContext"; import { useCookieUser } from "@src/lib/utils/hooks"; -import CloseSVG from "@public/images/close.svg"; - import SidebarMenu, { MenuSection } from "./DashboardSidebar"; import ProjectSettings from "./project/ProjectSettings"; import CollaboratorsSettings from "./project/CollaboratorsSettings"; import styles from "./DashboardModal.module.css"; import ExportProject from "./project/ExportProject"; -import { CreditCard, FileDown, Folder, Globe, Keyboard, Palette, PanelsTopLeft, User, Users } from "lucide-react"; +import { CreditCard, FileDown, Folder, Globe, Keyboard, Palette, PanelsTopLeft, User, Users, X } from "lucide-react"; import { useTranslations } from "next-intl"; import KeybindsSettings from "./preferences/KeybindsSettings"; import AppearanceSettings from "./preferences/AppearanceSettings"; @@ -84,11 +82,13 @@ const DashboardModal = () => { if (isSignedIn && activeTab === "Auth") { setActiveTab("Profile"); } - }, [isInProject, isSignedIn, activeTab, setActiveTab]); + }, [isInProject, isSignedIn, activeTab, setActiveTab, ACCOUNT_MENU, PREFERENCES_MENU, PROJECT_MENU]); - useEffect(() => { + const [prevActiveTab, setPrevActiveTab] = useState(activeTab); + if (prevActiveTab !== activeTab) { + setPrevActiveTab(activeTab); setDangerOpen(false); - }, [activeTab]); + } useEffect(() => { const handleEsc = (e: KeyboardEvent) => { @@ -108,7 +108,7 @@ const DashboardModal = () => {

{t(`tabs.${activeTab}` as Parameters[0])}

- +
diff --git a/components/dashboard/preferences/KeybindsSettings.tsx b/components/dashboard/preferences/KeybindsSettings.tsx index da14fa4a..250a4475 100644 --- a/components/dashboard/preferences/KeybindsSettings.tsx +++ b/components/dashboard/preferences/KeybindsSettings.tsx @@ -84,19 +84,16 @@ const KeybindsSettings = () => { const { settings, saveSettings } = useSettings(); const t = useTranslations("keybinds"); - const [userKeybinds, setUserKeybinds] = useState({}); + const [userKeybinds, setUserKeybinds] = useState(settings?.keybinds ?? {}); const [listeningFor, setListeningFor] = useState(null); const [tempCombo, setTempCombo] = useState(null); const [hasUpdatedKeybinds, setHasUpdatedKeybinds] = useState(false); const tinykeysStopRef = useRef<(() => void) | null>(null); - - useEffect(() => { - if (settings && settings.keybinds) { - setUserKeybinds(settings.keybinds); - } else { - setUserKeybinds({}); - } - }, [settings]); + const [prevSettings, setPrevSettings] = useState(settings); + if (prevSettings !== settings) { + setPrevSettings(settings); + setUserKeybinds(settings?.keybinds ?? {}); + } useEffect(() => { if (tinykeysStopRef.current) { @@ -185,7 +182,7 @@ const KeybindsSettings = () => { window.removeEventListener("keydown", onKeyDown); window.removeEventListener("keydown", onCancel); }; - }, [listeningFor]); + }, [listeningFor, saveSettings, t]); const startListening = (id: string) => { setListeningFor(id); diff --git a/components/dashboard/preferences/LanguageSettings.tsx b/components/dashboard/preferences/LanguageSettings.tsx index 6026f94c..a0292f36 100644 --- a/components/dashboard/preferences/LanguageSettings.tsx +++ b/components/dashboard/preferences/LanguageSettings.tsx @@ -41,10 +41,7 @@ const LanguageSettings = () => { // Sync word list from Yjs map and observe live changes useEffect(() => { - if (!dictMap) { - setCustomWords([]); - return; - } + if (!dictMap) return; const sync = () => { const words: string[] = []; @@ -54,7 +51,10 @@ const LanguageSettings = () => { sync(); dictMap.observe(sync); - return () => dictMap.unobserve(sync); + return () => { + dictMap.unobserve(sync); + setCustomWords([]); + }; }, [dictMap]); const handleAddWord = useCallback(() => { diff --git a/components/dashboard/project/CollaboratorsSettings.tsx b/components/dashboard/project/CollaboratorsSettings.tsx index 3e7cb694..05c3d6e7 100644 --- a/components/dashboard/project/CollaboratorsSettings.tsx +++ b/components/dashboard/project/CollaboratorsSettings.tsx @@ -17,6 +17,7 @@ import * as Roles from "@src/lib/utils/roles"; import { ApiResponse } from "@src/lib/utils/api-utils"; import { DashboardContext } from "@src/context/DashboardContext"; import { redirect } from "next/navigation"; +import Link from "next/link"; const MAX_COLLABORATORS = 5; @@ -34,7 +35,11 @@ const CollaboratorsSettings = () => { const owner = collaborators.find((c) => c.role === ProjectRole.OWNER); const otherMembers = collaborators.filter((c) => c.role !== ProjectRole.OWNER); - const result: { type: "MEMBER" | "INVITE" | "EMPTY"; data: any; key: string }[] = []; + type Slot = + | { type: "MEMBER"; data: Collaborator; key: string } + | { type: "INVITE"; data: ProjectInvite; key: string } + | { type: "EMPTY"; data: null; key: string }; + const result: Slot[] = []; if (owner) result.push({ type: "MEMBER", data: owner, key: `member-${owner.user.id}` }); otherMembers.forEach((m) => result.push({ type: "MEMBER", data: m, key: `member-${m.user.id}` })); @@ -87,7 +92,7 @@ const CollaboratorsSettings = () => {
{t("proRequired")} - {t("upgrade")} + {t("upgrade")}
)} diff --git a/components/dashboard/project/ExportProject.tsx b/components/dashboard/project/ExportProject.tsx index a34f1293..7ceb25f1 100644 --- a/components/dashboard/project/ExportProject.tsx +++ b/components/dashboard/project/ExportProject.tsx @@ -89,7 +89,7 @@ const ExportProject = () => { const authorEmail = user?.email || "Unknown"; const projectAuthor = membership?.project.author || localAuthor || undefined; - let baseOptions: BaseExportOptions = { + const baseOptions: BaseExportOptions = { title: projectTitle, author: authorEmail, projectAuthor, @@ -122,13 +122,13 @@ const ExportProject = () => { editorElement: editor?.view?.dom, titlePageElement: titlePageEditor?.view?.dom, }; - await adapter.export(ydoc, pdfOptions as any); + await adapter.export(ydoc, pdfOptions as BaseExportOptions); } else if (format === ExportFormat.SCRIPTIO) { const scriptioOptions: ScriptioExportOptions = { ...baseOptions, readable: readableExport, }; - await adapter.export(ydoc, scriptioOptions as any); + await adapter.export(ydoc, scriptioOptions as BaseExportOptions); } else { await adapter.export(ydoc, baseOptions); } diff --git a/components/dashboard/project/LayoutSettings.tsx b/components/dashboard/project/LayoutSettings.tsx index e40f18df..8eb6b092 100644 --- a/components/dashboard/project/LayoutSettings.tsx +++ b/components/dashboard/project/LayoutSettings.tsx @@ -1,6 +1,6 @@ "use client"; -import { useContext, useEffect, useState, useCallback, useMemo } from "react"; +import { useContext, useEffect, useState, useMemo } from "react"; import { useTranslations } from "next-intl"; import { ProjectContext } from "@src/context/ProjectContext"; import { @@ -35,7 +35,15 @@ import Dropdown, { DropdownOption } from "@components/utils/Dropdown"; import form from "./../../utils/Form.module.css"; import sharedStyles from "./ProjectSettings.module.css"; import styles from "./LayoutSettings.module.css"; -import optionCard from "./OptionCard.module.css"; +const MARGIN_ELEMENTS = [ + "scene", + "action", + "character", + "dialogue", + "parenthetical", + "transition", + "section", +] as const; const LayoutSettings = () => { const t = useTranslations("layout"); @@ -62,16 +70,6 @@ const LayoutSettings = () => { setElementStyles, } = context; - const MARGIN_ELEMENTS = [ - "scene", - "action", - "character", - "dialogue", - "parenthetical", - "transition", - "section", - ] as const; - // Strip wrapping parentheses for display const stripParens = (s: string) => (s.startsWith("(") && s.endsWith(")") ? s.slice(1, -1) : s); @@ -110,15 +108,18 @@ const LayoutSettings = () => { // Sync when context changes externally useEffect(() => { - setLocalFormat(pageFormat); - setLocalPageMargins(initialPageMargins); - setLocalDisplaySceneNumbers(displaySceneNumbers); - setLocalSceneNumberOnRight(sceneNumberOnRight); - setLocalHeadingSpacing(sceneHeadingSpacing); - setLocalContdLabel(stripParens(contdLabel)); - setLocalMoreLabel(stripParens(moreLabel)); - setLocalMargins(initialMargins); - setLocalStyles(initialStyles); + const sync = () => { + setLocalFormat(pageFormat); + setLocalPageMargins(initialPageMargins); + setLocalDisplaySceneNumbers(displaySceneNumbers); + setLocalSceneNumberOnRight(sceneNumberOnRight); + setLocalHeadingSpacing(sceneHeadingSpacing); + setLocalContdLabel(stripParens(contdLabel)); + setLocalMoreLabel(stripParens(moreLabel)); + setLocalMargins(initialMargins); + setLocalStyles(initialStyles); + }; + sync(); }, [ pageFormat, initialPageMargins, @@ -237,7 +238,7 @@ const LayoutSettings = () => { setLocalPageMargins((prev) => ({ ...prev, [side]: newValue })); }; - const updateLocalStyle = (e: React.MouseEvent, element: string, styleKey: keyof ElementStyle, value: any) => { + const updateLocalStyle = (e: React.MouseEvent, element: string, styleKey: keyof ElementStyle, value: ElementStyle[keyof ElementStyle]) => { e.preventDefault(); e.stopPropagation(); diff --git a/components/dashboard/project/ProjectSettings.tsx b/components/dashboard/project/ProjectSettings.tsx index 2f8dde90..4430d4c0 100644 --- a/components/dashboard/project/ProjectSettings.tsx +++ b/components/dashboard/project/ProjectSettings.tsx @@ -1,6 +1,7 @@ "use client"; import { cropImageBase64 } from "@src/lib/utils/misc"; +import Image from "next/image"; import { useTranslations } from "next-intl"; import { editProject } from "@src/lib/utils/requests"; import { useContext, useEffect, useState } from "react"; @@ -50,7 +51,11 @@ const ProjectSettings = ({ dangerOpen, onDangerToggle }: { dangerOpen: boolean; setLoading(true); setDirty(false); - const target = e.target as any; + const target = e.target as typeof e.target & { + title: { value: string }; + description: { value: string }; + author: { value: string }; + }; const newTitle = target.title.value; const newDescription = target.description.value; const newAuthor = target.author.value; @@ -65,7 +70,7 @@ const ProjectSettings = ({ dangerOpen, onDangerToggle }: { dangerOpen: boolean; if (!isLocalOnly && membership) { // Also save to remote API - const body: any = { + const body: { title: string; description: string; author: string; poster?: string } = { title: newTitle, description: newDescription, author: newAuthor, @@ -142,7 +147,7 @@ const ProjectSettings = ({ dangerOpen, onDangerToggle }: { dangerOpen: boolean;
{previewUrl ? ( - Preview + Preview ) : (
{t("noPoster")}
)} diff --git a/components/editor/CommentCards.tsx b/components/editor/CommentCards.tsx index eeed89d6..29571f3e 100644 --- a/components/editor/CommentCards.tsx +++ b/components/editor/CommentCards.tsx @@ -7,6 +7,7 @@ import { useUser } from "@src/lib/utils/hooks"; import { getCommentPositions } from "@src/lib/screenplay/extensions/comment-highlight-extension"; import { useViewContext } from "@src/context/ViewContext"; import { Editor } from "@tiptap/react"; +import { Transaction } from "@tiptap/pm/state"; import styles from "./CommentCard.module.css"; function formatTimestamp(ts: number): string { @@ -313,7 +314,7 @@ const CommentCards = ({ if (!editor || editor.isDestroyed || !showComments) return; // Performance: Debounce transaction handler to prevent layout thrashing during typing - const handleTransaction = ({ transaction }: any) => { + const handleTransaction = ({ transaction }: { transaction: Transaction }) => { if (!transaction.docChanged) return; if (transactionDebounceRef.current !== null) { cancelAnimationFrame(transactionDebounceRef.current); @@ -359,7 +360,7 @@ const CommentCards = ({ // Also compute the connecting line for the active comment useEffect(() => { if (!showComments) { - setActiveLine(null); + requestAnimationFrame(() => setActiveLine(null)); return; } diff --git a/components/editor/DocumentEditorPanel.tsx b/components/editor/DocumentEditorPanel.tsx index 9ddd1bdb..a403ef54 100644 --- a/components/editor/DocumentEditorPanel.tsx +++ b/components/editor/DocumentEditorPanel.tsx @@ -19,6 +19,7 @@ import CommentCards from "@components/editor/CommentCards"; import Loading from "@components/utils/Loading"; import { TextSelection } from "@tiptap/pm/state"; +import { EditorView } from "@tiptap/pm/view"; import { DocumentEditorConfig } from "@src/lib/editor/document-editor-config"; import { useDocumentComments } from "@src/lib/editor/use-document-comments"; import { useDocumentEditor } from "@src/lib/editor/use-document-editor"; @@ -48,7 +49,6 @@ const DocumentEditorPanel = ({ onEditorCreated, suggestions = [], updateSuggestions, - suggestionData, updateSuggestionData, userKeybinds, globalContext, @@ -285,7 +285,7 @@ const DocumentEditorPanel = ({ editor.setOptions({ editorProps: { - handleKeyDown(view: any, event: any) { + handleKeyDown(view: EditorView, event: KeyboardEvent) { const selection = view.state.selection; const node = selection.$anchor.parent; const nodeSize = node.content.size; @@ -317,7 +317,6 @@ const DocumentEditorPanel = ({ } if (event.key === "Enter") { - const currentSuggestions = updateSuggestionsRef.current; // suggestions.length check: read from ref to avoid stale closure if (suggestions.length > 0) { event.preventDefault(); @@ -396,7 +395,7 @@ const DocumentEditorPanel = ({ }, }, }); - }, [editor, config.type]); + }, [editor, config.type, suggestions.length]); // ---- Global keybinds (screenplay only) ---- const globalActions = useMemo( diff --git a/components/editor/DraftEditorPanel.tsx b/components/editor/DraftEditorPanel.tsx index cae480e6..c367a7d9 100644 --- a/components/editor/DraftEditorPanel.tsx +++ b/components/editor/DraftEditorPanel.tsx @@ -26,7 +26,7 @@ const DraftEditorPanel = ({ isVisible }: { isVisible: boolean }) => { const config = useMemo(() => { if (!activeShelfVersion) return null; return createShelfEditorConfig(activeShelfVersion.nodeId, activeShelfVersion.versionId); - }, [activeShelfVersion?.nodeId, activeShelfVersion?.versionId]); + }, [activeShelfVersion]); const handleEditorCreated = useCallback( (editor: import("@tiptap/react").Editor | null) => { diff --git a/components/editor/EditorTab.tsx b/components/editor/EditorTab.tsx index 8e36aa9c..3a237593 100644 --- a/components/editor/EditorTab.tsx +++ b/components/editor/EditorTab.tsx @@ -3,7 +3,7 @@ import { join } from "@src/lib/utils/misc"; import tab from "./EditorTab.module.css"; -import SelectorSVG from "@public/images/selector.svg"; +import { ChevronRight } from "lucide-react"; import { ScreenplayElement } from "@src/lib/utils/enums"; type Props = { @@ -20,7 +20,7 @@ const EditorTab = ({ setActiveElement, currentElement, element, content }: Props return ( ); diff --git a/components/editor/SuggestionMenu.tsx b/components/editor/SuggestionMenu.tsx index b33993a5..0892b9f5 100644 --- a/components/editor/SuggestionMenu.tsx +++ b/components/editor/SuggestionMenu.tsx @@ -29,6 +29,11 @@ export type SuggestionData = { const SuggestionMenu = ({ suggestionData, suggestions, onSelect }: Props) => { const [selectedIdx, setSelectedIdx] = useState(0); + const [prevSuggestions, setPrevSuggestions] = useState(suggestions); + if (prevSuggestions !== suggestions) { + setPrevSuggestions(suggestions); + setSelectedIdx(0); + } const { editor } = useContext(ProjectContext); const itemRefs = useRef<(HTMLDivElement | null)[]>([]); @@ -50,9 +55,7 @@ const SuggestionMenu = ({ suggestionData, suggestions, onSelect }: Props) => { suggestionsRef.current = suggestions; }, [suggestions]); - // Reset selection when suggestions change useEffect(() => { - setSelectedIdx(0); itemRefs.current = []; }, [suggestions]); diff --git a/components/editor/TitlePagePanel.tsx b/components/editor/TitlePagePanel.tsx index da356f33..3fb878bc 100644 --- a/components/editor/TitlePagePanel.tsx +++ b/components/editor/TitlePagePanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import { useCallback, useContext, useEffect, useState } from "react"; import type { Editor } from "@tiptap/react"; import { ProjectContext } from "@src/context/ProjectContext"; @@ -11,25 +11,20 @@ import { TitlePageElement } from "@src/lib/utils/enums"; import { TITLEPAGE_EDITOR_CONFIG } from "@src/lib/editor/document-editor-config"; import DocumentEditorPanel from "./DocumentEditorPanel"; +interface TitlePageStorage { + projectTitle: string; + projectAuthor: string; + nodeViewUpdaters?: Array<() => void>; +} + +type EditorStorage = { titlePageMetadata?: TitlePageStorage }; + const TitlePagePanel = ({ isVisible }: { isVisible?: boolean }) => { const projectCtx = useContext(ProjectContext); const { updateTitlePageEditor, isYjsReady, repository, projectTitle, projectAuthor } = projectCtx; const [titleEditor, setTitleEditor] = useState(null); - // Keep the module-level ref in sync so format node views always get the latest values - titlePageMetadataRef.projectTitle = projectTitle || ""; - titlePageMetadataRef.projectAuthor = projectAuthor || ""; - - // Synchronous storage update for nodes rendered before effects run - if (titleEditor && typeof titleEditor.storage === "object") { - const storage = (titleEditor.storage as any).titlePageMetadata; - if (storage) { - storage.projectTitle = projectTitle || ""; - storage.projectAuthor = projectAuthor || ""; - } - } - const handleEditorCreated = useCallback( (editor: Editor | null) => { updateTitlePageEditor(editor); @@ -71,14 +66,18 @@ const TitlePagePanel = ({ isVisible }: { isVisible?: boolean }) => { } }, [titleEditor, isYjsReady, repository]); - // Sync project metadata into editor storage for node view rendering + // Sync project metadata into the module-level ref and editor storage for node view rendering useEffect(() => { + titlePageMetadataRef.projectTitle = projectTitle || ""; + titlePageMetadataRef.projectAuthor = projectAuthor || ""; + if (!titleEditor || titleEditor.isDestroyed) return; - const storage = (titleEditor.storage as any).titlePageMetadata; + const storage = (titleEditor.storage as EditorStorage).titlePageMetadata; if (storage) { + // eslint-disable-next-line react-hooks/immutability storage.projectTitle = projectTitle || ""; storage.projectAuthor = projectAuthor || ""; - storage.nodeViewUpdaters?.forEach((fn: () => void) => fn()); + storage.nodeViewUpdaters?.forEach((fn) => fn()); if (titleEditor.view && !titleEditor.view.isDestroyed) { titleEditor.view.dispatch(titleEditor.state.tr.setMeta("titlePageMetadataUpdate", true)); } @@ -95,4 +94,3 @@ const TitlePagePanel = ({ isVisible }: { isVisible?: boolean }) => { }; export default TitlePagePanel; - diff --git a/components/editor/sidebar/ContextMenu.tsx b/components/editor/sidebar/ContextMenu.tsx index 41ec17d8..6185731a 100644 --- a/components/editor/sidebar/ContextMenu.tsx +++ b/components/editor/sidebar/ContextMenu.tsx @@ -9,7 +9,7 @@ import { Scene } from "@src/lib/screenplay/scenes"; import context from "./ContextMenu.module.css"; import { CharacterData, deleteCharacter } from "@src/lib/screenplay/characters"; import { LocationData, deleteLocation } from "@src/lib/screenplay/locations"; -import { copyText, cutText, focusOnPosition, pasteText, selectTextInEditor } from "@src/lib/screenplay/editor"; +import { cutText, focusOnPosition, pasteText, selectTextInEditor } from "@src/lib/screenplay/editor"; import { addCharacterPopup, editCharacterPopup, editScenePopup } from "@src/lib/screenplay/popup"; import { ProjectContext } from "@src/context/ProjectContext"; import { useTranslations } from "next-intl"; @@ -42,7 +42,7 @@ import { ScreenplayElement } from "@src/lib/utils/enums"; export type ContextMenuProps = { type: ContextMenuType; position: { x: number; y: number }; - typeSpecificProps: any; + typeSpecificProps: unknown; }; export const enum ContextMenuType { @@ -66,6 +66,8 @@ type ContextMenuItemProps = { disabled?: boolean; }; +type SubMenuProps = { props: T }; + export const ContextMenuItem = ({ text, action, icon: Icon, disabled }: ContextMenuItemProps) => { return (
@@ -83,11 +85,11 @@ export type SceneContextProps = { scene: Scene; }; -const SceneItemMenu = (props: any) => { +const SceneItemMenu = ({ props }: SubMenuProps) => { const t = useTranslations("contextMenu"); const userCtx = useContext(UserContext); const { editor } = useContext(ProjectContext); - const scene: Scene = props.props.scene; + const scene: Scene = props.scene; return ( <> @@ -111,13 +113,7 @@ const SceneItemMenu = (props: any) => { ); }; -const SceneListMenu = (props: any) => { - const title = props.props.title; - - const addScene = () => { - console.log("add scene ", name); - }; - +const SceneListMenu = () => { return <>; //return <>{}; }; @@ -130,12 +126,12 @@ export type CharacterContextProps = { character: CharacterData; }; -const CharacterItemMenu = (props: any) => { +const CharacterItemMenu = ({ props }: SubMenuProps) => { const t = useTranslations("contextMenu"); const userCtx = useContext(UserContext); const projectCtx = useContext(ProjectContext); const { toggleCharacterHighlight } = projectCtx; - const character: CharacterData = props.props.character; + const character: CharacterData = props.character; return ( <> @@ -155,7 +151,7 @@ const CharacterItemMenu = (props: any) => { ); }; -const CharacterListMenu = (props: any) => { +const CharacterListMenu = () => { const t = useTranslations("contextMenu"); const userCtx = useContext(UserContext); return addCharacterPopup(userCtx)} />; @@ -169,10 +165,10 @@ export type LocationContextProps = { location: LocationData; }; -const LocationItemMenu = (props: any) => { +const LocationItemMenu = ({ props }: SubMenuProps) => { const t = useTranslations("contextMenu"); const projectCtx = useContext(ProjectContext); - const location: LocationData = props.props.location; + const location: LocationData = props.location; return ( <> @@ -196,12 +192,12 @@ export type EditorSelectionContextProps = { onAddComment: () => void; }; -const EditorSelectionMenu = (props: any) => { +const EditorSelectionMenu = ({ props }: SubMenuProps) => { const t = useTranslations("contextMenu"); const projectCtx = useContext(ProjectContext); const { editor } = projectCtx; const { updateContextMenu } = useContext(UserContext); - const { from, to, onAddComment } = props.props as EditorSelectionContextProps; + const { from, to, onAddComment } = props; const hasSelection = from !== to; const handleCopy = async () => { @@ -259,19 +255,17 @@ export type SpellcheckContextProps = { to: number; }; -const SpellcheckMenu = (props: any) => { +const SpellcheckMenu = ({ props }: SubMenuProps) => { const t = useTranslations("contextMenu"); const { editor, repository } = useContext(ProjectContext); const { worker } = useSpellcheck(); const { updateContextMenu } = useContext(UserContext); - const { word, from, to } = props.props as SpellcheckContextProps; + const { word, from, to } = props; const [suggestions, setSuggestions] = useState(null); + const displaySuggestions = worker ? suggestions : []; useEffect(() => { - if (!worker) { - setSuggestions([]); - return; - } + if (!worker) return; const handler = (e: MessageEvent) => { if (e.data.type === "SUGGEST_RESULT" && e.data.word === word) { @@ -310,22 +304,22 @@ const SpellcheckMenu = (props: any) => { return ( <> - {suggestions === null && ( + {displaySuggestions === null && (
)} - {suggestions !== null && suggestions.length === 0 && ( + {displaySuggestions !== null && displaySuggestions.length === 0 && (
{t("noSuggestions")}
)} - {suggestions?.map((s) => ( + {displaySuggestions?.map((s) => (
handleReplace(s)}>

{s}

))} - {suggestions !== null && suggestions.length > 0 &&
} + {displaySuggestions !== null && displaySuggestions.length > 0 &&
} ); @@ -335,11 +329,11 @@ const SpellcheckMenu = (props: any) => { /* Dual Dialogue context menu */ /* ============================ */ -const DualDialogueMenu = (props: any) => { +const DualDialogueMenu = ({ props }: SubMenuProps<{ pos: number }>) => { const t = useTranslations("contextMenu"); const { editor } = useContext(ProjectContext); const { updateContextMenu } = useContext(UserContext); - const { pos } = props.props as { pos: number }; + const { pos } = props; return ( { /* Shelve Node context menu */ /* ============================ */ -const ShelveNodeMenu = (props: any) => { +const ShelveNodeMenu = ({ props }: SubMenuProps<{ pos: number; nodeClass: string }>) => { const t = useTranslations("contextMenu"); const { editor, repository } = useContext(ProjectContext); const { updateContextMenu } = useContext(UserContext); - const { pos, nodeClass } = props.props as { pos: number; nodeClass: string }; + const { pos, nodeClass } = props; const handleShelve = () => { if (!editor || !repository) return; @@ -428,21 +422,19 @@ export type EditorContextMenuProps = { nodeClass?: string; }; -const EditorContextMenu = (props: any) => { +const EditorContextMenu = ({ props }: SubMenuProps) => { const t = useTranslations("contextMenu"); const { editor, repository } = useContext(ProjectContext); const { worker } = useSpellcheck(); const { updateContextMenu } = useContext(UserContext); - const { from, to, onAddComment, spellError, nodePos, nodeClass } = props.props as EditorContextMenuProps; + const { from, to, onAddComment, spellError, nodePos, nodeClass } = props; const hasSelection = from !== to; const [suggestions, setSuggestions] = useState(null); + const displaySuggestions = spellError && !worker ? [] : suggestions; useEffect(() => { - if (!spellError || !worker) { - if (spellError) setSuggestions([]); - return; - } + if (!spellError || !worker) return; const handler = (e: MessageEvent) => { if (e.data.type === "SUGGEST_RESULT" && e.data.word === spellError.word) { worker.removeEventListener("message", handler); @@ -537,17 +529,17 @@ const EditorContextMenu = (props: any) => { {/* Spellcheck section — shown first when on a spellcheck error */} {spellError && ( <> - {suggestions === null && ( + {displaySuggestions === null && (
)} - {suggestions !== null && suggestions.length === 0 && ( + {displaySuggestions !== null && displaySuggestions.length === 0 && (
{t("noSuggestions")}
)} - {suggestions?.map((s) => ( + {displaySuggestions?.map((s) => (
handleSpellReplace(s)}>

{s}

@@ -605,25 +597,25 @@ const EditorContextMenu = (props: any) => { const renderContextMenu = (contextMenu: ContextMenuProps) => { switch (contextMenu.type) { case ContextMenuType.SceneList: - return ; + return ; case ContextMenuType.SceneItem: - return ; + return ; case ContextMenuType.CharacterList: - return ; + return ; case ContextMenuType.CharacterItem: - return ; + return ; case ContextMenuType.LocationItem: - return ; + return ; case ContextMenuType.EditorSelection: - return ; + return ; case ContextMenuType.Spellcheck: - return ; + return ; case ContextMenuType.DualDialogue: - return ; + return ; case ContextMenuType.ShelveNode: - return ; + return ; case ContextMenuType.EditorContextMenu: - return ; + return ; } }; @@ -650,7 +642,7 @@ const ContextMenu = () => { useEffect(() => { updateContextMenu(undefined); - }, []); + }, [updateContextMenu]); return (
{ setIndicatorIndex(null); }, [dragIndex, indicatorIndex, scenes, editor, updateScenes]); - const handleDragEnd = useCallback(() => { - setDragIndex(null); - setIndicatorIndex(null); - }, []); - // Window-level pointerup so the drop works even if cursor leaves the list useEffect(() => { if (dragIndex === null) return; diff --git a/components/editor/sidebar/SidebarCharacterItem.tsx b/components/editor/sidebar/SidebarCharacterItem.tsx index 01f330fa..d0165087 100644 --- a/components/editor/sidebar/SidebarCharacterItem.tsx +++ b/components/editor/sidebar/SidebarCharacterItem.tsx @@ -23,7 +23,7 @@ const SidebarCharacterItem = memo(({ character, isHighlighted }: SidebarCharacte const highlightColor = character.color || DEFAULT_HIGHLIGHT_COLOR; - const handleDropdown = useCallback((e: any) => { + const handleDropdown = useCallback((e: React.MouseEvent) => { e.preventDefault(); updateContextMenu({ type: ContextMenuType.CharacterItem, diff --git a/components/editor/sidebar/SidebarLocationItem.tsx b/components/editor/sidebar/SidebarLocationItem.tsx index 790bf875..e0886aab 100644 --- a/components/editor/sidebar/SidebarLocationItem.tsx +++ b/components/editor/sidebar/SidebarLocationItem.tsx @@ -7,14 +7,14 @@ import { pasteText } from "@src/lib/screenplay/editor"; import { ProjectContext } from "@src/context/ProjectContext"; import { join } from "@src/lib/utils/misc"; -import LinkSVG from "@public/images/link.svg"; +import { Link } from "lucide-react"; import item from "./SidebarItem.module.css"; const SidebarLocationItem = memo(({ location }: LocationContextProps) => { const { updateContextMenu } = useContext(UserContext); const { editor } = useContext(ProjectContext); - const handleDropdown = useCallback((e: any) => { + const handleDropdown = useCallback((e: React.MouseEvent) => { e.preventDefault(); updateContextMenu({ type: ContextMenuType.LocationItem, @@ -34,7 +34,7 @@ const SidebarLocationItem = memo(({ location }: LocationContextProps) => {

{location.name}

- {location.persistent && } + {location.persistent && }
); diff --git a/components/editor/sidebar/SidebarSceneItem.tsx b/components/editor/sidebar/SidebarSceneItem.tsx index 8185c1ce..b52cdc56 100644 --- a/components/editor/sidebar/SidebarSceneItem.tsx +++ b/components/editor/sidebar/SidebarSceneItem.tsx @@ -22,7 +22,7 @@ type SidebarSceneItemProps = SceneContextProps & { const SidebarSceneItem = memo(({ scene, index, showDropIndicator, isDragging, isCurrent, scrollRef, onPointerDown, onDoubleClick }: SidebarSceneItemProps) => { const { updateContextMenu } = useContext(UserContext); - const handleDropdown = (e: any) => { + const handleDropdown = (e: React.MouseEvent) => { e.preventDefault(); updateContextMenu({ type: ContextMenuType.SceneItem, diff --git a/components/home/HomePageContainer.tsx b/components/home/HomePageContainer.tsx index 7959b2ce..9e19a173 100644 --- a/components/home/HomePageContainer.tsx +++ b/components/home/HomePageContainer.tsx @@ -16,6 +16,7 @@ import { Users, } from "lucide-react"; import Footer from "./Footer"; +import Image from "next/image"; export default function HomePageContainer() { return ( @@ -34,7 +35,7 @@ export default function HomePageContainer() { FADE IN: A world built for storytellers. CUT TO: - (V.O.) "It starts with a single page..." + (V.O.) "It starts with a single page..." CLOSE UP on the keyboard. Fingers flying. DISSOLVE TO: @@ -47,16 +48,18 @@ export default function HomePageContainer() {
{/* Layer 1.1: Preview Image (Behind Content, In Front of Stripes) */}
- Scriptio Interface Preview
{/* Layer 1.2: Branding (Logo) */}
- Scriptio Logo + Scriptio Logo
{/* Layer 1.3: Platform CTAs */} @@ -123,9 +126,11 @@ export default function HomePageContainer() {

- Auto-complete Preview
@@ -145,9 +150,11 @@ export default function HomePageContainer() {

- Auto-complete Preview
@@ -166,9 +173,11 @@ export default function HomePageContainer() { locked into one ecosystem.

- Formats Preview
@@ -187,9 +196,11 @@ export default function HomePageContainer() {

- Search Preview
@@ -209,9 +220,11 @@ export default function HomePageContainer() {

- Search Preview
@@ -230,7 +243,7 @@ export default function HomePageContainer() { sessions. Your environment should inspire, not distract.

- Themes Preview + Themes Preview
{/* Scene Navigation */} @@ -246,9 +259,11 @@ export default function HomePageContainer() { scenes, add synopses, and color-code your story structure.

- Scene Navigation Preview @@ -262,13 +277,15 @@ export default function HomePageContainer() {

Rename a character across your entire script in one click. Assign traits and - descriptions to build rich character profiles. Highlight any character's lines to + descriptions to build rich character profiles. Highlight any character's lines to stay locked in their voice.

- Character Highlight Preview @@ -286,9 +303,11 @@ export default function HomePageContainer() { Scriptio works fully offline, no account required.

- Editor Preview @@ -306,9 +325,11 @@ export default function HomePageContainer() { glance. Data-driven insights to sharpen your story before it hits the screen.

- Editor Preview @@ -328,9 +349,11 @@ export default function HomePageContainer() {

- Editor Preview
@@ -346,13 +369,15 @@ export default function HomePageContainer() {

Invite up to 5 collaborators and write the same screenplay simultaneously in real - time. See each other's cursors, edits, and comments as they happen. No merging, no + time. See each other's cursors, edits, and comments as they happen. No merging, no conflicts — just seamless creative flow.

- Collaboration Preview diff --git a/components/home/auth/MagicLinkLanding.tsx b/components/home/auth/MagicLinkLanding.tsx index 27f0dc7f..7a5350fd 100644 --- a/components/home/auth/MagicLinkLanding.tsx +++ b/components/home/auth/MagicLinkLanding.tsx @@ -25,15 +25,12 @@ const MagicLinkLanding = () => { const router = useRouter(); const token = searchParams.get("token"); - const [status, setStatus] = useState("working"); + const [status, setStatus] = useState(token ? "working" : "error"); // Strict mode mounts effects twice in dev — guard against double-consuming the token. const consumedRef = useRef(false); useEffect(() => { - if (!token) { - setStatus("error"); - return; - } + if (!token) return; if (consumedRef.current) return; consumedRef.current = true; diff --git a/components/home/desktop-oauth/DesktopOAuthComplete.tsx b/components/home/desktop-oauth/DesktopOAuthComplete.tsx index c3223d7d..8659a7a1 100644 --- a/components/home/desktop-oauth/DesktopOAuthComplete.tsx +++ b/components/home/desktop-oauth/DesktopOAuthComplete.tsx @@ -20,13 +20,10 @@ type Status = "working" | "done" | "error"; const DesktopOAuthComplete = () => { const searchParams = useSearchParams(); const nonce = searchParams.get("nonce"); - const [status, setStatus] = useState("working"); + const [status, setStatus] = useState(nonce ? "working" : "error"); useEffect(() => { - if (!nonce) { - setStatus("error"); - return; - } + if (!nonce) return; (async () => { try { const res = await fetch("/api/desktop/token", { diff --git a/components/navbar/LandingPageNavbar.tsx b/components/navbar/LandingPageNavbar.tsx index e9262ce7..092b8882 100644 --- a/components/navbar/LandingPageNavbar.tsx +++ b/components/navbar/LandingPageNavbar.tsx @@ -2,6 +2,7 @@ import { usePage } from "@src/lib/utils/hooks"; import Link from "next/link"; +import Image from "next/image"; import styles from "./LandingPageNavbar.module.css"; @@ -26,7 +27,7 @@ export default function LandingPageNavbar() { ) : ( - Scriptio Logo + Scriptio Logo )} diff --git a/components/navbar/ProjectNavbar.tsx b/components/navbar/ProjectNavbar.tsx index 744c4525..a626e81e 100644 --- a/components/navbar/ProjectNavbar.tsx +++ b/components/navbar/ProjectNavbar.tsx @@ -3,7 +3,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { ConnectionStatus } from "@src/lib/utils/enums"; -import { useCookieUser, useIsPro, useProjectIdFromUrl } from "@src/lib/utils/hooks"; +import { useIsPro, useProjectIdFromUrl } from "@src/lib/utils/hooks"; import { redirectHome } from "@src/lib/utils/redirects"; import { ProjectContext } from "@src/context/ProjectContext"; @@ -95,7 +95,6 @@ const ProjectNavbar = () => { const [isSavesOpen, setIsSavesOpen] = useState(false); const isLocalEdit = useRef(false); - const { user } = useCookieUser(); const { isPro } = useIsPro(); const projectId = useProjectIdFromUrl(); diff --git a/components/navbar/SavesPanel.tsx b/components/navbar/SavesPanel.tsx index 83bb98a7..2e3c6265 100644 --- a/components/navbar/SavesPanel.tsx +++ b/components/navbar/SavesPanel.tsx @@ -1,6 +1,7 @@ "use client"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import Link from "next/link"; import { useTranslations } from "next-intl"; import { X, @@ -51,26 +52,28 @@ const SavesPanel = ({ projectId, isOpen, onClose, isPro }: SavesPanelProps) => { const manualSaves = saves.filter((s) => s.type === "manual"); const autoSaves = saves.filter((s) => s.type === "auto"); - // Fetch saves when panel opens - const fetchSaves = useCallback(async () => { - setLoading(true); - const data = await listSaves(projectId); - setSaves(data); - setLoading(false); - }, [projectId]); - - useEffect(() => { - if (isOpen) { - fetchSaves(); - } else { - // Reset state when closing + const [prevIsOpen, setPrevIsOpen] = useState(isOpen); + if (prevIsOpen !== isOpen) { + setPrevIsOpen(isOpen); + if (!isOpen) { setShowNameInput(false); setSaveName(""); setConfirmRestoreKey(null); setConfirmDeleteKey(null); setEditingKey(null); } - }, [isOpen, fetchSaves]); + } + + useEffect(() => { + if (!isOpen) return; + const fetchSaves = async () => { + setLoading(true); + const data = await listSaves(projectId); + setSaves(data); + setLoading(false); + }; + fetchSaves(); + }, [isOpen, projectId]); // Focus name input when shown useEffect(() => { @@ -183,9 +186,9 @@ const SavesPanel = ({ projectId, isOpen, onClose, isPro }: SavesPanelProps) => {

{t("proRequired")}

{t("proRequiredDesc")}

- + {t("upgradeBtn")} - + ); diff --git a/components/navbar/dropdown/DropdownItem.tsx b/components/navbar/dropdown/DropdownItem.tsx index b394c4b3..f006c431 100644 --- a/components/navbar/dropdown/DropdownItem.tsx +++ b/components/navbar/dropdown/DropdownItem.tsx @@ -1,6 +1,7 @@ "use client"; import { ForwardedRef, forwardRef } from "react"; +import Image from "next/image"; import dropdown from "./DropdownItem.module.css"; type Props = { @@ -13,10 +14,12 @@ type Props = { const DropdownItem = forwardRef(({ hovering, content, action, icon }: Props, ref: ForwardedRef) => { return ( ); }); +DropdownItem.displayName = "DropdownItem"; + export default DropdownItem; diff --git a/components/popup/PopupCharacterItem.tsx b/components/popup/PopupCharacterItem.tsx index 57e9343f..221804a5 100644 --- a/components/popup/PopupCharacterItem.tsx +++ b/components/popup/PopupCharacterItem.tsx @@ -1,7 +1,7 @@ "use client"; import assert from "assert"; -import { useContext, useState } from "react"; +import { useContext, useState, type FormEvent } from "react"; import { CharacterGender, doesCharacterExist, @@ -18,20 +18,22 @@ import { countOccurrences } from "@src/lib/screenplay/screenplay"; import { ColorPicker } from "@components/utils/ColorPicker"; import { useTranslations } from "next-intl"; -import CloseSVG from "@public/images/close.svg"; +import { X } from "lucide-react"; import form from "@components/utils/Form.module.css"; import form_info from "@components/utils/FormInfo.module.css"; import styles from "@components/popup/PopupCharacterItem.module.css"; import popup from "@components/popup/Popup.module.css"; +type TFunction = ReturnType; + type NewNameWarningProps = { setNewNameWarning: (value: boolean) => void; onNewNameConfirm: () => void; nameOccurrences: number; oldName: string; newName: string; - t: any; + t: TFunction; }; const NewNameWarning = (props: NewNameWarningProps) => { @@ -52,7 +54,7 @@ const NewNameWarning = (props: NewNameWarningProps) => { ); }; -const TakenNameError = (newName: string, t: any) => { +const TakenNameError = (newName: string, t: TFunction) => { return (

@@ -77,12 +79,16 @@ export const PopupCharacterItem = ({ type, data: { character } }: PopupData(""); const [newColor, setNewColor] = useState(character?.color); - const onCreate = (e: any) => { + const onCreate = (e: FormEvent) => { e.preventDefault(); - - const _name = e.target.name.value; - const _gender = e.target.gender.value; - const _synopsis = e.target.synopsis.value; + const form = e.currentTarget as typeof e.currentTarget & { + name: HTMLInputElement; + gender: HTMLSelectElement; + synopsis: HTMLTextAreaElement; + }; + const _name = form.name.value; + const _gender = +form.gender.value as CharacterGender; + const _synopsis = form.synopsis.value; const doesExist = doesCharacterExist(_name, projectCtx); if (doesExist) { @@ -103,15 +109,19 @@ export const PopupCharacterItem = ({ type, data: { character } }: PopupData { + const onEdit = (e: FormEvent) => { e.preventDefault(); assert(character, "A character must be defined on edit mode"); - // need to store in local variables because stateful is async - const _newName = e.target.name.value; - const _newGender = +e.target.gender.value; - const _newSynopsis = e.target.synopsis.value; + const form = e.currentTarget as typeof e.currentTarget & { + name: HTMLInputElement; + gender: HTMLSelectElement; + synopsis: HTMLTextAreaElement; + }; + const _newName = form.name.value; + const _newGender = +form.gender.value; + const _newSynopsis = form.synopsis.value; setNewName(_newName.toUpperCase()); // to display it in popup UI setNewGender(_newGender); @@ -124,7 +134,7 @@ export const PopupCharacterItem = ({ type, data: { character } }: PopupData) => void; + name: string | undefined; + synopsis: string | undefined; + gender: CharacterGender | string | undefined; + color: string | undefined; + }; + + const def: FormDef = { title: t("create"), onSubmit: onCreate, name: "", @@ -195,7 +214,7 @@ export const PopupCharacterItem = ({ type, data: { character } }: PopupData

{def.title}

- closePopup(userCtx)} alt="Close icon" /> + closePopup(userCtx)} />
{takenNameError && TakenNameError(newName, t)} @@ -204,7 +223,7 @@ export const PopupCharacterItem = ({ type, data: { character } }: PopupData

{t("title")}

- closePopup(userCtx)} alt="Close icon" /> + closePopup(userCtx)} />

diff --git a/components/popup/PopupSceneItem.tsx b/components/popup/PopupSceneItem.tsx index cd69b5a8..7814df40 100644 --- a/components/popup/PopupSceneItem.tsx +++ b/components/popup/PopupSceneItem.tsx @@ -7,11 +7,8 @@ import { ProjectContext } from "@src/context/ProjectContext"; import { UserContext } from "@src/context/UserContext"; import { PopupData, PopupSceneData, closePopup } from "@src/lib/screenplay/popup"; import { ColorPicker } from "@components/utils/ColorPicker"; -import { ScreenplayElement } from "@src/lib/utils/enums"; import { useTranslations } from "next-intl"; -import { Save } from "lucide-react"; - -import CloseSVG from "@public/images/close.svg"; +import { Save, X } from "lucide-react"; import form from "@components/utils/Form.module.css"; import styles from "@components/popup/PopupCharacterItem.module.css"; @@ -43,16 +40,6 @@ export const PopupSceneItem = ({ data: { scene } }: PopupData) = closePopup(userCtx); }; - const onDelete = () => { - if (!repository || !scene.id) { - closePopup(userCtx); - return; - } - - repository.deleteScene(scene.id); - closePopup(userCtx); - }; - return (

@@ -62,7 +49,7 @@ export const PopupSceneItem = ({ data: { scene } }: PopupData) = style={{ cursor: isDragging ? "grabbing" : "grab" }} >

{t("edit")}

- closePopup(userCtx)} alt="Close icon" /> + closePopup(userCtx)} />
diff --git a/components/projects/CreateProjectPage.tsx b/components/projects/CreateProjectPage.tsx index 2e70ac4c..edab4e63 100644 --- a/components/projects/CreateProjectPage.tsx +++ b/components/projects/CreateProjectPage.tsx @@ -28,7 +28,6 @@ const CreateProjectPage = ({ setIsCreating }: Props) => { const [formInfo, setFormInfo] = useState(null); const [selectedFile, setSelectedFile] = useState(null); - const [previewUrl, setPreviewUrl] = useState(null); const exitCreating = () => { setIsCreating(false); @@ -38,13 +37,18 @@ const CreateProjectPage = ({ setIsCreating }: Props) => { setFormInfo(null); }; - const onSubmit = async (e: any) => { + const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); resetFormInfo(); - const title = e.target.title.value; - const description = e.target.description.value; - const author = e.target.author.value; + const form = e.target as typeof e.target & { + title: { value: string }; + description: { value: string }; + author: { value: string }; + }; + const title = form.title.value; + const description = form.description.value; + const author = form.author.value; // Desktop: offline-first project creation // Always create locally. If Pro and signed in, try cloud first to use its ID. diff --git a/components/projects/ProjectItem.tsx b/components/projects/ProjectItem.tsx index a6c63484..b38e44fc 100644 --- a/components/projects/ProjectItem.tsx +++ b/components/projects/ProjectItem.tsx @@ -2,6 +2,7 @@ import { getElapsedDaysFrom, join } from "@src/lib/utils/misc"; import { useTranslations } from "next-intl"; +import Image from "next/image"; import item from "./ProjectItem.module.css"; import { redirectScreenplay } from "@src/lib/utils/redirects"; @@ -30,7 +31,7 @@ const ProjectItem = ({ project, isLocalOnly = false }: Props) => { return (