P2Pad In-browser Code Editor

Building a Real-Time Browser-Based Code Editor with Agregore

Introduction

This tutorial will guide you through creating a real-time code editor in your browser using Agregore. This editor will allow you to write HTML, CSS, and JavaScript code and see live results. By the end of this tutorial, this real-time code editor will be capable of interacting with the decentralized web, giving you practical experience in both front-end web development and decentralized web protocols. Additionally, the code written can be easily shared as the interface includes buttons that allow you to upload and fetch code from the decentralized web, using IPFS and Hypercore protocols.

Prerequisites

Setting Up Your Environment

  1. Install Agregore Browser: Download and install the Agregore browser from Agregore's official website.
  2. Create a Project Folder: Create a new folder on your computer where you will store the following files:

Building the Application

Step 1: HTML Structure

Add the following content to index.html:

<meta lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P2Pad: Real-Time Editor</title>
<link rel="stylesheet" type="text/css" href="styles.css">

<main>
    <div id="backdrop"></div>
    <div id="loadingSpinner" style="display: none;">
        <div class="emoji-loader"> ↻ </div>
    </div>
    <div class="grid-container">
        
        <!-- Text area for Html Code -->
        <textarea id="htmlCode" placeholder="Type HTML code here" spellcheck="false"></textarea>

        <!-- Text area for Javascript Code -->
        <textarea id="javascriptCode" spellcheck="false" placeholder="Type JavaScript code here"></textarea>

        <!-- Text area for Css Code -->
        <textarea id="cssCode" placeholder="Type CSS code here" spellcheck="false"></textarea>

        <!-- Iframe for Code Output -->
        <iframe id="viewer"></iframe>
    
    </div>
    <div id="dweb-container">
        <div>
            <label for="protocolSelect">
                Protocol:
                <select id="protocolSelect">
                    <option value="ipfs" selected>Inter-Planetary File System (IPFS://)</option>
                    <option value="hyper">Hypercore-Protocol (HYPER://)</option>
                </select>
            </label>
            <button id="uploadButton">Upload to DWeb</button>
            
        </div>
        <div id="fetchContainer">
            <input id="fetchCidInput" type="text" placeholder="Enter IPFS CID or Hyperdrive URL ">
            <button id="fetchButton">Fetch from DWeb</button>
        </div>
        
    </div>
    <ul id="uploadListBox"></ul>
</main>
<script type="module" src="common.js"></script>
<script type="module" src="dweb.js"></script>
<script type="module" src="codeEditor.js"></script>

Let's do a quick overview of what each of these sections do:

HTML Meta and Title

<meta lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-Time Editor</title>
<link rel="stylesheet" type="text/css" href="styles.css">

Main Container

<main>
    <!-- Content inside main goes here -->
</main>

Loading Spinner

<div id="backdrop"></div>
<div id="loadingSpinner" style="display: none;">
    <!-- SVG for Spinner -->
</div>

Grid Container

<div class="grid-container">
    <!-- Textareas and Iframe -->
</div>

Code Input Areas

<textarea id="htmlCode" placeholder="Type HTML code here" spellcheck="false"></textarea>
<textarea id="javascriptCode" spellcheck="false" placeholder="Type JavaScript code here"></textarea>
<textarea id="cssCode" placeholder="Type CSS code here" spellcheck="false"></textarea>

Iframe for Output

<iframe id="viewer"></iframe>

DWeb Interaction Container

<div id="dweb-container">
    <!-- Protocol selection and buttons -->
</div>

Script Tags

<script type="module" src="common.js"></script>
<script type="module" src="dweb.js"></script>
<script type="module" src="codeEditor.js"></script>

Step 2: Styling

In styles.css, include the necessary styles:

@import url("agregore://theme/vars.css");

:root {
    --gap: 5px;
    --half-gap: calc(var(--gap) / 2);
}

body, * {
    padding: 0;
    margin: 0;
    font-family: var(--ag-theme-font-family);
    background: var(--ag-theme-background);
    color: var(--ag-theme-text);
    box-sizing: border-box;
}

main {
    padding: var(--gap);
    height: 100vh;
    display: flex;
    flex-direction: column;
    background: var(--ag-theme-background);
    color: var(--ag-theme-text);
}

.grid-container {
    display: grid;
    grid-template-columns: 1fr 1fr; /* Two columns */
    grid-template-rows: 1fr 1fr; /* Two rows */
    height: 95vh;
    padding: var(--half-gap);
    row-gap: var(--gap);
    column-gap: var(--gap);
}

.grid-container > * {
    padding: var(--gap);
    width: 100%;
    height: 100%;
    overflow: auto; /* To handle content overflow */
    border: 1px solid var(--ag-theme-primary);
}

.grid-container > textarea {
    resize: none;
    font-size: 1.2rem;
}

#viewer {
    color: var(--ag-theme-text);
}

div textarea:focus {
    outline: 2px solid var(--ag-theme-secondary);
    color: var(--ag-theme-text);
}

#dweb-container,
#uploadListBox {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    padding: 0 var(--half-gap);
}

