From 882ac72c3619c5a8a5632f658eff445069788934 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:50:21 +0000 Subject: [PATCH 1/3] Initial plan From 6f14f5aaf74b88808464d75c4f77e6b1410766b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 02:10:48 +0000 Subject: [PATCH 2/3] Fix blank website: modernize to React 19 + Firebase 12 + react-router 7 with esbuild bundler Co-authored-by: ProLoser <67395+ProLoser@users.noreply.github.com> --- index.html | 7 +- package.json | 4 +- script.jsx | 1176 +++++++++++++++++++++++++++----------------------- yarn.lock | 105 +++-- 4 files changed, 677 insertions(+), 615 deletions(-) diff --git a/index.html b/index.html index f5e8cd9..eed812a 100644 --- a/index.html +++ b/index.html @@ -4,12 +4,7 @@ Screenwriter - - - - - - + diff --git a/package.json b/package.json index a3b8b06..681a180 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "underscore": "^1.13.7" }, "devDependencies": { - "@babel/cli": "^7.28.6", + "esbuild": "^0.27.4", "@babel/core": "^7.29.0", "@babel/preset-env": "^7.29.0", "@babel/preset-react": "^7.28.5", @@ -31,7 +31,7 @@ }, "scripts": { "build": "npm run build:jsx && npm run build:css", - "build:jsx": "babel script.jsx --out-file script.js --presets=@babel/preset-react --no-babelrc", + "build:jsx": "esbuild script.jsx --bundle --outfile=script.js --platform=browser --jsx=automatic", "build:css": "gulp sass", "test": "jest", "test:watch": "jest --watch", diff --git a/script.jsx b/script.jsx index 556558d..2774475 100644 --- a/script.jsx +++ b/script.jsx @@ -1,3 +1,9 @@ +import { createRoot } from 'react-dom/client'; +import { HashRouter, Routes, Route, Link, useParams } from 'react-router'; +import { useState, useEffect, useRef, forwardRef, useImperativeHandle, useLayoutEffect, useCallback } from 'react'; +import { initializeApp } from 'firebase/app'; +import { getDatabase, ref as dbRef, onValue, set, push, update, remove } from 'firebase/database'; + var types = ['scene', 'action', 'character', 'dialogue', 'parenthetical', 'transition', 'shot', 'text']; var nextTypes = { scene: 'action', @@ -10,11 +16,10 @@ var nextTypes = { text: 'text' }; -var StopPropagationMixin = { - stopProp: function(event) { - event.nativeEvent.stopImmediatePropagation(); - }, -}; +// Firebase setup using the modern modular SDK +var firebaseApp = initializeApp({ databaseURL: 'https://screenwrite.firebaseio.com' }); +var db = getDatabase(firebaseApp); + function cursorPos(element) { var caretOffset = 0; var doc = element.ownerDocument || element.document; @@ -37,7 +42,7 @@ function cursorPos(element) { caretOffset = preCaretTextRange.text.length; } return caretOffset; -}; +} function placeCaretAtEnd(el) { el.focus(); @@ -57,601 +62,670 @@ function placeCaretAtEnd(el) { } } -function S4() { - return (((1+Math.random())*0x10000)|0).toString(16).substring(1); -} -function guid() { - return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); -} - +// ContentEditable component – keeps cursor stable while user types +var ContentEditable = forwardRef(function ContentEditable(props, ref) { + var html = props.html; + var onChange = props.onChange; + var onKeyDown = props.onKeyDown; + var onClick = props.onClick; + var className = props.className; + var onFocus = props.onFocus; + var onBlur = props.onBlur; + var suggest = props.suggest; + + var domRef = useRef(null); + var lastHtml = useRef(html || ''); + + var setRef = function(el) { + domRef.current = el; + if (typeof ref === 'function') ref(el); + else if (ref) ref.current = el; + }; + + // Sync the html prop to the DOM only when it differs from what is already there. + // Using useLayoutEffect ensures the content is set before the browser paints + // (avoiding a flash of empty content on first mount), while the comparison + // prevents resetting innerHTML – and thus the cursor – when the user is typing. + useLayoutEffect(function() { + if (!domRef.current) return; + if ((html || '') !== domRef.current.innerHTML) { + domRef.current.innerHTML = html || ''; + lastHtml.current = html || ''; + } + }); -var Script = React.createClass({ - mixins: [ReactFireMixin, ReactRouter.State], - getInitialState: function() { - highlight = ''; + var emitChange = function() { + if (!domRef.current) return; + var currentHtml = domRef.current.innerHTML; + if (onChange && currentHtml !== lastHtml.current) { + onChange({ target: { value: currentHtml } }); + } + lastHtml.current = currentHtml; + }; + + var stripPaste = function(e) { + var items = Array.from(e.clipboardData.items); + var textItem = items.find(function(i) { return i.type === 'text/plain'; }); + if (textItem) { + var tempDiv = document.createElement('div'); + textItem.getAsString(function(value) { + tempDiv.innerHTML = value; + document.execCommand('inserttext', false, tempDiv.innerText); + }); + } + e.preventDefault(); + }; + + return ( +
+ ); +}); +// Line component – renders a single script line and exposes a focus() method +var Line = forwardRef(function Line(props, ref) { + var line = props.line; + var index = props.index; + var previous = props.previous; + var prevPrevious = props.prevPrevious; + var onFocusProp = props.onFocus; + var getSuggestion = props.getSuggestion; + var readonly = props.readonly; + var onKeyDown = props.onKeyDown; + var scriptId = props.scriptId; + var highlight = props.highlight; + + var focused = useState(false); + var setFocused = focused[1]; + focused = focused[0]; + + var commentingState = useState(false); + var setCommenting = commentingState[1]; + var commenting = commentingState[0]; + + var textRef = useRef(null); + var commentBoxRef = useRef(null); + var firebaseLineRef = useRef(dbRef(db, scriptId + '/lines/' + index)); + + useImperativeHandle(ref, function() { return { - scriptId: this.getParams().scriptId, - action: this.getParams().action, - script: {}, - editing: {} - }; - }, - componentWillMount: function() { - this.loadScript(); - }, - componentWillReceiveProps: function() { - this.loadScript(); - }, - loadScript: function() { - if (this.firebaseRefs.script) this.unbind('script'); - this.bindAsObject(new Firebase("https://screenwrite.firebaseio.com/"+this.getParams().scriptId), "script"); - // CLEANUP OLD DATA - var fb = new Firebase("https://screenwrite.firebaseio.com/"+this.state.scriptId); - fb.once('value', (function(snapshot){ - if (!snapshot.val()) { - fb.set({}); - var newLine = fb.child('lines').push({ type: 'scene' }); - fb.update({ firstLine: newLine.key }); - return; - } - if (snapshot.val().firstLine) return; - var previous, previousIndex; - fb.update({firstLine: '0'}); - _.each(snapshot.val().lines, function(line, index) { - if (previous) { - fb.child('lines/'+previousIndex+'/next').set(index); + focus: function(atEnd) { + if (textRef.current) { + if (atEnd) placeCaretAtEnd(textRef.current); + else textRef.current.focus(); } - previous = line; - previousIndex = index; - }); - }).bind(this)); - - window.onunload = (function(){ - if (_.keys(this.state.script.lines).length <= 2) - fb.remove(); - }).bind(this); - }, - editing: function(line) { - this.setState({editing:line}); - }, - getSuggestion: function(lineIndex, fromValue) { - if (!this.state.script.lines[lineIndex].text) return ''; - var type = this.state.script.lines[lineIndex].type; - var text = fromValue && fromValue.toUpperCase() || this.state.script.lines[lineIndex].text.toUpperCase(); - - var suggestions = []; - var passed = false; - var iterate = (function(index){ - var line = this.state.script.lines[index]; - if (!line) { - console.warn('Line not found at index:', index); - return; } - if (line.type == type - && line.text - && line.text.length > text.length - && line.text.toUpperCase().indexOf(text) === 0) - suggestions.push(line.text.toUpperCase()); - if (index == lineIndex) - passed = true; - if (passed && suggestions.length) return; - if (line.next && this.state.script.lines[line.next]) - iterate(line.next); - }).bind(this); - iterate(this.state.script.firstLine); - return (suggestions.pop() || '').substr(text.length); - }, - handleKey: function(event, line, index, prevIndex, prevPrevIndex) { - switch (event.keyCode) { - case 38: // up - if (prevIndex) { - if (event.metaKey || event.ctrlKey) { - // [a, b, C, d] => [a, C, b, d] - // A points to C - if (prevPrevIndex) - this.firebaseRefs.script.child('lines/'+prevPrevIndex).update({next: index}); - else - this.firebaseRefs.script.update({firstLine:index}); - // C points to B - var newNext = line.next; - this.firebaseRefs.script.child('lines/'+index).update({next: prevIndex }); - // B points to D - if (line.next) - this.firebaseRefs.script.child('lines/'+prevIndex).update({next: newNext }); - else - this.firebaseRefs.script.child('lines/'+prevIndex+'/next').remove(); - this.refs['line'+index].focus(true); - event.preventDefault(); - } else if (!cursorPos(event.target)) { - this.refs['line'+prevIndex].focus(true); - event.preventDefault(); - } - } - break; - case 40: // down - if (line.next) { - if (event.metaKey || event.ctrlKey) { - // [a, b, c, d] => [a, c, b, d] + }; + }); - // A points to C - if (prevIndex) - this.firebaseRefs.script.child('lines/'+prevIndex).update({next: line.next}); - else - this.firebaseRefs.script.update({firstLine:line.next}); - var newNext = this.state.script.lines[line.next].next; - // C points to B - this.firebaseRefs.script.child('lines/'+line.next).update({next: index}); - // B points to D - if (newNext) - this.firebaseRefs.script.child('lines/'+index).update({ next: newNext }); - else - this.firebaseRefs.script.child('lines/'+index+'/next').remove(); - this.refs['line'+index].focus(); - event.preventDefault(); - } else if (cursorPos(event.target) >= event.target.textContent.length ) { - this.refs['line'+line.next].focus(); - event.preventDefault(); - } - } - break; - case 8: // backspace - if (!line.text && prevIndex) { - // update previous line - if (line.next) - this.firebaseRefs.script.child('lines/'+prevIndex).update({next:line.next}); - else - this.firebaseRefs.script.child('lines/'+prevIndex+'/next').remove(); - - // remove line - this.firebaseRefs.script.child('lines/'+index).remove(); - this.refs['line'+prevIndex].focus(true); - event.preventDefault(); - } - break; - case 13: // enter - if (line.text) { - // create new line pointing to current line's `next` - var newItem = { type: nextTypes[line.type] }; - if (line.next) newItem.next = line.next; - var newRef = this.firebaseRefs.script.child('lines').push(newItem); - // point current line to the new line - this.firebaseRefs.script.child('lines/'+index+'/next').set(newRef.key); - setTimeout((function(){ - this.refs['line'+newRef.key].focus(); - }).bind(this)); - } - } - }, - render: function() { - var indexes = {}; - var lines = []; - var previous = null, prevPrevious = null; - var next = (function(line, index){ - if (!line) { - console.warn('Line not found at index:', index); - return; - } - lines.push( - - ); - prevPrevious = previous; - previous = index; - if (line.next && this.state.script.lines[line.next]) next(this.state.script.lines[line.next], line.next); - }).bind(this); - - if (this.state.script && this.state.script.lines && this.state.script.firstLine) { - next(this.state.script.lines[this.state.script.firstLine], this.state.script.firstLine); - } else { - lines =

