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.
index.html
: The main HTML file. codeEditor.js
: JavaScript file to handle the editor's functionality. dweb.js
: JavaScript file to handle interactions with the DWeb. common.js
: JavaScript file for common functions and selectors. styles.css
: CSS file for styling the application.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:
<meta lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-Time Editor</title>
lang="en"
: Sets the language of the document to English. charset="UTF-8"
: Specifies the character encoding for the HTML document (UTF-8 is standard for Unicode). viewport
tag: Ensures the page is responsive and renders well on all devices, particularly mobile devices. title
: Sets the title of the webpage, which appears in the browser tab.<link rel="stylesheet" type="text/css" href="styles.css">
styles.css
to the HTML document. This file will style the elements of your page.<main>
<!-- Content inside main goes here -->
</main>
<main>
element acts as the primary container for the main content of the page.<div id="backdrop"></div>
<div id="loadingSpinner" style="display: none;">
<!-- SVG for Spinner -->
</div>
backdrop
: A div element used as a backdrop for the loading indicator.loadingSpinner
: Contains an SVG spinner which is displayed while content is loading. Initially set to display: none;
so it's hidden.<div class="grid-container">
<!-- Textareas and Iframe -->
</div>
<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>
<textarea>
elements for users to input HTML, JavaScript, and CSS code. spellcheck="false"
disables the browser's spell check feature.<iframe id="viewer"></iframe>
<iframe>
where the combined output of the HTML, CSS, and JavaScript code is rendered and displayed.<div id="dweb-container">
<!-- Protocol selection and buttons -->
</div>
<script type="module" src="common.js"></script>
<script type="module" src="dweb.js"></script>
<script type="module" src="codeEditor.js"></script>
<script>
tags at the bottom import the JavaScript modules common.js
, dweb.js
, and codeEditor.js
. Placing them at the end ensures that the HTML elements are loaded before the scripts run.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:
@import url("agregore://theme/vars.css");
:root {
--gap: 5px;
--half-gap: calc(var(--gap) / 2);
}
--gap
for standard spacing and --half-gap
for half that spacing. These are used throughout the stylesheet for consistent spacingbody, * {
/* Padding, margin, font, background, color, and box-sizing styles */
}
main {
/* Styling for the main container */
}
.grid-container {
/* Grid layout for text areas 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 */
}
#viewer
).div textarea:focus {
/* Styles when a textarea is focused */
}
#dweb-container,
#uploadListBox {
/* Flex layout for DWeb interaction elements */
}
@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.
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';
}
import { $, loadingSpinner, backdrop, iframe } from './common.js';
common.js
. The $
function simplifies the process of selecting DOM elements.document.addEventListener('DOMContentLoaded', () => {
// Code to initialize the editor
});
[htmlCode, javascriptCode, cssCode].forEach(element => {
element.addEventListener('input', () => update());
});
htmlCode
, javascriptCode
, and cssCode
textareas. input
event triggers the update function for live rendering.export let basicCSS = `
@import url("agregore://theme/vars.css");
body, * {
// Basic CSS styles
}
`;
export function update(i) {
// Code to update the iframe content live as the user types
}
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.
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);
}
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:
export async function assembleCode() {
// Implementation to assemble and upload the code
}
assembleCode
: Prepares the HTML, CSS, and JavaScript code written in the editor into a single HTML file format. This combined code is then converted into a Blob for uploading.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
}
cidOrName
input is provided. If not, it alerts the user. fetch
API to retrieve content from the specified DWeb URL. 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
}
<style>
elements, skips the first one (assuming it's the Agregore theme), and then combines the CSS from the remaining elements. <script>
element and extracts its content. <script>
and <style>
tags from the HTML content to isolate the body content. Together, these functions enable the code editor to interact with the DWeb, allowing users to fetch and display content dynamically within the editor environment.
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:
$
Functionexport function $(query) {
return document.querySelector(query);
}
document.querySelector
.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');
common.js
, and the change will propagate throughout the application.common.js
can be reused across different modules, promoting DRY (Don't Repeat Yourself) principles.common.js
.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.
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