vibe coded a neat frontend and started to code the backend in flask
This commit is contained in:
@@ -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)
|
||||
@@ -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));
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user