Skip to content

Commit 6aaf437

Browse files
author
Raffael Herrmann
committed
Initial commit: MassCode web frontend with search, themes, sidebar, copy buttons, and lazy loading
0 parents  commit 6aaf437

File tree

3 files changed

+407
-0
lines changed

3 files changed

+407
-0
lines changed

app.js

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
document.addEventListener('DOMContentLoaded', () => {
2+
const searchInput = document.getElementById('search');
3+
const snippetsContainer = document.getElementById('snippets-container');
4+
const sidebar = document.getElementById('sidebar');
5+
const themeSelect = document.getElementById('theme-select');
6+
let dbData = null;
7+
let currentSnippets = [];
8+
let loadedCount = 0;
9+
const batchSize = 10;
10+
11+
// Theme handling
12+
function setPrismTheme(isDark) {
13+
const prismLink = document.getElementById('prism-theme');
14+
if (isDark) {
15+
prismLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css';
16+
} else {
17+
prismLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css';
18+
}
19+
}
20+
21+
function applyTheme(theme) {
22+
const html = document.documentElement;
23+
localStorage.setItem('theme', theme);
24+
let isDark = false;
25+
if (theme === 'dark') {
26+
html.classList.add('dark');
27+
isDark = true;
28+
} else if (theme === 'light') {
29+
html.classList.remove('dark');
30+
isDark = false;
31+
} else { // auto
32+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
33+
if (prefersDark) {
34+
html.classList.add('dark');
35+
isDark = true;
36+
} else {
37+
html.classList.remove('dark');
38+
isDark = false;
39+
}
40+
}
41+
themeSelect.value = theme;
42+
setPrismTheme(isDark);
43+
}
44+
45+
// Load saved theme or default to auto
46+
const savedTheme = localStorage.getItem('theme') || 'auto';
47+
applyTheme(savedTheme);
48+
49+
// Theme selector change
50+
themeSelect.addEventListener('change', (e) => {
51+
applyTheme(e.target.value);
52+
});
53+
54+
// Listen for system theme changes when in auto mode
55+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
56+
if (localStorage.getItem('theme') === 'auto') {
57+
if (e.matches) {
58+
document.documentElement.classList.add('dark');
59+
setPrismTheme(true);
60+
} else {
61+
document.documentElement.classList.remove('dark');
62+
setPrismTheme(false);
63+
}
64+
}
65+
});
66+
67+
// Sidebar toggle
68+
window.toggleSidebar = function() {
69+
sidebar.classList.toggle('sidebar-hidden');
70+
};
71+
72+
// Clear search function
73+
window.clearSearch = function() {
74+
searchInput.value = '';
75+
searchInput.dispatchEvent(new Event('input'));
76+
};
77+
78+
// Load db.json
79+
fetch('db.json')
80+
.then(response => response.json())
81+
.then(data => {
82+
dbData = data;
83+
const activeSnippets = data.snippets.filter(s => !s.isDeleted);
84+
currentSnippets = activeSnippets;
85+
loadedCount = 0;
86+
const initialBatch = currentSnippets.slice(0, batchSize);
87+
renderSnippets(initialBatch, true);
88+
loadedCount += initialBatch.length;
89+
renderTree(data.folders, activeSnippets);
90+
})
91+
.catch(error => console.error('Error loading db.json:', error));
92+
93+
// Search functionality
94+
const clearSearchButton = document.getElementById('clear-search');
95+
searchInput.addEventListener('input', (e) => {
96+
const query = e.target.value.toLowerCase();
97+
const activeSnippets = dbData.snippets.filter(s => !s.isDeleted);
98+
const filteredSnippets = activeSnippets.filter(snippet =>
99+
snippet.name.toLowerCase().includes(query) ||
100+
snippet.description?.toLowerCase().includes(query) ||
101+
snippet.content.some(content => content.value.toLowerCase().includes(query))
102+
);
103+
currentSnippets = filteredSnippets;
104+
loadedCount = 0;
105+
const initialBatch = currentSnippets.slice(0, batchSize);
106+
renderSnippets(initialBatch, true);
107+
loadedCount += initialBatch.length;
108+
109+
// Show/hide clear button
110+
clearSearchButton.style.display = e.target.value ? 'block' : 'none';
111+
});
112+
113+
// Sidebar toggle
114+
sidebar.addEventListener('click', (e) => {
115+
if (e.target.classList.contains('folder')) {
116+
e.target.classList.toggle('open');
117+
} else if (e.target.classList.contains('snippet-item')) {
118+
const snippetId = e.target.dataset.snippetId;
119+
const element = document.getElementById(`snippet-${snippetId}`);
120+
if (element) {
121+
element.scrollIntoView({ behavior: 'smooth' });
122+
}
123+
}
124+
});
125+
126+
// Infinite scroll
127+
snippetsContainer.addEventListener('scroll', () => {
128+
if (snippetsContainer.scrollTop + snippetsContainer.clientHeight >= snippetsContainer.scrollHeight - 100) {
129+
if (loadedCount < currentSnippets.length) {
130+
const nextBatch = currentSnippets.slice(loadedCount, loadedCount + batchSize);
131+
renderSnippets(nextBatch, false);
132+
loadedCount += nextBatch.length;
133+
}
134+
}
135+
});
136+
137+
function renderSnippets(snippets, clear = false) {
138+
if (clear) {
139+
snippetsContainer.innerHTML = '';
140+
}
141+
snippets.forEach(snippet => {
142+
const snippetDiv = document.createElement('div');
143+
snippetDiv.className = 'bg-white rounded-lg shadow-md mb-4 p-4 dark:bg-gray-700 dark:shadow-lg transition-colors';
144+
snippetDiv.id = `snippet-${snippet.id}`;
145+
146+
const title = document.createElement('h2');
147+
title.className = 'text-xl font-semibold text-gray-800 mb-2 dark:text-white';
148+
title.textContent = snippet.name;
149+
snippetDiv.appendChild(title);
150+
151+
if (snippet.tagsIds.length > 0) {
152+
const tagsDiv = document.createElement('div');
153+
tagsDiv.className = 'mb-2';
154+
snippet.tagsIds.forEach(tagId => {
155+
const tag = dbData.tags.find(t => t.id === tagId);
156+
if (tag) {
157+
const tagSpan = document.createElement('span');
158+
tagSpan.className = 'inline-block bg-gray-200 text-gray-800 px-2 py-1 rounded text-sm mr-2 dark:bg-gray-600 dark:text-gray-200';
159+
tagSpan.textContent = tag.name;
160+
tagsDiv.appendChild(tagSpan);
161+
}
162+
});
163+
snippetDiv.appendChild(tagsDiv);
164+
}
165+
166+
if (snippet.description) {
167+
const desc = document.createElement('p');
168+
desc.className = 'text-gray-600 mb-2 dark:text-gray-300';
169+
desc.textContent = snippet.description;
170+
snippetDiv.appendChild(desc);
171+
}
172+
173+
snippet.content.forEach(content => {
174+
if (content.value.trim()) {
175+
const pre = document.createElement('pre');
176+
pre.className = 'bg-gray-100 rounded p-4 overflow-x-auto mb-4 dark:bg-gray-900 relative';
177+
const code = document.createElement('code');
178+
code.className = `language-${content.language}`;
179+
code.textContent = content.value;
180+
pre.appendChild(code);
181+
182+
// Add copy button
183+
const copyButton = document.createElement('button');
184+
copyButton.className = 'copy-to-clipboard-button';
185+
copyButton.innerHTML = '📋';
186+
copyButton.title = 'Copy to clipboard';
187+
copyButton.onclick = async () => {
188+
try {
189+
await navigator.clipboard.writeText(content.value);
190+
copyButton.innerHTML = '✅';
191+
setTimeout(() => copyButton.innerHTML = '📋', 2000);
192+
} catch (err) {
193+
console.error('Failed to copy: ', err);
194+
copyButton.innerHTML = '❌';
195+
setTimeout(() => copyButton.innerHTML = '📋', 2000);
196+
}
197+
};
198+
pre.appendChild(copyButton);
199+
200+
snippetDiv.appendChild(pre);
201+
Prism.highlightElement(code);
202+
}
203+
});
204+
205+
snippetsContainer.appendChild(snippetDiv);
206+
});
207+
}
208+
209+
function renderTree(folders, snippets) {
210+
const folderMap = {};
211+
folders.forEach(folder => {
212+
folderMap[folder.id] = { ...folder, children: [], snippets: [] };
213+
});
214+
215+
// Group snippets by folder
216+
snippets.forEach(snippet => {
217+
if (folderMap[snippet.folderId]) {
218+
folderMap[snippet.folderId].snippets.push(snippet);
219+
}
220+
});
221+
222+
// Build tree
223+
const rootFolders = [];
224+
folders.forEach(folder => {
225+
if (!folder.parentId || !folderMap[folder.parentId]) {
226+
rootFolders.push(folderMap[folder.id]);
227+
} else {
228+
folderMap[folder.parentId].children.push(folderMap[folder.id]);
229+
}
230+
});
231+
232+
function buildTree(folders) {
233+
const ul = document.createElement('ul');
234+
ul.className = 'tree';
235+
folders.forEach(folder => {
236+
const li = document.createElement('li');
237+
const folderDiv = document.createElement('div');
238+
folderDiv.className = 'folder';
239+
folderDiv.textContent = folder.name;
240+
li.appendChild(folderDiv);
241+
242+
if (folder.snippets.length > 0 || folder.children.length > 0) {
243+
const subUl = document.createElement('ul');
244+
folder.children.forEach(child => {
245+
subUl.appendChild(buildTree([child]).firstChild);
246+
});
247+
folder.snippets.forEach(snippet => {
248+
const snippetLi = document.createElement('li');
249+
snippetLi.className = 'snippet-item';
250+
snippetLi.textContent = snippet.name;
251+
snippetLi.dataset.snippetId = snippet.id;
252+
subUl.appendChild(snippetLi);
253+
});
254+
li.appendChild(subUl);
255+
}
256+
ul.appendChild(li);
257+
});
258+
return ul;
259+
}
260+
261+
const tree = buildTree(rootFolders);
262+
document.getElementById('sidebar-content').appendChild(tree);
263+
}
264+
});

