Skip to content

Commit

Permalink
Code editor App (#51)
Browse files Browse the repository at this point in the history
* code-editor init

* Refactor assembleCode and parseAndDisplayData functions

- Separate agregore's basic CSS and user's CSS in assembleCode
- Update CSS extraction logic to exclude the display of agregore theme CSS in the code-editor

* make spinner round

* Refactor fetchButton event listener into fetchFromDWeb function

- Refactor button event listener to its own fetchFromDWeb function
- Add protocol detection so that users paste their CID with the protocol prefix
- Removes the dependency on a separate protocol selector.

- Improves the code's modularity and user input handling by directly
- Detects whether the URL starts with 'ipfs://' or 'hyper://', and alerts the user for invalid protocols.

* Enhance URL Display with Inline Copy-to-Clipboard Feature

- Refactoring of the addURL function to improve UX
- Adds a 20x20 copy to clipboard icon that loads `var(--ag-theme-primary)` as fill color
- Implements a copy-to-clipboard functionality for quick sharing of the code's  URL
- The Function temporarily replaces the SVG with a "Copied!" message upon successful copy, reverting back to the original SVG after 3 seconds.

These enhancements contribute to a more interactive and user-friendly interface.
Streamlines the process of copying URLs directly from the displayed list.

* Refactor HTML for dweb fetch and upload containers

* Switch to secondary colour for bigger visual impact

* Improve alignment for dweb container's children elements

* Add media query for mobile devices

* Split script script.js into modules; dweb, code editor and common functions

* Load new js files in index

* Update code editor layout

* Update protocol labels and button text

* Add responsive layout for mobile devices on uploadand fetch buttons

* Update code editor layout by moving list at the bottom

* Refactor code editor styles and improve responsiveness

* Remove script.js

* Update script imports in index.html

* Remove console.log statements in parseAndDisplayData function

* Refactor code editor event listeners and remove unused code

* Add Tutorial (v1) for code editor app

* Update code editor styles, replace bottom margin with gap

* Improve comments for clarity

* Code Editor Retrospective v0.0.1

* Fix typo, explain jsfiddle and codepen

* Consolidate achievements and reduce repetition

* Rename code editor tutorial, move to docs/tutorials/

* Remove DOMContentLoaded listener

* Transition to using the $ selector

* Streamline event listener attachment in codeEditoor.js

* Simplify fetchFromDWeb function to streamline URL validation

* Replace SVG loading spinner with CSS-animated emoji for simplicity

- Removed SVG path from HTML
- Introduced emoji-based loader with CSS rotation animation
- Adjusted styles for new loading animation, ensuring consistency with design

* Rename code editor tutorial files and update links

* Add P2Pad Code Editor links to documentation and explore pages

* Fix display on spinner

* Simplify clipboard copy icon implementation

* Move Copy Icon Style to CSS

* Update tutorial following review
  • Loading branch information
tripledoublev authored Feb 26, 2024
1 parent 4ad4bfd commit 60a6882
Show file tree
Hide file tree
Showing 9 changed files with 1,254 additions and 1 deletion.
42 changes: 42 additions & 0 deletions blog/2024/01/p2pad-code-editor-retrospective.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# P2Pad Retrospective: Building a Real-Time Browser-Based Code Editor with Agregore 🌐

## Reflecting on the Journey 🎉

### Original Objective and Outcome 🎯
The goal was to create an introductory tutorial for building a real-time, browser-based code editor similar to platforms like jsfiddle or codepen, using Agregore. Focusing on simplicity and beginner-friendliness, the tutorial aimed to cover HTML, CSS, and JavaScript essentials, along with an introduction to decentralized web protocols. The end result is a functional code editor that facilitates interaction with IPFS and Hypercore, aligning well with these objectives.

### Achievements and Evolution 📈
- Successfully developed P2Pad, a user-friendly, real-time code editor capable of interacting with decentralized web technologies.
- Struck a delicate balance between simplicity for beginners and a meaningful introduction to the practical applications of decentralized web protocols.

### Challenges and Insights 💡
- Balancing simplicity with the complexity of a real-time in-browser code editor that interfaces with the decentralized web.
- Keeping it simple without oversimplifying was crucial for the tutorial's effectiveness.
- Insight: Keeping tutorials modular and reusing code enhances learning and development efficiency.

## The Development Experience 🛠️

### Crafting the Application 🏗️
- Constructed using a blend of HTML, CSS, and JavaScript, with detailed steps provided in the tutorial.
- Key aspects of the development process:
- **What Went Well:** The integration of basic web technologies and the exploration of the potential of decentralized web protocols.
- **Areas of Complexity:** Ensuring that the code was both simple enough for beginners while offering the desired DWeb interactions.
- **Discovery:** The power of modularity and reusability in coding tutorials, especially when dealing with multiple features.

### Advice for Future Endeavors 🗣️
- Emphasize the importance of modularity and reusability in tutorial design as I found myself reusing some code from my previous Drag and Drop tutorial.
- Consider developing a dedicated library for DWeb interactions to streamline future tutorial creation.

## Future Directions and Discussions 🚀

### Potential Improvements and Extensions 🔧
- **Simplification:** Identify areas where the tutorial or the app itself could be simplified further, making it even more accessible to beginners.
- **Modularity:** Enhance the separation of concerns and organize code more optimally, possibly through a dedicated library for DWeb interactions.
- **Documentation:** Improve and expand documentation for reusable code snippets, aiding future users in locating and applying these functions effectively.

### Suggestions for Agregore Styling Guidelines 📝
- **Agregore Style Guide:** A formal style guide that taps into and extends the Agregore theme would be highly beneficial. This could standardize the look and feel of applications developed using Agregore, leading to a more cohesive user experience.
- **Documentation Emphasis:** The value of comprehensive and accessible documentation cannot be overstated, especially for beginners exploring new technologies like Agregore and the decentralized web.

## Conclusion 🌟
This project was a great opportunity for a continued exploration into the realm of web development and decentralized web technologies. It underscored the importance of keeping tutorials simple yet informative, modular, and reusable. The journey highlighted the need for clear documentation and style guidelines in emerging software like Agregore, paving the way for future innovations and learning.
48 changes: 48 additions & 0 deletions docs/examples/p2pad/codeEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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 preview
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;
console.log('HTML Code:', htmlCode);
let cssCode = $('#cssCode').value;
console.log('CSS Code:', cssCode);
let javascriptCode = $('#javascriptCode').value;
console.log('JavaScript Code:', javascriptCode);
// 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';
}
12 changes: 12 additions & 0 deletions docs/examples/p2pad/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Common module for exports
export function $(query) {
return document.querySelector(query);
}

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');
172 changes: 172 additions & 0 deletions docs/examples/p2pad/dweb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
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('drag-and-drop');
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 detects which protocol is used and fetches the content
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.");
}
}

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

// Parse the data and display it in the code editor
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);
}
51 changes: 51 additions & 0 deletions docs/examples/p2pad/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<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>

Loading

0 comments on commit 60a6882

Please sign in to comment.