🎮ArcadeLab

HTML2 Forever

by BoldPenguin14
617 lines9.5 KB
▶ Play

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML2 Forever</title>

<style>
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: Arial, sans-serif;
  background: #0e0e11;
  color: #eee;
  min-height: 100vh;
  overflow-x: hidden;
  overflow-y: auto;
}

header {
  min-height: 58px;
  background: #18181d;
  border-bottom: 1px solid #303036;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 18px;
  position: sticky;
  top: 0;
  z-index: 10;
}

.logo {
  font-size: 19px;
  font-weight: bold;
}

.logo span {
  color: #9275ff;
}

button {
  background: #292930;
  border: 1px solid #38383f;
  color: white;
  padding: 9px 14px;
  border-radius: 6px;
  cursor: pointer;
}

button:hover {
  background: #393942;
}

.primary {
  background: #7959ed;
  border-color: #7959ed;
}

.actions {
  display: flex;
  gap: 8px;
}

.layout {
  display: flex;
  min-height: calc(100vh - 58px);
  height: auto;
}

.sidebar {
  width: 54px;
  flex-shrink: 0;
  background: #18181d;
  border-right: 1px solid #303036;
  display: flex;
  align-items: center;
  flex-direction: column;
  padding: 15px 0;
  gap: 22px;
}

.sideicon {
  font-size: 20px;
  color: #92929c;
  cursor: pointer;
}

.sideicon:hover {
  color: white;
}

.workspace {
  flex: 1;
  display: flex;
  min-width: 0;
  min-height: 0;
}

.left,
.right {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 500px;
}

.left {
  border-right: 1px solid #303036;
}

.panelhead {
  height: 49px;
  min-height: 49px;
  background: #18181d;
  border-bottom: 1px solid #303036;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 14px;
  font-size: 13px;
}

.tabs {
  display: flex;
  gap: 5px;
  height: 100%;
  align-items: center;
}

.tab {
  padding: 9px 14px;
  color: #999;
  cursor: pointer;
  border-radius: 5px;
  font-size: 13px;
}

.tab.active {
  background: #303039;
  color: white;
}

.editor {
  height: 600px;
  min-height: 300px;
  position: relative;
}

textarea {
  width: 100%;
  height: 100%;
  background: #111115;
  color: #e3e3ec;
  border: 0;
  outline: none;
  resize: vertical;
  overflow: auto;
  padding: 20px;
  font: 14px/1.7 Consolas, monospace;
  tab-size: 2;
}

textarea::placeholder {
  color: #555560;
}

iframe {
  display: block;
  width: 100%;
  height: 700px;
  min-height: 400px;
  flex: none;
  border: 0;
  background: white;
}

.bottom {
  height: 28px;
  min-height: 28px;
  background: #18181d;
  border-top: 1px solid #303036;
  padding: 6px 12px;
  font-size: 11px;
  color: #85858e;
  display: flex;
  justify-content: space-between;
}

.empty {
  position: absolute;
  pointer-events: none;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
  color: #484850;
  font-size: 13px;
  display: none;
}

/* Desktop */
@media(min-width: 651px) {
  .left,
  .right {
    width: 50%;
  }
}

/* Mobile */
@media(max-width:650px) {
  header {
    padding: 0 9px;
    flex-wrap: wrap;
    gap: 8px;
    min-height: 58px;
  }

  .logo {
    font-size: 15px;
  }

  .actions {
    gap: 5px;
  }

  .actions button {
    padding: 8px;
    font-size: 12px;
  }

  .sidebar {
    width: 40px;
  }

  .workspace {
    flex-direction: column;
  }

  .left,
  .right {
    width: 100%;
    flex: none;
    min-height: 0;
  }

  .left {
    border-right: 0;
    border-bottom: 1px solid #303036;
  }

  .editor {
    height: 450px;
    min-height: 300px;
  }

  iframe {
    height: 650px;
    min-height: 400px;
  }
}
</style>
</head>

<body>

<header>
  <div class="logo">HTML2<span>FOREVER</span></div>

  <div class="actions">
    <button onclick="newProject()">+ New</button>
    <button onclick="saveProject()">Save</button>
    <button class="primary" onclick="run()">▶ Run</button>
  </div>
</header>

<div class="layout">

  <aside class="sidebar">
    <div class="sideicon" title="Files" onclick="showFiles()">▤</div>
    <div class="sideicon" title="Editor" onclick="focusEditor()">⌘</div>
    <div class="sideicon" title="Preview" onclick="scrollToPreview()">◫</div>
    <div class="sideicon" title="Download" onclick="downloadProject()">⇩</div>
  </aside>

  <main class="workspace">

    <section class="left">

      <div class="panelhead">
        <div class="tabs">
          <div class="tab active" onclick="changeTab('html',this)">HTML</div>
          <div class="tab" onclick="changeTab('css',this)">CSS</div>
          <div class="tab" onclick="changeTab('js',this)">JS</div>
        </div>

        <span>● Editor</span>
      </div>

      <div class="editor">
        <textarea
          id="editor"
          placeholder="Start coding here..."
          spellcheck="false"
        ></textarea>

        <div class="empty" id="empty">
          Your workspace is empty
        </div>
      </div>

      <div class="bottom">
        <span id="fileName">index.html</span>
        <span id="status">Ready</span>
      </div>

    </section>

    <section class="right" id="previewSection">

      <div class="panelhead">
        <span>◉ Live Preview</span>

        <div>
          <button onclick="run()">↻</button>
          <button onclick="fullScreen()">⛶</button>
        </div>
      </div>

      <iframe
        id="preview"
        title="Live website preview"
        sandbox="allow-scripts allow-forms allow-modals allow-popups"
      ></iframe>

      <div class="bottom">
        <span>Preview</span>
        <span id="previewStatus">Waiting for code</span>
      </div>

    </section>

  </main>
