JSFiddle - React, Tailwind, and code Playground

by Gwyn Milcote

HTML

<h2>CYOA Editor</h2>

<p>Although making the game <i>work</i> is pretty simple, I soon got tired of organising the different scenes and choices by ID numbers. Even the short demo was a chore to get right!</p>

<p>This editor lets you arrange a game more easily, and export the <b>db</b> object ready to use.</p>

<div id='editor_container'>

<button class='tab_button active_button' data-tab='scenes'>Scenes</button>
<button class='tab_button' data-tab='stats'>Stats</button>
<button class='tab_button' data-tab='load'>Import/Export</button>
<button class='tab_button' data-tab='test'>Test</button>

<div class='tab_content' id='tab_content_scenes'>
    <div id='scene_editor_side'>
        <h2>Your Scenes</h2>
        <div id='scene_list'>No scenes yet</div>
    </div>
    <div id='scene_editor_main'>
        <h2>Editing Scene #<span id='edit_scene_id'>0</span></h2>
        
        <div id='scene_options'>
            <button id='delete_scene'>Delete</button>
            <button id='save_scene'>Save</button>
            <button id='create_scene'>Save as new scene</button>
        </div>
        
        Text:<br>
        <textarea id='edit_scene_text'></textarea>
        
        <p>The properties below are optional. Summary will be seen only by you, not the player.</p>
        
        <div class='scene_input'>
            <span>Title:</span> 
            <input type='text' id='edit_scene_title'>
        </div>
        <div class='scene_input'>
            <span>Summary:</span> 
            <input type='text' id='edit_scene_summary'>
        </div>
        <div class='scene_input'>
            <span>Image URL:</span>
            <input type='text' id='edit_scene_image'>
        </div>
        
        <p>Choices that lead <b>to this scene</b> from others:</p>
        <div id='choicesTo_list'>No choices yet</div>
        
        <p>Choices that lead away <b>from this scene</b>:</p>
        <div id='choicesIn_list'>No choices yet</div>
        
    </div>
</div>

<div...

CSS

div, img, p, a, button, input, textarea {
    box-sizing: border-box;
}


#editor-container {
    width: 1000px;
}


.tab_button {
    position: relative;
    top: 1px;
    left: 20px;
    padding: 5px 10px;
    border: 1px solid grey;
    border-radius: 5px 5px 0px 0px;
    cursor: pointer;
    background-color: lightpink;
    transition: 0.3s;
}
.tab_button:hover {
    cursor: pointer;
    background-color: ivory;
    transition: 0.3s;
}
.active_button {
    background-color: ivory;
    border-bottom: 1px solid ivory;
}

.tab_content {
    border: 1px solid grey;
    padding: 10px;
    background-color: ivory;
    border-radius: 5px;
}

#editor_container {
    max-width: 860px;
    margin: 20px auto;
}

button {
    cursor: pointer;
}

#import_data, #export_data {
    width: 100%;
    min-height: 200px;
}

/* Middle of page */
#popup_holder {
    position: fixed;
    top: 50%;
    left: 50%;
    margin-top: -50px;
    margin-left: -100px;
    background-color: #fff;
    padding: 10px 20px;
    text-align: center;
    border-radius: 10px;
    box-shadow: 0 0 20px 10px rgba(0,0,0,0.4);
}

.stat_thumb,
.choice_thumb {
    border: 1px solid silver;
    background-color: #fff;
    padding: 5px;
    margin-bottom: 10px;
    border-radius: 5px;
    transition: 0.3s;
}

.scene_thumb:hover,
.stat_thumb:hover,
.choice_thumb:hover {
    cursor: pointer;
    border: 1px solid grey;
    background-color: #eee;
    transition: 0.3s;
}

/* For .scene_thumb, .stat_thumb etc */
.selected {
    border: 1px solid grey;
    background-color: silver;
}


#tab_content_scenes {
    display: flex;
    flex-flow: row wrap;
    justify-content: space-between;
}

.tab_content h2 {
    text-align: center;
    margin: 10px auto;
}

/* SCENES */

/* Panel that contains #scene_list */
#scene_editor_side {
    width: 250px;
    border: 1px dotted blue;
    text-align: center;
}

/* Scrollable. Contains many .scene_thumb */
#scene_list {
    max-height: 500px;
    overflow: auto;
    padding:...

JavaScript

// Default, demo.
let db = {
    scenes: [
        {
            id: 1, title: "The Beginning", summary: "Intro scene",
            text: "This is a demo. Here's the first scene. Choose a colour.",
        },{
            id: 2, text: "Second scene. No title or summary."
        },{
            id: 3, text: "Third scene. No title or summary."
        }
    ],
    
    choices: [
        {
            id: 1, sceneId: 1, toScene: 2,
            text: "This is a choice.",
        },{
            id: 2, sceneId: 1, toScene: 2,
            text: "This is another choice.",
        },{
            id: 3, sceneId: 2, toScene: 3,
            text: "Going from scene 2 to scene 3.",
        }
    ],
    
    stats: [
        {id: 1, name: "hp", displayName: "Health", type: "int", default: 0, description: "About this stat..."}
    ]
};
/*
// when loading/starting new game
this.stats = {};
for(let s of db.stats){
    this.stats[s.name] = s.default;
}
*/



class CyoaEditor {

    constructor(){
        $("#tab_content_stats, #tab_content_load").hide();
        this.listScenes();
        this.listStats();
    }
    
    popup(msg){
        $("#popup_message").html(msg);
        $("#popup_holder").fadeIn();
        console.log(msg);
    }
    
    export(){
        let html = JSON.stringify(db);
        $("#export_data").val(html);
    }
    
    import(){
        let val = $("#import_data").val();
        if(val == ""){
            this.popup("Can't import, the textarea is empty.");
        }
        console.log(val);
        // Hopefully it's the right kinda object structure.
        // Try to correct/build it, if not.
        try{
            val = JSON.parse(val);
        }catch(err){
            this.popup("Can't parse this into an object. Check your code structure.");
        }
        
        if(!val.scenes){
            val.scenes = [];
        }else if(!val.scenes.isArray){
            this.popup("Your <b>db.scenes</b> is not an array.");
        }
       ...