From a32b894e19806c4fc192a6ab5ccfc7a3aa2806d9 Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 9 Apr 2024 10:23:10 +0000 Subject: [PATCH] initial commit --- .gitignore | 1 + index.html | 103 ++++++++++++++++++++++++++++ main.js | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 172 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 468 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 main.js create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..48cc62a --- /dev/null +++ b/index.html @@ -0,0 +1,103 @@ + + + + + Meeting Roulette + + + + + + + + + + + + +
+
+

Meeting Roulette

+
+
    +
  • + Remaining meeting time: {{ timeText(totalRemainingTime) }} +
  • +
  • Total meeting time: {{ timeText(totalTime) }}
  • +
  • +   +
  • +
+
+
+   + +
+

+ + {{ timerText() }} + +

+
+
+

Current topic: {{ showSelected ? selectedData.text : '???' }}

+

+ click to spin +

+
+ + + + + + {{ d.text }} + + + + +
+
+
+ +
+
+ + diff --git a/main.js b/main.js new file mode 100644 index 0000000..9785fa4 --- /dev/null +++ b/main.js @@ -0,0 +1,192 @@ +/* exported utils */ + +let app = { + data() { + return { + rawData: + "Topic 1: 60min\nTopic 2: 20min\nTopic 3: 50min\nTopic 4: 20min\nTopic 5:10min\nTopic 6: 10min\nTopic 7: 10min", + data: [], + wheelPosition: 0, + wheighted: false, + timeoutId: 0, + showSelected: false, + selected: 0, + timerEnd: new Date(), + timerStarted: false, + initialSpin: true, + initialColor: Math.random() * 360, + rid: 0, + }; + }, + watch: { + rawData() { + this.data = this.getData(); + }, + }, + computed: { + selectedData() { + return ( + this.data[this.selected] ?? { + text: "???", + time: 1, + disabled: false, + } + ); + }, + filteredData() { + return this.data.filter((item) => !item.disabled); + }, + totalTime() { + return this.data.map((item) => item.time).reduce((a, b) => a + b, 0); + }, + totalRemainingTime() { + return this.filteredData + .map((item) => item.time) + .reduce((a, b) => a + b, 0); + }, + svgData() { + let totalAngle = 0; + return this.filteredData.map((item, index) => { + const ratio = this.wheighted + ? item.time / this.totalRemainingTime + : 1 / this.filteredData.length; + const textScale = item.text.length * 1.1; + const angleRad = 2 * Math.PI * ratio; + const angleDeg = 360 * ratio; + totalAngle += angleDeg; + return { + id: item.id, + text: item.text, + textPosition: textScale * 0.95, + textTransform: `scale(${1 / textScale})`, + path: `M 0 0 L ${Math.cos(-angleRad / 2)} ${Math.sin( + -angleRad / 2 + )} A 1 1 0 ${ratio > 0.5 ? 1 : 0} 1 ${Math.cos( + angleRad / 2 + )} ${Math.sin(angleRad / 2)} z`, + transform: `rotate(${totalAngle - angleDeg / 2}, 0, 0)`, + color: `hsl(${this.initialColor + 100 * index} 80% 50%)`, + from: totalAngle - angleDeg, + to: totalAngle, + }; + }); + }, + }, + methods: { + overtime() { + return this.timerStarted && this.timerEnd - new Date() <= 0; + }, + timerText() { + const delta = this.timerStarted + ? Math.floor(Math.abs(this.timerEnd - new Date()) / 1000) + : this.showSelected + ? this.selectedData.time * 60 + : 0; + return `${String(Math.floor(delta / 60)).padStart(2, "0")}:${String( + delta % 60 + ).padStart(2, "0")}`; + }, + timeText(minutes) { + if (minutes > 60) { + return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart( + 2, + "0" + )}`; + } else { + return `${String(minutes % 60).padStart(2, "0")}min`; + } + }, + spin() { + if (this.timerStarted) return; + this.showSelected = false; + this.initialSpin = false; + this.wheelPosition += 360 * 10 + Math.random() * 360; + clearTimeout(this.timeoutId); + this.selected = this.getSelected(); + this.timeoutId = setTimeout(() => { + this.showSelected = true; + }, 5000); + }, + getSelected() { + const angle = 360 - (this.wheelPosition % 360); + for (let index = 0; index < this.data.length; index++) { + const element = this.svgData[index]; + if (angle >= element.from && angle < element.to) { + return element.id; + } + } + return 0; + }, + getData() { + const re = /:\s?(?:(?:(\d+)\s?h)?(\d+)?(?:m(?:in)?)?)\s?$/i; + return this.rawData + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length) + .map((line, index) => { + const result = re.exec(line); + if (result === null) { + return { + id: index, + text: line, + time: 1, + disabled: line.substring(0, 1) === "-", + }; + } else { + return { + id: index, + text: line.substring(0, line.indexOf(result[0])), + time: parseInt(result[1] ?? 0) * 60 + parseInt(result[2] ?? 0), + disabled: line.substring(0, 1) === "-", + }; + } + }); + }, + showApp() { + document.getElementById("app").setAttribute("style", ""); + }, + removeTopic() { + let i = 0; + this.rawData = this.rawData + .split("\n") + .map((line) => { + if (line.trim().length) { + if (i === this.selected) { + line = "-" + line; + } + i += 1; + } + return line; + }) + .join("\n"); + this.showSelected = false; + this.initialSpin = true; + }, + timerFunction() { + this.timerStarted = !this.timerStarted; + if (this.timerStarted) { + this.timerEnd = new Date( + new Date().getTime() + this.selectedData.time * 60 * 1000 + ); + } else { + document.title = "Meeting Roulette"; + } + }, + }, + mounted: function () { + console.log("app mounted"); + this.data = this.getData(); + setTimeout(this.showApp); + setInterval(() => { + this.rid = Math.random(); + if (this.timerStarted) { + document.title = this.timerText(); + } + }, 200); + }, +}; + +window.onload = () => { + app = Vue.createApp(app); + app.mount("#app"); +}; diff --git a/style.css b/style.css new file mode 100644 index 0000000..09ab456 --- /dev/null +++ b/style.css @@ -0,0 +1,172 @@ +/* +================================================= +https://www.joshwcomeau.com/css/custom-css-reset/ +================================================= +*/ + +/* + 1. Use a more-intuitive box-sizing model. +*/ +*, +*::before, +*::after { + box-sizing: border-box; +} +/* + 2. Remove default margin + */ +* { + margin: 0; +} +/* + 3. Allow percentage-based heights in the application + */ +html, +body { + height: 100%; +} +/* + Typographic tweaks! + 4. Add accessible line-height + 5. Improve text rendering + */ +body { + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} +/* + 6. Improve media defaults + */ +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; +} +/* + 7. Remove built-in form typography styles + */ +input, +button, +textarea, +select { + font: inherit; +} +/* + 8. Avoid text overflows + */ +p, +h1, +h2, +h3, +h4, +h5, +h6 { + overflow-wrap: break-word; +} +/* + 9. Create a root stacking context + */ +#root, +#__next { + isolation: isolate; +} + +/* +================================================= +CUSTOM STYLE +================================================= +*/ + +#app { + width: 100vw; + height: 100vh; + overflow: hidden; + position: relative; + display: flex; + flex-direction: row; + justify-content: center; + align-items: stretch; + background-color: #eceff1; + color: #263238; + font-family: sans-serif; +} + +.panel { + flex-grow: 2; + position: relative; +} + +.panel.right, +.panel.left { + background-color: #cfd8dc; + margin: 1em; + border-radius: 2em; + padding: 1em; +} + +.panel.middle { + height: 100%; + flex-grow: 1; +} + +.panel.middle h1 { + position: absolute; + top: 1em; + width: 100%; + text-align: center; +} + +.panel.middle h2 { + position: absolute; + bottom: 1em; + width: 100%; + text-align: center; +} + +.wheel { + position: relative; + height: 80%; + width: fit-content; + margin: 10% auto; +} + +.wheel svg { + transition: transform 5s ease-out; + height: 100%; +} + +svg text { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.wheel::after { + content: ""; + position: absolute; + left: 94%; + top: 50%; + width: 0; + height: 0; + border-right: 6vh solid #263238; + border-bottom: 2vh solid transparent; + border-top: 2vh solid transparent; + transform: translateY(-50%); + clear: both; +} + +.panel.right textarea { + background-color: #cfd8dc; + color: #263238; + font-family: sans-serif; + padding: 0.5em; + border: 1px solid #263238; + height: 100%; + width: 100%; + border-radius: 1em; +}