SolarumAsteridion commited on
Commit
20a3962
·
verified ·
1 Parent(s): 43f431b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +207 -46
index.html CHANGED
@@ -45,7 +45,7 @@ body.dark{
45
  --desk-bg: #2c2a27;
46
  --desk-dot: #3a3733;
47
 
48
- --paper-bg: #302e3b; /* slightly deeper to add contrast with tiles */
49
  --paper-text: #e9e7e2;
50
  --shadow: rgba(0,0,0,.55);
51
 
@@ -139,12 +139,10 @@ h1{font-family:'Libre Baskerville',serif;margin:0;font-size:28px;letter-spacing:
139
  gap:16px;
140
  overflow:visible;
141
 
142
- /* NEW: prevent short images from becoming tall boxes */
143
  align-items:start;
144
 
145
- /* NEW: dynamic columns
146
- --cols: 1 -> one column (full width), 2 -> two columns (50/50),
147
- else auto-fit with a sensible min width */
148
  grid-template-columns: repeat(var(--cols, auto-fit), minmax(var(--minCol, 220px), 1fr));
149
  }
150
  #board::before{
@@ -176,8 +174,7 @@ h1{font-family:'Libre Baskerville',serif;margin:0;font-size:28px;letter-spacing:
176
  transform:rotate(var(--tilt,0deg)) scale(1);
177
  transition:transform .12s ease, box-shadow .12s ease, opacity .16s ease;
178
  animation:pop .18s ease-out;
179
- /* NEW: ensure the tile itself doesn't stretch via grid */
180
- align-self:start;
181
  }
182
  .sticky:hover{
183
  transform:rotate(var(--tilt,0deg)) translateY(-2px) scale(1.01);
@@ -212,8 +209,7 @@ body.dark .badge{background:rgba(255,255,255,.12)}
212
  display:block;
213
  border-bottom-left-radius:10px;border-bottom-right-radius:10px;
214
  background: linear-gradient(180deg,rgba(255,255,255,.25),rgba(255,255,255,0));
215
- /* NEW: keep gigantic/tall screenshots sane on viewport */
216
- max-height:80vh;
217
  }
218
 
219
  /* delete button */
@@ -245,7 +241,6 @@ body.dark .badge{background:rgba(255,255,255,.12)}
245
  @media(max-width:768px){
246
  .container{margin:20px 16px;padding:24px}
247
  h1{font-size:24px}
248
- /* CHANGED: still use dynamic columns on mobile, but with a smaller min */
249
  #board{
250
  grid-template-columns: repeat(var(--cols, auto-fit), minmax(180px,1fr));
251
  }
@@ -283,21 +278,30 @@ body.dark .badge{background:rgba(255,255,255,.12)}
283
  <div class="processing">Processing…</div>
284
 
285
  <script>
286
- /* ======= helpers ======= */
287
  const board = document.getElementById('board');
288
  const processingNode = document.querySelector('.processing');
289
  const themeBtn = document.getElementById('themeToggle');
290
 
291
  let noteCount = 0;
292
 
293
- function showProcessing(){ processingNode.classList.add('show') }
294
- function hideProcessing(){ setTimeout(()=>processingNode.classList.remove('show'),250) }
 
 
 
 
 
 
 
 
 
295
 
 
296
  function removePlaceholder(){
297
  const ph = board.querySelector('.placeholder');
298
  if(ph) ph.remove();
299
  }
300
-
301
  function ensurePlaceholder(){
302
  if(!board.querySelector('.sticky') && !board.querySelector('.placeholder')){
303
  const ph = document.createElement('div');
@@ -307,25 +311,22 @@ function ensurePlaceholder(){
307
  }
308
  }
309
 
310
- /* NEW: set grid columns smartly based on number of stickies */
311
  function updateLayout(){
312
  const count = board.querySelectorAll('.sticky').length;
313
  if(count === 1){
314
- // 1 image → full width (one column)
315
  board.style.setProperty('--cols', '1');
316
  board.style.setProperty('--minCol', '0px');
317
  }else if(count === 2){
318
- // 2 images → half/half
319
  board.style.setProperty('--cols', '2');
320
  board.style.setProperty('--minCol', '0px');
321
  }else{
322
- // 3+ → responsive, nice min width
323
  board.style.setProperty('--cols', 'auto-fit');
324
  board.style.setProperty('--minCol', '220px');
325
  }
326
  }
327
 
328
- /* random tilt & color */
329
  function randTilt(){
330
  const d = (Math.random()*4.4 - 2.2).toFixed(2);
331
  return d + 'deg';
@@ -334,41 +335,178 @@ function pickColorClass(){
334
  const choices = ['note-yellow','note-mint','note-blush','note-blue'];
335
  return choices[Math.floor(Math.random()*choices.length)];
336
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
- /* ======= create sticky from an image URL ======= */
339
- function createSticky(src, isBlobUrl=false){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  removePlaceholder();
341
  noteCount++;
342
 
343
  const wrap = document.createElement('div');
344
- const colorClass = pickColorClass();
345
- wrap.className = `sticky ${colorClass}`;
346
- wrap.style.setProperty('--tilt', randTilt());
 
 
347
 
348
- // header bar
349
  const bar = document.createElement('div');
350
  bar.className = 'bar';
351
- bar.innerHTML = `<span class="badge">Q${noteCount}</span><span style="opacity:.7">pasted</span>`;
352
 
353
- // image
354
  const img = document.createElement('img');
355
  img.alt = `Question ${noteCount}`;
356
  img.src = src;
357
 
358
- // delete button
359
  const btn = document.createElement('button');
360
  btn.className = 'btn-del';
361
  btn.setAttribute('aria-label','Delete sticky');
362
  btn.textContent = '×';
363
 
364
- btn.addEventListener('click',()=>{
365
  wrap.style.opacity = '0';
366
  wrap.style.transform = 'scale(.96)';
 
 
367
  setTimeout(()=>{
368
  if(isBlobUrl) URL.revokeObjectURL(src);
369
  wrap.remove();
370
  ensurePlaceholder();
371
- updateLayout(); // NEW: update grid when items are removed
372
  },140);
373
  });
374
 
@@ -376,11 +514,9 @@ function createSticky(src, isBlobUrl=false){
376
  wrap.appendChild(img);
377
  wrap.appendChild(btn);
378
  board.prepend(wrap); // newest on top
379
-
380
- updateLayout(); // NEW: update grid after adding
381
  }
382
 
383
- /* ======= handle pasted images ======= */
384
  function filesFromClipboard(dataTransfer){
385
  const items = dataTransfer?.items || [];
386
  const files = [];
@@ -395,16 +531,25 @@ function filesFromClipboard(dataTransfer){
395
 
396
  async function handleImages(files){
397
  if(!files.length){
398
- processingNode.textContent = 'No images found in clipboard';
399
- showProcessing(); hideProcessing();
400
- processingNode.textContent = 'Processing…';
401
  return;
402
  }
403
- showProcessing();
404
  for(const file of files){
405
- const url = URL.createObjectURL(file);
406
- createSticky(url, true);
 
 
 
 
 
 
 
 
 
407
  }
 
408
  hideProcessing();
409
  }
410
 
@@ -451,15 +596,31 @@ function updateIcon(){
451
  themeBtn.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
452
  }
453
 
454
- /* ======= smooth fade in ======= */
455
- document.addEventListener('DOMContentLoaded',()=>{
456
  const sheet=document.querySelector('.container');
457
  sheet.style.opacity='0';
458
- setTimeout(()=>{
459
- sheet.style.transition='opacity .6s ease';
460
- sheet.style.opacity='1';
461
- updateLayout(); // NEW: set initial grid
462
- },80);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  });
464
  </script>
465
  </body>
 
45
  --desk-bg: #2c2a27;
46
  --desk-dot: #3a3733;
47
 
48
+ --paper-bg: #302e2b;
49
  --paper-text: #e9e7e2;
50
  --shadow: rgba(0,0,0,.55);
51
 
 
139
  gap:16px;
140
  overflow:visible;
141
 
142
+ /* prevent short images from becoming tall boxes */
143
  align-items:start;
144
 
145
+ /* dynamic columns via CSS vars set from JS */
 
 
146
  grid-template-columns: repeat(var(--cols, auto-fit), minmax(var(--minCol, 220px), 1fr));
147
  }
148
  #board::before{
 
174
  transform:rotate(var(--tilt,0deg)) scale(1);
175
  transition:transform .12s ease, box-shadow .12s ease, opacity .16s ease;
176
  animation:pop .18s ease-out;
177
+ align-self:start; /* don't stretch tile height */
 
178
  }
179
  .sticky:hover{
180
  transform:rotate(var(--tilt,0deg)) translateY(-2px) scale(1.01);
 
209
  display:block;
210
  border-bottom-left-radius:10px;border-bottom-right-radius:10px;
211
  background: linear-gradient(180deg,rgba(255,255,255,.25),rgba(255,255,255,0));
212
+ max-height:80vh; /* keep ultra-tall screenshots sane */
 
213
  }
214
 
215
  /* delete button */
 
241
  @media(max-width:768px){
242
  .container{margin:20px 16px;padding:24px}
243
  h1{font-size:24px}
 
244
  #board{
245
  grid-template-columns: repeat(var(--cols, auto-fit), minmax(180px,1fr));
246
  }
 
278
  <div class="processing">Processing…</div>
279
 
280
  <script>
281
+ /* ======= DOM refs ======= */
282
  const board = document.getElementById('board');
283
  const processingNode = document.querySelector('.processing');
284
  const themeBtn = document.getElementById('themeToggle');
285
 
286
  let noteCount = 0;
287
 
288
+ /* ======= processing UI ======= */
289
+ function showProcessing(text){
290
+ if(text) processingNode.textContent = text;
291
+ processingNode.classList.add('show');
292
+ }
293
+ function hideProcessing(){
294
+ setTimeout(()=>{
295
+ processingNode.classList.remove('show');
296
+ processingNode.textContent = 'Processing…';
297
+ },250);
298
+ }
299
 
300
+ /* ======= placeholder helpers ======= */
301
  function removePlaceholder(){
302
  const ph = board.querySelector('.placeholder');
303
  if(ph) ph.remove();
304
  }
 
305
  function ensurePlaceholder(){
306
  if(!board.querySelector('.sticky') && !board.querySelector('.placeholder')){
307
  const ph = document.createElement('div');
 
311
  }
312
  }
313
 
314
+ /* ======= layout: 1 image full width, 2 images 50/50, 3+ responsive ======= */
315
  function updateLayout(){
316
  const count = board.querySelectorAll('.sticky').length;
317
  if(count === 1){
 
318
  board.style.setProperty('--cols', '1');
319
  board.style.setProperty('--minCol', '0px');
320
  }else if(count === 2){
 
321
  board.style.setProperty('--cols', '2');
322
  board.style.setProperty('--minCol', '0px');
323
  }else{
 
324
  board.style.setProperty('--cols', 'auto-fit');
325
  board.style.setProperty('--minCol', '220px');
326
  }
327
  }
328
 
329
+ /* ======= random style bits ======= */
330
  function randTilt(){
331
  const d = (Math.random()*4.4 - 2.2).toFixed(2);
332
  return d + 'deg';
 
335
  const choices = ['note-yellow','note-mint','note-blush','note-blue'];
336
  return choices[Math.floor(Math.random()*choices.length)];
337
  }
338
+ function uuid(){
339
+ return (crypto?.randomUUID?.() || ('id-' + Math.random().toString(16).slice(2) + Date.now()));
340
+ }
341
+
342
+ /* ======= PERSISTENCE (IndexedDB with localStorage fallback) ======= */
343
+ const PERSIST = { mode: 'idb' };
344
+ const DB_NAME = 'question-board';
345
+ const STORE = 'stickies';
346
+ let dbConn = null;
347
+
348
+ function initDB(){
349
+ return new Promise((resolve, reject)=>{
350
+ const req = indexedDB.open(DB_NAME, 1);
351
+ req.onupgradeneeded = () => {
352
+ const db = req.result;
353
+ if(!db.objectStoreNames.contains(STORE)){
354
+ const store = db.createObjectStore(STORE, { keyPath: 'id' });
355
+ store.createIndex('createdAt', 'createdAt');
356
+ }
357
+ };
358
+ req.onsuccess = () => resolve(req.result);
359
+ req.onerror = () => reject(req.error);
360
+ });
361
+ }
362
+ async function initPersistence(){
363
+ if(!('indexedDB' in window)){
364
+ PERSIST.mode = 'ls';
365
+ return;
366
+ }
367
+ try{
368
+ dbConn = await initDB();
369
+ PERSIST.mode = 'idb';
370
+ }catch(e){
371
+ console.warn('IndexedDB unavailable, falling back to localStorage', e);
372
+ PERSIST.mode = 'ls';
373
+ }
374
+ }
375
+
376
+ /* --- IndexedDB ops --- */
377
+ function idbAdd(rec){
378
+ return new Promise((resolve,reject)=>{
379
+ const tx = dbConn.transaction(STORE,'readwrite');
380
+ tx.objectStore(STORE).put(rec);
381
+ tx.oncomplete = ()=> resolve();
382
+ tx.onerror = ()=> reject(tx.error);
383
+ });
384
+ }
385
+ function idbGetAll(){
386
+ return new Promise((resolve,reject)=>{
387
+ const tx = dbConn.transaction(STORE,'readonly');
388
+ const idx = tx.objectStore(STORE).index('createdAt');
389
+ const req = idx.getAll();
390
+ req.onsuccess = ()=> {
391
+ const arr = req.result || [];
392
+ arr.sort((a,b)=> a.createdAt - b.createdAt);
393
+ resolve(arr);
394
+ };
395
+ req.onerror = ()=> reject(req.error);
396
+ });
397
+ }
398
+ function idbDelete(id){
399
+ return new Promise((resolve,reject)=>{
400
+ const tx = dbConn.transaction(STORE,'readwrite');
401
+ tx.objectStore(STORE).delete(id);
402
+ tx.oncomplete = ()=> resolve();
403
+ tx.onerror = ()=> reject(tx.error);
404
+ });
405
+ }
406
+
407
+ /* --- localStorage ops (fallback) --- */
408
+ function lsMeta(){
409
+ try{
410
+ return JSON.parse(localStorage.getItem('stickies-meta')||'[]');
411
+ }catch{ return []; }
412
+ }
413
+ function lsSetMeta(arr){
414
+ localStorage.setItem('stickies-meta', JSON.stringify(arr));
415
+ }
416
+ function fileToDataURL(file){
417
+ return new Promise((resolve,reject)=>{
418
+ const r = new FileReader();
419
+ r.onload = ()=> resolve(r.result);
420
+ r.onerror = reject;
421
+ r.readAsDataURL(file);
422
+ });
423
+ }
424
+ function lsAdd(rec){
425
+ const meta = lsMeta();
426
+ meta.push({ id: rec.id, createdAt: rec.createdAt, color: rec.color, tilt: rec.tilt });
427
+ lsSetMeta(meta);
428
+ localStorage.setItem('sticky-img-' + rec.id, rec.dataUrl);
429
+ }
430
+ function lsGetAll(){
431
+ const meta = lsMeta().sort((a,b)=> a.createdAt - b.createdAt);
432
+ return meta.map(m => ({
433
+ id: m.id,
434
+ createdAt: m.createdAt,
435
+ color: m.color,
436
+ tilt: m.tilt,
437
+ dataUrl: localStorage.getItem('sticky-img-' + m.id)
438
+ })).filter(r => !!r.dataUrl);
439
+ }
440
+ function lsDelete(id){
441
+ const meta = lsMeta().filter(m => m.id !== id);
442
+ lsSetMeta(meta);
443
+ localStorage.removeItem('sticky-img-' + id);
444
+ }
445
 
446
+ /* --- persist a new file and return its record --- */
447
+ async function persistFile(file, color, tilt){
448
+ const rec = { id: uuid(), createdAt: Date.now(), color, tilt };
449
+ if(PERSIST.mode === 'idb'){
450
+ // store blob directly
451
+ await idbAdd({ ...rec, blob: file });
452
+ return { ...rec, blob: file };
453
+ }else{
454
+ // fallback: store base64 (size-limited)
455
+ const dataUrl = await fileToDataURL(file);
456
+ lsAdd({ ...rec, dataUrl });
457
+ return { ...rec, dataUrl };
458
+ }
459
+ }
460
+ async function loadPersisted(){
461
+ if(PERSIST.mode === 'idb'){
462
+ return await idbGetAll();
463
+ }else{
464
+ return lsGetAll();
465
+ }
466
+ }
467
+ async function deletePersisted(id){
468
+ if(PERSIST.mode === 'idb'){
469
+ await idbDelete(id);
470
+ }else{
471
+ lsDelete(id);
472
+ }
473
+ }
474
+
475
+ /* ======= create sticky DOM ======= */
476
+ function createSticky(src, { isBlobUrl=false, id=null, colorClass=null, tilt=null } = {}){
477
  removePlaceholder();
478
  noteCount++;
479
 
480
  const wrap = document.createElement('div');
481
+ const color = colorClass || pickColorClass();
482
+ const tlt = tilt || randTilt();
483
+ wrap.className = `sticky ${color}`;
484
+ wrap.style.setProperty('--tilt', tlt);
485
+ if(id) wrap.dataset.id = id;
486
 
 
487
  const bar = document.createElement('div');
488
  bar.className = 'bar';
489
+ bar.innerHTML = `<span class="badge">Q${noteCount}</span><span style="opacity:.7">saved</span>`;
490
 
 
491
  const img = document.createElement('img');
492
  img.alt = `Question ${noteCount}`;
493
  img.src = src;
494
 
 
495
  const btn = document.createElement('button');
496
  btn.className = 'btn-del';
497
  btn.setAttribute('aria-label','Delete sticky');
498
  btn.textContent = '×';
499
 
500
+ btn.addEventListener('click', async ()=>{
501
  wrap.style.opacity = '0';
502
  wrap.style.transform = 'scale(.96)';
503
+ const stickyId = wrap.dataset.id;
504
+ try{ if(stickyId) await deletePersisted(stickyId); }catch(e){ console.warn('Delete failed', e); }
505
  setTimeout(()=>{
506
  if(isBlobUrl) URL.revokeObjectURL(src);
507
  wrap.remove();
508
  ensurePlaceholder();
509
+ updateLayout();
510
  },140);
511
  });
512
 
 
514
  wrap.appendChild(img);
515
  wrap.appendChild(btn);
516
  board.prepend(wrap); // newest on top
 
 
517
  }
518
 
519
+ /* ======= handle pasted or dropped images ======= */
520
  function filesFromClipboard(dataTransfer){
521
  const items = dataTransfer?.items || [];
522
  const files = [];
 
531
 
532
  async function handleImages(files){
533
  if(!files.length){
534
+ showProcessing('No images found in clipboard');
535
+ hideProcessing();
 
536
  return;
537
  }
538
+ showProcessing('Saving…');
539
  for(const file of files){
540
+ const color = pickColorClass();
541
+ const tilt = randTilt();
542
+ // persist first to get an id
543
+ const rec = await persistFile(file, color, tilt);
544
+
545
+ if(PERSIST.mode === 'idb'){
546
+ const url = URL.createObjectURL(file);
547
+ createSticky(url, { isBlobUrl:true, id: rec.id, colorClass: color, tilt });
548
+ }else{
549
+ createSticky(rec.dataUrl, { isBlobUrl:false, id: rec.id, colorClass: color, tilt });
550
+ }
551
  }
552
+ updateLayout();
553
  hideProcessing();
554
  }
555
 
 
596
  themeBtn.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
597
  }
598
 
599
+ /* ======= boot: fade in and load saved stickies ======= */
600
+ document.addEventListener('DOMContentLoaded', async ()=>{
601
  const sheet=document.querySelector('.container');
602
  sheet.style.opacity='0';
603
+ setTimeout(()=>{sheet.style.transition='opacity .6s ease';sheet.style.opacity='1'},80);
604
+
605
+ showProcessing('Loading saved…');
606
+ await initPersistence();
607
+
608
+ try{
609
+ const saved = await loadPersisted(); // ascending by createdAt
610
+ for(const rec of saved){
611
+ if(PERSIST.mode === 'idb'){
612
+ const url = URL.createObjectURL(rec.blob);
613
+ createSticky(url, { isBlobUrl:true, id: rec.id, colorClass: rec.color, tilt: rec.tilt });
614
+ }else{
615
+ createSticky(rec.dataUrl, { isBlobUrl:false, id: rec.id, colorClass: rec.color, tilt: rec.tilt });
616
+ }
617
+ }
618
+ }catch(e){
619
+ console.warn('Load failed', e);
620
+ }
621
+
622
+ updateLayout();
623
+ hideProcessing();
624
  });
625
  </script>
626
  </body>