Translate the dashboard to English; euro instead of lira
Dashboard: every label, empty state, chart legend and the login page are English; numbers and dates use en-GB (24-hour clock, day-month dates), money is EUR, percentages read 80% instead of %80, durations "2 h 05 min". html lang is en. Server version 1.1.2; manifests pin it. Car app: the charging tab's price suffix, validation message and total cost use € instead of ₺. Checked by rendering all three pages from the 1.1.2 image against InfluxDB 2.7.12 in podman. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "nilsmg4-web",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nilsmg4-web",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@influxdata/influxdb-client": "^1.35.0",
|
||||
"next": "^15.5.4",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nilsmg4-web",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -12,10 +12,10 @@ export default async function ChargingPage() {
|
||||
|
||||
if (!installationId || sessions.length === 0) {
|
||||
return (
|
||||
<Shell active="/charging" title="Şarj">
|
||||
<Shell active="/charging" title="Charging">
|
||||
<div className="card">
|
||||
<p className="empty">
|
||||
Şarj kaydı yok. Yalnızca 10 kW üstü DC şarjlar ölçülür; AC şarj kaydedilmez.
|
||||
No charging sessions yet. Only DC charges above 10 kW are metered; AC charging is not recorded.
|
||||
</p>
|
||||
</div>
|
||||
</Shell>
|
||||
@@ -25,22 +25,22 @@ export default async function ChargingPage() {
|
||||
const total = sumCharging(sessions);
|
||||
|
||||
return (
|
||||
<Shell active="/charging" title="Şarj" subtitle={`${sessions.length} DC seans`}>
|
||||
<Shell active="/charging" title="Charging" subtitle={`${sessions.length} DC session${sessions.length === 1 ? "" : "s"}`}>
|
||||
<div className="tiles">
|
||||
<Tile label="Bataryaya giren net" value={precise(total.energyKwh)} unit="kWh" tone="gold" />
|
||||
<Tile label="Şebekeden alınan (+%10)" value={precise(total.gridEnergyKwh)} unit="kWh" tone="gold" />
|
||||
<Tile label="Toplam ödenen" value={money(total.cost)} tone="gold" />
|
||||
<Tile label="Net into battery" value={precise(total.energyKwh)} unit="kWh" tone="gold" />
|
||||
<Tile label="Drawn from grid (+10%)" value={precise(total.gridEnergyKwh)} unit="kWh" tone="gold" />
|
||||
<Tile label="Total paid" value={money(total.cost)} tone="gold" />
|
||||
<Tile
|
||||
label="Ortalama birim"
|
||||
label="Average price"
|
||||
value={total.gridEnergyKwh > 0 ? money(total.cost / total.gridEnergyKwh) : "—"}
|
||||
note="kWh başına"
|
||||
note="per kWh"
|
||||
tone="teal"
|
||||
/>
|
||||
<Tile label="En yüksek güç" value={num(total.peakKw)} unit="kW" tone="teal" />
|
||||
<Tile label="Peak power" value={num(total.peakKw)} unit="kW" tone="teal" />
|
||||
</div>
|
||||
|
||||
<section className="card">
|
||||
<h2 className="card-title">Seanslar</h2>
|
||||
<h2 className="card-title">Sessions</h2>
|
||||
<ul className="list">
|
||||
{sessions.map((session) => {
|
||||
const gained =
|
||||
@@ -59,7 +59,7 @@ export default async function ChargingPage() {
|
||||
<div className="session-meta">
|
||||
{session.isDemo && "Demo · "}
|
||||
{duration(session.durationSeconds)}
|
||||
{gained !== null && ` · %${num(session.startSocPercent as number)} → %${num(session.endSocPercent as number)}`}
|
||||
{gained !== null && ` · ${num(session.startSocPercent as number)}% → ${num(session.endSocPercent as number)}%`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="session-energy">
|
||||
@@ -77,16 +77,16 @@ export default async function ChargingPage() {
|
||||
}))}
|
||||
/>
|
||||
<div className="rows" style={{ marginTop: 14 }}>
|
||||
<Row label="Başlangıç" value={dateTime(session.startedAt)} />
|
||||
<Row label="Bitiş" value={dateTime(session.endedAt)} />
|
||||
<Row label="Ölçülen süre" value={duration(session.durationSeconds)} />
|
||||
<Row label="Bataryaya giren net" value={`${precise(session.energyKwh)} kWh`} />
|
||||
<Row label="Şebekeden alınan (+%10)" value={`${precise(session.gridEnergyKwh ?? session.energyKwh * 1.1)} kWh`} />
|
||||
<Row label="Kazanılan doluluk" value={gained === null ? "—" : `%${num(gained)}`} />
|
||||
<Row label="Tepe güç" value={peak > 0 ? `${num(peak)} kW` : "—"} />
|
||||
<Row label="Ortalama güç" value={average > 0 ? `${num(average)} kW` : "—"} />
|
||||
<Row label="Birim fiyat" value={`${money(session.pricePerKwh)}/kWh`} />
|
||||
<Row label="Tutar" value={money(session.totalCost)} />
|
||||
<Row label="Start" value={dateTime(session.startedAt)} />
|
||||
<Row label="End" value={dateTime(session.endedAt)} />
|
||||
<Row label="Metered duration" value={duration(session.durationSeconds)} />
|
||||
<Row label="Net into battery" value={`${precise(session.energyKwh)} kWh`} />
|
||||
<Row label="Drawn from grid (+10%)" value={`${precise(session.gridEnergyKwh ?? session.energyKwh * 1.1)} kWh`} />
|
||||
<Row label="Charge added" value={gained === null ? "—" : `${num(gained)}%`} />
|
||||
<Row label="Peak power" value={peak > 0 ? `${num(peak)} kW` : "—"} />
|
||||
<Row label="Average power" value={average > 0 ? `${num(average)} kW` : "—"} />
|
||||
<Row label="Unit price" value={`${money(session.pricePerKwh)}/kWh`} />
|
||||
<Row label="Amount" value={money(session.totalCost)} />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -18,9 +18,9 @@ export default async function ConsumptionPage({
|
||||
|
||||
if (!installationId || days.length === 0) {
|
||||
return (
|
||||
<Shell active="/consumption" title="Tüketim">
|
||||
<Shell active="/consumption" title="Consumption">
|
||||
<div className="card">
|
||||
<p className="empty">Günlük tüketim kaydı yok. Araç günü kapattığında yüklenir.</p>
|
||||
<p className="empty">No daily consumption records yet. The car uploads them as it drives.</p>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
@@ -46,14 +46,14 @@ export default async function ConsumptionPage({
|
||||
return (
|
||||
<Shell
|
||||
active="/consumption"
|
||||
title="Tüketim"
|
||||
subtitle={hasFilter ? (validFilter ? isoDate(selectedDate!) : "Seçili gün kaydı yok") : `${days.length} günlük kayıt`}
|
||||
title="Consumption"
|
||||
subtitle={hasFilter ? (validFilter ? isoDate(selectedDate!) : "No record for the selected day") : `${days.length} day${days.length === 1 ? "" : "s"} recorded`}
|
||||
>
|
||||
{/* Date filter */}
|
||||
<section className="card">
|
||||
<form className="filter-form" method="get" action="/consumption">
|
||||
<label className="filter-label" htmlFor="date-picker">
|
||||
Tarihe göre filtrele
|
||||
Filter by date
|
||||
</label>
|
||||
<div className="filter-row">
|
||||
<input
|
||||
@@ -65,9 +65,9 @@ export default async function ConsumptionPage({
|
||||
max={maxDate}
|
||||
className="field date-input"
|
||||
/>
|
||||
<button className="button filter-button" type="submit">Filtrele</button>
|
||||
<button className="button filter-button" type="submit">Filter</button>
|
||||
{hasFilter && (
|
||||
<Link className="filter-clear" href="/consumption">Temizle</Link>
|
||||
<Link className="filter-clear" href="/consumption">Clear</Link>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
@@ -80,60 +80,60 @@ export default async function ConsumptionPage({
|
||||
{validFilter && selected ? (
|
||||
<>
|
||||
<div className="tiles">
|
||||
<Tile label="Mesafe" value={num(selected.day.km)} unit="km" tone="teal" />
|
||||
<Tile label="Enerji" value={precise(selected.day.kwh)} unit="kWh" tone="gold" />
|
||||
<Tile label="Ortalama" value={efficiency(selected.day.kwh, selected.day.km)} tone="teal" />
|
||||
<Tile label="Distance" value={num(selected.day.km)} unit="km" tone="teal" />
|
||||
<Tile label="Energy" value={precise(selected.day.kwh)} unit="kWh" tone="gold" />
|
||||
<Tile label="Average" value={efficiency(selected.day.kwh, selected.day.km)} tone="teal" />
|
||||
<Tile
|
||||
label="Ortalama hız"
|
||||
label="Average speed"
|
||||
value={selected.day.hours > 0 ? num(selected.day.km / selected.day.hours) : "—"}
|
||||
unit={selected.day.hours > 0 ? "km/h" : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="rows">
|
||||
<Row label="Sürüş süresi" value={duration(selected.day.hours * 3600)} />
|
||||
<Row label="Doluluk düşüşü" value={`%${num(selected.day.socDrop)}`} />
|
||||
<Row label="O gün sonu ömür boyu" value={selected.lifetime ? `${num(selected.lifetime.km)} km` : "—"} />
|
||||
<Row label="Driving time" value={duration(selected.day.hours * 3600)} />
|
||||
<Row label="SoC used" value={`${num(selected.day.socDrop)}%`} />
|
||||
<Row label="Lifetime at end of day" value={selected.lifetime ? `${num(selected.lifetime.km)} km` : "—"} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="empty">Bu tarih için tüketim kaydı yok.</p>
|
||||
<p className="empty">No consumption recorded for this date.</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="tiles">
|
||||
<Tile label="Toplam mesafe" value={num(total.km)} unit="km" tone="teal" />
|
||||
<Tile label="Toplam enerji" value={precise(total.kwh)} unit="kWh" tone="gold" />
|
||||
<Tile label="Ortalama" value={efficiency(total.kwh, total.km)} tone="teal" />
|
||||
<Tile label="Total distance" value={num(total.km)} unit="km" tone="teal" />
|
||||
<Tile label="Total energy" value={precise(total.kwh)} unit="kWh" tone="gold" />
|
||||
<Tile label="Average" value={efficiency(total.kwh, total.km)} tone="teal" />
|
||||
<Tile
|
||||
label="Ortalama hız"
|
||||
label="Average speed"
|
||||
value={total.hours > 0 ? num(total.km / total.hours) : "—"}
|
||||
unit={total.hours > 0 ? "km/h" : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="card">
|
||||
<h2 className="card-title">Günlük enerji</h2>
|
||||
<h2 className="card-title">Daily energy</h2>
|
||||
<DailyEnergyChart days={chartDays} />
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<h2 className="card-title">Verimlilik eğilimi</h2>
|
||||
<h2 className="card-title">Efficiency trend</h2>
|
||||
<EfficiencyChart days={chartDays} />
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<h2 className="card-title">Ömür boyu</h2>
|
||||
<h2 className="card-title">Lifetime</h2>
|
||||
<div className="rows">
|
||||
<Row label="Mesafe" value={`${num(lifetime.km)} km`} />
|
||||
<Row label="Enerji" value={`${precise(lifetime.kwh)} kWh`} />
|
||||
<Row label="Ortalama" value={efficiency(lifetime.kwh, lifetime.km)} />
|
||||
<Row label="Sürüş süresi" value={duration(lifetime.hours * 3600)} />
|
||||
<Row label="Distance" value={`${num(lifetime.km)} km`} />
|
||||
<Row label="Energy" value={`${precise(lifetime.kwh)} kWh`} />
|
||||
<Row label="Average" value={efficiency(lifetime.kwh, lifetime.km)} />
|
||||
<Row label="Driving time" value={duration(lifetime.hours * 3600)} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<h2 className="card-title">Gün gün</h2>
|
||||
<h2 className="card-title">Day by day</h2>
|
||||
<ul className="list">
|
||||
{table.map((day) => {
|
||||
const isSelected = hasFilter && day.date === selectedDate;
|
||||
@@ -158,13 +158,13 @@ export default async function ConsumptionPage({
|
||||
</summary>
|
||||
<div className="session-body">
|
||||
<div className="rows">
|
||||
<Row label="Sürüş süresi" value={duration(day.day.hours * 3600)} />
|
||||
<Row label="Driving time" value={duration(day.day.hours * 3600)} />
|
||||
<Row
|
||||
label="Ortalama hız"
|
||||
label="Average speed"
|
||||
value={day.day.hours > 0 ? `${num(day.day.km / day.day.hours)} km/h` : "—"}
|
||||
/>
|
||||
<Row label="Doluluk düşüşü" value={`%${num(day.day.socDrop)}`} />
|
||||
<Row label="O gün sonu ömür boyu" value={day.lifetime ? `${num(day.lifetime.km)} km` : "—"} />
|
||||
<Row label="SoC used" value={`${num(day.day.socDrop)}%`} />
|
||||
<Row label="Lifetime at end of day" value={day.lifetime ? `${num(day.lifetime.km)} km` : "—"} />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -3,7 +3,7 @@ import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Nils' MG4",
|
||||
description: "MG4 tüketim ve şarj geçmişi",
|
||||
description: "MG4 consumption and charging history",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
@@ -16,7 +16,7 @@ export const viewport: Viewport = {
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="tr">
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export default async function LoginPage({
|
||||
<div className="login">
|
||||
<form className="login-card" action="/api/session" method="post">
|
||||
<h1 className="title">Nils' MG4</h1>
|
||||
<p className="subtitle">Tüketim ve şarj geçmişi</p>
|
||||
<p className="subtitle">Consumption and charging history</p>
|
||||
{/*
|
||||
A hidden username so password managers will offer to save this.
|
||||
Most of them ignore a lone password field — they key a saved entry on
|
||||
@@ -33,15 +33,15 @@ export default async function LoginPage({
|
||||
className="field"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Parola"
|
||||
placeholder="Password"
|
||||
autoComplete="current-password"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<button className="button" type="submit">
|
||||
Giriş
|
||||
Sign in
|
||||
</button>
|
||||
{error && <p className="error">Parola hatalı.</p>}
|
||||
{error && <p className="error">Wrong password.</p>}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
+25
-25
@@ -12,10 +12,10 @@ export default async function OverviewPage() {
|
||||
|
||||
if (!installationId) {
|
||||
return (
|
||||
<Shell active="/" title="Özet">
|
||||
<Shell active="/" title="Overview">
|
||||
<div className="card">
|
||||
<p className="empty">
|
||||
Henüz veri yok. Araç ilk günlük kaydı gönderdiğinde burada görünecek.
|
||||
No data yet. It appears here once the car sends its first daily record.
|
||||
</p>
|
||||
</div>
|
||||
</Shell>
|
||||
@@ -31,26 +31,26 @@ export default async function OverviewPage() {
|
||||
return (
|
||||
<Shell
|
||||
active="/"
|
||||
title="Özet"
|
||||
subtitle={installationCount > 1 ? `${installationCount} araçtan en günceli` : "MG4"}
|
||||
title="Overview"
|
||||
subtitle={installationCount > 1 ? `Most recent of ${installationCount} cars` : "MG4"}
|
||||
>
|
||||
<div className="tiles">
|
||||
<Tile
|
||||
label="Şebekeden alınan"
|
||||
label="Drawn from grid"
|
||||
value={precise(charge.gridEnergyKwh)}
|
||||
unit="kWh"
|
||||
note={`${charge.sessions} DC şarj`}
|
||||
note={`${charge.sessions} DC charge${charge.sessions === 1 ? "" : "s"}`}
|
||||
tone="gold"
|
||||
/>
|
||||
<Tile
|
||||
label="Ödenen"
|
||||
label="Paid"
|
||||
value={money(charge.cost)}
|
||||
note={charge.gridEnergyKwh > 0 ? `${money(charge.cost / charge.gridEnergyKwh)}/kWh ort.` : undefined}
|
||||
note={charge.gridEnergyKwh > 0 ? `${money(charge.cost / charge.gridEnergyKwh)}/kWh avg.` : undefined}
|
||||
tone="gold"
|
||||
/>
|
||||
<Tile label="Son 30 gün" value={num(recent.km)} unit="km" note={efficiency(recent.kwh, recent.km)} tone="teal" />
|
||||
<Tile label="Last 30 days" value={num(recent.km)} unit="km" note={efficiency(recent.kwh, recent.km)} tone="teal" />
|
||||
<Tile
|
||||
label="Ömür boyu"
|
||||
label="Lifetime"
|
||||
value={lifetime ? num(lifetime.km) : "—"}
|
||||
unit="km"
|
||||
note={lifetime ? efficiency(lifetime.kwh, lifetime.km) : undefined}
|
||||
@@ -59,39 +59,39 @@ export default async function OverviewPage() {
|
||||
</div>
|
||||
|
||||
<section className="card">
|
||||
<h2 className="card-title">Son 30 gün</h2>
|
||||
<h2 className="card-title">Last 30 days</h2>
|
||||
<DailyEnergyChart days={days.map((day) => ({ date: day.date, kwh: day.day.kwh, km: day.day.km }))} />
|
||||
</section>
|
||||
|
||||
{lastSession && (
|
||||
<section className="card">
|
||||
<h2 className="card-title">Son şarj</h2>
|
||||
<h2 className="card-title">Last charge</h2>
|
||||
<div className="rows">
|
||||
<Row label="Tarih" value={dateTime(lastSession.startedAt)} />
|
||||
<Row label="Bataryaya giren net" value={`${precise(lastSession.energyKwh)} kWh`} />
|
||||
<Row label="Şebekeden alınan (+%10)" value={`${precise(lastSession.gridEnergyKwh ?? lastSession.energyKwh * 1.1)} kWh`} />
|
||||
<Row label="Date" value={dateTime(lastSession.startedAt)} />
|
||||
<Row label="Net into battery" value={`${precise(lastSession.energyKwh)} kWh`} />
|
||||
<Row label="Drawn from grid (+10%)" value={`${precise(lastSession.gridEnergyKwh ?? lastSession.energyKwh * 1.1)} kWh`} />
|
||||
<Row
|
||||
label="Doluluk"
|
||||
label="State of charge"
|
||||
value={
|
||||
lastSession.startSocPercent !== undefined && lastSession.endSocPercent !== undefined
|
||||
? `%${num(lastSession.startSocPercent)} → %${num(lastSession.endSocPercent)}`
|
||||
? `${num(lastSession.startSocPercent)}% → ${num(lastSession.endSocPercent)}%`
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
<Row label="Süre" value={duration(lastSession.durationSeconds)} />
|
||||
<Row label="Ücret" value={money(lastSession.totalCost)} />
|
||||
<Row label="Duration" value={duration(lastSession.durationSeconds)} />
|
||||
<Row label="Cost" value={money(lastSession.totalCost)} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="card">
|
||||
<h2 className="card-title">Batarya ve toplamlar</h2>
|
||||
<h2 className="card-title">Battery and totals</h2>
|
||||
<div className="rows">
|
||||
<Row label="Sağlık (SOH)" value={soh === undefined ? "—" : `%${num(soh)}`} />
|
||||
<Row label="Toplam şarj süresi" value={duration(charge.seconds)} />
|
||||
<Row label="Görülen en yüksek güç" value={charge.peakKw > 0 ? `${num(charge.peakKw)} kW` : "—"} />
|
||||
<Row label="Şarjla kazanılan doluluk" value={charge.socGained > 0 ? `%${num(charge.socGained)}` : "—"} />
|
||||
<Row label="Kayıtlı gün" value={`${days.length}`} />
|
||||
<Row label="Health (SoH)" value={soh === undefined ? "—" : `${num(soh)}%`} />
|
||||
<Row label="Total charging time" value={duration(charge.seconds)} />
|
||||
<Row label="Highest power seen" value={charge.peakKw > 0 ? `${num(charge.peakKw)} kW` : "—"} />
|
||||
<Row label="Charge added" value={charge.socGained > 0 ? `${num(charge.socGained)}%` : "—"} />
|
||||
<Row label="Days recorded" value={`${days.length}`} />
|
||||
</div>
|
||||
</section>
|
||||
</Shell>
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface DayDatum {
|
||||
* distinction the telemetry side makes between a failed read and a zero.
|
||||
*/
|
||||
export function DailyEnergyChart({ days }: { days: DayDatum[] }) {
|
||||
if (days.length === 0) return <p className="empty">Henüz günlük kayıt yok</p>;
|
||||
if (days.length === 0) return <p className="empty">No daily records yet</p>;
|
||||
const plot = PLOT;
|
||||
const max = niceMax(Math.max(...days.map((day) => day.kwh)));
|
||||
const slot = innerWidth(plot) / days.length;
|
||||
@@ -86,7 +86,7 @@ export function DailyEnergyChart({ days }: { days: DayDatum[] }) {
|
||||
return (
|
||||
<>
|
||||
<svg className="chart" viewBox={`0 0 ${plot.width} ${plot.height}`} role="img"
|
||||
aria-label="Gün gün tüketilen enerji">
|
||||
aria-label="Energy used per day">
|
||||
<Grid plot={plot} max={max} format={(value) => value.toFixed(0)} />
|
||||
{days.map((day, index) => {
|
||||
const height = (day.kwh / max) * innerHeight(plot);
|
||||
@@ -116,7 +116,7 @@ export function DailyEnergyChart({ days }: { days: DayDatum[] }) {
|
||||
<div className="legend">
|
||||
<span>
|
||||
<span className="swatch" style={{ background: GOLD }} />
|
||||
Günlük enerji (kWh)
|
||||
Daily energy (kWh)
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -128,7 +128,7 @@ export function EfficiencyChart({ days }: { days: DayDatum[] }) {
|
||||
const points = days
|
||||
.filter((day) => day.km >= 1 && day.kwh > 0)
|
||||
.map((day) => ({ date: day.date, value: (day.kwh * 100) / day.km }));
|
||||
if (points.length < 2) return <p className="empty">Grafik için yeterli gün yok</p>;
|
||||
if (points.length < 2) return <p className="empty">Not enough days for a chart</p>;
|
||||
const plot = PLOT;
|
||||
const max = niceMax(Math.max(...points.map((point) => point.value)));
|
||||
const step = innerWidth(plot) / Math.max(1, points.length - 1);
|
||||
@@ -144,7 +144,7 @@ export function EfficiencyChart({ days }: { days: DayDatum[] }) {
|
||||
return (
|
||||
<>
|
||||
<svg className="chart" viewBox={`0 0 ${plot.width} ${plot.height}`} role="img"
|
||||
aria-label="Gün gün ortalama tüketim">
|
||||
aria-label="Average consumption per day">
|
||||
<Grid plot={plot} max={max} format={(value) => value.toFixed(0)} />
|
||||
<line
|
||||
x1={plot.padLeft}
|
||||
@@ -176,7 +176,7 @@ export function EfficiencyChart({ days }: { days: DayDatum[] }) {
|
||||
</span>
|
||||
<span>
|
||||
<span className="swatch" style={{ background: GOLD }} />
|
||||
Ortalama {average.toFixed(1)}
|
||||
Average {average.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -196,7 +196,7 @@ export interface CurveDatum {
|
||||
* them — the taper is only legible next to the percentage that caused it.
|
||||
*/
|
||||
export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) {
|
||||
if (curve.length < 2) return <p className="empty">Bu seans için eğri kaydı yok</p>;
|
||||
if (curve.length < 2) return <p className="empty">No curve recorded for this session</p>;
|
||||
const plot = { ...PLOT, padRight: 26 };
|
||||
const times = curve.map((point) => new Date(point.timestamp).getTime());
|
||||
const start = times[0];
|
||||
@@ -223,7 +223,7 @@ export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) {
|
||||
return (
|
||||
<>
|
||||
<svg className="chart" viewBox={`0 0 ${plot.width} ${plot.height}`} role="img"
|
||||
aria-label="Şarj gücü ve doluluk eğrisi">
|
||||
aria-label="Charging power and state-of-charge curve">
|
||||
<Grid plot={plot} max={maxPower} format={(value) => value.toFixed(0)} />
|
||||
<defs>
|
||||
<linearGradient id="powerFill" x1="0" x2="0" y1="0" y2="1">
|
||||
@@ -241,20 +241,20 @@ export function ChargeCurveChart({ curve }: { curve: CurveDatum[] }) {
|
||||
0%
|
||||
</text>
|
||||
<text x={plot.padLeft} y={plot.height - 6} fill={MUTED} fontSize={9}>
|
||||
0 dk
|
||||
0 min
|
||||
</text>
|
||||
<text x={plot.width - plot.padRight} y={plot.height - 6} fill={MUTED} fontSize={9} textAnchor="end">
|
||||
{Math.round(span / 60000)} dk
|
||||
{Math.round(span / 60000)} min
|
||||
</text>
|
||||
</svg>
|
||||
<div className="legend">
|
||||
<span>
|
||||
<span className="swatch" style={{ background: GOLD }} />
|
||||
Güç (kW)
|
||||
Power (kW)
|
||||
</span>
|
||||
<span>
|
||||
<span className="swatch" style={{ background: CYAN }} />
|
||||
Doluluk (%)
|
||||
State of charge (%)
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -2,9 +2,9 @@ import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const TABS = [
|
||||
{ href: "/", label: "Özet", icon: "◈" },
|
||||
{ href: "/consumption", label: "Tüketim", icon: "▤" },
|
||||
{ href: "/charging", label: "Şarj", icon: "⚡" },
|
||||
{ href: "/", label: "Overview", icon: "◈" },
|
||||
{ href: "/consumption", label: "Consumption", icon: "▤" },
|
||||
{ href: "/charging", label: "Charging", icon: "⚡" },
|
||||
] as const;
|
||||
|
||||
export type Tab = (typeof TABS)[number]["href"];
|
||||
@@ -42,7 +42,7 @@ export function Shell({
|
||||
*/}
|
||||
<form action="/logout" method="post">
|
||||
<button className="signout" type="submit">
|
||||
Çıkış
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** Turkish formatting for every user-facing number and date. */
|
||||
/** English formatting (en-GB: 24-hour clock, day-month dates) with euro for every number and date. */
|
||||
|
||||
const NUMBER = new Intl.NumberFormat("tr-TR", { maximumFractionDigits: 1 });
|
||||
const PRECISE = new Intl.NumberFormat("tr-TR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
const CURRENCY = new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY", maximumFractionDigits: 2 });
|
||||
const DATE = new Intl.DateTimeFormat("tr-TR", { day: "numeric", month: "long", year: "numeric" });
|
||||
const DATE_TIME = new Intl.DateTimeFormat("tr-TR", {
|
||||
const NUMBER = new Intl.NumberFormat("en-GB", { maximumFractionDigits: 1 });
|
||||
const PRECISE = new Intl.NumberFormat("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
const CURRENCY = new Intl.NumberFormat("en-GB", { style: "currency", currency: "EUR", maximumFractionDigits: 2 });
|
||||
const DATE = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "long", year: "numeric" });
|
||||
const DATE_TIME = new Intl.DateTimeFormat("en-GB", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
@@ -32,13 +32,13 @@ export function dateTime(value: Date | string): string {
|
||||
return DATE_TIME.format(new Date(value));
|
||||
}
|
||||
|
||||
/** `2 sa 05 dk` — the same shape the car's charging pane uses. */
|
||||
/** `2 h 05 min` — the same shape the car's charging pane uses. */
|
||||
export function duration(seconds: number): string {
|
||||
const total = Math.max(0, Math.round(seconds));
|
||||
const hours = Math.floor(total / 3600);
|
||||
const minutes = Math.floor((total % 3600) / 60);
|
||||
if (hours === 0) return `${minutes} dk`;
|
||||
return `${hours} sa ${String(minutes).padStart(2, "0")} dk`;
|
||||
if (hours === 0) return `${minutes} min`;
|
||||
return `${hours} h ${String(minutes).padStart(2, "0")} min`;
|
||||
}
|
||||
|
||||
/** Energy per 100 km, or a dash — never a ratio computed from a distance of nothing. */
|
||||
|
||||
Reference in New Issue
Block a user