commit c38c13e54a7ad900ed13ddb95ad5a390cb85cc10 Author: ari melody Date: Tue Sep 23 14:25:19 2025 +0100 first commit! 🎉 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e778a0e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +html2canvas*.js diff --git a/background.png b/background.png new file mode 100644 index 0000000..753faa4 Binary files /dev/null and b/background.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..4cba51a --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ + + + + + + + + + +
+
+ schedule designer v0 + made with ♥ by ari melody +
+
+
+
+ + + + + +
+
+ + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..1bfee6c --- /dev/null +++ b/main.js @@ -0,0 +1,195 @@ +// import html2canvas from '/html2canvas-pro.min.js'; + +const scheduleDOM = document.getElementById('schedule'); + +const VERSION = "v1.0.0"; + +const days = [ + 'monday', 'tuesday', 'wednesday', 'thursday', + 'friday', 'saturday', 'sunday', +]; + +let schedule = { + version: VERSION, + days: {}, +}; + +const offlineTitle = 'OFFLINE'; +const offlineSubtitle = 'takin a nap'; + +document.addEventListener('readystatechange', () => { + document.getElementById('version').innerText = VERSION; + + loadSchedule(); + createSchedule(); + + document.getElementById('import').addEventListener('click', () => { + importJSON(); + }); + document.getElementById('save').addEventListener('click', () => { + saveSchedule(); + }); + document.getElementById('clipboard').addEventListener('click', () => { + copyClipboard(); + }); + document.getElementById('save-image').addEventListener('click', () => { + saveImage(); + }); + document.getElementById('export').addEventListener('click', () => { + exportJSON(); + }); +}); + +function importJSON() { + const filePicker = document.createElement('input'); + filePicker.type = 'file'; + filePicker.accept = 'application/json'; + filePicker.addEventListener('input', () => { + if (filePicker.files.length == 0) return; + const file = filePicker.files[0]; + file.text().then(text => { + try { + schedule = JSON.parse(text); + + for (const day in schedule.days) { + const data = schedule.days[day]; + const slot = document.getElementById(`slot-${day}`); + if (!data.enabled) { + slot.classList.add('disabled'); + slot.querySelector('.timeslot-title').innerText = offlineTitle + slot.querySelector('.timeslot-subtitle').innerText = offlineSubtitle + continue; + } + + slot.classList.remove('disabled'); + slot.querySelector('.timeslot-time').innerText = data.time + slot.querySelector('.timeslot-title').innerText = data.title + slot.querySelector('.timeslot-subtitle').innerText = data.subtitle + } + } catch (err) { + console.error(err); + alert('Failed to import schedule. Check the browser console for details.'); + } + }).catch(err => { + console.error(err); + alert('Failed to import schedule. Check the browser console for details.'); + }); + }); + filePicker.click(); +} + +function saveSchedule() { + localStorage.setItem('streamschedule', btoa(JSON.stringify(schedule))); +} + +function loadSchedule() { + const saved = localStorage.getItem('streamschedule'); + if (!saved) return; + schedule = JSON.parse(atob(saved)); +} + +function copyClipboard() { + html2canvas(scheduleDOM).then(/** @typeof HTMLCanvasElement */ canvas => { + canvas.toBlob(blob => { + const item = new ClipboardItem({ 'image/png': blob }); + navigator.clipboard.write([item]); + }) + }); +} + +function saveImage() { + html2canvas(scheduleDOM).then(canvas => { + document.body.appendChild(canvas); + }); +} + +function exportJSON() { + const json = JSON.stringify(schedule, null, '\t'); + const data = new Blob([json], { + type: 'application/json', + }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(data); + link.download = 'stream-schedule.json'; + link.click(); +} + +function createSchedule() { + for (let i = 0; i < days.length; i++) { + const day = days[i]; + + if (schedule.days[day] == undefined) schedule.days[day] = { + enabled: false, + time: '6pm UTC', + title: 'Untitled Event', + subtitle: "yep, that sure is an event if i've ever seen one", + }; + + const container = document.createElement('div'); + container.className = 'timeslot'; + container.id = `slot-${day}`; + if (!schedule.days[day].enabled) + container.classList.add('disabled'); + + const timeslotDateContainer = document.createElement('div'); + timeslotDateContainer.className = 'timeslot-date'; + const timeslotDay = document.createElement('div'); + timeslotDay.className = 'timeslot-day'; + timeslotDay.innerText = day.substring(0, 3).toUpperCase(); + timeslotDateContainer.appendChild(timeslotDay); + const timeslotTime = document.createElement('div'); + timeslotTime.className = 'timeslot-time'; + timeslotTime.innerText = schedule.days[day].time; + timeslotTime.contentEditable = true; + timeslotDateContainer.appendChild(timeslotTime); + container.appendChild(timeslotDateContainer); + + const timeslotContentContainer = document.createElement('div'); + timeslotContentContainer.className = 'timeslot-content'; + const timeslotTitle = document.createElement('div'); + timeslotTitle.className = 'timeslot-title'; + timeslotTitle.innerText = schedule.days[day].enabled ? + schedule.days[day].title : offlineTitle; + timeslotTitle.contentEditable = schedule.days[day].enabled; + timeslotTitle.spellcheck = false; + timeslotContentContainer.appendChild(timeslotTitle); + const timeslotSubtitle = document.createElement('div'); + timeslotSubtitle.className = 'timeslot-subtitle'; + timeslotSubtitle.innerText = schedule.days[day].enabled ? + schedule.days[day].subtitle : offlineSubtitle; + timeslotSubtitle.contentEditable = schedule.days[day].enabled; + timeslotSubtitle.spellcheck = false; + timeslotContentContainer.appendChild(timeslotSubtitle); + container.appendChild(timeslotContentContainer); + + scheduleDOM.appendChild(container); + + timeslotDay.addEventListener('click', () => { + schedule.days[day].enabled = !schedule.days[day].enabled; + + timeslotTitle.contentEditable = schedule.days[day].enabled; + timeslotSubtitle.contentEditable = schedule.days[day].enabled; + + if (schedule.days[day].enabled) { + container.classList.remove('disabled'); + timeslotTitle.innerText = schedule.days[day].title; + timeslotSubtitle.innerText = schedule.days[day].subtitle; + } else { + container.classList.add('disabled'); + timeslotTitle.innerText = offlineTitle; + timeslotSubtitle.innerText = offlineSubtitle; + } + }); + timeslotTitle.addEventListener('keyup', () => { + schedule.days[day].title = timeslotTitle.innerText; + }); + timeslotSubtitle.addEventListener('keyup', () => { + if (timeslotSubtitle.innerHTML.includes('
')) + timeslotSubtitle.innerText = ''; + schedule.days[day].subtitle = timeslotSubtitle.innerText; + }); + timeslotTime.addEventListener('keyup', () => { + schedule.days[day].time = timeslotTime.innerText; + }); + } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..86f77a7 --- /dev/null +++ b/style.css @@ -0,0 +1,227 @@ +:root { + --active: #b7fd49; + --inactive: #868686; + --foreground: #eee; + --background: #000; +} + +html { + font-family: "Inter", "Helvetica", "Arial", sans-serif; + font-size: 16px; +} + +body { + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + color: var(--foreground); + background: var(--background); +} + +#app { + margin: auto auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1em; +} + +#title { + width: 100%; + margin-bottom: -.5em; + display: flex; + justify-content: space-between; + font-weight: 500; + font-style: italic; +} + +#actions { + font-size: 1.2em; +} +#actions button { + padding: .3em .5em; + font-size: inherit; + font-weight: 600; + color: var(--active); + background: var(--background); + border: 1px solid var(--active); + border-radius: 4px; + transition-property: color, background-color, border-color; + transition-duration: .1s; + transition-timing-function: ease-out; + cursor: pointer; +} +#actions button:hover { + color: color-mix(in srgb, var(--active), white 25%); + border-color: color-mix(in srgb, var(--active), white 25%); + background: color-mix(in srgb, var(--active), var(--background) 75%); +} +#actions button:active { + color: color-mix(in srgb, var(--active), black 25%); + border-color: color-mix(in srgb, var(--active), black 25%); + background: color-mix(in srgb, var(--active), black 90%); +} +#actions button[disabled], +#actions button[disabled]:hover, +#actions button[disabled]:active { + color: var(--inactive); + border-color: var(--inactive); + background: transparent; + cursor: inherit; +} + +#schedule { + width: 1280px; + height: 720px; + margin: auto 0; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: end; + gap: 1.5em; + + overflow: hidden; + color: var(--foreground); + background-color: var(--background); + background: url("background.png"); + + border: 1px solid var(--foreground); + + z-index: 1; +} + +.timeslot { + position: relative; + height: 80px; + display: grid; + grid-template-columns: 5em 32em; + grid-gap: 1em; + align-items: center; + transition-property: opacity, transform, height; + transition-duration: .2s; + transition-timing-function: ease-out; +} +.timeslot.disabled { + height: 50px; + transform: translateX(2.5em); + opacity: .5; +} + +.timeslot-date { + width: 5em; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + font-family: "Monaspace Argon", monospace; + font-weight: bold; +} +.timeslot.disabled .timeslot-date { + opacity: .5; +} + +.timeslot-day { + font-size: 46px; + line-height: 46px; + cursor: pointer; +} +.timeslot-time { + font-size: 18px; + opacity: .75; +} +.timeslot.disabled .timeslot-time { + display: none; +} + +.timeslot-content { + position: relative; + height: 100%; + padding: 0 1.4em; + + display: flex; + flex-direction: column; + justify-content: center; + + font-weight: bold; + color: var(--active); + background: var(--background); + border-radius: 8px 0 0 8px; + + transition-property: padding, color; + transition-duration: .2s; + transition-timing-function: ease-out; +} +.timeslot.disabled .timeslot-content { + padding: 0 1em; + flex-direction: row; + justify-content: start; + align-items: center; + gap: 1em; + + font-family: 'Monaspace Argon', monospace; + + color: var(--inactive); +} +.timeslot-content::before { + content: ''; + position: absolute; + left: -1px; + top: -1px; + width: 100%; + height: 100%; + padding: 1px; + + background-color: #c7ed7a; + border-radius: 8px 0 0 8px; + mask-image: linear-gradient(150deg, white, transparent 90%); + + transition-property: background-color, height; + transition-duration: .2s; + transition-timing-function: ease-out; + + z-index: -1; +} +.timeslot.disabled .timeslot-content::before { + background-color: var(--inactive); +} + +.timeslot-title { + font-size: 1.6em; + font-weight: 700; + transition-property: margin-top, font-size; + transition-duration: .2s; + transition-timing-function: ease-out; +} +.timeslot:not(.disabled):has(.timeslot-subtitle:empty) .timeslot-title { + margin-top: .6em; + font-size: 2em; +} + +.timeslot-subtitle { + font-weight: 600; +} + +.timeslot.disabled .timeslot-title { + font-size: 1.8em; +} +.timeslot.disabled .timeslot-subtitle { + font-size: 1.3em; + opacity: .75; +} + +.timeslot:not(.disabled) .timeslot-title, +.timeslot:not(.disabled) .timeslot-subtitle { + background: linear-gradient(to top, #81ac34 0%, #b7ff00 33%, #d7ff83 60%, #d9ff6d); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + + text-shadow: 0 0 8px color-mix(in srgb, var(--active), transparent 50%); +}