#dweb-container > * {
    width: 100%;
    display:flex;
    flex-direction:row;
    align-items: flex-end;
    margin-bottom: 0.5rem;
    padding: var(--half-gap);
}

#uploadListBox {
    flex-direction: column;
    align-items: flex-start;
}

li {
    display: flex;
    align-items: flex-end;
}

#loadingSpinner, #backdrop {
    display: none;  
}

#loadingSpinner, .emoji-loader {
    background: transparent;
}

#loadingSpinner {
    position: absolute; 
    top: 50%; 
    left: 50%; 
    transform: translate(-50%, -50%); 
    z-index: 1000; 
    border-radius: 50%;
}

@keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

.emoji-loader {
    color: var(--ag-color-green);
    font-size: 10rem; 
    border-radius: 50%;
    animation: spin 2s linear infinite;
}

#backdrop {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5); 
    z-index: 999; 
}

#uploadButton {
    white-space: nowrap;
}

#fetchContainer {
    justify-content: flex-end;
    align-items: flex-end;
}

span {
    pointer-events: cursor;
    color: var(--ag-theme-secondary);
}

span:hover {
    color: var(--ag-theme-primary);
}

/* Media query for mobile devices */
@media screen and (max-width: 768px) {
    #dweb-container,
    #dweb-container > *  {
       flex-direction: column;
       align-items: flex-start;
    }
    .grid-container {
        grid-template-columns: 1fr; /* One column */
        grid-template-rows: repeat(4, 1fr); /* Four rows */
    }
}

Now, let's look at the purpose of each of these styles. Here's a breakdown of key elements in the stylesheet:

Importing Agregore Theme Variables

@import url("agregore://theme/vars.css");

Root Variables

:root {
    --gap: 5px;
    --half-gap: calc(var(--gap) / 2);
}

Global Styles

body, * {
    /* Padding, margin, font, background, color, and box-sizing styles */
}

Main Container

main {
    /* Styling for the main container */
}

Grid Container

.grid-container {
    /* Grid layout for text areas and iframe */
}

Textareas and Iframe

.grid-container > * {
    /* Common styles for all children of the grid container */
}
.grid-container > textarea {
    /* Additional styles specifically for textareas */
}
#viewer {
    /* Styles for the output iframe */
}

Focus State for Textareas

div textarea:focus {
    /* Styles when a textarea is focused */
}

DWeb and Upload List Container

#dweb-container,
#uploadListBox {
    /* Flex layout for DWeb interaction elements */
}

Responsive Design

@media screen and (max-width: 768px) {
    /* Responsive styles for mobile devices */
}

These styles collectively create a user-friendly, visually consistent, agregore-themed responsive interface for the real-time code editor, leveraging the power of modern CSS.

Step 3: Code Editor Functionality

In codeEditor.js, we define the functionality of that allows for writing code and seeing it rendered in the iFrame. Let's break down the key components:

import { $, loadingSpinner, backdrop, iframe } from './common.js'; // Import common functions

// Attach event listeners directly using the $ selector function
[$('#htmlCode'), $('#javascriptCode'), $('#cssCode')].forEach(element => {
    element.addEventListener('input', () => update());
});

// Import CSS from Agregore theme to use in the iFrame
export let basicCSS = `
@import url("agregore://theme/vars.css");
            body, * {
                font-size: 1.2rem;
                margin: 0;
                padding: 0;
                font-family: var(--ag-theme-font-family);
                background: var(--ag-theme-background);
                color: var(--ag-theme-text);
            }
`;

//Function for live Rendering
export function update() {
    let htmlCode = $('#htmlCode').value;
    let cssCode = $('#cssCode').value;
    let javascriptCode = $('#javascriptCode').value;

    // Assemble all elements and Include the basic CSS from Agregore theme
    let iframeContent = `
        <style>${basicCSS}</style>
        <style>${cssCode}</style>
        <script>${javascriptCode}</script>
        ${htmlCode}
    `;
    let iframeDoc = iframe.contentWindow.document;
    iframeDoc.open();
    iframeDoc.write(iframeContent);
    iframeDoc.close();
}

