Skip to content

Commit cdd7570

Browse files
author
Brian Genisio
committed
add a preview for the spreadsheet
1 parent b8175c3 commit cdd7570

File tree

6 files changed

+309
-25
lines changed

6 files changed

+309
-25
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
.env
33
spreadsheet.json
4+
.config-example.yaml

client/app.js

Lines changed: 207 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ let config = {
44
cellsToVerify: []
55
};
66

7+
// Escape HTML to prevent XSS attacks
8+
function escapeHtml(text) {
9+
if (text == null) return '';
10+
const map = {
11+
'&': '&',
12+
'<': '&lt;',
13+
'>': '&gt;',
14+
'"': '&quot;',
15+
"'": '&#x27;',
16+
'/': '&#x2F;'
17+
};
18+
return String(text).replace(/[&<>"'/]/g, s => map[s]);
19+
}
20+
721
// Load configuration on startup
822
async function loadConfig() {
923
try {
@@ -88,6 +102,7 @@ function updateStatus(message, type = 'info') {
88102
// Handle spreadsheet URL change
89103
function handleSpreadsheetURLChange(event) {
90104
config.spreadsheetURL = event.target.value;
105+
render();
91106
saveConfig();
92107
}
93108

@@ -112,13 +127,37 @@ function handleVerificationTypeChange(index, type) {
112127

113128
// Add new cell verification
114129
function addCellVerification() {
130+
const newIndex = config.cellsToVerify.length;
115131
config.cellsToVerify.push({
116132
cellName: '',
117133
expectedValue: '',
118134
expectedFunction: '',
119135
verificationType: 'value' // 'value' or 'function'
120136
});
121137
render();
138+
139+
// Scroll the newly added cell into view, ensuring the "Add Cell" button is also visible
140+
setTimeout(() => {
141+
const cellItems = document.querySelectorAll('.cell-item');
142+
const configEditor = document.querySelector('.config-editor');
143+
if (cellItems[newIndex] && configEditor) {
144+
// Scroll the cell into view
145+
cellItems[newIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
146+
147+
// Then scroll a bit more to ensure the "Add Cell" button is visible
148+
setTimeout(() => {
149+
const addButton = document.querySelector('.button-primary');
150+
if (addButton) {
151+
// Scroll the container a bit more to show the button
152+
const scrollAmount = addButton.getBoundingClientRect().bottom - configEditor.getBoundingClientRect().bottom + 20;
153+
if (scrollAmount > 0) {
154+
configEditor.scrollBy({ top: scrollAmount, behavior: 'smooth' });
155+
}
156+
}
157+
}, 300); // Wait for the first scroll to complete
158+
}
159+
}, 0);
160+
122161
saveConfig();
123162
}
124163

@@ -134,9 +173,147 @@ function render() {
134173
const app = document.getElementById('app');
135174
if (!app) return;
136175

176+
// Convert Google Sheets URL to embeddable format
177+
function getEmbedUrl(url) {
178+
if (!url) return '';
179+
// Extract spreadsheet ID from URL
180+
const match = url.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
181+
if (match) {
182+
const spreadsheetId = match[1];
183+
// Extract gid (sheet ID) if present
184+
const gidMatch = url.match(/[#&]gid=([0-9]+)/);
185+
const gid = gidMatch ? gidMatch[1] : '';
186+
187+
// Use edit endpoint which shows headers - clipboard access depends on Google's iframe policies
188+
let embedUrl = `https://docs.google.com/spreadsheets/d/${spreadsheetId}/edit`;
189+
const params = [];
190+
if (gid) {
191+
params.push(`gid=${gid}`);
192+
}
193+
// rm=minimal removes some UI but keeps headers visible
194+
params.push('rm=minimal');
195+
if (params.length > 0) {
196+
embedUrl += '?' + params.join('&');
197+
}
198+
return embedUrl;
199+
}
200+
return url;
201+
}
202+
203+
const spreadsheetUrl = config.spreadsheetURL || '';
204+
const embedUrl = getEmbedUrl(spreadsheetUrl);
205+
206+
// Check if URL changed - if not, we can preserve the iframe
207+
const existingIframe = document.getElementById('spreadsheet-iframe');
208+
const urlChanged = !existingIframe || existingIframe.src !== embedUrl;
209+
210+
// If URL hasn't changed, preserve the iframe by updating only the cells section
211+
if (!urlChanged && existingIframe && spreadsheetUrl) {
212+
// Only update the cells list, not the entire app
213+
const cellsList = document.getElementById('cells-list');
214+
if (cellsList) {
215+
cellsList.innerHTML = `
216+
${config.cellsToVerify.map((cell, index) => {
217+
const verificationType = cell.verificationType || (cell.expectedFunction ? 'function' : 'value');
218+
return `
219+
<div class="cell-item" style="border: 1px solid var(--Colors-Box-Stroke); border-radius: var(--UI-Radius-radius-s); padding: var(--UI-Spacing-spacing-l); margin-bottom: var(--UI-Spacing-spacing-m); background: var(--Colors-Box-Background);">
220+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--UI-Spacing-spacing-m);">
221+
<div style="display: flex; align-items: center; gap: var(--UI-Spacing-spacing-l);">
222+
<label class="input-radio input-radio-small" style="margin: 0;">
223+
<input
224+
type="radio"
225+
name="verification-type-${index}"
226+
value="value"
227+
${verificationType === 'value' ? 'checked' : ''}
228+
onchange="handleVerificationTypeChange(${index}, 'value')"
229+
/>
230+
<span class="input-radio-circle">
231+
<span class="input-radio-dot"></span>
232+
</span>
233+
<span class="input-radio-label">Value</span>
234+
</label>
235+
236+
<label class="input-radio input-radio-small" style="margin: 0;">
237+
<input
238+
type="radio"
239+
name="verification-type-${index}"
240+
value="function"
241+
${verificationType === 'function' ? 'checked' : ''}
242+
onchange="handleVerificationTypeChange(${index}, 'function')"
243+
/>
244+
<span class="input-radio-circle">
245+
<span class="input-radio-dot"></span>
246+
</span>
247+
<span class="input-radio-label">Function</span>
248+
</label>
249+
</div>
250+
<button class="button button-text" onclick="removeCellVerification(${index})" style="color: var(--Colors-Base-Accent-Red-600);">
251+
Remove
252+
</button>
253+
</div>
254+
255+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--UI-Spacing-spacing-m); margin-bottom: var(--UI-Spacing-spacing-m);">
256+
<div>
257+
<label style="display: block; margin-bottom: var(--UI-Spacing-spacing-xs); font-weight: 500; font-size: var(--Fonts-Body-Default-sm); color: var(--Colors-Text-Body-Medium);">
258+
Cell Name
259+
</label>
260+
<input
261+
type="text"
262+
class="input"
263+
style="width: 100%; box-sizing: border-box;"
264+
value="${escapeHtml(cell.cellName || '')}"
265+
placeholder="e.g., A1"
266+
onchange="handleCellChange(${index}, 'cellName', event.target.value)"
267+
/>
268+
</div>
269+
270+
${verificationType === 'value' ? `
271+
<div>
272+
<label style="display: block; margin-bottom: var(--UI-Spacing-spacing-xs); font-weight: 500; font-size: var(--Fonts-Body-Default-sm); color: var(--Colors-Text-Body-Medium);">
273+
Expected Value
274+
</label>
275+
<input
276+
type="text"
277+
class="input"
278+
style="width: 100%; box-sizing: border-box;"
279+
value="${escapeHtml(cell.expectedValue || '')}"
280+
placeholder="e.g., 10"
281+
onchange="handleCellChange(${index}, 'expectedValue', event.target.value)"
282+
/>
283+
</div>
284+
` : `
285+
<div>
286+
<label style="display: block; margin-bottom: var(--UI-Spacing-spacing-xs); font-weight: 500; font-size: var(--Fonts-Body-Default-sm); color: var(--Colors-Text-Body-Medium);">
287+
Expected Function
288+
</label>
289+
<input
290+
type="text"
291+
class="input"
292+
style="width: 100%; box-sizing: border-box;"
293+
value="${escapeHtml(cell.expectedFunction || '')}"
294+
placeholder="e.g., =SUM(A1:A10)"
295+
onchange="handleCellChange(${index}, 'expectedFunction', event.target.value)"
296+
/>
297+
</div>
298+
`}
299+
</div>
300+
</div>
301+
`;
302+
}).join('')}
303+
304+
${config.cellsToVerify.length === 0 ? `
305+
<div style="text-align: left; padding: var(--UI-Spacing-spacing-xl); color: var(--Colors-Text-Body-Light);">
306+
No cells configured. Click "Add Cell" below to get started.
307+
</div>
308+
` : ''}
309+
`;
310+
return; // Early return to avoid full re-render
311+
}
312+
}
313+
137314
app.innerHTML = `
138-
<div class="config-editor">
139-
<div class="box box-elevated" style="margin-bottom: var(--UI-Spacing-spacing-xl);">
315+
<div style="grid-column: 1 / -1; margin-bottom: var(--UI-Spacing-spacing-xl);">
316+
<div class="box card">
140317
<div style="width: 100%;">
141318
<label for="spreadsheet-url" class="label-medium" style="display: block; margin-bottom: var(--UI-Spacing-spacing-xs); color: var(--Colors-Text-Body-Medium); text-align: left;">
142319
Spreadsheet URL
@@ -146,16 +323,18 @@ function render() {
146323
id="spreadsheet-url"
147324
class="input"
148325
style="width: 100%; max-width: 100%; box-sizing: border-box;"
149-
value="${config.spreadsheetURL || ''}"
326+
value="${escapeHtml(spreadsheetUrl)}"
150327
placeholder="https://docs.google.com/spreadsheets/d/..."
151328
onchange="handleSpreadsheetURLChange(event)"
152329
/>
153330
</div>
154331
</div>
332+
</div>
155333
156-
<div class="box box-elevated" style="display: flex; flex-direction: column; align-items: flex-start;">
157-
<h2 class="heading-medium" style="margin-top: 0; margin-bottom: var(--UI-Spacing-spacing-xl); color: var(--Colors-Text-Body-Strong); width: 100%;">Cells to Verify</h2>
334+
<div class="config-editor">
335+
<h2 class="heading-medium" style="margin-top: 0; margin-bottom: var(--UI-Spacing-spacing-xl); color: var(--Colors-Text-Body-Strong);">Cells to Verify</h2>
158336
337+
<div class="box card" style="display: flex; flex-direction: column; align-items: flex-start;">
159338
<div id="cells-list" style="width: 100%;">
160339
${config.cellsToVerify.map((cell, index) => {
161340
const verificationType = cell.verificationType || (cell.expectedFunction ? 'function' : 'value');
@@ -205,7 +384,7 @@ function render() {
205384
type="text"
206385
class="input"
207386
style="width: 100%; box-sizing: border-box;"
208-
value="${cell.cellName || ''}"
387+
value="${escapeHtml(cell.cellName || '')}"
209388
placeholder="e.g., A1"
210389
onchange="handleCellChange(${index}, 'cellName', event.target.value)"
211390
/>
@@ -220,7 +399,7 @@ function render() {
220399
type="text"
221400
class="input"
222401
style="width: 100%; box-sizing: border-box;"
223-
value="${cell.expectedValue || ''}"
402+
value="${escapeHtml(cell.expectedValue || '')}"
224403
placeholder="e.g., 10"
225404
onchange="handleCellChange(${index}, 'expectedValue', event.target.value)"
226405
/>
@@ -234,7 +413,7 @@ function render() {
234413
type="text"
235414
class="input"
236415
style="width: 100%; box-sizing: border-box;"
237-
value="${cell.expectedFunction || ''}"
416+
value="${escapeHtml(cell.expectedFunction || '')}"
238417
placeholder="e.g., =SUM(A1:A10)"
239418
onchange="handleCellChange(${index}, 'expectedFunction', event.target.value)"
240419
/>
@@ -259,6 +438,26 @@ function render() {
259438
</div>
260439
</div>
261440
</div>
441+
442+
<div class="spreadsheet-preview">
443+
${spreadsheetUrl ? `
444+
<iframe
445+
id="spreadsheet-iframe"
446+
src="${escapeHtml(embedUrl)}"
447+
frameborder="0"
448+
allowfullscreen
449+
allow="clipboard-read; clipboard-write"
450+
></iframe>
451+
` : `
452+
<div class="spreadsheet-preview-placeholder" style="flex: 1; min-height: 0;">
453+
<div>
454+
<p style="font-size: var(--Fonts-Body-Default-md); color: var(--Colors-Text-Body-Light);">
455+
Enter a spreadsheet URL to see a preview here.
456+
</p>
457+
</div>
458+
</div>
459+
`}
460+
</div>
262461
`;
263462
}
264463

client/index.html

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,67 @@
3838
background: var(--Colors-Box-Background);
3939
border-bottom: 1px solid var(--Colors-Box-Stroke);
4040
gap: var(--UI-Spacing-spacing-m);
41+
height: 80px;
42+
box-sizing: border-box;
4143
}
4244

4345
.header h1 {
4446
margin: 0;
4547
color: var(--Colors-Text-Body-Strong);
4648
}
4749

50+
:root {
51+
--header-height: 80px;
52+
}
53+
4854
#app {
49-
max-width: 1200px;
50-
margin: 0 auto;
55+
display: grid;
56+
grid-template-columns: 1fr 1fr;
57+
grid-auto-rows: min-content 1fr;
58+
gap: var(--UI-Spacing-spacing-xl);
59+
padding: var(--UI-Spacing-spacing-xl);
60+
height: calc(100vh - var(--header-height));
61+
overflow: hidden;
62+
}
63+
64+
.config-editor {
65+
display: flex;
66+
flex-direction: column;
67+
overflow-y: auto;
68+
padding-right: var(--UI-Spacing-spacing-m);
69+
min-height: 0;
70+
height: 100%;
71+
}
72+
73+
.spreadsheet-preview {
74+
display: flex;
75+
flex-direction: column;
76+
min-height: 0;
77+
height: 100%;
78+
}
79+
80+
.spreadsheet-preview-placeholder {
81+
display: flex;
82+
align-items: center;
83+
justify-content: center;
84+
background: var(--Colors-Box-Background);
85+
border: 1px solid var(--Colors-Box-Stroke);
86+
border-radius: var(--UI-Radius-radius-s);
87+
color: var(--Colors-Text-Body-Light);
88+
text-align: center;
5189
padding: var(--UI-Spacing-spacing-xl);
5290
}
5391

92+
.spreadsheet-preview iframe {
93+
width: 100%;
94+
border: 2px solid var(--Colors-Box-Stroke);
95+
border-radius: var(--UI-Radius-radius-s);
96+
background: var(--Colors-Box-Background);
97+
flex: 1;
98+
min-height: 0;
99+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
100+
}
101+
54102
.config-editor h3 {
55103
font-size: var(--Fonts-Body-Default-md);
56104
font-weight: 600;

config-example.yaml

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)