benresnic commited on
Commit
ef329eb
·
verified ·
1 Parent(s): 5ca7459

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -115
app.py CHANGED
@@ -199,16 +199,18 @@ app_ui = ui.page_fluid(
199
  color: var(--navy);
200
  border: 1px solid rgba(0,0,0,.08);
201
  border-radius: 12px;
202
- padding: 10px 18px;
203
  font-weight: 900;
204
- font-size: 20px;
205
  line-height: 1;
206
  cursor: pointer;
207
  box-shadow: 0 2px 8px rgba(0,0,0,.14);
208
  }
 
209
 
210
  .err{
211
- margin: 0 6px 10px 6px;
 
212
  padding: 10px 12px;
213
  border-radius: 10px;
214
  background: #fff1f2;
@@ -218,102 +220,93 @@ app_ui = ui.page_fluid(
218
  display:none;
219
  }
220
 
221
- /* Grid */
222
  .cards-grid{
223
  display:grid;
224
- grid-template-columns: repeat(4, minmax(260px, 1fr));
225
- gap: 16px;
226
- padding: 2px 6px 18px 6px;
227
- align-items: start;
228
- }
229
- @media (max-width: 1200px){
230
- .cards-grid{ grid-template-columns: repeat(3, minmax(240px, 1fr)); }
231
- }
232
- @media (max-width: 900px){
233
- .cards-grid{ grid-template-columns: repeat(2, minmax(240px, 1fr)); }
234
  }
235
- @media (max-width: 600px){
236
  .cards-grid{ grid-template-columns: 1fr; }
237
  }
238
 
239
- /* Card = TRUE square; content cannot force it taller */
240
  .card{
241
  background: var(--cardBg);
242
  border: 1px solid var(--cardBorder);
243
- border-radius: 12px;
244
  box-shadow: 0 1px 0 rgba(0,0,0,.02);
245
  aspect-ratio: 1 / 1;
246
  width: 100%;
247
  overflow: hidden;
248
- display: flex;
249
- flex-direction: column;
250
- min-height: 0; /* critical so children don't "push" height */
251
  }
252
 
253
  .card-header{
254
  display:flex;
255
  align-items:center;
256
  justify-content:space-between;
257
- padding: 10px 10px 6px 10px;
258
  border-bottom: 1px solid #eef2f8;
259
  flex: 0 0 auto;
260
  }
 
 
 
 
 
 
 
 
 
 
261
 
262
  .card-main{
263
- padding: 10px;
264
  display:flex;
265
  flex-direction:column;
266
- gap: 8px;
267
  flex: 1 1 auto;
268
- min-height: 0; /* allow flex children to shrink */
269
- overflow: hidden;/* NO card scrolling */
270
  }
271
 
272
  .card-footer{
273
- padding: 10px;
274
  border-top: 1px solid #eef2f8;
275
- flex: 0 0 auto; /* always visible */
276
- }
277
-
278
- .del-btn{
279
- border: none;
280
- background: transparent;
281
- color: #7b879c;
282
- font-weight: 900;
283
- font-size: 18px;
284
- cursor:pointer;
285
- padding: 0 6px;
286
  }
287
 
288
  label{
289
  display:block;
290
  font-size: 12px;
291
  color: var(--muted);
292
- margin: 0 0 4px;
293
- font-weight: 650;
 
294
  }
295
 
296
  input, textarea{
297
  width:100%;
298
  box-sizing:border-box;
299
  border: 1px solid #d8e1ee;
300
- border-radius: 10px;
301
- padding: 8px 10px;
302
  font-size: 14px;
303
  outline: none;
304
  background: white;
305
  }
306
 
307
- /* Flex textareas so they fit inside the square; scroll inside textarea only */
308
- .grow{
309
- display:flex;
310
- flex-direction:column;
311
- flex: 1 1 0;
312
- min-height: 0;
313
- }
314
- .grow textarea{
315
- flex: 1 1 0;
316
- min-height: 0;
317
  resize: none;
318
  overflow: auto;
319
  }
@@ -325,7 +318,8 @@ app_ui = ui.page_fluid(
325
  }
326
 
327
  .empty{
328
- margin: 0 6px;
 
329
  padding: 18px;
330
  background: white;
331
  border: 1px dashed #cfd9ea;
@@ -341,10 +335,10 @@ app_ui = ui.page_fluid(
341
  display:flex;
342
  align-items:center;
343
  justify-content:center;
344
- z-index: 9999;
345
  }
346
  .modal{
347
- width: min(520px, calc(100vw - 28px));
348
  background: white;
349
  border-radius: 14px;
350
  box-shadow: 0 10px 30px rgba(0,0,0,.25);
@@ -367,7 +361,7 @@ app_ui = ui.page_fluid(
367
  }
368
  .modal .checks label{
369
  margin: 0;
370
- font-weight: 600;
371
  font-size: 13px;
372
  color: #1f2a44;
373
  display:flex;
@@ -381,9 +375,9 @@ app_ui = ui.page_fluid(
381
  margin-top: 12px;
382
  }
383
  .btn{
384
- border-radius: 10px;
385
- padding: 8px 12px;
386
- font-weight: 800;
387
  cursor:pointer;
388
  border: 1px solid #d8e1ee;
389
  background: #ffffff;
@@ -447,71 +441,90 @@ app_ui = ui.page_fluid(
447
  .replaceAll(">", ">");
448
  }
449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  function openDeleteModal(cardId){
451
- const backdrop = document.createElement("div");
452
- backdrop.className = "modal-backdrop";
453
- backdrop.innerHTML = `
454
- <div class="modal" role="dialog" aria-modal="true">
455
- <h3>Delete card?</h3>
456
- <div class="muted">This is permanent and affects everyone viewing this Space.</div>
457
-
458
- <div class="checks">
459
- <label><input type="checkbox" id="chk1" /> I understand this is permanent</label>
460
- <label><input type="checkbox" id="chk2" /> I confirm I want to delete this card</label>
461
- </div>
 
462
 
463
- <label style="margin:8px 0 4px; color:#1f2a44; font-weight:700;">Type DELETE to confirm</label>
464
- <input type="text" id="typed" value="" />
465
 
466
- <div class="actions">
467
- <button class="btn" id="cancel">Cancel</button>
468
- <button class="btn btn-danger" id="confirm" disabled>Delete</button>
 
469
  </div>
470
- </div>
471
- `;
472
- document.body.appendChild(backdrop);
473
-
474
- const chk1 = backdrop.querySelector("#chk1");
475
- const chk2 = backdrop.querySelector("#chk2");
476
- const typed = backdrop.querySelector("#typed");
477
- const cancel = backdrop.querySelector("#cancel");
478
- const confirm = backdrop.querySelector("#confirm");
479
-
480
- function updateEnabled(){
481
- confirm.disabled = !(chk1.checked && chk2.checked && (typed.value || "") === "DELETE");
482
- }
483
- chk1.addEventListener("change", updateEnabled);
484
- chk2.addEventListener("change", updateEnabled);
485
- typed.addEventListener("input", updateEnabled);
486
 
487
- function close(){ backdrop.remove(); }
488
- cancel.addEventListener("click", close);
489
- backdrop.addEventListener("click", (e) => { if(e.target === backdrop) close(); });
 
 
490
 
491
- confirm.addEventListener("click", async () => {
492
- try{
493
- const data = await api(`/api/cards/${encodeURIComponent(cardId)}`, { method: "DELETE" });
494
- state.version = data.version;
495
- state.cards = data.cards;
496
- showErr("");
497
- close();
498
- render();
499
- }catch(e){
500
- showErr(String(e.message || e));
501
- close();
502
  }
503
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
- typed.focus();
 
 
 
 
506
  }
507
 
508
  function render(){
509
  if(state.cards.length === 0){
510
  grid.innerHTML = `<div class="empty"><b>No cards yet.</b><div>Click the + to add your first card.</div></div>`;
511
- grid.style.gridTemplateColumns = "1fr";
512
  return;
513
  }
514
- grid.style.gridTemplateColumns = "";
515
  grid.innerHTML = "";
516
 
517
  state.cards.forEach((c) => {
@@ -522,7 +535,7 @@ app_ui = ui.page_fluid(
522
  card.innerHTML = `
523
  <div class="card-header">
524
  <div></div>
525
- <button class="del-btn" title="Delete">×</button>
526
  </div>
527
 
528
  <div class="card-main">
@@ -531,14 +544,14 @@ app_ui = ui.page_fluid(
531
  <input type="text" class="model" value="${escapeHtml(c.model)}" />
532
  </div>
533
 
534
- <div class="grow">
535
  <label>How it Works</label>
536
- <textarea class="how">${escapeHtml(c.how_it_works)}</textarea>
537
  </div>
538
 
539
- <div class="grow">
540
  <label>Use Cases</label>
541
- <textarea class="use">${escapeHtml(c.use_cases)}</textarea>
542
  </div>
543
  </div>
544
 
@@ -592,7 +605,12 @@ app_ui = ui.page_fluid(
592
  }
593
 
594
  [modelEl, howEl, useEl, linkEl].forEach(el => el.addEventListener("input", scheduleSave));
595
- delBtn.addEventListener("click", () => openDeleteModal(c.id));
 
 
 
 
 
596
 
597
  updateLink();
598
  grid.appendChild(card);
@@ -630,7 +648,7 @@ app_ui = ui.page_fluid(
630
  });
631
 
632
  refresh().then(render);
633
- setInterval(refresh, 3000);
634
  })();
635
  """
636
  ),
 
199
  color: var(--navy);
200
  border: 1px solid rgba(0,0,0,.08);
201
  border-radius: 12px;
202
+ padding: 12px 22px;
203
  font-weight: 900;
204
+ font-size: 22px;
205
  line-height: 1;
206
  cursor: pointer;
207
  box-shadow: 0 2px 8px rgba(0,0,0,.14);
208
  }
209
+ #add-btn:hover{ filter: brightness(.97); }
210
 
211
  .err{
212
+ max-width: 1200px;
213
+ margin: 0 auto 10px auto;
214
  padding: 10px 12px;
215
  border-radius: 10px;
216
  background: #fff1f2;
 
220
  display:none;
221
  }
222
 
223
+ /* GRID: 2 columns, bigger spacing, centered */
224
  .cards-grid{
225
  display:grid;
226
+ grid-template-columns: repeat(2, minmax(480px, 1fr));
227
+ gap: 22px;
228
+ padding: 10px 16px 22px 16px;
229
+ max-width: 1200px;
230
+ margin: 0 auto;
231
+ align-items:start;
 
 
 
 
232
  }
233
+ @media (max-width: 1050px){
234
  .cards-grid{ grid-template-columns: 1fr; }
235
  }
236
 
237
+ /* Card = true square & big */
238
  .card{
239
  background: var(--cardBg);
240
  border: 1px solid var(--cardBorder);
241
+ border-radius: 14px;
242
  box-shadow: 0 1px 0 rgba(0,0,0,.02);
243
  aspect-ratio: 1 / 1;
244
  width: 100%;
245
  overflow: hidden;
246
+ display:flex;
247
+ flex-direction:column;
248
+ min-height: 0;
249
  }
250
 
251
  .card-header{
252
  display:flex;
253
  align-items:center;
254
  justify-content:space-between;
255
+ padding: 12px 12px 8px 12px;
256
  border-bottom: 1px solid #eef2f8;
257
  flex: 0 0 auto;
258
  }
259
+ .del-btn{
260
+ border: none;
261
+ background: transparent;
262
+ color: #7b879c;
263
+ font-weight: 900;
264
+ font-size: 20px;
265
+ cursor:pointer;
266
+ padding: 0 8px;
267
+ }
268
+ .del-btn:hover{ color:#1c2b44; }
269
 
270
  .card-main{
271
+ padding: 12px;
272
  display:flex;
273
  flex-direction:column;
274
+ gap: 10px;
275
  flex: 1 1 auto;
276
+ min-height: 0;
277
+ overflow:hidden; /* no card scroll */
278
  }
279
 
280
  .card-footer{
281
+ padding: 12px;
282
  border-top: 1px solid #eef2f8;
283
+ flex: 0 0 auto;
 
 
 
 
 
 
 
 
 
 
284
  }
285
 
286
  label{
287
  display:block;
288
  font-size: 12px;
289
  color: var(--muted);
290
+ margin: 0 0 5px;
291
+ font-weight: 750;
292
+ letter-spacing: .2px;
293
  }
294
 
295
  input, textarea{
296
  width:100%;
297
  box-sizing:border-box;
298
  border: 1px solid #d8e1ee;
299
+ border-radius: 12px;
300
+ padding: 10px 12px;
301
  font-size: 14px;
302
  outline: none;
303
  background: white;
304
  }
305
 
306
+ /* Each textarea shows ~3-4 lines by default */
307
+ textarea.ta{
308
+ height: 92px; /* ~4 lines depending on font */
309
+ line-height: 1.25;
 
 
 
 
 
 
310
  resize: none;
311
  overflow: auto;
312
  }
 
318
  }
319
 
320
  .empty{
321
+ max-width: 1200px;
322
+ margin: 0 auto;
323
  padding: 18px;
324
  background: white;
325
  border: 1px dashed #cfd9ea;
 
335
  display:flex;
336
  align-items:center;
337
  justify-content:center;
338
+ z-index: 99999;
339
  }
340
  .modal{
341
+ width: min(560px, calc(100vw - 28px));
342
  background: white;
343
  border-radius: 14px;
344
  box-shadow: 0 10px 30px rgba(0,0,0,.25);
 
361
  }
362
  .modal .checks label{
363
  margin: 0;
364
+ font-weight: 650;
365
  font-size: 13px;
366
  color: #1f2a44;
367
  display:flex;
 
375
  margin-top: 12px;
376
  }
377
  .btn{
378
+ border-radius: 12px;
379
+ padding: 9px 12px;
380
+ font-weight: 850;
381
  cursor:pointer;
382
  border: 1px solid #d8e1ee;
383
  background: #ffffff;
 
441
  .replaceAll(">", "&gt;");
442
  }
443
 
444
+ function fallbackDelete(cardId){
445
+ // 3 checks fallback (browser-native)
446
+ if(!confirm("Delete this card? This is permanent.")) return;
447
+ if(!confirm("Second check: confirm you REALLY want to delete.")) return;
448
+ const typed = prompt('Final check: type DELETE to confirm');
449
+ if(typed !== "DELETE") return;
450
+ return doDelete(cardId);
451
+ }
452
+
453
+ async function doDelete(cardId){
454
+ const data = await api(`/api/cards/${encodeURIComponent(cardId)}`, { method: "DELETE" });
455
+ state.version = data.version;
456
+ state.cards = data.cards;
457
+ showErr("");
458
+ render();
459
+ }
460
+
461
  function openDeleteModal(cardId){
462
+ try{
463
+ const backdrop = document.createElement("div");
464
+ backdrop.className = "modal-backdrop";
465
+ backdrop.innerHTML = `
466
+ <div class="modal" role="dialog" aria-modal="true">
467
+ <h3>Delete card?</h3>
468
+ <div class="muted">This is permanent and affects everyone viewing this Space.</div>
469
+
470
+ <div class="checks">
471
+ <label><input type="checkbox" id="chk1" /> I understand this is permanent</label>
472
+ <label><input type="checkbox" id="chk2" /> I confirm I want to delete this card</label>
473
+ </div>
474
 
475
+ <label style="margin:8px 0 4px; color:#1f2a44; font-weight:800;">Type DELETE to confirm</label>
476
+ <input type="text" id="typed" value="" />
477
 
478
+ <div class="actions">
479
+ <button class="btn" id="cancel" type="button">Cancel</button>
480
+ <button class="btn btn-danger" id="confirm" type="button" disabled>Delete</button>
481
+ </div>
482
  </div>
483
+ `;
484
+ document.body.appendChild(backdrop);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
+ const chk1 = backdrop.querySelector("#chk1");
487
+ const chk2 = backdrop.querySelector("#chk2");
488
+ const typed = backdrop.querySelector("#typed");
489
+ const cancel = backdrop.querySelector("#cancel");
490
+ const confirmBtn = backdrop.querySelector("#confirm");
491
 
492
+ function updateEnabled(){
493
+ confirmBtn.disabled = !(chk1.checked && chk2.checked && (typed.value || "") === "DELETE");
 
 
 
 
 
 
 
 
 
494
  }
495
+ chk1.addEventListener("change", updateEnabled);
496
+ chk2.addEventListener("change", updateEnabled);
497
+ typed.addEventListener("input", updateEnabled);
498
+
499
+ function close(){ backdrop.remove(); }
500
+ cancel.addEventListener("click", (e) => { e.preventDefault(); close(); });
501
+ backdrop.addEventListener("click", (e) => { if(e.target === backdrop) close(); });
502
+
503
+ confirmBtn.addEventListener("click", async (e) => {
504
+ e.preventDefault();
505
+ try{
506
+ await doDelete(cardId);
507
+ close();
508
+ }catch(err){
509
+ showErr(String(err.message || err));
510
+ close();
511
+ }
512
+ });
513
 
514
+ typed.focus();
515
+ }catch(e){
516
+ // If anything about modal creation fails, use fallback popups
517
+ fallbackDelete(cardId);
518
+ }
519
  }
520
 
521
  function render(){
522
  if(state.cards.length === 0){
523
  grid.innerHTML = `<div class="empty"><b>No cards yet.</b><div>Click the + to add your first card.</div></div>`;
524
+ // keep the grid as-is; empty block will just sit in first column
525
  return;
526
  }
527
+
528
  grid.innerHTML = "";
529
 
530
  state.cards.forEach((c) => {
 
535
  card.innerHTML = `
536
  <div class="card-header">
537
  <div></div>
538
+ <button class="del-btn" type="button" title="Delete">×</button>
539
  </div>
540
 
541
  <div class="card-main">
 
544
  <input type="text" class="model" value="${escapeHtml(c.model)}" />
545
  </div>
546
 
547
+ <div>
548
  <label>How it Works</label>
549
+ <textarea class="how ta">${escapeHtml(c.how_it_works)}</textarea>
550
  </div>
551
 
552
+ <div>
553
  <label>Use Cases</label>
554
+ <textarea class="use ta">${escapeHtml(c.use_cases)}</textarea>
555
  </div>
556
  </div>
557
 
 
605
  }
606
 
607
  [modelEl, howEl, useEl, linkEl].forEach(el => el.addEventListener("input", scheduleSave));
608
+
609
+ delBtn.addEventListener("click", (e) => {
610
+ e.preventDefault();
611
+ e.stopPropagation();
612
+ openDeleteModal(c.id);
613
+ });
614
 
615
  updateLink();
616
  grid.appendChild(card);
 
648
  });
649
 
650
  refresh().then(render);
651
+ setInterval(refresh, 2500);
652
  })();
653
  """
654
  ),