initial commit

This commit is contained in:
Klemek
2024-04-09 10:23:10 +00:00
commit a32b894e19
4 changed files with 468 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
.idea
+103
View File
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Meeting Roulette</title>
<link rel="stylesheet" href="style.css" />
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript" src="main.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- card related -->
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:title" content="Meeting Roulette" />
<meta property="og:description" content="🎡 Spin your meetings" />
<!-- <meta property="og:image" content="https://.../preview_640x320.jpg" /> -->
<!-- <meta property="org:url" content="https://..." /> -->
</head>
<body>
<main id="app" style="display: none">
<div class="left panel">
<h2>Meeting Roulette</h2>
<br />
<ul>
<li>
Remaining meeting time: <b>{{ timeText(totalRemainingTime) }}</b>
</li>
<li>Total meeting time: <b>{{ timeText(totalTime) }}</b></li>
<li>
<label for="wheighted">Wheighted topics</label>&nbsp;<input
id="wheighted"
v-model="wheighted"
type="checkbox"
/>
</li>
</ul>
<br />
<div class="buttons">
<button :disabled="!showSelected" @click="timerFunction">
<span v-if="!timerStarted">Start timer</span>
<span v-else>Stop timer</span></button
>&nbsp;
<button
:disabled="!showSelected || timerStarted"
@click="removeTopic"
>
Remove topic
</button>
</div>
<h1 :id="rid">
<span :style="overtime() ? 'color: #B71C1C' : ''">
{{ timerText() }}
</span>
</h1>
</div>
<div class="middle panel">
<h1>Current topic: {{ showSelected ? selectedData.text : '???' }}</h1>
<h2 v-if="(showSelected || initialSpin) && !timerStarted">
click to spin
</h2>
<div class="wheel">
<svg
viewBox="-1.05 -1.05 2.1 2.1"
:style="`transform: rotate(${wheelPosition}deg)`"
@click="spin"
>
<circle
r="1"
cx="0"
cy="0"
fill="#263238"
stroke="#263238"
stroke-width="0.04"
/>
<g v-for="d in svgData" :transform="d.transform">
<path :d="d.path" :fill="d.color" />
<text
:x="d.textPosition"
y="0"
style="font: bold 1px sans-serif; text-align: center"
dominant-baseline="middle"
text-anchor="end"
fill="white"
:transform="d.textTransform"
>
{{ d.text }}
</text>
</g>
<circle
r="10%"
cx="0"
cy="0"
fill="#cfd8dc"
stroke="#263238"
stroke-width="0.02"
/>
</svg>
</div>
</div>
<div class="right panel">
<textarea v-model="rawData"></textarea>
</div>
</main>
</body>
</html>
+192
View File
@@ -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");
};
+172
View File
@@ -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;
}