feat: enhance insights page with highlighted text for ratings and seasonality
Some checks failed
CI / Check / Test (push) Has been cancelled
Some checks failed
CI / Check / Test (push) Has been cancelled
This commit is contained in:
25
Makefile
25
Makefile
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user