// Show or hide the loading spinner
export function showSpinner(show) {
    backdrop.style.display = show ? 'block' : 'none';
    loadingSpinner.style.display = show ? 'block' : 'none';
}

Importing Common Functions and Elements

import { $, loadingSpinner, backdrop, iframe } from './common.js';

Initializing the Editor

document.addEventListener('DOMContentLoaded', () => {
    // Code to initialize the editor
});

Event Listeners for Textareas

[htmlCode, javascriptCode, cssCode].forEach(element => {
    element.addEventListener('input', () => update());
});

Agregore Theme CSS Import

export let basicCSS = `
@import url("agregore://theme/vars.css");
            body, * {
                // Basic CSS styles
            }
`;

Live Rendering Function

export function update(i) {
    // Code to update the iframe content live as the user types
}

Loading Spinner Visibility Control

export function showSpinner(show) {
    backdrop.style.display = show ? 'block' : 'none';
    loadingSpinner.style.display = show ? 'block' : 'none';
}

This script is the core of the code editor's functionality, handling user inputs, live preview rendering, and integrating with the Agregore browser's theming system. It provides a seamless and interactive coding experience within the browser.

Step 4: DWeb Integration with dweb.js

In dweb.js, we extend the functionality of our code editor to integrate with the decentralized web (DWeb), specifically IPFS and Hypercore. This script shares some functionalities with the previous tutorial on Drag and Drop to IPFS and Hypercore, which can be revisited for a more in-depth understanding. Drag and Drop Tutorial

Here is the content of dweb.js:

import { update, showSpinner, basicCSS } from './codeEditor.js';
import { $, uploadButton, protocolSelect, fetchButton, fetchCidInput } from './common.js';

// assemble code before uploading
export async function assembleCode() {
    // Display loading spinner
    showSpinner(true);

    // Combine your code into a single HTML file
    let combinedCode = `
    <!DOCTYPE html>
    <html>
    <head>
        <style>${basicCSS}</style>
        <style>${document.getElementById("cssCode").value}</style>
    </head>
    <body>
        ${document.getElementById("htmlCode").value}
        <script>${document.getElementById("javascriptCode").value}</script>
    </body>
    </html>`;

    // Convert the combined code into a Blob
    const blob = new Blob([combinedCode], { type: 'text/html' });
    const file = new File([blob], "index.html", { type: 'text/html' });

    // Upload the file
    await uploadFile(file);
    showSpinner(false);
}

uploadButton.addEventListener('click', assembleCode);

// Upload code to Dweb
async function uploadFile(file) {
    const protocol = protocolSelect.value;

    const formData = new FormData();

    // Append file to the FormData
    formData.append('file', file, file.name);


    // Construct the URL based on the protocol
    let url;
    if (protocol === 'hyper') {
        const hyperdriveUrl = await generateHyperdriveKey('p2pad');
        url = `${hyperdriveUrl}`;
    } else {
        url = `ipfs://bafyaabakaieac/`;
    }

    // Perform the upload for each file
    try {
        const response = await fetch(url, {
            method: 'PUT',
            body: formData,
        });

        if (!response.ok) {
            addError(file, await response.text());
        }
        const urlResponse = protocol === 'hyper' ? response.url : response.headers.get('Location');
        addURL(urlResponse);
    } catch (error) {
        console.error(`Error uploading ${file}:`, error);
    } finally {
        showSpinner(false);
    }
}

async function generateHyperdriveKey(name) {
    try {
        const response = await fetch(`hyper://localhost/?key=${name}`, { method: 'POST' });
        if (!response.ok) {
            throw new Error(`Failed to generate Hyperdrive key: ${response.statusText}`);
        }
        return await response.text();  // This returns the hyper:// URL
    } catch (error) {
        console.error('Error generating Hyperdrive key:', error);
        throw error;
    }
}

