opentwig 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +323 -0
- package/API.md +582 -0
- package/CODE_OF_CONDUCT.md +91 -0
- package/CONTRIBUTING.md +312 -0
- package/README.md +171 -7
- package/SECURITY.md +56 -0
- package/THEME_DEVELOPMENT.md +388 -0
- package/package.json +19 -3
- package/src/constants.js +14 -2
- package/src/index.js +14 -2
- package/src/live-ui/editor.js +173 -0
- package/src/live-ui/preview.js +77 -0
- package/src/live-ui/sidebar.js +523 -0
- package/src/utils/escapeHTML.js +10 -0
- package/src/utils/generateOGImage.js +51 -10
- package/src/utils/parseArgs.js +33 -2
- package/src/utils/readImageAsBase64.js +16 -4
- package/src/utils/setupWatcher.js +69 -0
- package/src/utils/showHelp.js +15 -2
- package/src/utils/startLiveServer.js +218 -0
- package/src/utils/websocketServer.js +53 -0
- package/test-og.js +40 -0
- package/theme/dark/style.css +1 -0
- package/theme/default/index.js +10 -8
- package/validateConfig.js +59 -0
- package/vitest.config.js +11 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const previewFrame = document.getElementById('previewFrame');
|
|
2
|
+
const refreshBtn = document.getElementById('refreshBtn');
|
|
3
|
+
|
|
4
|
+
let ws = null;
|
|
5
|
+
|
|
6
|
+
const initWebSocket = () => {
|
|
7
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
8
|
+
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
|
9
|
+
|
|
10
|
+
ws = new WebSocket(wsUrl);
|
|
11
|
+
|
|
12
|
+
ws.onopen = () => {
|
|
13
|
+
console.log('WebSocket connected');
|
|
14
|
+
updateStatus('connected');
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
ws.onmessage = (event) => {
|
|
18
|
+
const message = JSON.parse(event.data);
|
|
19
|
+
|
|
20
|
+
switch (message.type) {
|
|
21
|
+
case 'reload':
|
|
22
|
+
console.log('Received reload signal');
|
|
23
|
+
reloadPreview();
|
|
24
|
+
break;
|
|
25
|
+
case 'config-update':
|
|
26
|
+
console.log('Config updated:', message.config);
|
|
27
|
+
break;
|
|
28
|
+
case 'theme-change':
|
|
29
|
+
console.log('Theme changed to:', message.theme);
|
|
30
|
+
reloadPreview();
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
ws.onclose = () => {
|
|
36
|
+
console.log('WebSocket disconnected');
|
|
37
|
+
updateStatus('disconnected');
|
|
38
|
+
setTimeout(initWebSocket, 3000);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
ws.onerror = (error) => {
|
|
42
|
+
console.error('WebSocket error:', error);
|
|
43
|
+
updateStatus('disconnected');
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const updateStatus = (status) => {
|
|
48
|
+
const statusIndicator = document.getElementById('statusIndicator');
|
|
49
|
+
const statusDot = statusIndicator.querySelector('.status-dot');
|
|
50
|
+
const statusText = statusIndicator.querySelector('.status-text');
|
|
51
|
+
|
|
52
|
+
statusDot.classList.remove('connected', 'disconnected');
|
|
53
|
+
statusDot.classList.add(status);
|
|
54
|
+
|
|
55
|
+
if (status === 'connected') {
|
|
56
|
+
statusText.textContent = 'Connected';
|
|
57
|
+
} else {
|
|
58
|
+
statusText.textContent = 'Disconnected';
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const reloadPreview = () => {
|
|
63
|
+
const currentUrl = previewFrame.src;
|
|
64
|
+
const separator = currentUrl.includes('?') ? '&' : '?';
|
|
65
|
+
previewFrame.src = `${currentUrl}${separator}t=${Date.now()}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const refreshBtnHandler = () => {
|
|
69
|
+
reloadPreview();
|
|
70
|
+
showNotification('Preview refreshed', 'info');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
refreshBtn.addEventListener('click', refreshBtnHandler);
|
|
74
|
+
|
|
75
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
76
|
+
initWebSocket();
|
|
77
|
+
});
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
const renderConfigForm = (config) => {
|
|
2
|
+
const formContainer = document.getElementById('configForm');
|
|
3
|
+
formContainer.innerHTML = '';
|
|
4
|
+
|
|
5
|
+
renderThemeSection(formContainer, config);
|
|
6
|
+
renderProfileSection(formContainer, config);
|
|
7
|
+
renderLinksSection(formContainer, config);
|
|
8
|
+
renderFooterLinksSection(formContainer, config);
|
|
9
|
+
renderShareSection(formContainer, config);
|
|
10
|
+
renderAdvancedSection(formContainer, config);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const renderThemeSection = (container, config) => {
|
|
14
|
+
const section = document.createElement('div');
|
|
15
|
+
section.className = 'section';
|
|
16
|
+
|
|
17
|
+
const title = document.createElement('h3');
|
|
18
|
+
title.className = 'section-title';
|
|
19
|
+
title.textContent = 'Theme';
|
|
20
|
+
|
|
21
|
+
const themeGrid = document.createElement('div');
|
|
22
|
+
themeGrid.className = 'theme-grid';
|
|
23
|
+
|
|
24
|
+
const themes = ['default', 'dark', 'minimal', 'colorful'];
|
|
25
|
+
|
|
26
|
+
themes.forEach(theme => {
|
|
27
|
+
const card = document.createElement('div');
|
|
28
|
+
card.className = `theme-card ${config.theme === theme ? 'active' : ''}`;
|
|
29
|
+
|
|
30
|
+
const name = document.createElement('div');
|
|
31
|
+
name.className = 'theme-card-name';
|
|
32
|
+
name.textContent = theme.charAt(0).toUpperCase() + theme.slice(1);
|
|
33
|
+
|
|
34
|
+
card.appendChild(name);
|
|
35
|
+
card.addEventListener('click', () => {
|
|
36
|
+
config.theme = theme;
|
|
37
|
+
window.configEditor.updateConfig(config);
|
|
38
|
+
document.querySelectorAll('.theme-card').forEach(c => c.classList.remove('active'));
|
|
39
|
+
card.classList.add('active');
|
|
40
|
+
window.configEditor.autoSave(config);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
themeGrid.appendChild(card);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
section.appendChild(title);
|
|
47
|
+
section.appendChild(themeGrid);
|
|
48
|
+
container.appendChild(section);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const renderProfileSection = (container, config) => {
|
|
52
|
+
const section = document.createElement('div');
|
|
53
|
+
section.className = 'section';
|
|
54
|
+
|
|
55
|
+
const title = document.createElement('h3');
|
|
56
|
+
title.className = 'section-title';
|
|
57
|
+
title.textContent = 'Profile';
|
|
58
|
+
|
|
59
|
+
const avatarUpload = document.createElement('div');
|
|
60
|
+
avatarUpload.className = 'avatar-upload';
|
|
61
|
+
|
|
62
|
+
const avatarPreview = document.createElement('img');
|
|
63
|
+
avatarPreview.className = 'avatar-preview';
|
|
64
|
+
avatarPreview.src = config.avatar && config.avatar.path ? `/${config.avatar.path}` : 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><circle cx="24" cy="24" r="24" fill="%23e5e5e5"/><text x="24" y="30" font-size="24" text-anchor="middle" fill="%23999">👤</text></svg>';
|
|
65
|
+
|
|
66
|
+
const avatarText = document.createElement('div');
|
|
67
|
+
avatarText.className = 'avatar-upload-text';
|
|
68
|
+
avatarText.textContent = config.avatar && config.avatar.path ? 'Click to change avatar' : 'Click to upload avatar';
|
|
69
|
+
|
|
70
|
+
const avatarInput = document.createElement('input');
|
|
71
|
+
avatarInput.type = 'file';
|
|
72
|
+
avatarInput.accept = 'image/*';
|
|
73
|
+
avatarInput.style.display = 'none';
|
|
74
|
+
|
|
75
|
+
avatarUpload.addEventListener('click', () => {
|
|
76
|
+
avatarInput.click();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
avatarInput.addEventListener('change', async (e) => {
|
|
80
|
+
const file = e.target.files[0];
|
|
81
|
+
if (file) {
|
|
82
|
+
const formData = new FormData();
|
|
83
|
+
formData.append('avatar', file);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch('/api/avatar', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: formData
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = await response.json();
|
|
92
|
+
|
|
93
|
+
if (result.success) {
|
|
94
|
+
config.avatar = { path: result.path };
|
|
95
|
+
window.configEditor.updateConfig(config);
|
|
96
|
+
avatarPreview.src = `/${result.path}`;
|
|
97
|
+
avatarText.textContent = 'Click to change avatar';
|
|
98
|
+
window.configEditor.autoSave(config);
|
|
99
|
+
window.configEditor.showNotification('Avatar uploaded', 'success');
|
|
100
|
+
} else {
|
|
101
|
+
window.configEditor.showNotification('Failed to upload avatar', 'error');
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error uploading avatar:', error);
|
|
105
|
+
window.configEditor.showNotification('Failed to upload avatar', 'error');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const removeAvatarBtn = document.createElement('button');
|
|
111
|
+
removeAvatarBtn.className = 'btn btn-secondary';
|
|
112
|
+
removeAvatarBtn.style.width = '100%';
|
|
113
|
+
removeAvatarBtn.style.marginTop = '8px';
|
|
114
|
+
removeAvatarBtn.textContent = 'Remove Avatar';
|
|
115
|
+
removeAvatarBtn.addEventListener('click', (e) => {
|
|
116
|
+
e.stopPropagation();
|
|
117
|
+
config.avatar = null;
|
|
118
|
+
window.configEditor.updateConfig(config);
|
|
119
|
+
avatarPreview.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><circle cx="24" cy="24" r="24" fill="%23e5e5e5"/><text x="24" y="30" font-size="24" text-anchor="middle" fill="%23999">👤</text></svg>';
|
|
120
|
+
avatarText.textContent = 'Click to upload avatar';
|
|
121
|
+
window.configEditor.autoSave(config);
|
|
122
|
+
window.configEditor.showNotification('Avatar removed', 'success');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
avatarUpload.appendChild(avatarPreview);
|
|
126
|
+
avatarUpload.appendChild(avatarText);
|
|
127
|
+
avatarUpload.appendChild(avatarInput);
|
|
128
|
+
|
|
129
|
+
const urlGroup = createFormGroup('URL', 'url', config.url, 'text', (value) => {
|
|
130
|
+
const errors = window.configEditor.validateField('url', value);
|
|
131
|
+
if (errors.length === 0) {
|
|
132
|
+
config.url = value;
|
|
133
|
+
window.configEditor.updateConfig(config);
|
|
134
|
+
window.configEditor.autoSave(config);
|
|
135
|
+
}
|
|
136
|
+
return errors;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const nameGroup = createFormGroup('Name', 'name', config.name, 'text', (value) => {
|
|
140
|
+
const errors = window.configEditor.validateField('name', value);
|
|
141
|
+
if (errors.length === 0) {
|
|
142
|
+
config.name = value;
|
|
143
|
+
window.configEditor.updateConfig(config);
|
|
144
|
+
window.configEditor.autoSave(config);
|
|
145
|
+
}
|
|
146
|
+
return errors;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const contentGroup = createFormGroup('Bio', 'content', config.content, 'textarea', (value) => {
|
|
150
|
+
config.content = value;
|
|
151
|
+
window.configEditor.updateConfig(config);
|
|
152
|
+
window.configEditor.autoSave(config);
|
|
153
|
+
return [];
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
section.appendChild(title);
|
|
157
|
+
section.appendChild(avatarUpload);
|
|
158
|
+
section.appendChild(removeAvatarBtn);
|
|
159
|
+
section.appendChild(urlGroup);
|
|
160
|
+
section.appendChild(nameGroup);
|
|
161
|
+
section.appendChild(contentGroup);
|
|
162
|
+
container.appendChild(section);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const renderLinksSection = (container, config) => {
|
|
166
|
+
const section = document.createElement('div');
|
|
167
|
+
section.className = 'section';
|
|
168
|
+
|
|
169
|
+
const title = document.createElement('h3');
|
|
170
|
+
title.className = 'section-title';
|
|
171
|
+
title.textContent = 'Links';
|
|
172
|
+
|
|
173
|
+
const linksContainer = document.createElement('div');
|
|
174
|
+
linksContainer.id = 'linksContainer';
|
|
175
|
+
|
|
176
|
+
const addLinkBtn = document.createElement('button');
|
|
177
|
+
addLinkBtn.className = 'add-link-btn';
|
|
178
|
+
addLinkBtn.textContent = '+ Add Link';
|
|
179
|
+
addLinkBtn.addEventListener('click', () => {
|
|
180
|
+
config.links.push({ title: '', url: '' });
|
|
181
|
+
window.configEditor.updateConfig(config);
|
|
182
|
+
window.configEditor.autoSave(config);
|
|
183
|
+
renderLinksSection(container, config);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
section.appendChild(title);
|
|
187
|
+
section.appendChild(linksContainer);
|
|
188
|
+
section.appendChild(addLinkBtn);
|
|
189
|
+
container.appendChild(section);
|
|
190
|
+
|
|
191
|
+
config.links.forEach((link, index) => {
|
|
192
|
+
renderLinkItem(linksContainer, config, link, index);
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const renderLinkItem = (container, config, link, index) => {
|
|
197
|
+
const item = document.createElement('div');
|
|
198
|
+
item.className = 'link-item';
|
|
199
|
+
|
|
200
|
+
const header = document.createElement('div');
|
|
201
|
+
header.className = 'link-item-header';
|
|
202
|
+
|
|
203
|
+
const title = document.createElement('span');
|
|
204
|
+
title.className = 'link-item-title';
|
|
205
|
+
title.textContent = link.title || link.url || `Link ${index + 1}`;
|
|
206
|
+
|
|
207
|
+
const actions = document.createElement('div');
|
|
208
|
+
actions.className = 'link-item-actions';
|
|
209
|
+
|
|
210
|
+
const moveUpBtn = document.createElement('button');
|
|
211
|
+
moveUpBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>';
|
|
212
|
+
moveUpBtn.disabled = index === 0;
|
|
213
|
+
moveUpBtn.addEventListener('click', () => {
|
|
214
|
+
if (index > 0) {
|
|
215
|
+
const temp = config.links[index];
|
|
216
|
+
config.links[index] = config.links[index - 1];
|
|
217
|
+
config.links[index - 1] = temp;
|
|
218
|
+
window.configEditor.updateConfig(config);
|
|
219
|
+
window.configEditor.autoSave(config);
|
|
220
|
+
renderLinksSection(document.getElementById('linksContainer').parentElement, config);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const moveDownBtn = document.createElement('button');
|
|
225
|
+
moveDownBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
226
|
+
moveDownBtn.disabled = index === config.links.length - 1;
|
|
227
|
+
moveDownBtn.addEventListener('click', () => {
|
|
228
|
+
if (index < config.links.length - 1) {
|
|
229
|
+
const temp = config.links[index];
|
|
230
|
+
config.links[index] = config.links[index + 1];
|
|
231
|
+
config.links[index + 1] = temp;
|
|
232
|
+
window.configEditor.updateConfig(config);
|
|
233
|
+
window.configEditor.autoSave(config);
|
|
234
|
+
renderLinksSection(document.getElementById('linksContainer').parentElement, config);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const deleteBtn = document.createElement('button');
|
|
239
|
+
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
|
|
240
|
+
deleteBtn.addEventListener('click', () => {
|
|
241
|
+
config.links.splice(index, 1);
|
|
242
|
+
window.configEditor.updateConfig(config);
|
|
243
|
+
window.configEditor.autoSave(config);
|
|
244
|
+
renderLinksSection(document.getElementById('linksContainer').parentElement, config);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
actions.appendChild(moveUpBtn);
|
|
248
|
+
actions.appendChild(moveDownBtn);
|
|
249
|
+
actions.appendChild(deleteBtn);
|
|
250
|
+
|
|
251
|
+
header.appendChild(title);
|
|
252
|
+
header.appendChild(actions);
|
|
253
|
+
|
|
254
|
+
const titleInput = createFormGroup('Title', '', link.title, 'text', (value) => {
|
|
255
|
+
link.title = value;
|
|
256
|
+
title.textContent = value || link.url || `Link ${index + 1}`;
|
|
257
|
+
window.configEditor.updateConfig(config);
|
|
258
|
+
window.configEditor.autoSave(config);
|
|
259
|
+
return [];
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const urlInput = createFormGroup('URL', '', link.url, 'text', (value) => {
|
|
263
|
+
const errors = window.configEditor.validateField('url', value);
|
|
264
|
+
if (errors.length === 0) {
|
|
265
|
+
link.url = value;
|
|
266
|
+
title.textContent = link.title || value || `Link ${index + 1}`;
|
|
267
|
+
window.configEditor.updateConfig(config);
|
|
268
|
+
window.configEditor.autoSave(config);
|
|
269
|
+
}
|
|
270
|
+
return errors;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
item.appendChild(header);
|
|
274
|
+
item.appendChild(titleInput);
|
|
275
|
+
item.appendChild(urlInput);
|
|
276
|
+
container.appendChild(item);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const renderFooterLinksSection = (container, config) => {
|
|
280
|
+
const section = document.createElement('div');
|
|
281
|
+
section.className = 'section';
|
|
282
|
+
|
|
283
|
+
const title = document.createElement('h3');
|
|
284
|
+
title.className = 'section-title';
|
|
285
|
+
title.textContent = 'Footer Links';
|
|
286
|
+
|
|
287
|
+
const footerLinksContainer = document.createElement('div');
|
|
288
|
+
footerLinksContainer.id = 'footerLinksContainer';
|
|
289
|
+
|
|
290
|
+
const addFooterLinkBtn = document.createElement('button');
|
|
291
|
+
addFooterLinkBtn.className = 'add-link-btn';
|
|
292
|
+
addFooterLinkBtn.textContent = '+ Add Footer Link';
|
|
293
|
+
addFooterLinkBtn.addEventListener('click', () => {
|
|
294
|
+
config.footerLinks.push({ title: '', url: '' });
|
|
295
|
+
window.configEditor.updateConfig(config);
|
|
296
|
+
window.configEditor.autoSave(config);
|
|
297
|
+
renderFooterLinksSection(container, config);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
section.appendChild(title);
|
|
301
|
+
section.appendChild(footerLinksContainer);
|
|
302
|
+
section.appendChild(addFooterLinkBtn);
|
|
303
|
+
container.appendChild(section);
|
|
304
|
+
|
|
305
|
+
config.footerLinks.forEach((link, index) => {
|
|
306
|
+
renderFooterLinkItem(footerLinksContainer, config, link, index);
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const renderFooterLinkItem = (container, config, link, index) => {
|
|
311
|
+
const item = document.createElement('div');
|
|
312
|
+
item.className = 'link-item';
|
|
313
|
+
|
|
314
|
+
const header = document.createElement('div');
|
|
315
|
+
header.className = 'link-item-header';
|
|
316
|
+
|
|
317
|
+
const title = document.createElement('span');
|
|
318
|
+
title.className = 'link-item-title';
|
|
319
|
+
title.textContent = link.title || link.content || `Footer Link ${index + 1}`;
|
|
320
|
+
|
|
321
|
+
const actions = document.createElement('div');
|
|
322
|
+
actions.className = 'link-item-actions';
|
|
323
|
+
|
|
324
|
+
const deleteBtn = document.createElement('button');
|
|
325
|
+
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
|
|
326
|
+
deleteBtn.addEventListener('click', () => {
|
|
327
|
+
config.footerLinks.splice(index, 1);
|
|
328
|
+
window.configEditor.updateConfig(config);
|
|
329
|
+
window.configEditor.autoSave(config);
|
|
330
|
+
renderFooterLinksSection(document.getElementById('footerLinksContainer').parentElement, config);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
actions.appendChild(deleteBtn);
|
|
334
|
+
|
|
335
|
+
header.appendChild(title);
|
|
336
|
+
header.appendChild(actions);
|
|
337
|
+
|
|
338
|
+
const titleInput = createFormGroup('Title', '', link.title, 'text', (value) => {
|
|
339
|
+
link.title = value;
|
|
340
|
+
title.textContent = value || link.content || `Footer Link ${index + 1}`;
|
|
341
|
+
window.configEditor.updateConfig(config);
|
|
342
|
+
window.configEditor.autoSave(config);
|
|
343
|
+
return [];
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const urlInput = createFormGroup('URL', '', link.url, 'text', (value) => {
|
|
347
|
+
link.url = value;
|
|
348
|
+
delete link.content;
|
|
349
|
+
window.configEditor.updateConfig(config);
|
|
350
|
+
window.configEditor.autoSave(config);
|
|
351
|
+
return [];
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const contentInput = createFormGroup('Content (Modal)', '', link.content, 'textarea', (value) => {
|
|
355
|
+
link.content = value;
|
|
356
|
+
delete link.url;
|
|
357
|
+
window.configEditor.updateConfig(config);
|
|
358
|
+
window.configEditor.autoSave(config);
|
|
359
|
+
return [];
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
item.appendChild(header);
|
|
363
|
+
item.appendChild(titleInput);
|
|
364
|
+
item.appendChild(urlInput);
|
|
365
|
+
item.appendChild(contentInput);
|
|
366
|
+
container.appendChild(item);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const renderShareSection = (container, config) => {
|
|
370
|
+
const section = document.createElement('div');
|
|
371
|
+
section.className = 'section';
|
|
372
|
+
|
|
373
|
+
const title = document.createElement('h3');
|
|
374
|
+
title.className = 'section-title';
|
|
375
|
+
title.textContent = 'Share Settings';
|
|
376
|
+
|
|
377
|
+
const shareTitleGroup = createFormGroup('Share Title', '', config.share.title, 'text', (value) => {
|
|
378
|
+
config.share.title = value;
|
|
379
|
+
window.configEditor.updateConfig(config);
|
|
380
|
+
window.configEditor.autoSave(config);
|
|
381
|
+
return [];
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const shareUrlGroup = createFormGroup('Share URL', '', config.share.url, 'text', (value) => {
|
|
385
|
+
config.share.url = value;
|
|
386
|
+
window.configEditor.updateConfig(config);
|
|
387
|
+
window.configEditor.autoSave(config);
|
|
388
|
+
return [];
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const shareTextGroup = createFormGroup('Share Button Text', '', config.share.text, 'text', (value) => {
|
|
392
|
+
config.share.text = value;
|
|
393
|
+
window.configEditor.updateConfig(config);
|
|
394
|
+
window.configEditor.autoSave(config);
|
|
395
|
+
return [];
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
section.appendChild(title);
|
|
399
|
+
section.appendChild(shareTitleGroup);
|
|
400
|
+
section.appendChild(shareUrlGroup);
|
|
401
|
+
section.appendChild(shareTextGroup);
|
|
402
|
+
container.appendChild(section);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const renderAdvancedSection = (container, config) => {
|
|
406
|
+
const section = document.createElement('div');
|
|
407
|
+
section.className = 'section';
|
|
408
|
+
|
|
409
|
+
const title = document.createElement('h3');
|
|
410
|
+
title.className = 'section-title';
|
|
411
|
+
title.textContent = 'Advanced';
|
|
412
|
+
|
|
413
|
+
const pageTitleGroup = createFormGroup('Page Title', '', config.title, 'text', (value) => {
|
|
414
|
+
config.title = value;
|
|
415
|
+
window.configEditor.updateConfig(config);
|
|
416
|
+
window.configEditor.autoSave(config);
|
|
417
|
+
return [];
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const minifyGroup = document.createElement('div');
|
|
421
|
+
minifyGroup.className = 'form-group';
|
|
422
|
+
|
|
423
|
+
const label = document.createElement('label');
|
|
424
|
+
label.textContent = 'Minify CSS';
|
|
425
|
+
|
|
426
|
+
const checkbox = document.createElement('input');
|
|
427
|
+
checkbox.type = 'checkbox';
|
|
428
|
+
checkbox.checked = config.minify;
|
|
429
|
+
checkbox.addEventListener('change', (e) => {
|
|
430
|
+
config.minify = e.target.checked;
|
|
431
|
+
window.configEditor.updateConfig(config);
|
|
432
|
+
window.configEditor.autoSave(config);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
minifyGroup.appendChild(label);
|
|
436
|
+
minifyGroup.appendChild(checkbox);
|
|
437
|
+
|
|
438
|
+
section.appendChild(title);
|
|
439
|
+
section.appendChild(pageTitleGroup);
|
|
440
|
+
section.appendChild(minifyGroup);
|
|
441
|
+
container.appendChild(section);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const createFormGroup = (label, name, value, type, onChange) => {
|
|
445
|
+
const group = document.createElement('div');
|
|
446
|
+
group.className = 'form-group';
|
|
447
|
+
|
|
448
|
+
const labelElement = document.createElement('label');
|
|
449
|
+
labelElement.textContent = label;
|
|
450
|
+
|
|
451
|
+
let input;
|
|
452
|
+
|
|
453
|
+
if (type === 'textarea') {
|
|
454
|
+
input = document.createElement('textarea');
|
|
455
|
+
input.value = value || '';
|
|
456
|
+
input.addEventListener('input', (e) => {
|
|
457
|
+
const errors = onChange(e.target.value);
|
|
458
|
+
if (errors.length > 0) {
|
|
459
|
+
input.style.borderColor = '#f5222d';
|
|
460
|
+
showErrors(input, errors);
|
|
461
|
+
} else {
|
|
462
|
+
input.style.borderColor = '';
|
|
463
|
+
clearErrors(input);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
} else {
|
|
467
|
+
input = document.createElement('input');
|
|
468
|
+
input.type = type;
|
|
469
|
+
input.value = value || '';
|
|
470
|
+
input.addEventListener('input', (e) => {
|
|
471
|
+
const errors = onChange(e.target.value);
|
|
472
|
+
if (errors.length > 0) {
|
|
473
|
+
input.style.borderColor = '#f5222d';
|
|
474
|
+
showErrors(input, errors);
|
|
475
|
+
} else {
|
|
476
|
+
input.style.borderColor = '';
|
|
477
|
+
clearErrors(input);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
group.appendChild(labelElement);
|
|
483
|
+
group.appendChild(input);
|
|
484
|
+
|
|
485
|
+
return group;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const showErrors = (input, errors) => {
|
|
489
|
+
const existingError = input.parentNode.querySelector('.error-message');
|
|
490
|
+
if (existingError) {
|
|
491
|
+
existingError.remove();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const errorDiv = document.createElement('div');
|
|
495
|
+
errorDiv.className = 'error-message';
|
|
496
|
+
errorDiv.style.color = '#f5222d';
|
|
497
|
+
errorDiv.style.fontSize = '11px';
|
|
498
|
+
errorDiv.style.marginTop = '4px';
|
|
499
|
+
errorDiv.textContent = errors[0];
|
|
500
|
+
|
|
501
|
+
input.parentNode.appendChild(errorDiv);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const clearErrors = (input) => {
|
|
505
|
+
const errorElement = input.parentNode.querySelector('.error-message');
|
|
506
|
+
if (errorElement) {
|
|
507
|
+
errorElement.remove();
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const toggleSidebar = () => {
|
|
512
|
+
const sidebar = document.querySelector('.sidebar');
|
|
513
|
+
sidebar.classList.toggle('collapsed');
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
517
|
+
const toggleBtn = document.getElementById('toggleSidebar');
|
|
518
|
+
if (toggleBtn) {
|
|
519
|
+
toggleBtn.addEventListener('click', toggleSidebar);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
window.renderConfigForm = renderConfigForm;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Escapes HTML special characters to prevent injection/malformed HTML
|
|
2
|
+
module.exports = function escapeHTML(str = '') {
|
|
3
|
+
return String(str).replace(/[&<>"']/g, (c) => ({
|
|
4
|
+
'&': '&',
|
|
5
|
+
'<': '<',
|
|
6
|
+
'>': '>',
|
|
7
|
+
'"': '"',
|
|
8
|
+
"'": '''
|
|
9
|
+
}[c]));
|
|
10
|
+
};
|