Compare commits

...

4 Commits

Author SHA1 Message Date
5999eccc21 Merge branch 'main' of ssh://adam-french.co.uk:2222/adamf/web_server
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 59s
2026-03-16 16:47:22 +00:00
7155255733 Add rowing to store 2026-03-16 16:44:02 +00:00
6ff30a37f7 Add rowing to store 2026-03-16 16:41:30 +00:00
84e18dddfa Update go version to 1.25 2026-03-16 15:45:34 +00:00
2 changed files with 309 additions and 229 deletions

View File

@@ -1,6 +1,7 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { gql } from "@/graphql";
import axios from "axios";
export const useHomeDataStore = defineStore("homeData", () => {
const loaded = ref(false);
@@ -11,22 +12,31 @@ export const useHomeDataStore = defineStore("homeData", () => {
const favorites = ref([]);
const activities = ref([]);
const spotifyRecent = ref([]);
const rowingSessions = ref([]);
const gitFeed = ref(null);
const radioLive = ref(false);
async function fetchAll() {
try {
const data = await gql(`
const [data] = await Promise.all([
gql(`
query HomeData {
posts { id title content createdAt updatedAt author { id username } }
favorites { id type name link createdAt }
activities { id type name link createdAt }
spotifyRecent { track { name album { name images { url } } artists { name } } playedAt }
rowingSessions { id date time distance timePer500m calories }
me { id username admin }
}
`);
`),
fetchGitFeed(),
fetchRadioStatus(),
]);
posts.value = data.posts;
favorites.value = data.favorites;
activities.value = data.activities;
spotifyRecent.value = data.spotifyRecent;
rowingSessions.value = data.rowingSessions;
me.value = data.me || null;
loaded.value = true;
} catch (err) {
@@ -35,6 +45,24 @@ export const useHomeDataStore = defineStore("homeData", () => {
}
}
async function fetchGitFeed() {
try {
const res = await axios.get("/gitea/api/v1/users/adamf/activities/feeds?limit=1");
gitFeed.value = res.data[0] || null;
} catch {
gitFeed.value = null;
}
}
async function fetchRadioStatus() {
try {
await axios.head("/radio/stream");
radioLive.value = true;
} catch {
radioLive.value = false;
}
}
fetchAll();
return {
@@ -45,6 +73,10 @@ export const useHomeDataStore = defineStore("homeData", () => {
favorites,
activities,
spotifyRecent,
rowingSessions,
gitFeed,
radioLive,
fetchAll,
fetchRadioStatus,
};
});

View File

@@ -1,11 +1,15 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import axios from "axios";
import { ref, computed } from "vue";
import Header from "@/components/text/Header.vue";
import { useHomeDataStore } from "@/stores/homeData";
import { storeToRefs } from "pinia";
const store = useHomeDataStore();
const { loaded, error, rowingSessions } = storeToRefs(store);
const rows = computed(() => rowingSessions.value.slice().reverse());
const loading = computed(() => !loaded.value);
const rows = ref([]);
const loading = ref(true);
const error = ref(null);
const metric = ref("distance");
const hovered = ref(null);
@@ -15,18 +19,9 @@ const METRICS = [
{ key: "calories", label: "Calories", color: "#62ff57" },
];
onMounted(async () => {
try {
const res = await axios.get("/api/rowing");
rows.value = res.data.slice().reverse(); // API returns DESC, reverse to chronological
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
});
const activeMetric = computed(() => METRICS.find((m) => m.key === metric.value));
const activeMetric = computed(() =>
METRICS.find((m) => m.key === metric.value),
);
// SVG layout constants
const W = 290;
@@ -43,7 +38,7 @@ const chartData = computed(() =>
date: new Date(r.date),
value: r[metric.value],
raw: r,
}))
})),
);
const minVal = computed(() => Math.min(...chartData.value.map((d) => d.value)));
@@ -64,16 +59,25 @@ const points = computed(() => {
}));
});
const polyline = computed(() => points.value.map((p) => `${p.x},${p.y}`).join(" "));
const polyline = computed(() =>
points.value.map((p) => `${p.x},${p.y}`).join(" "),
);
const xLabels = computed(() => {
const data = chartData.value;
const pts = points.value;
if (!data.length) return [];
const indices = new Set([0, Math.floor((data.length - 1) / 2), data.length - 1]);
const indices = new Set([
0,
Math.floor((data.length - 1) / 2),
data.length - 1,
]);
return [...indices].map((i) => ({
x: pts[i].x,
label: data[i].date.toLocaleDateString("en-GB", { month: "short", day: "numeric" }),
label: data[i].date.toLocaleDateString("en-GB", {
month: "short",
day: "numeric",
}),
}));
});
@@ -194,7 +198,9 @@ function formatValue(key, val) {
font-size="10"
fill="var(--primary)"
font-family="var(--font_heading)"
>{{ yl.label }}</text>
>
{{ yl.label }}
</text>
<!-- X axis labels -->
<text
@@ -206,11 +212,27 @@ function formatValue(key, val) {
font-size="10"
fill="var(--primary)"
font-family="var(--font_heading)"
>{{ xl.label }}</text>
>
{{ xl.label }}
</text>
<!-- Axes -->
<line :x1="PL" :y1="PT" :x2="PL" :y2="PT + PLOT_H" stroke="var(--primary)" stroke-width="0.5" />
<line :x1="PL" :y1="PT + PLOT_H" :x2="W - PR" :y2="PT + PLOT_H" stroke="var(--primary)" stroke-width="0.5" />
<line
:x1="PL"
:y1="PT"
:x2="PL"
:y2="PT + PLOT_H"
stroke="var(--primary)"
stroke-width="0.5"
/>
<line
:x1="PL"
:y1="PT + PLOT_H"
:x2="W - PR"
:y2="PT + PLOT_H"
stroke="var(--primary)"
stroke-width="0.5"
/>
<!-- Tooltip -->
<g v-if="hovered !== null && points[hovered]">
@@ -230,14 +252,24 @@ function formatValue(key, val) {
font-size="12"
fill="var(--secondary)"
font-family="var(--font_heading)"
>{{ points[hovered].date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "2-digit" }) }}</text>
>
{{
points[hovered].date.toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "2-digit",
})
}}
</text>
<text
:x="Math.min(points[hovered].x + 7, W - 82)"
:y="points[hovered].y + 8"
font-size="14"
:fill="activeMetric.color"
font-family="var(--font_heading)"
>{{ formatValue(metric, points[hovered].value) }}</text>
>
{{ formatValue(metric, points[hovered].value) }}
</text>
</g>
</svg>
</div>
@@ -246,15 +278,29 @@ function formatValue(key, val) {
<div class="flex justify-between text-xs border-t border-quaternary pt-1">
<div class="flex flex-col items-center">
<span class="text-primary font-heading">{{ rows.length }}</span>
<span class="text-quaternary" style="font-size: 0.6rem">sessions</span>
<span class="text-quaternary" style="font-size: 0.6rem"
>sessions</span
>
</div>
<div class="flex flex-col items-center">
<span class="text-primary font-heading">{{ rows.reduce((s, r) => s + r.distance, 0).toLocaleString() }}m</span>
<span class="text-quaternary" style="font-size: 0.6rem">total dist</span>
<span class="text-primary font-heading"
>{{
rows.reduce((s, r) => s + r.distance, 0).toLocaleString()
}}m</span
>
<span class="text-quaternary" style="font-size: 0.6rem"
>total dist</span
>
</div>
<div class="flex flex-col items-center">
<span class="text-primary font-heading">{{ formatTime(rows.reduce((s, r) => s + r.timePer500m, 0) / (rows.length || 1)) }}</span>
<span class="text-quaternary" style="font-size: 0.6rem">avg pace</span>
<span class="text-primary font-heading">{{
formatTime(
rows.reduce((s, r) => s + r.timePer500m, 0) / (rows.length || 1),
)
}}</span>
<span class="text-quaternary" style="font-size: 0.6rem"
>avg pace</span
>
</div>
</div>
</div>
@@ -264,7 +310,9 @@ function formatValue(key, val) {
<style scoped>
.metric-btn {
cursor: pointer;
transition: background-color 0.15s, color 0.15s;
transition:
background-color 0.15s,
color 0.15s;
letter-spacing: 0.03em;
}
</style>