function addURL(url) {
    const listItem = document.createElement('li');
    const link = document.createElement('a');
    link.href = url;
    link.textContent = url;

    const copyContainer = document.createElement('span');
    const copyIcon = '⊕'
    copyContainer.innerHTML = copyIcon;
    copyContainer.onclick = function() {
        navigator.clipboard.writeText(url).then(() => {
            copyContainer.textContent = ' Copied!';
            setTimeout(() => {
                copyContainer.innerHTML = copyIcon;
            }, 3000);
        }).catch(err => {
            console.error('Error in copying text: ', err);
        });
    };

    listItem.appendChild(link);
    listItem.appendChild(copyContainer);
    uploadListBox.appendChild(listItem);
}

function addError(name, text) {
    uploadListBox.innerHTML += `<li class="log">Error in ${name}: ${text}</li>`
}

// The fetchFromDWeb function
async function fetchFromDWeb(url) {
    if (!url) {
        alert("Please enter a CID or Name.");
        return;
    }

    if (!url.startsWith('ipfs://') && !url.startsWith('hyper://')) {
        alert("Invalid protocol. URL must start with ipfs:// or hyper://");
        return;
    }

    try {
        const response = await fetch(url);
        
        const data = await response.text();
        parseAndDisplayData(data);
    } catch (error) {
        console.error("Error fetching from DWeb:", error);
        alert("Failed to fetch from DWeb.");
    }
}

// Event listener for fetchButton
fetchButton.addEventListener('click', () => {
    const cidOrName = fetchCidInput.value;
    fetchFromDWeb(cidOrName);
});

function parseAndDisplayData(data) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(data, 'text/html');

    // Extracting CSS
    const styleElements = Array.from(doc.querySelectorAll('style'));

    // Remove the first element (agregore theme CSS)
    styleElements.shift();

    // Now combine the CSS from the remaining <style> elements
    let cssContent = styleElements.map(style => style.innerHTML).join('');

    // Extracting JavaScript
    const jsContent = doc.querySelector('script') ? doc.querySelector('script').innerHTML : '';

    // Remove script and style tags from the HTML content
    doc.querySelectorAll('script, style').forEach(el => el.remove());
    const htmlContent = doc.body.innerHTML; // Get the content inside the body tag without script/style tags

    // Displaying the content in respective textareas
    $('#htmlCode').value = htmlContent;
    $('#cssCode').value = cssContent;
    $('#javascriptCode').value = jsContent;
    update(0);
}

New Functionalities in dweb.js

The script includes functions for assembling code, uploading to the DWeb, and fetching from the DWeb. Here's an overview of the new functionalities:

Assembling Code for Uploading

export async function assembleCode() {
    // Implementation to assemble and upload the code
}

Fetching code from the DWeb

This function is responsible for fetching data from the decentralized web (DWeb) using either IPFS or Hypercore protocols. Here's how it works:

async function fetchFromDWeb(cidOrName) {
    // Validation check for input
    // Determine the protocol based on the input format
    // Attempt to fetch data from the specified URL
}

Parse and Display Data

You might have noticed a parseAndDisplayData in the previous function. Let's look at how this function parses and displays the fetched data from the DWeb in the respective text areas of the code editor.

function parseAndDisplayData(data) {
    // 1. Parse the fetched HTML content
    // 2. Extract and combine CSS content
    // 3. Extract JavaScript content
    // 4. Clean up the HTML content
    // 5. Update the textareas with the fetched content
    // 6. Update the iframe for live rendering
}

Together, these functions enable the code editor to interact with the DWeb, allowing users to fetch and display content dynamically within the editor environment.

Step 5: Common Functions

The common.js file in our project plays a crucial role in enhancing code maintainability and reducing redundancy. It defines and exports a set of commonly used functionalities and DOM element references, which can be easily imported and used across different scripts in the application. Here’s a breakdown of its contents:

$ Function

export function $(query) {
    return document.querySelector(query);
}

DOM Element References

export const uploadButton = $('#uploadButton');
export const protocolSelect = $('#protocolSelect');
export const loadingSpinner = $('#loadingSpinner');
export const backdrop = $('#backdrop');
export const iframe = $('#viewer');
export const fetchButton = $('#fetchButton');
export const fetchCidInput = $('#fetchCidInput');

Overall Impact

Including common.js in your application is a best practice in web development. It not only streamlines your code but also improves overall project structure, making it more maintainable and scalable in the long run.

Testing the Application

Conclusion

Congratulations! You've built a versatile code editor in your browser that interacts with the decentralized web. This project not only enhances your web development skills but also introduces you to the realm of DWeb technologies.

Feel free to expand upon this application by adding more features or refining the UI. Explore the possibilities with Agregore and the decentralized web!

You can find the finished result of this tutorial here