feat: enhance insights page with highlighted text for ratings and seasonality
Some checks failed
CI / Check / Test (push) Has been cancelled

This commit is contained in:
2026-10-12 20:27:12 +02:00
parent dbdf68129a
commit ee06f4794c
3 changed files with 75 additions and 28 deletions

View File

@@ -83,4 +83,27 @@ fix:
cargo fmt
cargo clippy --fix --allow-dirty --allow-staged
.PHONY: check fmt fmt-check clippy test fix check-appcontext check-handler-deps check-handler-repos check-presentation-adapters check-federation-off-social check-social-crates-are-ap-free
# Run the dev stack:
# server (:3000, in the foreground, Ctrl-C stops it)
# worker (background, log at /tmp/movies-diary-worker.log)
# spa (:5173, background, log at /tmp/movies-diary-spa.log)
# Tear down the background processes with `make dev-stop`.
# .env enables EVENT_BUS_BACKEND=nats, so both binaries are built with the nats feature.
dev: dev-worker dev-spa
cargo run -p server --features nats
dev-stop:
-pkill -f '[t]arget.debug.worker' || true
-pkill -f '[s]pa/node_modules/[.]bin/vite' || true
@echo "dev stack stopped"
dev-worker:
@bash -c 'nohup cargo run -p worker --features nats >> /tmp/movies-diary-worker.log 2>&1 & echo "worker → pid $$! (log: /tmp/movies-diary-worker.log)"'
dev-spa:
@bash -c 'nohup npm --prefix spa run dev >> /tmp/movies-diary-spa.log 2>&1 & echo "spa → pid $$! (log: /tmp/movies-diary-spa.log, http://localhost:5173)"'
spa-dev:
@npm --prefix spa run dev
.PHONY: check fmt fmt-check clippy test fix dev dev-stop dev-worker dev-spa spa-dev check-appcontext check-handler-deps check-handler-repos check-presentation-adapters check-federation-off-social check-social-crates-are-ap-free

View File

@@ -267,14 +267,14 @@
"seasons": "Seasons",
"mediums": "How you watch",
"rhythm": "When you watch",
"ratingLiftUp": "You rate {{genre}} higher than your own average — {{genreAvg}}★ vs {{userAvg}}★",
"ratingLiftDown": "You rate {{genre}} lower than your own average — {{genreAvg}}★ vs {{userAvg}}★",
"ratingLiftUp": "You rate <hl>{{genre}}</hl> higher than your own average — <hl>{{genreAvg}}</hl>★ vs <hl>{{userAvg}}</hl>★",
"ratingLiftDown": "You rate <hl>{{genre}}</hl> lower than your own average — <hl>{{genreAvg}}</hl>★ vs <hl>{{userAvg}}</hl>★",
"yourAverage": "your average",
"seasonality": "{{genre}} is your {{month}} genre — {{multiplier}}× your yearly baseline",
"mediumTiming": "You watch {{medium}} on weekends {{multiplier}}× more than your baseline",
"seasonality": "<hl>{{genre}}</hl> is your <hl>{{month}}</hl> genre — <hl>{{multiplier}}</hl>× your yearly baseline",
"mediumTiming": "You watch <hl>{{medium}}</hl> on weekends <hl>{{multiplier}}</hl>× more than your baseline",
"weekends": "weekends",
"yourBaseline": "your baseline",
"rhythm": "You watch mostly {{start}}–{{end}} on {{days}}",
"rhythmBody": "You watch mostly <hl>{{start}}</hl>–<hl>{{end}}</hl> on <hl>{{days}}</hl>",
"filmsSample": "{{count}} films"
},
"wrapup": {

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router"
import type { ReactNode } from "react"
import { useTranslation } from "react-i18next"
import { Trans, useTranslation } from "react-i18next"
import {
CalendarDays,
Clapperboard,
@@ -26,6 +26,8 @@ export const Route = createFileRoute("/_app/insights")({
const MONTH_BASE = new Date(2024, 0, 1) // any year; only the month name matters
const MONDAY_BASE = new Date(2024, 2, 4) // 2024-03-04 is a Monday
const hl = <span className="text-primary" />
function isKind<K extends Insight["kind"]>(kind: K) {
return (insight: Insight): insight is Extract<Insight, { kind: K }> => insight.kind === kind
}
@@ -138,11 +140,17 @@ function RatingLiftCard({ insight }: RatingLiftCardProps) {
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<Trend className={`size-4 ${up ? "text-emerald-500" : "text-rose-500"}`} />
{t(up ? "insights.ratingLiftUp" : "insights.ratingLiftDown", {
genre: insight.genre,
genreAvg: insight.genre_avg.toFixed(1),
userAvg: insight.user_avg.toFixed(1),
})}
<span>
<Trans
i18nKey={up ? "insights.ratingLiftUp" : "insights.ratingLiftDown"}
values={{
genre: insight.genre,
genreAvg: insight.genre_avg.toFixed(1),
userAvg: insight.user_avg.toFixed(1),
}}
components={{ hl }}
/>
</span>
</CardTitle>
</CardHeader>
<CardContent>
@@ -161,17 +169,22 @@ function RatingLiftCard({ insight }: RatingLiftCardProps) {
}
function SeasonalityCard({ insight }: SeasonalityCardProps) {
const { t } = useTranslation()
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<CalendarDays className="size-4 text-primary" />
{t("insights.seasonality", {
genre: insight.genre,
multiplier: insight.multiplier.toFixed(1),
month: monthName(insight.month),
})}
<span>
<Trans
i18nKey="insights.seasonality"
values={{
genre: insight.genre,
multiplier: insight.multiplier.toFixed(1),
month: monthName(insight.month),
}}
components={{ hl }}
/>
</span>
</CardTitle>
</CardHeader>
<CardContent>
@@ -194,10 +207,16 @@ function MediumTimingCard({ insight }: MediumTimingCardProps) {
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<Clapperboard className="size-4 text-primary" />
{t("insights.mediumTiming", {
medium: mediumLabel(insight.medium, t),
multiplier: insight.multiplier.toFixed(1),
})}
<span>
<Trans
i18nKey="insights.mediumTiming"
values={{
medium: mediumLabel(insight.medium, t),
multiplier: insight.multiplier.toFixed(1),
}}
components={{ hl }}
/>
</span>
</CardTitle>
</CardHeader>
<CardContent>
@@ -216,17 +235,22 @@ function MediumTimingCard({ insight }: MediumTimingCardProps) {
}
function RhythmCard({ insight }: RhythmCardProps) {
const { t } = useTranslation()
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm">
<Clock className="size-4 text-primary" />
{t("insights.rhythm", {
start: hourLabel(insight.peak_hour_start),
end: hourLabel(insight.peak_hour_end),
days: dayNames(insight.top_days).join(", "),
})}
<span>
<Trans
i18nKey="insights.rhythmBody"
values={{
start: hourLabel(insight.peak_hour_start),
end: hourLabel(insight.peak_hour_end),
days: dayNames(insight.top_days).join(", "),
}}
components={{ hl }}
/>
</span>
</CardTitle>
</CardHeader>
<CardContent>