vibe coded a neat frontend and started to code the backend in flask

This commit is contained in:
2026-08-09 21:47:43 +02:00
parent 930a966156
commit 9fad65de54
4 changed files with 380 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
from flask import Flask, jsonify, render_template
app = Flask(__name__)
sessions = [
{ "subject": "Krypto", "minutes": 50, "ts": "26.03.2026 08:16" },
{ "subject": "Netzwerke", "minutes": 40, "ts": "25.03.2026 19:02" },
{ "subject": "Statistik", "minutes": 65, "ts": "25.03.2026 14:30" },
{ "subject": "Krypto", "minutes": 30, "ts": "24.03.2026 09:12" },
{ "subject": "Math", "minutes": 90, "ts": "23.03.2026 20:45" },
{ "subject": "Databases", "minutes": 20, "ts": "23.03.2026 08:05" },
{ "subject": "Krypto", "minutes": 50, "ts": "26.03.2026 08:16" },
{ "subject": "Netzwerke", "minutes": 40, "ts": "25.03.2026 19:02" },
{ "subject": "Statistik", "minutes": 65, "ts": "25.03.2026 14:30" },
{ "subject": "Krypto", "minutes": 30, "ts": "24.03.2026 09:12" },
{ "subject": "Math", "minutes": 90, "ts": "23.03.2026 20:45" },
{ "subject": "Databases", "minutes": 20, "ts": "23.03.2026 08:05" },
]
def calculate_totals(sessions):
totals = {}
for session in sessions:
subject = session["subject"]
minutes = session["minutes"]
hours = minutes / 60
if subject in totals.keys():
totals[subject] += hours
else:
totals[subject] = hours
return totals
def get_daily_minutes():
return [
{"date": "2026-03-23", "minutes": 45},
{"date": "2026-04-08", "minutes": 45},
]
# web ui
@app.route("/")
def index():
labels = [s["subject"] for s in sessions]
values = [s["minutes"] for s in sessions]
return render_template(
"index.html",
labels=labels,
values=values,
sessions=sessions
)
# internal api
@app.route("/api/dashboard")
def api_dashboard():
return jsonify({
"sessions": sessions,
"subjectTotals": calculate_totals(sessions),
"dailyMinutes": get_daily_minutes()
})
# external api
@app.route("/api/sessions")
def api_sessions():
return jsonify(sessions)
if __name__ == "__main__":
app.run(debug=True)
+77
View File
@@ -0,0 +1,77 @@
/* ---------------------------------------------------------
Fetches everything from the Flask API:
GET /api/dashboard ->
{
sessions: [{ subject, minutes, ts }, ...],
subjectTotals: { "Math": 40, ... },
dailyMinutes: [{ date: "2026-03-23", minutes: 45 }, ...]
}
--------------------------------------------------------- */
async function loadDashboard() {
const res = await fetch("/api/dashboard");
if (!res.ok) throw new Error(`API error: ${res.status}`);
const data = await res.json();
renderSessions(data.sessions);
renderDailyDots(data.dailyMinutes);
renderSubjectChart(data.subjectTotals);
}
/* ---------------- render session list ---------------- */
function renderSessions(sessions) {
const listEl = document.getElementById("sessionList");
listEl.innerHTML = "";
sessions.forEach(s => {
const card = document.createElement("div");
card.className = "session-card";
card.innerHTML = `
<div class="subj">${s.subject}: <span class="min">${s.minutes}min</span></div>
<div class="ts">${s.ts}</div>`;
listEl.appendChild(card);
});
}
/* ---------------- render daily dot strip ---------------- */
function renderDailyDots(dailyMinutes) {
const stripEl = document.getElementById("dotStrip");
stripEl.innerHTML = "";
const dayLetters = ["S","M","T","W","T","F","S"];
const maxMin = Math.max(...dailyMinutes.map(d => d.minutes), 1);
dailyMinutes.forEach(d => {
const date = new Date(d.date);
const size = 8 + (d.minutes / maxMin) * 28; // 8px..36px
const col = document.createElement("div");
col.className = "day-col";
col.innerHTML = `
<div class="dot" style="width:${size}px;height:${size}px;opacity:${0.35 + (d.minutes/maxMin)*0.65}"
title="${date.toLocaleDateString('de-DE')}${d.minutes} min"></div>
<div class="day-letter">${dayLetters[date.getDay()]}</div>`;
stripEl.appendChild(col);
});
}
/* ---------------- pie chart ---------------- */
function renderSubjectChart(subjectTotals) {
new Chart(document.getElementById("subjectChart"), {
type: "pie",
data: {
labels: Object.keys(subjectTotals),
datasets: [{
data: Object.values(subjectTotals),
backgroundColor: ["#3B6E5E", "#C9A227", "#B4483A", "#5B594F", "#8FA998"],
borderColor: "#1C1C1A",
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
legend: { position: "bottom", labels: { font: { family: "Space Mono" } } }
}
}
});
}
loadDashboard().catch(err => console.error("Failed to load dashboard:", err));
+173
View File
@@ -0,0 +1,173 @@
:root{
--paper:#F6F4EC;
--ink:#1C1C1A;
--ink-soft:#5B594F;
--line:rgba(28,28,26,0.16);
--accent:#3B6E5E; /* pine primary highlight */
--accent-2:#C9A227; /* amber secondary highlight */
--warn:#B4483A; /* brick "worst subject" flag */
--radius:4px;
}
*{ box-sizing:border-box; }
body{
margin:0;
background:var(--paper);
background-image: radial-gradient(var(--line) 1px, transparent 1px);
background-size: 22px 22px;
color:var(--ink);
font-family:'Space Mono', monospace;
}
h1,h2,h3{ margin:0; font-family:'Caveat', cursive; font-weight:700; }
/* ---------- top-level grid: sidebar | main ---------- */
.app{
display:grid;
grid-template-columns: 260px 1fr;
height:100vh;
overflow:hidden;
}
/* ---------- sidebar ---------- */
.sidebar{
border-right:2px solid var(--ink);
padding:20px 16px;
display:flex;
flex-direction:column;
min-height:0; /* allow child scroll */
}
.logo{ font-size:2rem; padding-bottom:10px; border-bottom:2px solid var(--ink); }
.section-label{
margin-top:20px;
font-family:'Space Mono', monospace;
font-size:0.78rem;
letter-spacing:0.06em;
color:var(--ink-soft);
text-transform:uppercase;
}
.session-list{
margin-top:10px;
flex:1;
min-height:0;
overflow-y:auto;
display:flex;
flex-direction:column;
gap:10px;
padding-right:4px;
scrollbar-width:none; /* Firefox */
-ms-overflow-style:none; /* old Edge/IE */
}
.session-list::-webkit-scrollbar{
display:none; /* Chrome, Safari, new Edge */
}
.session-card{
border:2px solid var(--ink);
border-radius:var(--radius);
padding:8px 10px;
background:var(--paper);
}
.session-card .subj{ font-weight:700; }
.session-card .subj .min{ font-weight:400; color:var(--ink-soft); }
.session-card .ts{ font-size:0.72rem; color:var(--ink-soft); margin-top:2px; }
/* ---------- main content grid ---------- */
.main{
padding:20px;
display:grid;
gap:16px;
grid-template-columns: 2fr 1fr 1fr;
grid-template-areas:
"chart stat1 stat2"
"chart stat3 stat4"
"daily daily daily";
overflow-y:auto;
min-height:0;
scrollbar-width:none;
-ms-overflow-style:none;
}
.main::-webkit-scrollbar{
display:none;
}
.box{
border:2px solid var(--ink);
border-radius:var(--radius);
background:var(--paper);
padding:14px 16px;
}
.box-title{
font-size:1.3rem;
margin-bottom:10px;
}
.chart-box{ grid-area:chart; display:flex; flex-direction:column; }
.chart-box canvas{ max-height:280px; }
.stat-box{
display:flex;
flex-direction:column;
justify-content:center;
gap:6px;
}
.stat-1{ grid-area:stat1; }
.stat-2{ grid-area:stat2; }
.stat-3{ grid-area:stat3; }
.stat-4{ grid-area:stat4; }
.stat-label{ font-size:0.72rem; color:var(--ink-soft); text-transform:uppercase; letter-spacing:0.05em; }
.stat-value{ font-size:1.9rem; font-weight:700; }
.stat-box.accent .stat-value{ color:var(--accent); }
.stat-box.warn .stat-value{ color:var(--warn); }
.daily-box{ grid-area:daily; }
.dot-strip{
display:flex;
align-items:flex-end;
gap:14px;
overflow-x:auto;
padding:10px 4px 4px;
}
.day-col{
display:flex;
flex-direction:column;
align-items:center;
gap:6px;
flex:0 0 auto;
}
.dot{
border:2px solid var(--ink);
border-radius:50%;
background:var(--accent);
}
.day-letter{ font-size:0.72rem; color:var(--ink-soft); }
/* ---------- mobile ---------- */
@media (max-width:760px){
.app{ grid-template-columns:1fr; }
.sidebar{
border-right:none;
border-bottom:2px solid var(--ink);
max-height:200px;
}
.main{
grid-template-columns: 1fr 1fr;
grid-template-areas:
"chart chart"
"stat1 stat2"
"stat3 stat4"
"daily daily";
}
}
@media (max-width:420px){
.main{
grid-template-columns: 1fr;
grid-template-areas:
"chart"
"stat1"
"stat2"
"stat3"
"stat4"
"daily";
}
}
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>studytime_</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="app">
<aside class="sidebar">
<h1 class="logo">studytime</h1>
<h2 class="section-label">Last sessions</h2>
<div class="session-list" id="sessionList"></div>
</aside>
<main class="main">
<section class="box chart-box">
<h3 class="box-title">By subject</h3>
<canvas id="subjectChart"></canvas>
</section>
<section class="box stat-box stat-1">
<span class="stat-label">This semester</span>
<span class="stat-value">260h</span>
</section>
<section class="box stat-box stat-2 accent">
<span class="stat-label">Top subject</span>
<span class="stat-value">Math</span>
</section>
<section class="box stat-box stat-3">
<span class="stat-label">This month</span>
<span class="stat-value">30h</span>
</section>
<section class="box stat-box stat-4 warn">
<span class="stat-label">Worst subject</span>
<span class="stat-value">Databases</span>
</section>
<section class="box daily-box">
<h3 class="box-title">Minutes / day</h3>
<div class="dot-strip" id="dotStrip"></div>
</section>
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>