Skip to content

Commit 90fda64

Browse files
authored
Add project management functionality with local storage
1 parent 8d5a1ab commit 90fda64

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

‎js/projects.js‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
(function () {
2+
'use strict';
3+
4+
var LS_KEY = 'dtd_projects';
5+
6+
function loadProjects() {
7+
try { return JSON.parse(localStorage.getItem(LS_KEY) || '[]'); } catch (e) { return []; }
8+
}
9+
function saveProjects(arr) {
10+
try { localStorage.setItem(LS_KEY, JSON.stringify(arr)); } catch (e) {}
11+
}
12+
function fmt(ts) {
13+
var d = new Date(ts);
14+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) +
15+
' · ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
16+
}
17+
function esc(s) {
18+
return String(s || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
19+
}
20+
function buildHTML(c) {
21+
return '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width,initial-scale=1">\n<title>' + (c.logoName||'Page') + '</title>\n<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/devtemdesign@0.0.2/style.css">\n<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">\n<script src="https://cdn.jsdelivr.net/npm/devtemdesign@0.0.2/script.js"><\/script>\n</head>\n<body>\n<div id="app"></div>\n<script>devtemDesign("app",' + JSON.stringify(c) + ');<\/script>\n</body>\n</html>';
22+
}
23+
24+
/* ── THEME ── */
25+
document.getElementById('btn-dark').addEventListener('click', function () {
26+
document.documentElement.removeAttribute('data-theme');
27+
document.getElementById('btn-dark').classList.add('active');
28+
document.getElementById('btn-light').classList.remove('active');
29+
});
30+
document.getElementById('btn-light').addEventListener('click', function () {
31+
document.documentElement.setAttribute('data-theme', 'light');
32+
document.getElementById('btn-light').classList.add('active');
33+
document.getElementById('btn-dark').classList.remove('active');
34+
});
35+
36+
/* ── PREVIEW MODAL ── */
37+
var previewModal = document.getElementById('preview-modal');
38+
var modalTitle = document.getElementById('modal-title');
39+
var modalFrame = document.getElementById('modal-frame');
40+
var modalClose = document.getElementById('modal-close');
41+
var modalEditBtn = document.getElementById('modal-edit-btn');
42+
var modalExportBtn = document.getElementById('modal-export-btn');
43+
var _modalCfg = null;
44+
var _modalId = null;
45+
46+
function openModal(id, name, cfg) {
47+
_modalCfg = cfg; _modalId = id;
48+
modalTitle.textContent = name;
49+
modalFrame.srcdoc = buildHTML(cfg);
50+
modalEditBtn.href = 'index.html?id=' + id;
51+
previewModal.classList.add('open');
52+
}
53+
modalClose.addEventListener('click', function () {
54+
previewModal.classList.remove('open');
55+
modalFrame.srcdoc = '';
56+
});
57+
previewModal.addEventListener('click', function (e) {
58+
if (e.target === previewModal) modalClose.click();
59+
});
60+
modalExportBtn.addEventListener('click', function () {
61+
if (!_modalCfg) return;
62+
var html = buildHTML(_modalCfg);
63+
var name = (_modalCfg.logoName||'page').toLowerCase().replace(/\s+/g,'-');
64+
var blob = new Blob([html],{type:'text/html'});
65+
var a = document.createElement('a');
66+
a.href = URL.createObjectURL(blob); a.download = name+'.html'; a.click();
67+
URL.revokeObjectURL(a.href);
68+
});
69+
document.addEventListener('keydown', function (e) {
70+
if (e.key === 'Escape' && previewModal.classList.contains('open')) modalClose.click();
71+
});
72+
73+
/* ── RENDER ── */
74+
var grid = document.getElementById('projects-grid');
75+
var emptyState = document.getElementById('empty-state');
76+
var countBadge = document.getElementById('count-badge');
77+
var searchInput= document.getElementById('search-input');
78+
79+
function render(filter) {
80+
var list = loadProjects();
81+
if (filter) {
82+
var f = filter.toLowerCase();
83+
list = list.filter(function (p) { return p.name.toLowerCase().includes(f); });
84+
}
85+
countBadge.textContent = loadProjects().length; // always real count
86+
87+
if (list.length === 0) {
88+
emptyState.style.display = 'flex';
89+
grid.innerHTML = '';
90+
return;
91+
}
92+
emptyState.style.display = 'none';
93+
94+
// Sort newest first
95+
list = list.slice().sort(function (a, b) { return b.updatedAt - a.updatedAt; });
96+
97+
grid.innerHTML = list.map(function (p) {
98+
return '<div class="proj-card" data-id="' + p.id + '">' +
99+
'<div class="proj-thumb" data-preview-id="' + p.id + '">' +
100+
'<iframe srcdoc="' + esc(buildHTML(p.config)) + '" sandbox="allow-scripts allow-same-origin"></iframe>' +
101+
'<div class="proj-thumb-overlay"><button class="ov-btn" data-preview-id="' + p.id + '"><i class="fas fa-eye"></i> Preview</button></div>' +
102+
'</div>' +
103+
'<div class="proj-body">' +
104+
'<div class="proj-name-row">' +
105+
'<span class="proj-name" data-rename-id="' + p.id + '">' + esc(p.name) + '</span>' +
106+
'<input class="proj-name-input" type="text" value="' + esc(p.name) + '" data-rename-input="' + p.id + '" maxlength="60">' +
107+
'<span class="proj-tpl">' + esc(p.config.template||'figsh') + '</span>' +
108+
'</div>' +
109+
'<div class="proj-meta">' + fmt(p.updatedAt) + '</div>' +
110+
'<div class="proj-actions">' +
111+
'<a class="pa-btn" href="/builder?id=' + p.id + '"><i class="fas fa-pen"></i> Edit</a>' +
112+
'<button class="pa-btn" data-preview-id="' + p.id + '"><i class="fas fa-eye"></i> Preview</button>' +
113+
'<button class="pa-btn danger" data-delete-id="' + p.id + '"><i class="fas fa-trash"></i> Delete</button>' +
114+
'</div>' +
115+
'</div>' +
116+
'</div>';
117+
}).join('');
118+
119+
/* rename: click name */
120+
grid.querySelectorAll('.proj-name[data-rename-id]').forEach(function (el) {
121+
el.addEventListener('click', function () {
122+
el.style.display = 'none';
123+
var inp = el.closest('.proj-name-row').querySelector('.proj-name-input');
124+
inp.style.display = 'block'; inp.focus(); inp.select();
125+
});
126+
});
127+
grid.querySelectorAll('.proj-name-input[data-rename-input]').forEach(function (inp) {
128+
function commit() {
129+
var val = inp.value.trim(); if (!val) { render(searchInput.value); return; }
130+
var id = inp.dataset.renameInput;
131+
var all = loadProjects(); var p = all.find(function(x){return x.id===id;});
132+
if (p) { p.name = val; saveProjects(all); }
133+
render(searchInput.value);
134+
}
135+
inp.addEventListener('blur', commit);
136+
inp.addEventListener('keydown', function (e) {
137+
if (e.key === 'Enter') { e.preventDefault(); commit(); }
138+
if (e.key === 'Escape') render(searchInput.value);
139+
});
140+
});
141+
142+
/* preview */
143+
grid.querySelectorAll('[data-preview-id]').forEach(function (el) {
144+
el.addEventListener('click', function (e) {
145+
e.preventDefault();
146+
var id = el.dataset.previewId;
147+
var all = loadProjects(); var p = all.find(function(x){return x.id===id;});
148+
if (p) openModal(p.id, p.name, p.config);
149+
});
150+
});
151+
152+
/* delete */
153+
grid.querySelectorAll('[data-delete-id]').forEach(function (btn) {
154+
btn.addEventListener('click', function () {
155+
if (!confirm('Delete "' + btn.closest('.proj-card').querySelector('.proj-name').textContent + '"?')) return;
156+
var id = btn.dataset.deleteId;
157+
saveProjects(loadProjects().filter(function(x){return x.id!==id;}));
158+
render(searchInput.value);
159+
});
160+
});
161+
}
162+
163+
searchInput.addEventListener('input', function () { render(this.value); });
164+
render();
165+
166+
})();

0 commit comments

Comments
 (0)