index.html

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>MassCode-Web</title>
7+
<script src="https://cdn.tailwindcss.com"></script>
8+
<script>
9+
tailwind.config = {
10+
darkMode: 'class'
11+
}
12+
</script>
13+
<link id="prism-theme" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />
14+
<link rel="stylesheet" href="styles.css">
15+
</head>
16+
<body class="bg-gray-100 font-sans flex flex-col h-screen dark:bg-gray-900 transition-colors">
17+
<header class="bg-gray-800 text-white p-4 flex items-center justify-between dark:bg-gray-900 transition-colors">
18+
<button id="sidebar-toggle" onclick="toggleSidebar()" class="bg-gray-700 text-white text-xl p-2 hover:bg-gray-600 rounded transition-colors dark:bg-gray-800 dark:hover:bg-gray-700" type="button"></button>
19+
<h1 class="text-2xl font-bold flex-1 text-center">massCode-Web</h1>
20+
<div class="flex items-center space-x-2">
21+
<select id="theme-select" class="bg-gray-700 text-white px-3 py-2 rounded border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:border-gray-500 transition-colors">
22+
<option value="auto">Auto</option>
23+
<option value="light">Light</option>
24+
<option value="dark">Dark</option>
25+
</select>
26+
<div class="relative">
27+
<input type="text" id="search" placeholder="Search snippets..." class="w-64 px-3 py-2 pr-8 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 transition-colors">
28+
<button id="clear-search" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors" onclick="clearSearch()" style="display: none;">×</button>
29+
</div>
30+
</div>
31+
</header>
32+
<div class="content flex flex-1 overflow-hidden">
33+
<aside id="sidebar" class="w-80 bg-gray-900 text-white flex flex-col transition-all duration-300 flex-shrink-0 dark:bg-gray-800">
34+
<div id="sidebar-content" class="flex-1 overflow-y-auto p-4">
35+
<!-- Tree structure will be rendered here -->
36+
</div>
37+
<footer class="p-4 border-t border-gray-700 dark:border-gray-600">
38+
<div class="flex items-center justify-between">
39+
<a href="https://github.com/codebude/masscode-web" target="_blank" class="text-gray-400 hover:text-white transition-colors">
40+
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
41+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
42+
</svg>
43+
</a>
44+
<span class="text-gray-400 text-sm">v1.0.0</span>
45+
</div>
46+
</footer>
47+
</aside>
48+
<main id="snippets-container" class="flex-1 p-4 overflow-y-auto bg-white dark:bg-gray-900 transition-colors">
49+
<!-- Snippets will be rendered here -->
50+
</main>
51+
</div>
52+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
53+
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
54+
<script src="app.js"></script>
55+
</body>
56+
</html>

0 commit comments

Comments
 (0)