chatgpt
This commit is contained in:
commit
a9d22e8312
18 changed files with 2308 additions and 0 deletions
223
src/web.rs
Normal file
223
src/web.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
use axum::{
|
||||
extract::State,
|
||||
response::Html,
|
||||
};
|
||||
use axum_extra::extract::cookie::CookieJar;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
db,
|
||||
leaderboard,
|
||||
};
|
||||
|
||||
pub async fn index(
|
||||
State(state): State<AppState>,
|
||||
jar: CookieJar,
|
||||
) -> Html<String> {
|
||||
let groups =
|
||||
leaderboard::build(&state.db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let current_user =
|
||||
match jar.get("county_session") {
|
||||
Some(cookie) =>
|
||||
db::athlete_for_session(
|
||||
&state.db,
|
||||
cookie.value(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|(_, name)| name),
|
||||
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut html = String::from(
|
||||
r#"<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Landkreis-Sprints</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background: #111827;
|
||||
color: #f9fafb;
|
||||
}
|
||||
a { color: #93c5fd; }
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: .55rem;
|
||||
border-bottom: 1px solid #374151;
|
||||
}
|
||||
.rank {
|
||||
font-weight: bold;
|
||||
}
|
||||
.time {
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
.small {
|
||||
color: #9ca3af;
|
||||
font-size: .9rem;
|
||||
}
|
||||
button, .button {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
padding: .6rem .9rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<h1>🚴 Landkreis-Sprints</h1>
|
||||
<div class="small">
|
||||
Grenzübertritte · Millisekunden · Intervals.icu
|
||||
</div>
|
||||
</div>
|
||||
"#,
|
||||
);
|
||||
|
||||
if let Some(name) = current_user {
|
||||
html.push_str(&format!(
|
||||
"<div>Angemeldet als <strong>{}</strong> · \
|
||||
<a class=\"button\" href=\"/logout\">Logout</a></div>",
|
||||
escape_html(&name)
|
||||
));
|
||||
} else {
|
||||
html.push_str(
|
||||
r#"<a class="button" href="/oauth/start">
|
||||
Mit Intervals.icu verbinden
|
||||
</a>"#
|
||||
);
|
||||
}
|
||||
|
||||
html.push_str("</header>");
|
||||
|
||||
if groups.is_empty() {
|
||||
html.push_str(
|
||||
r#"<div class="card">
|
||||
Noch keine Landkreisüberquerungen.
|
||||
</div>"#
|
||||
);
|
||||
}
|
||||
|
||||
for group in groups {
|
||||
html.push_str("<div class=\"card\">");
|
||||
|
||||
html.push_str(&format!(
|
||||
"<h2>{} → {}</h2>",
|
||||
escape_html(&group.from_county),
|
||||
escape_html(&group.to_county)
|
||||
));
|
||||
|
||||
html.push_str(&format!(
|
||||
"<div class=\"small\">10-Minuten-Fenster ab {}</div>",
|
||||
group.bucket_start.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
));
|
||||
|
||||
html.push_str(
|
||||
r#"<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Fahrer</th>
|
||||
<th>Übertritt</th>
|
||||
<th>Intervall</th>
|
||||
<th>Power</th>
|
||||
<th>HR</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>"#
|
||||
);
|
||||
|
||||
for row in group.rows {
|
||||
let interval =
|
||||
match row.interval_url {
|
||||
Some(url) =>
|
||||
format!(
|
||||
"<a href=\"{}\" target=\"_blank\">Intervall</a>",
|
||||
escape_html(&url)
|
||||
),
|
||||
|
||||
None =>
|
||||
"<span class=\"small\">—</span>".into(),
|
||||
};
|
||||
|
||||
let power =
|
||||
row.average_watts
|
||||
.map(|v| format!("{:.0} W", v))
|
||||
.unwrap_or_else(|| "—".into());
|
||||
|
||||
let hr =
|
||||
row.average_heartrate
|
||||
.map(|v| format!("{:.0} bpm", v))
|
||||
.unwrap_or_else(|| "—".into());
|
||||
|
||||
html.push_str(&format!(
|
||||
r#"<tr>
|
||||
<td class="rank">{}</td>
|
||||
<td>{}</td>
|
||||
<td class="time">{}</td>
|
||||
<td>{}</td>
|
||||
<td>{}</td>
|
||||
<td>{}</td>
|
||||
</tr>"#,
|
||||
row.rank,
|
||||
escape_html(&row.athlete),
|
||||
row.crossing_time.format("%H:%M:%S%.3f"),
|
||||
interval,
|
||||
power,
|
||||
hr
|
||||
));
|
||||
}
|
||||
|
||||
html.push_str("</tbody></table>");
|
||||
|
||||
html.push_str("</div>");
|
||||
}
|
||||
|
||||
html.push_str(
|
||||
r#"<footer class="small">
|
||||
Datenquelle: Intervals.icu · Landkreisgeometrien: VG250
|
||||
</footer>
|
||||
</body>
|
||||
</html>"#
|
||||
);
|
||||
|
||||
Html(html)
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
Loading…
Reference in a new issue