</div>

<script>
const editor = document.getElementById("editor");
const preview = document.getElementById("preview");

let active = "html";
let previewTimer;

let files = {
  html: "",
  css: "",
  js: ""
};

// Switch editor tabs
function changeTab(tab, element) {

  files[active] = editor.value;

  active = tab;

  editor.value = files[tab];

  document.querySelectorAll(".tab").forEach(t => {
    t.classList.remove("active");
  });

  element.classList.add("active");

  document.getElementById("fileName").textContent =
    tab === "js" ? "script.js" :
    tab === "css" ? "style.css" :
    "index.html";

  updateEmpty();
  schedulePreview();
}

// Automatic live preview
editor.addEventListener("input", () => {

  files[active] = editor.value;

  document.getElementById("status").textContent =
    "Unsaved changes";

  updateEmpty();
  schedulePreview();

});

// Delay preview slightly
function schedulePreview() {

  clearTimeout(previewTimer);

  previewTimer = setTimeout(() => {
    run();
  }, 300);

}

// Update empty editor message
function updateEmpty() {

  document.getElementById("empty").style.display =
    editor.value.length === 0 ? "block" : "none";

}

// Run website
function run() {

  files[active] = editor.value;

  const html = files.html;
  const css = files.css;
  const js = files.js;

  const safeCSS = css.replace(/<\/style/gi, "<\\/style");
  const safeJS = js.replace(/<\/script/gi, "<\\/script");

  let output = html;

  if (output.match(/<\/head>/i)) {

    output = output.replace(
      /<\/head>/i,
      `<style>${safeCSS}</style></head>`
    );

  } else {

    output = `<style>${safeCSS}</style>` + output;

  }

  if (output.match(/<\/body>/i)) {

    output = output.replace(
      /<\/body>/i,
      `<script>${safeJS}<\/script></body>`
    );

  } else {

    output += `<script>${safeJS}<\/script>`;

  }

  preview.srcdoc = output;

  document.getElementById("status").textContent =
    "Preview updated";

  document.getElementById("previewStatus").textContent =
    "Live";

}

// Save project
function saveProject() {

  files[active] = editor.value;

  try {

    localStorage.setItem(
      "html2forever",
      JSON.stringify(files)
    );

    document.getElementById("status").textContent =
      "Saved successfully";

  } catch(error) {

    document.getElementById("status").textContent =
      "Could not save project";

  }

}

// Load saved project
function loadProject() {

  try {

    const saved = localStorage.getItem("html2forever");

    if(saved) {
      files = {...files, ...JSON.parse(saved)};
    }

  } catch(error) {

    console.error("Could not load project", error);

  }

}

// Create new project
function newProject() {

  if(!confirm("Create a new empty project?")) return;

  files = {
    html: "",
    css: "",
    js: ""
  };

  editor.value = "";

  localStorage.removeItem("html2forever");

  changeTab(
    "html",
    document.querySelectorAll(".tab")[0]
  );

  run();

  document.getElementById("status").textContent =
    "New project";

}

// Download website
function downloadProject() {

  files[active] = editor.value;

  const html =
    files.html +
    `<style>${files.css}</style>` +
    `<script>${files.js}<\/script>`;

  const blob = new Blob(
    [html],
    {type:"text/html"}
  );

  const url = URL.createObjectURL(blob);

  const a = document.createElement("a");

  a.href = url;
  a.download = "index.html";

  a.click();

  setTimeout(() => URL.revokeObjectURL(url), 1000);

}

// Full screen preview
function fullScreen() {

  if(preview.requestFullscreen) {
    preview.requestFullscreen();
  }

}

// Scroll to preview
function scrollToPreview() {

  document.getElementById("previewSection").scrollIntoView({
    behavior: "smooth",
    block: "start"
  });

}

// Files menu
function showFiles() {

  alert(
    "PROJECT FILES\n\n" +
    "index.html\n" +
    "style.css\n" +
    "script.js"
  );

}

// Focus editor
function focusEditor() {
  editor.focus();
}

// Initialize
loadProject();

editor.value = files.html;

updateEmpty();

run();

</script>

</body>
</html>

Game Source: HTML2 Forever

Creator: BoldPenguin14

Libraries: none

Complexity: complex (617 lines, 9.5 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: html2-forever-boldpenguin14" to link back to the original. Then publish at arcadelab.ai/publish.