From a46fadc7c672cdccf45c8a40b39feb5766658558 Mon Sep 17 00:00:00 2001
From: Lycoon
Date: Wed, 22 Apr 2026 00:44:17 +0200
Subject: [PATCH] all eslint errors fixed
---
.github/workflows/deploy-staging.yaml | 13 ++-
.../analytics/stats/CharactersStats.tsx | 15 ++-
components/analytics/stats/LocationsStats.tsx | 3 +-
components/board/BoardCanvas.tsx | 20 ++--
components/board/BoardCard.tsx | 10 +-
components/dashboard/DashboardModal.tsx | 14 +--
.../preferences/KeybindsSettings.tsx | 17 ++--
.../preferences/LanguageSettings.tsx | 10 +-
.../project/CollaboratorsSettings.tsx | 9 +-
.../dashboard/project/ExportProject.tsx | 6 +-
.../dashboard/project/LayoutSettings.tsx | 45 ++++----
.../dashboard/project/ProjectSettings.tsx | 11 +-
components/editor/CommentCards.tsx | 5 +-
components/editor/DocumentEditorPanel.tsx | 7 +-
components/editor/DraftEditorPanel.tsx | 2 +-
components/editor/EditorTab.tsx | 4 +-
components/editor/SuggestionMenu.tsx | 7 +-
components/editor/TitlePagePanel.tsx | 34 +++----
components/editor/sidebar/ContextMenu.tsx | 96 ++++++++----------
.../sidebar/EditorSidebarNavigation.tsx | 5 -
.../editor/sidebar/SidebarCharacterItem.tsx | 2 +-
.../editor/sidebar/SidebarLocationItem.tsx | 6 +-
.../editor/sidebar/SidebarSceneItem.tsx | 2 +-
components/home/HomePageContainer.tsx | 59 +++++++----
components/home/auth/MagicLinkLanding.tsx | 7 +-
.../desktop-oauth/DesktopOAuthComplete.tsx | 7 +-
components/navbar/LandingPageNavbar.tsx | 3 +-
components/navbar/ProjectNavbar.tsx | 3 +-
components/navbar/SavesPanel.tsx | 37 +++----
components/navbar/dropdown/DropdownItem.tsx | 5 +-
components/popup/PopupCharacterItem.tsx | 55 ++++++----
components/popup/PopupImportFile.tsx | 4 +-
components/popup/PopupSceneItem.tsx | 17 +---
components/projects/CreateProjectPage.tsx | 14 ++-
components/projects/ProjectItem.tsx | 3 +-
components/projects/ProjectPageContainer.tsx | 2 +-
.../projects/ProjectUnavailableDialog.tsx | 2 +-
components/projects/UploadButton.tsx | 28 +++--
components/projects/stats/BarRatio.tsx | 3 +-
.../projects/stats/CharacterDistribution.tsx | 13 +--
.../projects/stats/CharacterFrequency.tsx | 3 +-
.../projects/stats/CharacterQuantity.tsx | 3 +-
.../projects/stats/ProjectStatsContainer.tsx | 8 +-
eslint.config.mjs | 3 +-
public/images/arrow.svg | 2 -
public/images/back.svg | 13 ---
public/images/back2.svg | 2 -
public/images/bold.svg | 21 ----
public/images/calendar.svg | 2 -
public/images/checkmark.svg | 1 -
public/images/close.svg | 19 ----
public/images/dropdown-arrow.svg | 47 ---------
public/images/export.svg | 2 -
public/images/eye.svg | 6 --
public/images/feather.svg | 2 -
public/images/folder.svg | 2 -
public/images/gear.svg | 2 -
public/images/gears.svg | 2 -
public/images/import.svg | 2 -
public/images/italic.svg | 16 ---
public/images/link.svg | 5 -
public/images/location.svg | 22 ----
public/images/lock.svg | 2 -
public/images/logout.svg | 25 -----
public/images/movie.svg | 13 ---
public/images/offline.svg | 5 -
public/images/online.svg | 6 --
public/images/pen.svg | 2 -
public/images/plus.svg | 1 -
public/images/profile.svg | 20 ----
public/images/saving.svg | 6 --
public/images/selector.svg | 12 ---
public/images/social/discord.png | Bin 11924 -> 0 bytes
public/images/social/github.png | Bin 13104 -> 0 bytes
public/images/social/linkedin.png | Bin 8794 -> 0 bytes
public/images/social/twitter.png | Bin 10763 -> 0 bytes
public/images/team.svg | 2 -
public/images/trash.svg | 19 ----
public/images/underline.svg | 16 ---
public/images/world.svg | 2 -
scripts/generate-apple-jwt.ts | 4 +-
.../[projectId]/members/[userId]/route.ts | 1 -
.../[projectId]/saves/manual/route.ts | 2 +-
.../[projectId]/saves/restore/route.ts | 2 +-
.../api/projects/[projectId]/saves/route.ts | 2 +-
src/app/api/projects/route.ts | 2 +-
src/app/api/users/cookie/route.ts | 2 +-
src/auth.ts | 7 +-
src/context/ProjectContext.tsx | 34 +++----
src/lib/adapters/fountain/fountain_parser.ts | 4 +-
src/lib/adapters/pdf/pdf-adapter.ts | 1 +
src/lib/editor/use-document-editor.ts | 21 ++--
.../screenplay/extensions/contd-extension.ts | 13 ++-
.../extensions/pagination-extension.ts | 3 +-
src/lib/utils/hooks.ts | 18 ++--
src/tests/setup.ts | 2 +-
96 files changed, 400 insertions(+), 639 deletions(-)
delete mode 100644 public/images/arrow.svg
delete mode 100644 public/images/back.svg
delete mode 100644 public/images/back2.svg
delete mode 100644 public/images/bold.svg
delete mode 100644 public/images/calendar.svg
delete mode 100644 public/images/checkmark.svg
delete mode 100644 public/images/close.svg
delete mode 100644 public/images/dropdown-arrow.svg
delete mode 100644 public/images/export.svg
delete mode 100644 public/images/eye.svg
delete mode 100644 public/images/feather.svg
delete mode 100644 public/images/folder.svg
delete mode 100644 public/images/gear.svg
delete mode 100644 public/images/gears.svg
delete mode 100644 public/images/import.svg
delete mode 100644 public/images/italic.svg
delete mode 100644 public/images/link.svg
delete mode 100644 public/images/location.svg
delete mode 100644 public/images/lock.svg
delete mode 100644 public/images/logout.svg
delete mode 100644 public/images/movie.svg
delete mode 100644 public/images/offline.svg
delete mode 100644 public/images/online.svg
delete mode 100644 public/images/pen.svg
delete mode 100644 public/images/plus.svg
delete mode 100644 public/images/profile.svg
delete mode 100644 public/images/saving.svg
delete mode 100644 public/images/selector.svg
delete mode 100644 public/images/social/discord.png
delete mode 100644 public/images/social/github.png
delete mode 100644 public/images/social/linkedin.png
delete mode 100644 public/images/social/twitter.png
delete mode 100644 public/images/team.svg
delete mode 100644 public/images/trash.svg
delete mode 100644 public/images/underline.svg
delete mode 100644 public/images/world.svg
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 = () => {
)}
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 ? (
-
+
) : (
{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 (
setActiveElement(element)} className={tabStyle}>
- {isActive && }
+ {isActive && }
{content}
);
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) => (
))}
- {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) */}
-
{/* Layer 1.2: Branding (Logo) */}
-
+
{/* Layer 1.3: Platform CTAs */}
@@ -123,9 +126,11 @@ export default function HomePageContainer() {
-
@@ -145,9 +150,11 @@ export default function HomePageContainer() {
-
@@ -166,9 +173,11 @@ export default function HomePageContainer() {
locked into one ecosystem.
-
@@ -187,9 +196,11 @@ export default function HomePageContainer() {
-
@@ -209,9 +220,11 @@ export default function HomePageContainer() {
-
@@ -230,7 +243,7 @@ export default function HomePageContainer() {
sessions. Your environment should inspire, not distract.
-
+
{/* Scene Navigation */}
@@ -246,9 +259,11 @@ export default function HomePageContainer() {
scenes, add synopses, and color-code your story structure.
-
@@ -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.
-
@@ -286,9 +303,11 @@ export default function HomePageContainer() {
Scriptio works fully offline, no account required.
-
@@ -306,9 +325,11 @@ export default function HomePageContainer() {
glance. Data-driven insights to sharpen your story before it hits the screen.
-
@@ -328,9 +349,11 @@ export default function HomePageContainer() {
-
@@ -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.
-
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() {
>
) : (
-
+
)}
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 (
- {icon && }
+ {icon && }
{content}
);
});
+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)} />