Loading Script...

- } - return ( -
-
- ); - } -}); + var handleChange = function(event) { + update(firebaseLineRef.current, { text: event.target.value }); + }; -var highlight = ''; + var handleComment = function(event) { + update(firebaseLineRef.current, { comment: event.target.value }); + }; -var Line = React.createClass({ - mixins: [ReactFireMixin, StopPropagationMixin, ReactRouter.State], - getInitialState: function() { - return { - comments: this.props.line.comments, - commenting: false, - scriptId: this.getParams().scriptId, - focused: false, - }; - }, - componentWillMount: function() { - this.bindAsObject(new Firebase("https://screenwrite.firebaseio.com/"+this.state.scriptId+"/lines/" + this.props.index), "line"); - }, - handleChange: function(event) { - this.firebaseRefs.line.update({'text':event.target.value}); - }, - handleComment: function(event) { - this.firebaseRefs.line.update({'comment':event.target.value}); - }, - nextType: function(){ - var index = types.indexOf(this.props.line.type) + 1; - index = (index < types.length) ? index : 0; - this.setType(types[index]); - }, - prevType: function() { - var index = types.indexOf(this.props.line.type) - 1; - index = (index >= 0) ? index : types.length - 1; - this.setType(types[index]); - }, - setType: function(type) { - this.firebaseRefs.line.update({type:type}); - }, - handleKey: function(event) { + var nextTypeAction = function() { + var idx = types.indexOf(line.type) + 1; + update(firebaseLineRef.current, { type: idx < types.length ? types[idx] : types[0] }); + }; + + var prevTypeAction = function() { + var idx = types.indexOf(line.type) - 1; + update(firebaseLineRef.current, { type: idx >= 0 ? types[idx] : types[types.length - 1] }); + }; + + var handleKey = function(event) { switch (event.keyCode) { - case 39: // right - if (~['character', 'scene'].indexOf(this.props.line.type) && cursorPos(event.target) >= event.target.textContent.length) { - var suggestion; - if (suggestion = this.props.getSuggestion(this.props.index)) { - this.firebaseRefs.line.update({ text: this.props.line.text + suggestion }, (function(){ - placeCaretAtEnd(this.refs.text.getDOMNode()); - }).bind(this)); + case 39: { // right – autocomplete + if (~['character', 'scene'].indexOf(line.type) && cursorPos(event.target) >= event.target.textContent.length) { + var suggestion = getSuggestion(index); + if (suggestion) { + update(firebaseLineRef.current, { text: line.text + suggestion }).then(function() { + if (textRef.current) placeCaretAtEnd(textRef.current); + }); } } break; + } case 13: // enter event.preventDefault(); - if (this.props.line.text) { - break; - } + if (line.text) break; + // fall through to tab + /* falls through */ case 9: // tab event.preventDefault(); - if (event.shiftKey) { - this.prevType(); - } else { - this.nextType(); - } + if (event.shiftKey) prevTypeAction(); + else nextTypeAction(); } + onKeyDown(event, line, index, previous, prevPrevious); + }; - this.props.onKeyDown(event, this.props.line, this.props.index, this.props.previous, this.props.prevPrevious); - }, - comment: function(event) { + var handleCommentToggle = function(event) { event.stopPropagation(); - this.setState({ commenting: !this.state.commenting }, function(){ - if (this.state.commenting) { - var that = this; - document.addEventListener('click', function listener(){ - that.setState({ commenting: false }); - document.removeEventListener('click', listener); - }); - this.refs.commentBox.getDOMNode().focus(); + setCommenting(function(c) { + var next = !c; + if (next) { + setTimeout(function() { + var listener = function() { + setCommenting(false); + document.removeEventListener('click', listener); + }; + document.addEventListener('click', listener); + if (commentBoxRef.current) commentBoxRef.current.focus(); + }, 0); } + return next; }); - }, - focus: function(atEnd) { - if (atEnd) - placeCaretAtEnd(this.refs.text.getDOMNode()); - else - this.refs.text.getDOMNode().focus(); - }, - onFocus: function(event) { - this.setState({focused:true}); - this.props.onFocus(event); - }, - onBlur: function(event) { - this.setState({focused:false}); - }, - render: function() { - var classes = { - line: true, - commented: this.props.line.comment, - highlight: highlight && this.props.line.text && highlight.toUpperCase()==this.props.line.text.toUpperCase() - }; - classes[this.props.line.type] = true; - classes = React.addons.classSet(classes); - - var line, suggest; - if (this.props.readonly) { - line =
; - } else { - if (this.state.focused) { - suggest = this.props.getSuggestion(this.props.index); - } - - line = - } - - return ( -
  • - {line} - - - - - {this.state.commenting && } -
  • + }; + + var classes = [ + 'line', + line.type, + line.comment ? 'commented' : null, + highlight && line.text && highlight.toUpperCase() === line.text.toUpperCase() ? 'highlight' : null + ].filter(Boolean).join(' '); + + var lineElement; + var suggest; + if (readonly) { + lineElement =
    ; + } else { + if (focused) suggest = getSuggestion(index); + lineElement = ( + ); } -}); -var ContentEditable = React.createClass({ - shouldComponentUpdate: function(nextProps) { - // Only update if the html prop has changed AND it's different from what we have in the DOM - // This prevents cursor jumping when the user is typing (internal changes) - // but allows updates from external sources (autocomplete, Firebase updates, etc.) - var currentHTML = this.getDOMNode().innerHTML; - return nextProps.html !== currentHTML; - }, - stripPaste: function(e){ - // Strip formatting on paste - var tempDiv = document.createElement("DIV"); - var item = _.findWhere(e.clipboardData.items, { type: 'text/plain' }); - item.getAsString(function (value) { - tempDiv.innerHTML = value; - document.execCommand('inserttext', false, tempDiv.innerText); - }); - e.preventDefault(); - }, - emitChange: function(){ - var html = this.getDOMNode().innerHTML; - if (this.props.onChange && html !== this.lastHtml) { - - this.props.onChange({ - target: { - value: html - } - }); - } - this.lastHtml = html; - }, - render: function(){ - return
    ; - } + return ( +
  • + {lineElement} + + + + {commenting && ( + + )} +
  • + ); }); -var Nav = React.createClass({ - mixins: [ReactFireMixin, StopPropagationMixin, ReactRouter.State], - getInitialState: function() { - return { - open: null, - script: {}, - scriptId: this.getParams().scriptId, - highlight: '' - }; - }, - componentWillMount: function() { - this.bindAsObject(new Firebase("https://screenwrite.firebaseio.com/"+this.state.scriptId), "script"); - }, - toggle: function(dropdown, event) { - var that = this; - if (this.state.open != dropdown) { - setTimeout((function(){ - document.addEventListener('click', function listener(){ - that.setState({ open: false }); +// Nav component – top navigation bar +function Nav(props) { + var script = props.script; + var editingIndex = props.editingIndex; + var readonly = props.readonly; + var scriptId = props.scriptId; + var highlight = props.highlight; + var onHighlightChange = props.onHighlightChange; + + var openState = useState(null); + var setOpen = openState[1]; + var open = openState[0]; + + var toggle = function(dropdown) { + if (open !== dropdown) { + setTimeout(function() { + var listener = function() { + setOpen(null); document.removeEventListener('click', listener); - }); - this.setState({ open: dropdown }); - }).bind(this)); + }; + document.addEventListener('click', listener); + }, 0); + setOpen(dropdown); } - }, - setType: function(type) { - if (!this.props.editingIndex) return; - this.firebaseRefs.script.child('lines/'+this.props.editingIndex+'/type').set(type); - }, - print: function() { - window.print(); - }, - highlight: function(event) { - highlight = event.target.value; - this.setState({highlight: event.target.value}); - }, - handleChange: function(input, event) { - this.firebaseRefs.script.child(input).set(event.target.value); - }, - newScript: function(){ - var fb = new Firebase("https://screenwrite.firebaseio.com/"); - var newRef = fb.push(); + }; + + var setType = function(type) { + if (!editingIndex) return; + set(dbRef(db, scriptId + '/lines/' + editingIndex + '/type'), type); + }; + + var handleFieldChange = function(field, event) { + set(dbRef(db, scriptId + '/' + field), event.target.value); + }; + + var newScript = function() { + var newRef = push(dbRef(db)); window.location.hash = '#/' + newRef.key; - window.location.reload(); // force firebase to reload - }, - render: function() { - if (!this.state.script) return
    ; - - if (this.state.script.title) - document.title = 'Screenwriter: ' + this.state.script.title; - - var editing = this.state.script.lines && this.state.script.lines[this.props.editingIndex] || {}; - if (this.state.open=='print') { - var characters = []; - _.each(_.uniq(_.map(_.pluck(_.where(this.state.script.lines, {type:'character'}), 'text'), function(character){ - return character && character.toUpperCase(); - })), function(character){ - if (character) - characters.push() - }); - } - return ( -
    -
    -
    -
      -
    • -
      - - - - - - - - - - Report Issues on GitHub - - -
      - {this.state.open == 'print' &&
      -
      -

      Print Script

      + window.location.reload(); + }; + + if (!script) return
      ; + + if (script.title) document.title = 'Screenwriter: ' + script.title; + + var editing = (script.lines && script.lines[editingIndex]) || {}; + + var characters = []; + if (open === 'print' && script.lines) { + var seen = {}; + Object.values(script.lines).forEach(function(l) { + if (l && l.type === 'character' && l.text) { + var upper = l.text.toUpperCase(); + if (!seen[upper]) { + seen[upper] = true; + characters.push(); + } + } + }); + } + + return ( +
      +
      +
      +
        +
      • +
        + + + + + + + + + + Report Issues on GitHub + + +
        + {open === 'print' && ( +
        +
        +

        + Print Script +

        - + + +