This commit is contained in:
klemek
2025-03-03 14:59:49 +01:00
parent 85ca43a87c
commit a4b2408fe1
8 changed files with 2249 additions and 118 deletions
+13
View File
@@ -0,0 +1,13 @@
name: Lint
on: [push]
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: npm ci
- name: Run ESLint
run: npm run lint
+1
View File
@@ -1 +1,2 @@
.idea .idea
node_modules
+4
View File
@@ -0,0 +1,4 @@
# Meeting Roulette
*🎡 Spin your meetings*
### [Tool link](https://klemek.github.io/meeting-roulette/)
+31
View File
@@ -0,0 +1,31 @@
import eslintConfigPrettier from "eslint-config-prettier";
import globals from "globals";
import pluginJs from "@eslint/js";
import pluginVue from "eslint-plugin-vue";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
languageOptions: {
globals: {
...globals.browser,
confetti: "readonly",
},
},
},
pluginJs.configs.all,
...pluginVue.configs["flat/recommended"],
{
rules: {
"no-magic-numbers": "off",
"sort-keys": "off",
"no-warning-comments": "off",
"no-ternary": "off",
"one-var": "off",
"max-statements": ["warn", 50],
"max-params": ["warn", 5],
"max-lines": "off",
},
},
eslintConfigPrettier,
];
+35 -33
View File
@@ -3,8 +3,14 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Meeting Roulette</title> <title>Meeting Roulette</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script type="importmap">
<script type="text/javascript" src="main.js"></script> {
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module" src="main.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link <link
href="https://cdn.jsdelivr.net/npm/daisyui@5.0.0-beta.6/daisyui.css" href="https://cdn.jsdelivr.net/npm/daisyui@5.0.0-beta.6/daisyui.css"
@@ -123,40 +129,36 @@
</svg> </svg>
</button> </button>
</div> </div>
<div <div class="hidden lg:block p-3 bg-base-200 rounded-box">
class="hidden lg:block p-3 bg-base-200 rounded-box" <div class="stats stats-vertical w-full">
> <div class="stat">
<div <div class="stat-title">Meeting duration so far</div>
class="stats stats-vertical w-full" <div class="stat-value" :id="rid + 1">
> {{ timeText(elapsedTime) }}
<div class="stat"> </div>
<div class="stat-title">Meeting duration so far</div> <div class="stat-desc" :id="rid + 2">
<div class="stat-value" :id="rid + 1"> Started at: <b>{{ timeText(startedAt, 2) }}</b>
{{ timeText(elapsedTime) }} </div>
</div> </div>
<div class="stat-desc" :id="rid + 2"> <div class="stat">
Started at: <b>{{ timeText(startedAt, 2) }}</b> <div class="stat-title">Remaining meeting time</div>
<div class="stat-value" :id="rid + 3">
{{ timeText(totalRemainingTime) }}
</div>
<div class="stat-desc" :id="rid + 4">
End estimated at: <b>{{ timeText(estimatedEnd, 2) }}</b>
</div>
</div>
<div class="stat">
<div class="stat-title">Total meeting time</div>
<div class="stat-value" :id="rid + 4">
{{ timeText(totalTime) }}
</div>
<div class="stat-desc">
Overtime: <b>{{ timeText(overtimeTime) }}</b>
</div>
</div> </div>
</div> </div>
<div class="stat">
<div class="stat-title">Remaining meeting time</div>
<div class="stat-value" :id="rid + 3">
{{ timeText(totalRemainingTime) }}
</div>
<div class="stat-desc" :id="rid + 4">
End estimated at: <b>{{ timeText(estimatedEnd, 2) }}</b>
</div>
</div>
<div class="stat">
<div class="stat-title">Total meeting time</div>
<div class="stat-value" :id="rid + 4">
{{ timeText(totalTime) }}
</div>
<div class="stat-desc">
Overtime: <b>{{ timeText(overtimeTime) }}</b>
</div>
</div>
</div>
</div> </div>
<div class="hidden lg:block grow"></div> <div class="hidden lg:block grow"></div>
<div class="hidden lg:block p-3 bg-base-200 rounded-box"> <div class="hidden lg:block p-3 bg-base-200 rounded-box">
+86 -84
View File
@@ -1,3 +1,5 @@
import { createApp } from "vue";
const DAISYUI_THEMES = [ const DAISYUI_THEMES = [
"light", "light",
"dark", "dark",
@@ -32,7 +34,7 @@ const DAISYUI_THEMES = [
"abyss", "abyss",
]; ];
let app = { const app = createApp({
data() { data() {
return { return {
rawData: rawData:
@@ -51,18 +53,13 @@ let app = {
initialSpin: true, initialSpin: true,
initialColor: Math.floor(Math.random() * 4), initialColor: Math.floor(Math.random() * 4),
rid: 0, rid: 0,
beepTimer: undefined, beepTimer: null,
sound: undefined, sound: null,
themes: DAISYUI_THEMES, themes: DAISYUI_THEMES,
currentTheme: "cmyk", currentTheme: "cmyk",
spinning: false, spinning: false,
}; };
}, },
watch: {
rawData() {
this.data = this.getData();
},
},
computed: { computed: {
selectedData() { selectedData() {
return ( return (
@@ -83,12 +80,10 @@ let app = {
return this.elapsedTime + this.totalRemainingTime; return this.elapsedTime + this.totalRemainingTime;
}, },
totalExpectedTime() { totalExpectedTime() {
return this.data.map((item) => item.time).reduce((a, b) => a + b, 0); return this.data.reduce((sum, item) => sum + item.time, 0);
}, },
totalRemainingTime() { totalRemainingTime() {
return this.filteredData return this.filteredData.reduce((sum, item) => sum + item.time, 0);
.map((item) => item.time)
.reduce((a, b) => a + b, 0);
}, },
overtimeTime() { overtimeTime() {
return this.totalTime - this.totalExpectedTime; return this.totalTime - this.totalExpectedTime;
@@ -116,7 +111,7 @@ let app = {
const textScale = this.textScale(item.text, angleRad); const textScale = this.textScale(item.text, angleRad);
totalAngle += angleDeg; totalAngle += angleDeg;
const colorIndex = const colorIndex =
((index == this.filteredData.length - 1 && index % 4 == 0 ? 1 : 0) + ((index === this.filteredData.length - 1 && index % 4 === 0 ? 1 : 0) +
index + index +
this.initialColor) % this.initialColor) %
4; 4;
@@ -140,31 +135,56 @@ let app = {
}); });
}, },
}, },
watch: {
rawData() {
this.data = this.getData();
},
},
mounted() {
this.sound = new Audio("./sound.wav");
this.rawData = atob(this.getCookie("rawData", btoa(this.rawData)));
this.currentTheme = this.getCookie("theme", this.currentTheme);
this.data = this.getData();
setTimeout(this.showApp);
setInterval(() => {
this.rid = Math.random();
if (this.timerStarted) {
document.title = `${this.timerParts(0)}${this.timerParts(
1
)}:${this.timerParts(2)}:${this.timerParts(3)}`;
}
this.elapsedTime = (new Date() - this.meetingStart) / (1000 * 60);
this.date = new Date();
}, 200);
},
methods: { methods: {
textScale(text, angleRad) { textScale(text, angleRad) {
const r = 1.2; const ratio = 1.2;
const n = text.length; return (
const k = n + r / (2 * Math.tan(Math.min(Math.PI, angleRad) / 2)); (text.length +
return k / r; ratio / (2 * Math.tan(Math.min(Math.PI, angleRad) / 2))) /
ratio
);
}, },
overtime() { overtime() {
return this.timerStarted && this.timerEnd - new Date() <= 0; return this.timerStarted && this.timerEnd - new Date() <= 0;
}, },
timerParts(i) { timerParts(index) {
const delta = this.timerStarted let delta = 0;
? Math.floor((this.timerEnd - new Date()) / 1000) if (this.timerStarted) {
: this.showSelected delta = Math.floor((this.timerEnd - new Date()) / 1000);
? this.selectedData.time * 60 } else if (this.showSelected) {
: 0; delta = this.selectedData.time * 60;
if (i == 0) { }
if (index === 0) {
return delta < 0 ? "-" : ""; return delta < 0 ? "-" : "";
} }
const hours = Math.floor(Math.abs(delta) / 3600); const hours = Math.floor(Math.abs(delta) / 3600);
if (i == 1) { if (index === 1) {
return String(hours).padStart(2, "0"); return String(hours).padStart(2, "0");
} }
const minutes = Math.floor(Math.abs(delta) / 60 - hours * 60); const minutes = Math.floor(Math.abs(delta) / 60 - hours * 60);
if (i == 2) { if (index === 2) {
return String(minutes).padStart(2, "0"); return String(minutes).padStart(2, "0");
} }
const seconds = Math.abs(delta) % 60; const seconds = Math.abs(delta) % 60;
@@ -174,17 +194,16 @@ let app = {
this.sound.play(); this.sound.play();
}, },
timeText(minutes, padHours = 0) { timeText(minutes, padHours = 0) {
const prefix = minutes >= 0 ? '' : '-'; const prefix = minutes >= 0 ? "" : "-";
minutes = Math.abs(minutes); const absMinutes = Math.abs(minutes);
if (minutes >= 60 || padHours > 0) { if (absMinutes >= 60 || padHours > 0) {
return `${prefix}${Math.floor(minutes / 60) return `${prefix}${Math.floor(absMinutes / 60)
.toFixed(0) .toFixed(0)
.padStart(padHours, "0")}h${(minutes % 60) .padStart(padHours, "0")}h${(absMinutes % 60)
.toFixed(0) .toFixed(0)
.padStart(2, "0")}`; .padStart(2, "0")}`;
} else {
return `${prefix}${(minutes % 60).toFixed(0).padStart(2, "0")}min`;
} }
return `${prefix}${(absMinutes % 60).toFixed(0).padStart(2, "0")}min`;
}, },
spin() { spin() {
if (this.timerStarted || this.noData) return; if (this.timerStarted || this.noData) return;
@@ -204,7 +223,7 @@ let app = {
particleCount: 400, particleCount: 400,
startVelocity: 100, startVelocity: 100,
spread: 100, spread: 100,
origin: { y: 0.9 }, origin: { y: 0.9 }, // eslint-disable-line id-length
}); });
}, 5000); }, 5000);
}, },
@@ -213,7 +232,7 @@ let app = {
return this.svgData[0].id; return this.svgData[0].id;
} }
const angle = 360 - (this.wheelPosition % 360); const angle = 360 - (this.wheelPosition % 360);
for (let index = 0; index < this.svgData.length; index++) { for (let index = 0; index < this.svgData.length; index += 1) {
const element = this.svgData[index]; const element = this.svgData[index];
if (angle >= element.from && angle < element.to) { if (angle >= element.from && angle < element.to) {
return element.id; return element.id;
@@ -222,7 +241,8 @@ let app = {
return 0; return 0;
}, },
getData() { getData() {
const re = /:\s?(?:(?:(\d+)\s?h)?(\d+)?(?:\s?m(?:in)?)?)\s?$/i; const re =
/:\s?(?:(?:(?<hours>\d+)\s?h)?(?<minutes>\d+)?(?:\s?m(?:in)?)?)\s?$/iu;
this.setCookie("rawData", btoa(this.rawData)); this.setCookie("rawData", btoa(this.rawData));
const data = this.rawData const data = this.rawData
.split("\n") .split("\n")
@@ -237,14 +257,15 @@ let app = {
time: 1, time: 1,
disabled: line.substring(0, 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) === "-",
};
} }
return {
id: index,
text: line.substring(0, result.index),
time:
parseInt(result.groups.hours ?? 0, 10) * 60 +
parseInt(result.groups.minutes ?? 0, 10),
disabled: line.substring(0, 1) === "-",
};
}); });
if (data.length === 0) { if (data.length === 0) {
return [ return [
@@ -262,17 +283,18 @@ let app = {
document.getElementById("app").setAttribute("style", ""); document.getElementById("app").setAttribute("style", "");
}, },
removeTopic() { removeTopic() {
let i = 0; let index = 0;
this.rawData = this.rawData this.rawData = this.rawData
.split("\n") .split("\n")
.map((line) => { .map((line) => {
let newLine = line;
if (line.trim().length) { if (line.trim().length) {
if (i === this.selected) { if (index === this.selected) {
line = "-" + line; newLine = `-${line}`;
} }
i += 1; index += 1;
} }
return line; return newLine;
}) })
.join("\n"); .join("\n");
this.showSelected = false; this.showSelected = false;
@@ -292,23 +314,22 @@ let app = {
} }
}, },
setCookie(cname, cvalue, exdays) { setCookie(cname, cvalue, exdays) {
const d = new Date(); const date = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); date.setTime(date.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = "expires=" + d.toUTCString(); const expires = `expires=${date.toUTCString()}`;
console.log(cname + "=" + cvalue + "; path=/; " + expires); document.cookie = `${cname}=${cvalue}; path=/; ${expires}`;
document.cookie = cname + "=" + cvalue + "; path=/; " + expires;
}, },
getCookie(cname, defaultValue) { getCookie(cname, defaultValue) {
let name = cname + "="; const name = `${cname}=`;
let decodedCookie = decodeURIComponent(document.cookie); const decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(";"); const ca = decodedCookie.split(";");
for (let i = 0; i < ca.length; i++) { for (let index = 0; index < ca.length; index += 1) {
let c = ca[i]; let cookie = ca[index];
while (c.charAt(0) == " ") { while (cookie.charAt(0) === " ") {
c = c.substring(1); cookie = cookie.substring(1);
} }
if (c.indexOf(name) == 0) { if (cookie.indexOf(name) === 0) {
return c.substring(name.length, c.length); return cookie.substring(name.length, cookie.length);
} }
} }
return defaultValue; return defaultValue;
@@ -318,27 +339,8 @@ let app = {
this.setCookie("theme", this.currentTheme); this.setCookie("theme", this.currentTheme);
}, },
}, },
mounted: function () { });
console.log("app mounted");
this.sound = new Audio("./sound.wav");
this.rawData = atob(this.getCookie("rawData", btoa(this.rawData)));
this.currentTheme = this.getCookie("theme", this.currentTheme);
this.data = this.getData();
setTimeout(this.showApp);
setInterval(() => {
this.rid = Math.random();
if (this.timerStarted) {
document.title = `${this.timerParts(0)}${this.timerParts(
1
)}:${this.timerParts(2)}:${this.timerParts(3)}`;
}
this.elapsedTime = (new Date() - this.meetingStart) / (1000 * 60);
this.date = new Date();
}, 200);
},
};
window.onload = () => { window.onload = () => {
app = Vue.createApp(app);
app.mount("#app"); app.mount("#app");
}; };
+2052
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "meeting-roulette",
"version": "1.0.0",
"description": "🎡 Spin your meetings",
"scripts": {
"lint": "eslint",
"fix": "eslint --fix"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Klemek/meeting-roulette.git"
},
"author": "klemek",
"license": "ISC",
"bugs": {
"url": "https://github.com/Klemek/meeting-roulette/issues"
},
"homepage": "https://github.com/Klemek/meeting-roulette#readme",
"devDependencies": {
"@eslint/js": "^9.21.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.0.2",
"eslint-plugin-vue": "^9.32.0",
"globals": "^16.0.0"
}
}