mirror of
https://github.com/skoelle/PeopleCostCounter.git
synced 2026-09-17 20:00:25 +00:00
Add MIT license; move variant files to /variants and remove variant2-en
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Meeting Kostenuhr</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root, [data-theme="light"] {
|
||||
--color-bg: #f7f6f2; --color-surface: #f9f8f5; --color-surface-2: #fbfbf9;
|
||||
--color-surface-offset: #f3f0ec; --color-divider: #dcd9d5; --color-border: #d4d1ca;
|
||||
--color-text: #28251d; --color-text-muted: #7a7974; --color-text-faint: #bab9b4;
|
||||
--color-primary: #01696f; --color-primary-hover: #0c4e54;
|
||||
--color-warning: #964219; --color-error: #a12c7b;
|
||||
--shadow-sm: 0 1px 2px oklch(0.2 0.01 80 / 0.06);
|
||||
--shadow-md: 0 4px 12px oklch(0.2 0.01 80 / 0.08);
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--color-bg: #0f0e0d; --color-surface: #161513; --color-surface-2: #1c1b19;
|
||||
--color-surface-offset: #222120; --color-divider: #2a2927; --color-border: #333230;
|
||||
--color-text: #cdccca; --color-text-muted: #797876; --color-text-faint: #4a4948;
|
||||
--color-primary: #4f98a3; --color-primary-hover: #6ab8c3;
|
||||
--color-warning: #e8954a; --color-error: #d163a7;
|
||||
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.25);
|
||||
--shadow-md: 0 4px 12px oklch(0 0 0 / 0.35);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
--color-bg:#0f0e0d; --color-surface:#161513; --color-surface-2:#1c1b19;
|
||||
--color-surface-offset:#222120; --color-divider:#2a2927; --color-border:#333230;
|
||||
--color-text:#cdccca; --color-text-muted:#797876; --color-text-faint:#4a4948;
|
||||
--color-primary:#4f98a3; --color-primary-hover:#6ab8c3;
|
||||
--color-warning:#e8954a; --color-error:#d163a7;
|
||||
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.25);
|
||||
--shadow-md: 0 4px 12px oklch(0 0 0 / 0.35);
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem; --radius-full: 9999px;
|
||||
--tr: 180ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
||||
--text-sm: clamp(0.875rem, 0.8rem + 0.35vw, 1rem);
|
||||
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
|
||||
--font-body: 'Inter', 'Helvetica Neue', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
}
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
font-family: var(--font-body); font-size: var(--text-base);
|
||||
color: var(--color-text); background-color: var(--color-bg);
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 2rem 1rem;
|
||||
transition: background-color var(--tr), color var(--tr);
|
||||
}
|
||||
input, button, select { font: inherit; color: inherit; }
|
||||
button { cursor: pointer; background: none; border: none; }
|
||||
|
||||
.header {
|
||||
width: 100%; max-width: 680px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.logo { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.logo-icon { width: 32px; height: 32px; color: var(--color-primary); }
|
||||
.logo-text { font-size: var(--text-base); font-weight: 600; letter-spacing: -0.01em; }
|
||||
.theme-toggle {
|
||||
width: 36px; height: 36px; border-radius: var(--radius-md);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
transition: color var(--tr), background var(--tr);
|
||||
}
|
||||
.theme-toggle:hover { color: var(--color-text); background: var(--color-surface-offset); }
|
||||
|
||||
.card {
|
||||
width: 100%; max-width: 680px;
|
||||
background: var(--color-surface); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl); padding: 2rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: background var(--tr), border-color var(--tr);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid; grid-template-columns: 1fr 1fr auto;
|
||||
gap: 1rem; align-items: flex-end; margin-bottom: 2rem;
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.controls { grid-template-columns: 1fr 1fr; }
|
||||
.start-btn { grid-column: 1 / -1; }
|
||||
}
|
||||
.field { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
label {
|
||||
font-size: var(--text-xs); font-weight: 500;
|
||||
text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-muted);
|
||||
}
|
||||
.input-wrapper { position: relative; }
|
||||
.input-wrapper .unit {
|
||||
position: absolute; right: 1rem; top: 50%; transform: translateY(-50%);
|
||||
font-size: var(--text-sm); color: var(--color-text-muted); pointer-events: none;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 100%; padding: 0.75rem 1rem;
|
||||
background: var(--color-surface-2); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md); font-size: var(--text-base); color: var(--color-text);
|
||||
outline: none;
|
||||
transition: border-color var(--tr), box-shadow var(--tr);
|
||||
-moz-appearance: textfield; appearance: textfield;
|
||||
}
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; }
|
||||
input[type="number"]:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-primary) 20%, transparent);
|
||||
}
|
||||
input[type="number"].has-unit { padding-right: 3rem; }
|
||||
input:disabled { opacity: 0.5; }
|
||||
|
||||
.start-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--color-primary); color: #fff;
|
||||
border-radius: var(--radius-md); font-size: var(--text-sm); font-weight: 600;
|
||||
height: 44px; white-space: nowrap;
|
||||
transition: background var(--tr), transform var(--tr);
|
||||
box-shadow: var(--shadow-sm); border: 1px solid transparent;
|
||||
}
|
||||
.start-btn:hover { background: var(--color-primary-hover); }
|
||||
.start-btn:active { transform: scale(0.98); }
|
||||
.start-btn.running {
|
||||
background: transparent; border-color: var(--color-error); color: var(--color-error);
|
||||
}
|
||||
.start-btn.running:hover { background: color-mix(in oklch, var(--color-error) 10%, transparent); }
|
||||
|
||||
.meta-row { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
|
||||
.meta-item { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.meta-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-faint); font-weight: 500; }
|
||||
.meta-value { font-size: var(--text-sm); font-family: var(--font-mono); color: var(--color-text-muted); }
|
||||
|
||||
.divider { height: 1px; background: var(--color-divider); margin-bottom: 2rem; }
|
||||
|
||||
.cost-display {
|
||||
text-align: center; padding: 2.5rem 1.5rem;
|
||||
background: var(--color-surface-2); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg); position: relative; overflow: hidden;
|
||||
margin-bottom: 1.5rem; min-height: 180px;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.75rem;
|
||||
transition: background var(--tr);
|
||||
}
|
||||
.cost-display::before {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: radial-gradient(ellipse at 50% 0%, color-mix(in oklch, var(--color-primary) 8%, transparent) 0%, transparent 70%);
|
||||
pointer-events: none; opacity: 0; transition: opacity 0.4s ease;
|
||||
}
|
||||
.cost-display.running::before { opacity: 1; }
|
||||
|
||||
.cost-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.1em; color: var(--color-text-faint); font-weight: 500; }
|
||||
.cost-amount {
|
||||
font-family: var(--font-mono);
|
||||
font-size: clamp(2.5rem, 5vw + 1rem, 5rem);
|
||||
font-weight: 700; color: var(--color-text);
|
||||
line-height: 1; letter-spacing: -0.02em;
|
||||
transition: color 0.3s ease; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.cost-amount.running { color: var(--color-primary); }
|
||||
.cost-amount .currency {
|
||||
font-size: 0.45em; font-weight: 400; vertical-align: 0.15em;
|
||||
color: var(--color-text-muted); margin-right: 0.1em;
|
||||
}
|
||||
.cost-sub { font-size: var(--text-xs); color: var(--color-text-faint); font-family: var(--font-mono); }
|
||||
|
||||
.idle-hint { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; color: var(--color-text-faint); }
|
||||
.idle-hint svg { width: 32px; height: 32px; opacity: 0.4; }
|
||||
.idle-hint p { font-size: var(--text-sm); max-width: 36ch; }
|
||||
|
||||
.rate-row {
|
||||
display: flex; align-items: center; justify-content: center; gap: 0.75rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--color-surface-offset); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.rate-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-muted); font-weight: 500; }
|
||||
.rate-value { font-family: var(--font-mono); font-size: var(--text-base); font-weight: 700; color: var(--color-warning); font-variant-numeric: tabular-nums; }
|
||||
.rate-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: var(--color-text-faint); flex-shrink: 0; }
|
||||
.rate-dot.pulse { background: var(--color-warning); animation: pulse 1s ease-in-out infinite; }
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.7); }
|
||||
}
|
||||
|
||||
.timer-row {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--color-divider);
|
||||
}
|
||||
.timer-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-faint); font-weight: 500; }
|
||||
.timer-value { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
|
||||
|
||||
:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; border-radius: var(--radius-sm); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<svg class="logo-icon" viewBox="0 0 32 32" fill="none" aria-label="Meeting Kostenuhr">
|
||||
<circle cx="16" cy="16" r="13" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M16 8v8l5 3" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span class="logo-text">Meeting Kostenuhr</span>
|
||||
</div>
|
||||
<button class="theme-toggle" data-theme-toggle aria-label="Erscheinungsbild wechseln">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<div class="controls">
|
||||
<div class="field">
|
||||
<label for="developers">Anwesende Entwickler</label>
|
||||
<input type="number" id="developers" min="1" max="200" value="5" placeholder="z.B. 8">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="salary">Durchschnittsgehalt</label>
|
||||
<div class="input-wrapper">
|
||||
<input type="number" id="salary" min="1000" max="50000" value="5000" class="has-unit" placeholder="5000">
|
||||
<span class="unit">€/Mo</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="start-btn" id="startBtn" aria-label="Timer starten" onclick="toggleTimer()">
|
||||
▶ Start
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="meta-row" id="metaRow" style="display:none">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Jahresgehalt gesamt</span>
|
||||
<span class="meta-value" id="metaJahres">-</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">inkl. Arbeitgeberanteil (×1,2)</span>
|
||||
<span class="meta-value" id="metaAG">-</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">Gesamtkosten / Jahr</span>
|
||||
<span class="meta-value" id="metaGesamt">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider" id="metaDivider" style="display:none"></div>
|
||||
|
||||
<div class="cost-display" id="costDisplay">
|
||||
<div class="idle-hint" id="idleHint">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
<p>Entwickler-Anzahl und Gehalt eingeben, dann Start drücken</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rate-row" id="rateRow" style="opacity:0.4; pointer-events:none">
|
||||
<div class="rate-dot" id="rateDot"></div>
|
||||
<span class="rate-label">Kosten pro Sekunde</span>
|
||||
<span class="rate-value" id="rateValue">—</span>
|
||||
</div>
|
||||
|
||||
<div class="timer-row" id="timerRow" style="display:none">
|
||||
<span class="timer-label">Meeting-Dauer</span>
|
||||
<span class="timer-value" id="timerValue">00:00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var t = document.querySelector('[data-theme-toggle]');
|
||||
var r = document.documentElement;
|
||||
var d = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
r.setAttribute('data-theme', d);
|
||||
setIcon(t, d);
|
||||
if(t) t.addEventListener('click', function(){
|
||||
d = d==='dark' ? 'light' : 'dark';
|
||||
r.setAttribute('data-theme', d);
|
||||
setIcon(t, d);
|
||||
});
|
||||
function setIcon(b, mode){
|
||||
if(!b) return;
|
||||
if(mode==='dark'){
|
||||
b.innerHTML='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
|
||||
} else {
|
||||
b.innerHTML='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
var interval=null, startTime=null, elapsed=0, rate=0, running=false;
|
||||
|
||||
function fmt(v,d){ return v.toLocaleString('de-DE',{minimumFractionDigits:d,maximumFractionDigits:d}); }
|
||||
function fmtT(s){ var h=Math.floor(s/3600),m=Math.floor(s%3600/60),ss=s%60; return [h,m,ss].map(function(v){return String(v).padStart(2,'0');}).join(':'); }
|
||||
|
||||
function calc(){
|
||||
var dev=parseInt(document.getElementById('developers').value)||0;
|
||||
var sal=parseFloat(document.getElementById('salary').value)||0;
|
||||
var jg=sal*12, ag=jg*1.2, ges=ag*dev, ps=ges/220/8/3600;
|
||||
return {dev:dev,sal:sal,jg:jg,ag:ag,ges:ges,ps:ps};
|
||||
}
|
||||
|
||||
function render(cost){
|
||||
var cd=document.getElementById('costDisplay');
|
||||
var hint=document.getElementById('idleHint');
|
||||
if(hint) hint.style.display='none';
|
||||
var lbl=document.getElementById('cLbl'), amt=document.getElementById('cAmt'), sub=document.getElementById('cSub');
|
||||
if(!amt){
|
||||
lbl=document.createElement('span'); lbl.className='cost-label'; lbl.id='cLbl'; lbl.textContent='Bisher verbraucht';
|
||||
amt=document.createElement('div'); amt.className='cost-amount'; amt.id='cAmt';
|
||||
sub=document.createElement('span'); sub.className='cost-sub'; sub.id='cSub';
|
||||
cd.appendChild(lbl); cd.appendChild(amt); cd.appendChild(sub);
|
||||
}
|
||||
amt.innerHTML='<span class="currency">\u20ac</span>'+fmt(cost,2);
|
||||
amt.className='cost-amount'+(running?' running':'');
|
||||
sub.textContent=elapsed+' Sek. \u00d7 '+fmt(rate,4)+' \u20ac/s';
|
||||
}
|
||||
|
||||
function showMeta(c){
|
||||
document.getElementById('metaRow').style.display='';
|
||||
document.getElementById('metaDivider').style.display='';
|
||||
document.getElementById('metaJahres').textContent=fmt(c.jg,0)+' \u20ac';
|
||||
document.getElementById('metaAG').textContent=fmt(c.ag,0)+' \u20ac';
|
||||
document.getElementById('metaGesamt').textContent=fmt(c.ges,0)+' \u20ac';
|
||||
}
|
||||
|
||||
function toggleTimer(){ if(!running) startTimer(); else stopTimer(); }
|
||||
|
||||
function startTimer(){
|
||||
var c=calc();
|
||||
if(c.dev<1||c.sal<1){ alert('Bitte Werte eingeben.'); return; }
|
||||
rate=c.ps; running=true; elapsed=0; startTime=Date.now();
|
||||
var btn=document.getElementById('startBtn');
|
||||
btn.innerHTML='\u23f9 Stop'; btn.classList.add('running');
|
||||
document.getElementById('costDisplay').classList.add('running');
|
||||
var rr=document.getElementById('rateRow');
|
||||
rr.style.opacity='1'; rr.style.pointerEvents='';
|
||||
document.getElementById('rateDot').classList.add('pulse');
|
||||
document.getElementById('rateValue').textContent=fmt(c.ps,4)+' \u20ac';
|
||||
document.getElementById('timerRow').style.display='';
|
||||
document.getElementById('developers').disabled=true;
|
||||
document.getElementById('salary').disabled=true;
|
||||
showMeta(c); render(0);
|
||||
interval=setInterval(function(){
|
||||
elapsed=Math.floor((Date.now()-startTime)/1000);
|
||||
render(rate*elapsed);
|
||||
document.getElementById('timerValue').textContent=fmtT(elapsed);
|
||||
},200);
|
||||
}
|
||||
|
||||
function stopTimer(){
|
||||
clearInterval(interval); interval=null; running=false;
|
||||
var btn=document.getElementById('startBtn');
|
||||
btn.innerHTML='\u25b6 Start'; btn.classList.remove('running');
|
||||
document.getElementById('costDisplay').classList.remove('running');
|
||||
document.getElementById('rateDot').classList.remove('pulse');
|
||||
document.getElementById('developers').disabled=false;
|
||||
document.getElementById('salary').disabled=false;
|
||||
render(rate*elapsed);
|
||||
}
|
||||
|
||||
['developers','salary'].forEach(function(id){
|
||||
document.getElementById(id).addEventListener('input',function(){
|
||||
if(!running){ var c=calc(); if(c.ps>0){ document.getElementById('rateValue').textContent=fmt(c.ps,4)+' \u20ac'; document.getElementById('rateRow').style.opacity='1'; } }
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Meeting Cost Tracker (EN)</title>
|
||||
<style>
|
||||
/* Minimal styles to match variant2 look */
|
||||
body{font-family:Inter,system-ui,sans-serif;background:#f7f6f2;color:#28251d;padding:40px}
|
||||
.card{max-width:900px;margin:0 auto;background:#fff;padding:20px;border-radius:12px}
|
||||
.label{font-size:12px;color:#6b7280}
|
||||
.big{font-family:monospace;font-size:48px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Meeting Cost Tracker</h1>
|
||||
<div>
|
||||
<label class="label">Developers present</label>
|
||||
<input id="devCount" type="number" value="5" min="1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">Avg monthly gross salary (€)</label>
|
||||
<input id="avgSalary" type="number" value="5000" min="1" />
|
||||
</div>
|
||||
<div style="margin-top:12px">
|
||||
<button id="startBtn">Start</button>
|
||||
<button id="resetBtn">Reset</button>
|
||||
</div>
|
||||
<hr/>
|
||||
<div>
|
||||
<div class="label">Total cost so far</div>
|
||||
<div id="total" class="big">0,00 €</div>
|
||||
<div class="label">Cost per second: <span id="perSec">0,00 €</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
/* Simple duplicate of variant2 calculations in English */
|
||||
function fmt(n){return n.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'}
|
||||
function calc(){var d=Number(document.getElementById('devCount').value)||1;var s=Number(document.getElementById('avgSalary').value)||5000;var annual=s*12;var withEmp=annual*1.2;var total=withEmp*d;var perSec=total/220/8/3600;return{d:d,s:s,annual:annual,withEmp:withEmp,total:total,perSec:perSec}}
|
||||
var timer=null, acc=0;
|
||||
document.getElementById('startBtn').addEventListener('click',function(){ if(timer) return; var p=calc(); timer=setInterval(function(){acc+=p.perSec;document.getElementById('total').textContent=fmt(acc);},1000); document.getElementById('perSec').textContent=fmt(calc().perSec);});
|
||||
document.getElementById('resetBtn').addEventListener('click',function(){ clearInterval(timer);timer=null;acc=0;document.getElementById('total').textContent=fmt(0);});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,234 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Meeting Cost Tracker</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0b0f14;
|
||||
--panel:#0f1620;
|
||||
--muted:#9aa6b2;
|
||||
--accent:#00d4ff;
|
||||
--danger:#ff6b6b;
|
||||
--glass: rgba(255,255,255,0.03);
|
||||
}
|
||||
html,body{height:100%;}
|
||||
body{
|
||||
margin:0;padding:40px;box-sizing:border-box;
|
||||
font-family:Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
|
||||
background:linear-gradient(180deg,#06070a 0%, #081018 60%); color:#e6eef6;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr;gap:28px}
|
||||
|
||||
.panel{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); padding:22px;border-radius:12px;box-shadow:0 6px 30px rgba(0,0,0,0.6);}
|
||||
|
||||
.controls{display:flex;gap:18px;align-items:end;flex-wrap:wrap}
|
||||
.field{display:flex;flex-direction:column;gap:6px}
|
||||
label{font-size:13px;color:var(--muted)}
|
||||
input[type=number]{
|
||||
background:var(--panel);border:1px solid rgba(255,255,255,0.04);padding:10px 12px;border-radius:8px;color:inherit;font-size:16px;width:220px
|
||||
}
|
||||
.small{width:150px}
|
||||
.row-actions{display:flex;gap:10px}
|
||||
button{cursor:pointer;border:0;padding:10px 16px;border-radius:9px;font-weight:600}
|
||||
button.start{background:linear-gradient(90deg,var(--accent),#6ee7b7);color:#042028}
|
||||
button.start[disabled]{opacity:0.45;cursor:not-allowed}
|
||||
button.reset{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted)}
|
||||
|
||||
.big-display{display:flex;align-items:center;gap:20px;padding:28px;border-radius:12px}
|
||||
.amount{font-weight:800;font-size:86px;line-height:1;color:#fff;letter-spacing:-2px}
|
||||
.meta{display:flex;flex-direction:column;gap:8px}
|
||||
.label{color:var(--muted);font-size:15px}
|
||||
.per-sec{font-size:20px;color:var(--accent);font-weight:700}
|
||||
|
||||
.status{display:flex;align-items:center;gap:12px}
|
||||
.pulse{width:14px;height:14px;border-radius:999px;background:#17343b;box-shadow:0 0 0 4px rgba(0,0,0,0.2);position:relative}
|
||||
.pulse.active{background:var(--accent);box-shadow:0 0 20px rgba(0,212,255,0.18);}
|
||||
.pulse.active::after{content:"";position:absolute;inset:0;border-radius:999px;animation:ping 1.2s infinite}
|
||||
@keyframes ping{0%{transform:scale(1);opacity:0.9}50%{transform:scale(1.9);opacity:0.22}100%{transform:scale(2.6);opacity:0}}
|
||||
|
||||
.errors{color:var(--danger);font-size:13px;margin-top:6px}
|
||||
|
||||
/* layout for 1080p wide */
|
||||
@media(min-width:900px){
|
||||
.wrap{grid-template-columns:1fr}
|
||||
.big-display{background:var(--glass);justify-content:space-between}
|
||||
.meta{align-items:flex-end;text-align:right}
|
||||
}
|
||||
|
||||
footer{color:var(--muted);font-size:13px;padding-top:8px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="panel controls" style="align-items:flex-start">
|
||||
<div class="field">
|
||||
<label for="devs">Developers present</label>
|
||||
<input id="devs" type="number" min="1" step="1" placeholder="Anzahl Entwickler" aria-label="Developers present">
|
||||
<div id="devs-error" class="errors" aria-live="polite" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="salary">Avg. monthly gross salary (€)</label>
|
||||
<input id="salary" class="small" type="number" min="0.01" step="0.01" value="5000" aria-label="Avg monthly salary">
|
||||
<div id="salary-error" class="errors" aria-live="polite" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="row-actions" style="margin-left:6px">
|
||||
<button id="startBtn" class="start">Start</button>
|
||||
<button id="resetBtn" class="reset">Reset</button>
|
||||
</div>
|
||||
|
||||
<div style="flex:1"></div>
|
||||
|
||||
<div class="status" style="margin-top:6px">
|
||||
<div id="dot" class="pulse" title="Counter status"></div>
|
||||
<div style="color:var(--muted);font-size:13px">Counter status</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel big-display">
|
||||
<div>
|
||||
<div class="label">Total cost so far</div>
|
||||
<div id="total" class="amount">0,00 €</div>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<div class="label">Cost per second</div>
|
||||
<div id="perSecond" class="per-sec">0,00 €</div>
|
||||
<footer>Using 220 working days · employer contributions ×1.2</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const devsInput = document.getElementById('devs');
|
||||
const salaryInput = document.getElementById('salary');
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const totalEl = document.getElementById('total');
|
||||
const perSecEl = document.getElementById('perSecond');
|
||||
const devsErr = document.getElementById('devs-error');
|
||||
const salErr = document.getElementById('salary-error');
|
||||
const dot = document.getElementById('dot');
|
||||
|
||||
const fmt = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', minimumFractionDigits:2, maximumFractionDigits:2 });
|
||||
|
||||
const WORKING_SECONDS_PER_YEAR = 220 * 8 * 60 * 60; // 220 days * 8 hours * 3600 sec
|
||||
|
||||
let intervalId = null;
|
||||
let accumulated = 0;
|
||||
let costPerSecond = 0;
|
||||
|
||||
function parseInputs(){
|
||||
const devsRaw = devsInput.value.trim();
|
||||
const salaryRaw = salaryInput.value.trim();
|
||||
const devs = devsRaw === '' ? NaN : Number(devsRaw);
|
||||
const salary = salaryRaw === '' ? NaN : Number(salaryRaw);
|
||||
return {devs, salary};
|
||||
}
|
||||
|
||||
function validateBeforeStart(){
|
||||
const {devs, salary} = parseInputs();
|
||||
let ok = true;
|
||||
devsErr.style.display = 'none'; salErr.style.display = 'none';
|
||||
if (!Number.isInteger(devs) || devs < 1){
|
||||
devsErr.textContent = 'Please enter a whole number ≥ 1'; devsErr.style.display = 'block'; ok=false;
|
||||
}
|
||||
if (!(typeof salary === 'number') || Number.isNaN(salary) || salary <= 0){
|
||||
salErr.textContent = 'Monthly salary must be > 0'; salErr.style.display = 'block'; ok=false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
function recalc(){
|
||||
const {devs, salary} = parseInputs();
|
||||
// Clear inline errors while editing
|
||||
devsErr.style.display = 'none'; salErr.style.display = 'none';
|
||||
|
||||
if (!Number.isFinite(devs) || !Number.isFinite(salary) || devs <= 0 || salary <= 0){
|
||||
costPerSecond = 0;
|
||||
perSecEl.textContent = fmt.format(0);
|
||||
return;
|
||||
}
|
||||
const annual = salary * 12;
|
||||
const withContrib = annual * 1.2;
|
||||
const totalAnnual = withContrib * devs;
|
||||
costPerSecond = totalAnnual / WORKING_SECONDS_PER_YEAR;
|
||||
perSecEl.textContent = fmt.format(costPerSecond);
|
||||
}
|
||||
|
||||
function tick(){
|
||||
// add current costPerSecond (which may have changed) and update display
|
||||
accumulated += costPerSecond;
|
||||
totalEl.textContent = fmt.format(accumulated);
|
||||
}
|
||||
|
||||
function start(){
|
||||
if (intervalId) return;
|
||||
if (!validateBeforeStart()) return;
|
||||
// ensure latest calc applied
|
||||
recalc();
|
||||
intervalId = setInterval(tick, 1000);
|
||||
startBtn.disabled = true;
|
||||
dot.classList.add('active');
|
||||
// update UI immediately (don't wait 1s for first tick)
|
||||
totalEl.textContent = fmt.format(accumulated);
|
||||
}
|
||||
|
||||
function reset(){
|
||||
if (intervalId){
|
||||
clearInterval(intervalId); intervalId = null;
|
||||
}
|
||||
accumulated = 0;
|
||||
totalEl.textContent = fmt.format(0);
|
||||
startBtn.disabled = false;
|
||||
dot.classList.remove('active');
|
||||
}
|
||||
|
||||
// wire events
|
||||
startBtn.addEventListener('click', start);
|
||||
resetBtn.addEventListener('click', reset);
|
||||
|
||||
devsInput.addEventListener('input', function(){
|
||||
// If user types non-integer, show a gentle inline message but don't block running counter
|
||||
const v = devsInput.value.trim();
|
||||
if (v === ''){
|
||||
devsErr.textContent = 'Required'; devsErr.style.display='block';
|
||||
} else if (!Number.isInteger(Number(v)) || Number(v) < 1){
|
||||
devsErr.textContent = 'Must be a whole number ≥ 1'; devsErr.style.display='block';
|
||||
} else {
|
||||
devsErr.style.display='none';
|
||||
}
|
||||
recalc();
|
||||
});
|
||||
|
||||
salaryInput.addEventListener('input', function(){
|
||||
const v = salaryInput.value.trim();
|
||||
if (v === '' || Number(v) <= 0){
|
||||
salErr.textContent = 'Must be > 0'; salErr.style.display='block';
|
||||
} else {
|
||||
salErr.style.display='none';
|
||||
}
|
||||
recalc();
|
||||
});
|
||||
|
||||
// initialize displays
|
||||
perSecEl.textContent = fmt.format(0);
|
||||
totalEl.textContent = fmt.format(0);
|
||||
|
||||
// compute initial per-second using prefilled salary (devs empty so stays 0)
|
||||
recalc();
|
||||
|
||||
// Keyboard: allow Enter on salary or devs to start
|
||||
[devsInput, salaryInput].forEach(el=>{
|
||||
el.addEventListener('keydown', (e)=>{ if(e.key === 'Enter'){ start(); } });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,288 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Software-Entwickler Kostenzähler</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background: #f4f6f8;
|
||||
color: #1f2937;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
background: #ffffff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 32px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 16px;
|
||||
align-items: end;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 15px 28px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.counter-box {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
border-radius: 18px;
|
||||
padding: 36px 24px;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.counter-label {
|
||||
font-size: 18px;
|
||||
color: #d1d5db;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.counter-value {
|
||||
font-size: clamp(42px, 8vw, 84px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 14px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.formula {
|
||||
background: #f9fafb;
|
||||
border-left: 5px solid #2563eb;
|
||||
border-radius: 12px;
|
||||
padding: 18px 20px;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: none;
|
||||
margin-bottom: 20px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.inputs,
|
||||
.metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<h1>Software-Entwickler Kostenzähler</h1>
|
||||
<p class="subtitle">Live-Anzeige der laufenden Kosten während einer Besprechung oder Wartezeit.</p>
|
||||
|
||||
<section class="inputs">
|
||||
<div>
|
||||
<label for="softwareEntwicklerAnwesend">Software-Entwickler anwesend</label>
|
||||
<input id="softwareEntwicklerAnwesend" type="number" min="0" step="1" value="1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="durchschnittsgehalt">Durchschnittsgehalt pro Monat (€)</label>
|
||||
<input id="durchschnittsgehalt" type="number" min="0" step="100" value="5000" />
|
||||
</div>
|
||||
|
||||
<button id="startButton">Start</button>
|
||||
</section>
|
||||
|
||||
<div id="error" class="error">Bitte gültige Werte größer als 0 eingeben.</div>
|
||||
|
||||
<section class="counter-box">
|
||||
<div class="counter-label">Bisher verbraucht</div>
|
||||
<div id="verbrauch" class="counter-value">0,00 €</div>
|
||||
</section>
|
||||
|
||||
<section class="metrics">
|
||||
<div class="metric">
|
||||
<div class="metric-title">Kosten pro Sekunde</div>
|
||||
<div id="kostenProSekunde" class="metric-value">0,00 €</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-title">Gesamtkosten pro Jahr</div>
|
||||
<div id="gesamtkosten" class="metric-value">0,00 €</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-title">Laufzeit</div>
|
||||
<div id="laufzeit" class="metric-value">0 s</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="formula">
|
||||
<strong>Berechnung:</strong><br />
|
||||
Jahresgehalt = Durchschnittsgehalt × 12<br />
|
||||
Jahresgehalt mit Arbeitgeberanteil = Jahresgehalt × 1,2<br />
|
||||
Gesamtkosten = Jahresgehalt mit Arbeitgeberanteil × Software-Entwickler anwesend<br />
|
||||
Kosten pro Sekunde = Gesamtkosten ÷ 365 Tage ÷ 8 Stunden ÷ 60 Minuten ÷ 60 Sekunden
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const softwareEntwicklerInput = document.getElementById('softwareEntwicklerAnwesend');
|
||||
const durchschnittsgehaltInput = document.getElementById('durchschnittsgehalt');
|
||||
const startButton = document.getElementById('startButton');
|
||||
const verbrauchElement = document.getElementById('verbrauch');
|
||||
const kostenProSekundeElement = document.getElementById('kostenProSekunde');
|
||||
const gesamtkostenElement = document.getElementById('gesamtkosten');
|
||||
const laufzeitElement = document.getElementById('laufzeit');
|
||||
const errorElement = document.getElementById('error');
|
||||
|
||||
let intervalId = null;
|
||||
let startZeit = null;
|
||||
let kostenProSekunde = 0;
|
||||
|
||||
const euroFormatter = new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
|
||||
function berechneKosten() {
|
||||
const softwareEntwicklerAnwesend = Number(softwareEntwicklerInput.value);
|
||||
const durchschnittsgehalt = Number(durchschnittsgehaltInput.value);
|
||||
|
||||
if (softwareEntwicklerAnwesend <= 0 || durchschnittsgehalt <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const jahresgehalt = durchschnittsgehalt * 12;
|
||||
const jahresgehaltMitArbeitgeberanteil = jahresgehalt * 1.2;
|
||||
const gesamtkosten = jahresgehaltMitArbeitgeberanteil * softwareEntwicklerAnwesend;
|
||||
const kostenProSekunde = gesamtkosten / 365 / 8 / 60 / 60;
|
||||
|
||||
return {
|
||||
gesamtkosten,
|
||||
kostenProSekunde
|
||||
};
|
||||
}
|
||||
|
||||
function aktualisiereAnzeige() {
|
||||
const jetzt = new Date();
|
||||
const vergangeneSekunden = Math.floor((jetzt - startZeit) / 1000);
|
||||
const verbrauch = vergangeneSekunden * kostenProSekunde;
|
||||
|
||||
verbrauchElement.textContent = euroFormatter.format(verbrauch);
|
||||
laufzeitElement.textContent = `${vergangeneSekunden} s`;
|
||||
}
|
||||
|
||||
startButton.addEventListener('click', () => {
|
||||
const kosten = berechneKosten();
|
||||
|
||||
if (!kosten) {
|
||||
errorElement.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
errorElement.style.display = 'none';
|
||||
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
kostenProSekunde = kosten.kostenProSekunde;
|
||||
startZeit = new Date();
|
||||
|
||||
kostenProSekundeElement.textContent = euroFormatter.format(kostenProSekunde);
|
||||
gesamtkostenElement.textContent = euroFormatter.format(kosten.gesamtkosten);
|
||||
verbrauchElement.textContent = euroFormatter.format(0);
|
||||
laufzeitElement.textContent = '0 s';
|
||||
|
||||
aktualisiereAnzeige();
|
||||
intervalId = setInterval(aktualisiereAnzeige, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user