{
if (!isViewMode) {
- const newName = e.target.value;
- setStagePlot({ ...stagePlot, name: newName });
-
- // Broadcast name change to other collaborators
- if (collaborationEnabled && broadcast) {
- broadcast({
- type: "field_update",
- field: "name",
- value: newName,
- });
- }
+ setLocalName(e.target.value);
}
}}
className={`text-xl md:text-2xl font-bold text-white bg-transparent border-none focus:outline-none focus:ring-0 w-full max-w-[220px] sm:max-w-none ${isViewMode ? "cursor-default" : ""}`}
diff --git a/apps/web/src/pages/StandardPixelMapEditor.tsx b/apps/web/src/pages/StandardPixelMapEditor.tsx
index ec608f4..9f9f31c 100644
--- a/apps/web/src/pages/StandardPixelMapEditor.tsx
+++ b/apps/web/src/pages/StandardPixelMapEditor.tsx
@@ -79,6 +79,10 @@ const StandardPixelMapEditor = () => {
const effectiveUserEmail = user?.email || (isSharedEdit ? `${anonymousUserId}@shared` : "");
const effectiveUserName = user?.user_metadata?.name || (isSharedEdit ? "Anonymous User" : "");
+ // Local state for project name input to prevent reversion during typing
+ const [localProjectName, setLocalProjectName] = useState("");
+ const [localProjectNameInitialized, setLocalProjectNameInitialized] = useState(false);
+
// Enable collaboration for existing documents (including edit-mode shared links)
// For shared links, id will be undefined, so we check document?.id instead
// For edit-mode shared links, allow collaboration even without authentication
@@ -137,8 +141,21 @@ const StandardPixelMapEditor = () => {
userName: effectiveUserName,
enabled: collaborationEnabled,
onRemoteUpdate: (payload) => {
+ // GUARD: Don't process remote updates if collaboration is disabled
+ // This prevents input reversion on NEW documents where collaboration is off
+ if (!collaborationEnabled) {
+ console.log("[StandardPixelMapEditor] Ignoring remote update - collaboration disabled");
+ return;
+ }
+
if (payload.type === "field_update" && payload.field) {
- setMapData((prev: any) => (prev ? { ...prev, [payload.field!]: payload.value } : prev));
+ if (payload.field === "project_name") {
+ setMapData((prev: any) => (prev ? { ...prev, project_name: payload.value } : prev));
+ // Update local project name state when remote changes arrive
+ setLocalProjectName(payload.value);
+ } else {
+ setMapData((prev: any) => (prev ? { ...prev, [payload.field!]: payload.value } : prev));
+ }
}
},
});
@@ -148,6 +165,43 @@ const StandardPixelMapEditor = () => {
userId: effectiveUserId,
});
+ // Initialize local project name from mapData.project_name when document loads
+ useEffect(() => {
+ if (mapData.project_name && !localProjectNameInitialized) {
+ setLocalProjectName(mapData.project_name);
+ setLocalProjectNameInitialized(true);
+ }
+ }, [mapData.project_name, localProjectNameInitialized]);
+
+ // Debounced sync: Update mapData.project_name after user stops typing (500ms delay)
+ useEffect(() => {
+ if (!localProjectNameInitialized) return;
+
+ const handler = setTimeout(() => {
+ if (localProjectName !== mapData.project_name) {
+ setMapData((prev: any) => ({ ...prev, project_name: localProjectName }));
+
+ // Broadcast project name change to other collaborators
+ if (collaborationEnabled && broadcast) {
+ broadcast({
+ type: "field_update",
+ field: "project_name",
+ value: localProjectName,
+ });
+ }
+ }
+ }, 500);
+
+ return () => clearTimeout(handler);
+ }, [
+ localProjectName,
+ localProjectNameInitialized,
+ mapData.project_name,
+ setMapData,
+ collaborationEnabled,
+ broadcast,
+ ]);
+
// Real-time database subscription for syncing changes across users
useEffect(() => {
if (!collaborationEnabled || !document?.id) {
@@ -171,32 +225,12 @@ const StandardPixelMapEditor = () => {
},
(payload) => {
console.log("[StandardPixelMapEditor] Received database UPDATE event:", payload);
- // Update local state with the new data
- // IMPORTANT: Exclude metadata fields (version, last_edited, metadata) to prevent
- // triggering auto-save, which would create an infinite loop
- if (payload.new) {
- const { version, last_edited, metadata, ...userEditableFields } = payload.new as any;
- setMapData((prev: any) => ({
- ...prev,
- project_name: userEditableFields.project_name,
- screen_name: userEditableFields.screen_name,
- aspect_ratio_w: userEditableFields.aspect_ratio_w,
- aspect_ratio_h: userEditableFields.aspect_ratio_h,
- aspect_ratio_preset: `${userEditableFields.aspect_ratio_w}:${userEditableFields.aspect_ratio_h}`,
- resolution_w: userEditableFields.resolution_w,
- resolution_h: userEditableFields.resolution_h,
- resolution_preset: `${userEditableFields.resolution_w}x${userEditableFields.resolution_h}`,
- }));
- // Update document state with metadata preserved
- setDocument((prev: any) => ({
- ...prev,
- ...userEditableFields,
- // Keep existing metadata to avoid triggering auto-save
- version: prev?.version,
- last_edited: prev?.last_edited,
- metadata: prev?.metadata,
- }));
- }
+ // NOTE: We intentionally DO NOT update local state from database UPDATE events
+ // because they include our own saves echoing back, which would overwrite user typing
+ // Real-time collaboration updates happen via the broadcast channel in useCollaboration
+ console.log(
+ "[StandardPixelMapEditor] Ignoring database UPDATE to prevent input reversion",
+ );
},
)
.subscribe();
@@ -634,8 +668,26 @@ const StandardPixelMapEditor = () => {
{
+ if (typeof update === "function") {
+ const newData = update(mapData);
+ if (newData.project_name !== mapData.project_name) {
+ setLocalProjectName(newData.project_name);
+ } else {
+ setMapData(newData);
+ }
+ } else {
+ if (update.project_name !== mapData.project_name) {
+ setLocalProjectName(update.project_name);
+ } else {
+ setMapData(update);
+ }
+ }
+ }}
showColorSwatches={showColorSwatches}
setShowColorSwatches={setShowColorSwatches}
showGrid={showGrid}
diff --git a/apps/web/src/pages/TheaterMicPlotEditor.tsx b/apps/web/src/pages/TheaterMicPlotEditor.tsx
index b18644f..504e451 100644
--- a/apps/web/src/pages/TheaterMicPlotEditor.tsx
+++ b/apps/web/src/pages/TheaterMicPlotEditor.tsx
@@ -61,6 +61,10 @@ const TheaterMicPlotEditor: React.FC = () => {
const effectiveUserEmail = user?.email || (isSharedEdit ? `${anonymousUserId}@shared` : "");
const effectiveUserName = user?.user_metadata?.name || (isSharedEdit ? "Anonymous User" : "");
+ // Local state for name input to prevent reversion during typing
+ const [localName, setLocalName] = useState("");
+ const [localNameInitialized, setLocalNameInitialized] = useState(false);
+
// Enable collaboration for existing documents (including edit-mode shared links)
// For shared links, routeId will be undefined, so we check micPlot?.id instead
// For edit-mode shared links, allow collaboration even without authentication
@@ -120,14 +124,68 @@ const TheaterMicPlotEditor: React.FC = () => {
userName: effectiveUserName,
enabled: collaborationEnabled,
onRemoteUpdate: (payload) => {
+ // GUARD: Don't process remote updates if collaboration is disabled
+ // This prevents input reversion on NEW documents where collaboration is off
+ if (!collaborationEnabled) {
+ console.log("[TheaterMicPlotEditor] Ignoring remote update - collaboration disabled");
+ return;
+ }
+
if (payload.type === "field_update" && payload.field) {
- setMicPlot((prev: any) => (prev ? { ...prev, [payload.field!]: payload.value } : prev));
+ if (payload.field === "name") {
+ setMicPlot((prev: any) => (prev ? { ...prev, name: payload.value } : prev));
+ // Update local name state when remote changes arrive
+ setLocalName(payload.value);
+ } else {
+ setMicPlot((prev: any) => (prev ? { ...prev, [payload.field!]: payload.value } : prev));
+ }
}
},
});
const { setEditingField } = usePresence({ channel: null, userId: effectiveUserId });
+ // Initialize local name from micPlot.name when document loads
+ useEffect(() => {
+ if (micPlot?.name && !localNameInitialized) {
+ setLocalName(micPlot.name);
+ setLocalNameInitialized(true);
+ }
+ }, [micPlot?.name, localNameInitialized]);
+
+ // Debounced sync: Update micPlot.name after user stops typing (500ms delay)
+ useEffect(() => {
+ if (!localNameInitialized) return;
+
+ const handler = setTimeout(() => {
+ if (localName !== micPlot?.name) {
+ setMicPlot((prev: any) => (prev ? { ...prev, name: localName } : prev));
+
+ // Broadcast name change to other collaborators
+ if (collaborationEnabled && broadcast) {
+ broadcast({
+ type: "field_update",
+ field: "name",
+ value: localName,
+ userId: effectiveUserId,
+ }).catch((err) =>
+ console.error("[TheaterMicPlotEditor] Failed to broadcast name change:", err),
+ );
+ }
+ }
+ }, 500);
+
+ return () => clearTimeout(handler);
+ }, [
+ localName,
+ localNameInitialized,
+ micPlot?.name,
+ setMicPlot,
+ collaborationEnabled,
+ broadcast,
+ effectiveUserId,
+ ]);
+
// Real-time database subscription for syncing changes across users
useEffect(() => {
if (!collaborationEnabled || !micPlot?.id) {
@@ -151,20 +209,10 @@ const TheaterMicPlotEditor: React.FC = () => {
},
(payload) => {
console.log("[TheaterMicPlotEditor] Received database UPDATE event:", payload);
- // Update local state with the new data
- // IMPORTANT: Exclude metadata fields (version, last_edited, metadata) to prevent
- // triggering auto-save, which would create an infinite loop
- if (payload.new) {
- const { version, last_edited, metadata, ...userEditableFields } = payload.new as any;
- setMicPlot((prev: any) => ({
- ...prev,
- ...userEditableFields,
- // Keep existing metadata to avoid triggering auto-save
- version: prev?.version,
- last_edited: prev?.last_edited,
- metadata: prev?.metadata,
- }));
- }
+ // NOTE: We intentionally DO NOT update local state from database UPDATE events
+ // because they include our own saves echoing back, which would overwrite user typing
+ // Real-time collaboration updates happen via the broadcast channel in useCollaboration
+ console.log("[TheaterMicPlotEditor] Ignoring database UPDATE to prevent input reversion");
},
)
.subscribe();
@@ -262,22 +310,7 @@ const TheaterMicPlotEditor: React.FC = () => {
}, [routeId, shareCode, navigate, location.pathname, user]);
const handleNameChange = (e: React.ChangeEvent) => {
- if (micPlot) {
- const newName = e.target.value;
- setMicPlot({ ...micPlot, name: newName });
-
- // Broadcast name change to other collaborators
- if (collaborationEnabled && broadcast) {
- broadcast({
- type: "field_update",
- field: "name",
- value: newName,
- userId: effectiveUserId,
- }).catch((err) =>
- console.error("[TheaterMicPlotEditor] Failed to broadcast name change:", err),
- );
- }
- }
+ setLocalName(e.target.value);
};
const handleAddActor = () => {
@@ -505,7 +538,7 @@ const TheaterMicPlotEditor: React.FC = () => {