Buckets:
AdhyanshVerma/data-gen-storage / exported_code /Q30_Build a Sticky Notes Board application using HTML_ /solution_14.html
| **index.html** | |
| ```html | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Sticky Notes Board</title> | |
| <link rel="stylesheet" href="styles.css"> | |
| </head> | |
| <body> | |
| <header class="toolbar" role="banner"> | |
| <div class="toolbar-inner"> | |
| <button id="addNoteBtn" class="add-btn" type="button" aria-label="Add new sticky note"> | |
| <span class="plus" aria-hidden="true">+</span> Add Note | |
| </button> | |
| <div class="search-wrap"> | |
| <span class="search-icon" aria-hidden="true">⌕</span> | |
| <input type="search" id="searchInput" class="search-input" | |
| placeholder="Search notes…" autocomplete="off" | |
| aria-label="Search sticky notes"> | |
| </div> | |
| </div> | |
| </header> | |
| <main id="board" class="board" aria-label="Sticky notes board"></main> | |
| <script src="app.js"></script> | |
| </body> | |
| </html> | |
| ``` | |
| **styles.css** | |
| ```css | |
| * { box-sizing: border-box; } | |
| html, body { | |
| margin: 0; | |
| padding: 0; | |
| height: 100%; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| overflow: hidden; | |
| background: #f7f7f5; | |
| } | |
| body { overscroll-behavior: none; } | |
| .board { | |
| position: fixed; | |
| inset: 0; | |
| top: 64px; | |
| background-color: #f1f1ee; | |
| background-image: radial-gradient(#c9c9c4 1px, transparent 1px); | |
| background-size: 22px 22px; | |
| overflow: hidden; | |
| touch-action: none; | |
| } | |
| /* Toolbar */ | |
| .toolbar { | |
| position: fixed; | |
| top: 0; left: 0; right: 0; | |
| height: 64px; | |
| z-index: 1000; | |
| background: rgba(255,255,255,0.92); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| border-bottom: 1px solid #e0e0dc; | |
| box-shadow: 0 1px 4px rgba(0,0,0,0.04); | |
| } | |
| .toolbar-inner { | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| padding: 0 18px; | |
| max-width: 100%; | |
| } | |
| .add-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #1f2937; | |
| background: linear-gradient(180deg, #ffe066, #ffd43b); | |
| padding: 10px 16px; | |
| border-radius: 10px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.15); | |
| transition: transform .12s ease, box-shadow .12s ease; | |
| white-space: nowrap; | |
| } | |
| .add-btn:hover { transform: translateY(-1px); box-shadow: 0 3px 6px rgba(0,0,0,0.18); } | |
| .add-btn:active { transform: translateY(0); } | |
| .add-btn .plus { font-size: 18px; line-height: 1; } | |
| .search-wrap { | |
| position: relative; | |
| flex: 1; | |
| max-width: 420px; | |
| margin-left: auto; | |
| } | |
| .search-icon { | |
| position: absolute; | |
| left: 12px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: #9aa0a6; | |
| font-size: 16px; | |
| pointer-events: none; | |
| } | |
| .search-input { | |
| width: 100%; | |
| padding: 10px 14px 10px 34px; | |
| border: 1px solid #e0e0dc; | |
| border-radius: 10px; | |
| font-size: 14px; | |
| background: #fff; | |
| outline: none; | |
| transition: border-color .15s ease, box-shadow .15s ease; | |
| } | |
| .search-input:focus { | |
| border-color: #ffd43b; | |
| box-shadow: 0 0 0 3px rgba(255,212,59,0.25); | |
| } | |
| /* Note */ | |
| .note { | |
| position: absolute; | |
| min-width: 150px; | |
| min-height: 150px; | |
| width: 220px; | |
| height: 220px; | |
| border-radius: 6px; | |
| box-shadow: 0 4px 14px rgba(0,0,0,0.18), 0 1px 3px rgba(0,0,0,0.1); | |
| display: flex; | |
| flex-direction: column; | |
| user-select: none; | |
| touch-action: none; | |
| transition: box-shadow .15s ease, opacity .2s ease; | |
| outline: none; | |
| } | |
| .note:focus-within, .note.dragging { | |
| box-shadow: 0 10px 28px rgba(0,0,0,0.28), 0 2px 6px rgba(0,0,0,0.15); | |
| } | |
| .note.dimmed { opacity: 0.22; pointer-events: none; } | |
| .note.yellow { background-color: #fff59d; } | |
| .note.pink { background-color: #ff8fa3; } | |
| .note.blue { background-color: #90caf9; } | |
| .note.green { background-color: #a5d6a7; } | |
| .note-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 6px 8px 4px; | |
| gap: 6px; | |
| cursor: grab; | |
| flex: 0 0 auto; | |
| } | |
| .note.dragging .note-header { cursor: grabbing; } | |
| .timestamp { | |
| font-size: 10px; | |
| color: rgba(0,0,0,0.55); | |
| font-weight: 600; | |
| letter-spacing: .2px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| flex: 1; | |
| } | |
| .controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| opacity: 0; | |
| transition: opacity .15s ease; | |
| } | |
| .note:hover .controls, | |
| .note:focus-within .controls { opacity: 1; } | |
| .palette { | |
| display: flex; | |
| gap: 3px; | |
| padding: 2px; | |
| background: rgba(255,255,255,0.45); | |
| border-radius: 10px; | |
| } | |
| .swatch { | |
| width: 14px; | |
| height: 14px; | |
| border-radius: 50%; | |
| border: 1.5px solid rgba(255,255,255,0.85); | |
| cursor: pointer; | |
| padding: 0; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.2); | |
| } | |
| .swatch.active { outline: 2px solid rgba(0,0,0,0.55); outline-offset: 1px; } | |
| .swatch.yellow { background: #fff59d; } | |
| .swatch.pink { background: #ff8fa3; } | |
| .swatch.blue { background: #90caf9; } | |
| .swatch.green { background: #a5d6a7; } | |
| .delete-btn { | |
| border: none; | |
| background: rgba(0,0,0,0.18); | |
| color: #fff; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| font-size: 14px; | |
| line-height: 1; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 0; | |
| transition: background .15s ease; | |
| } | |
| .delete-btn:hover { background: rgba(0,0,0,0.55); } | |
| .note-content { | |
| flex: 1; | |
| width: 100%; | |
| border: none; | |
| background: transparent; | |
| resize: none; | |
| padding: 4px 12px 12px; | |
| font-family: inherit; | |
| font-size: 14px; | |
| line-height: 1.4; | |
| color: #1f2937; | |
| outline: none; | |
| user-select: text; | |
| touch-action: text; | |
| } | |
| .note-content::placeholder { color: rgba(0,0,0,0.35); } | |
| @media (max-width: 560px) { | |
| .toolbar-inner { gap: 8px; padding: 0 10px; } | |
| .add-btn { padding: 9px 12px; font-size: 13px; } | |
| .search-wrap { max-width: none; } | |
| .note { width: 180px; height: 180px; } | |
| } | |
| ``` | |
| **app.js** | |
| ```js | |
| (function () { | |
| 'use strict'; | |
| const STORAGE_KEY = 'stickyBoard.v1'; | |
| const COLORS = ['yellow', 'pink', 'blue', 'green']; | |
| const TOOLBAR_H = 64; | |
| const board = document.getElementById('board'); | |
| const addBtn = document.getElementById('addNoteBtn'); | |
| const searchInput = document.getElementById('searchInput'); | |
| let notes = []; // {id,x,y,color,content,z,createdAt} | |
| let topZ = 1; | |
| let dragState = null; | |
| // ---------- Storage ---------- | |
| function save() { | |
| try { localStorage.setItem(STORAGE_KEY, JSON.stringify(notes)); } | |
| catch (e) { /* ignore quota errors */ } | |
| } | |
| function load() { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| if (!raw) return []; | |
| const arr = JSON.parse(raw); | |
| return Array.isArray(arr) ? arr : []; | |
| } catch (e) { return []; } | |
| } | |
| // ---------- Helpers ---------- | |
| function uid() { | |
| return 'n_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7); | |
| } | |
| function bringToFront(note) { | |
| note.z = ++topZ; | |
| const el = note.__el; | |
| if (el) el.style.zIndex = note.z; | |
| save(); | |
| } | |
| function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); } | |
| function randomSpawnPos(w, h) { | |
| const vw = window.innerWidth; | |
| const vh = window.innerHeight; | |
| const maxX = Math.max(0, vw - w); | |
| const maxY = Math.max(TOOLBAR_H + 8, vh - h); | |
| const x = Math.floor(Math.random() * (maxX + 1)); | |
| const y = Math.floor(Math.random() * (maxY - TOOLBAR_H + 1)) + TOOLBAR_H + 8; | |
| return { x, y }; | |
| } | |
| function formatTime(ts) { | |
| const d = new Date(ts); | |
| return d.toLocaleString(undefined, { | |
| month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' | |
| }); | |
| } | |
| // ---------- Rendering ---------- | |
| function renderNote(note) { | |
| const el = document.createElement('section'); | |
| el.className = 'note ' + note.color; | |
| el.style.left = note.x + 'px'; | |
| el.style.top = note.y + 'px'; | |
| el.style.zIndex = note.z; | |
| el.setAttribute('tabindex', '0'); | |
| el.setAttribute('role', 'group'); | |
| el.setAttribute('aria-label', 'Sticky note created ' + formatTime(note.createdAt)); | |
| el.dataset.id = note.id; | |
| const header = document.createElement('div'); | |
| header.className = 'note-header'; | |
| const ts = document.createElement('span'); | |
| ts.className = 'timestamp'; | |
| ts.textContent = formatTime(note.createdAt); | |
| const controls = document.createElement('div'); | |
| controls.className = 'controls'; | |
| const palette = document.createElement('div'); | |
| palette.className = 'palette'; | |
| palette.setAttribute('role', 'group'); | |
| palette.setAttribute('aria-label', 'Note color'); | |
| COLORS.forEach(c => { | |
| const sw = document.createElement('button'); | |
| sw.type = 'button'; | |
| sw.className = 'swatch ' + c + (c === note.color ? ' active' : ''); | |
| sw.dataset.color = c; | |
| sw.setAttribute('aria-label', c.charAt(0).toUpperCase() + c.slice(1) + ' color'); | |
| sw.setAttribute('aria-pressed', c === note.color ? 'true' : 'false'); | |
| sw.addEventListener('click', function (e) { | |
| e.stopPropagation(); | |
| setColor(note, c); | |
| }); | |
| palette.appendChild(sw); | |
| }); | |
| const del = document.createElement('button'); | |
| del.type = 'button'; | |
| del.className = 'delete-btn'; | |
| del.setAttribute('aria-label', 'Delete note'); | |
| del.textContent = '×'; | |
| del.addEventListener('click', function (e) { | |
| e.stopPropagation(); | |
| deleteNote(note); | |
| }); | |
| controls.appendChild(palette); | |
| controls.appendChild(del); | |
| header.appendChild(ts); | |
| header.appendChild(controls); | |
| const content = document.createElement('textarea'); | |
| content.className = 'note-content'; | |
| content.value = note.content || ''; | |
| content.placeholder = 'Write something…'; | |
| content.setAttribute('aria-label', 'Note content'); | |
| content.setAttribute('aria-multiline', 'true'); | |
| let saveTimer = null; | |
| content.addEventListener('input', function () { | |
| note.content = content.value; | |
| clearTimeout(saveTimer); | |
| saveTimer = setTimeout(save, 300); | |
| }); | |
| content.addEventListener('focus', function () { bringToFront(note); }); | |
| el.appendChild(header); | |
| el.appendChild(content); | |
| // Keyboard on note container | |
| el.addEventListener('keydown', function (e) { | |
| if (e.key === 'Enter' && document.activeElement !== content) { | |
| e.preventDefault(); | |
| content.focus(); | |
| } else if (e.key === 'Escape' && document.activeElement === content) { | |
| content.blur(); | |
| el.focus(); | |
| } | |
| }); | |
| el.addEventListener('focus', function () { bringToFront(note); }, true); | |
| attachDrag(note, el, header); | |
| note.__el = el; | |
| note.__content = content; | |
| board.appendChild(el); | |
| return el; | |
| } | |
| function setColor(note, color) { | |
| note.color = color; | |
| const el = note.__el; | |
| COLORS.forEach(c => el.classList.remove(c)); | |
| el.classList.add(color); | |
| el.querySelectorAll('.swatch').forEach(sw => { | |
| const active = sw.dataset.color === color; | |
| sw.classList.toggle('active', active); | |
| sw.setAttribute('aria-pressed', active ? 'true' : 'false'); | |
| }); | |
| save(); | |
| } | |
| function deleteNote(note) { | |
| const idx = notes.indexOf(note); | |
| if (idx > -1) notes.splice(idx, 1); | |
| if (note.__el && note.__el.parentNode) note.__el.parentNode.removeChild(note.__el); | |
| save(); | |
| } | |
| function createNote(opts) { | |
| const w = 220, h = 220; | |
| const pos = opts && opts.x != null ? { x: opts.x, y: opts.y } : randomSpawnPos(w, h); | |
| const note = { | |
| id: opts && opts.id ? opts.id : uid(), | |
| x: pos.x, | |
| y: pos.y, | |
| color: (opts && opts.color) || 'yellow', | |
| content: (opts && opts.content) || '', | |
| z: ++topZ, | |
| createdAt: (opts && opts.createdAt) || Date.now() | |
| }; | |
| notes.push(note); | |
| renderNote(note); | |
| save(); | |
| return note; | |
| } | |
| // ---------- Drag (mouse + touch, no HTML5 DnD) ---------- | |
| function attachDrag(note, el, handle) { | |
| function isInteractive(target) { | |
| return target.closest('textarea, button, .controls, .palette'); | |
| } | |
| function getPoint(e) { | |
| if (e.touches && e.touches.length) { | |
| return { x: e.touches[0].clientX, y: e.touches[0].clientY }; | |
| } | |
| return { x: e.clientX, y: e.clientY }; | |
| } | |
| function onStart(e) { | |
| if (e.button !== undefined && e.button !== 0) return; | |
| if (isInteractive(e.target)) return; | |
| // Don't drag while editing text | |
| if (document.activeElement === note.__content) return; | |
| e.preventDefault(); | |
| const p = getPoint(e); | |
| const rect = el.getBoundingClientRect(); | |
| dragState = { | |
| note: note, | |
| el: el, | |
| offsetX: p.x - rect.left, | |
| offsetY: p.y - rect.top, | |
| pointerId: e.pointerId | |
| }; | |
| el.classList.add('dragging'); | |
| bringToFront(note); | |
| document.addEventListener('mousemove', onMove, { passive: false }); | |
| document.addEventListener('mouseup', onEnd); | |
| document.addEventListener('touchmove', onMove, { passive: false }); | |
| document.addEventListener('touchend', onEnd); | |
| document.addEventListener('touchcancel', onEnd); | |
| } | |
| function onMove(e) { | |
| if (!dragState) return; | |
| e.preventDefault(); | |
| const p = getPoint(e); | |
| const vw = window.innerWidth; | |
| const vh = window.innerHeight; | |
| const w = el.offsetWidth; | |
| const h = el.offsetHeight; | |
| // Boundary: keep at least 40px visible on each axis | |
| let nx = p.x - dragState.offsetX; | |
| let ny = p.y - dragState.offsetY; | |
| nx = clamp(nx, -(w - 40), vw - 40); | |
| ny = clamp(ny, TOOLBAR_H, vh - 40); | |
| note.x = nx; | |
| note.y = ny; | |
| el.style.left = nx + 'px'; | |
| el.style.top = ny + 'px'; | |
| } | |
| function onEnd() { | |
| if (!dragState) return; | |
| el.classList.remove('dragging'); | |
| save(); | |
| dragState = null; | |
| document.removeEventListener('mousemove', onMove); | |
| document.removeEventListener('mouseup', onEnd); | |
| document.removeEventListener('touchmove', onMove); | |
| document.removeEventListener('touchend', onEnd); | |
| document.removeEventListener('touchcancel', onEnd); | |
| } | |
| handle.addEventListener('mousedown', onStart); | |
| handle.addEventListener('touchstart', onStart, { passive: false }); | |
| } | |
| // ---------- Search ---------- | |
| function applyFilter() { | |
| const q = searchInput.value.trim().toLowerCase(); | |
| notes.forEach(n => { | |
| const el = n.__el; | |
| if (!el) return; | |
| const match = !q || (n.content || '').toLowerCase().includes(q); | |
| el.classList.toggle('dimmed', !match); | |
| }); | |
| } | |
| searchInput.addEventListener('input', applyFilter); | |
| // ---------- Add button ---------- | |
| addBtn.addEventListener('click', function () { | |
| const n = createNote(); | |
| setTimeout(() => { if (n.__content) n.__content.focus(); }, 0); | |
| }); | |
| // ---------- Init ---------- | |
| function init() { | |
| const loaded = load(); | |
| if (loaded.length === 0) { | |
| // seed a welcome note | |
| createNote({ | |
| content: 'Welcome! Click "Add Note" to create a sticky.\nDrag from the header. Hover for color & delete.', | |
| color: 'yellow', | |
| x: Math.max(20, window.innerWidth / 2 - 110), | |
| y: 90 | |
| }); | |
| } else { | |
| // restore z ordering | |
| loaded.sort((a, b) => a.z - b.z); | |
| loaded.forEach(n => { | |
| if (n.z > topZ) topZ = n.z; | |
| notes.push(n); | |
| renderNote(n); | |
| }); | |
| } | |
| applyFilter(); | |
| } | |
| // Keep notes within bounds on resize | |
| window.addEventListener('resize', function () { | |
| const vw = window.innerWidth; | |
| const vh = window.innerHeight; | |
| notes.forEach(n => { | |
| const w = n.__el ? n.__el.offsetWidth : 220; | |
| const h = n.__el ? n.__el.offsetHeight : 220; | |
| n.x = clamp(n.x, -(w - 40), vw - 40); | |
| n.y = clamp(n.y, TOOLBAR_H, vh - 40); | |
| if (n.__el) { | |
| n.__el.style.left = n.x + 'px'; | |
| n.__el.style.top = n.y + 'px'; | |
| } | |
| }); | |
| save(); | |
| }); | |
| init(); | |
| })(); | |
| ``` |
Xet Storage Details
- Size:
- 15.8 kB
- Xet hash:
- 8f69e73239181a82450a292a95f5754e77a726a3f04773125c6a9dffd1f02aa1
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.