Add GH style day/month labels to season summary

This commit is contained in:
Kalle
2026-08-25 07:24:01 +03:00
parent 5e4908e6cc
commit 2cb7a140fb
2 changed files with 150 additions and 24 deletions

View File

@@ -151,17 +151,54 @@ below win regardless of which CSS chunk loads last
}
.calendar {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(7, auto);
--calendar-cell-size: 18px;
--calendar-gap: 4px;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
margin-top: var(--s-4);
gap: var(--calendar-gap);
margin-top: var(--s-2-5);
}
.calendarWeekdays {
display: flex;
flex-direction: column;
gap: var(--calendar-gap);
margin-inline-end: var(--s-1-5);
}
.calendarWeekday {
display: flex;
align-items: center;
justify-content: flex-end;
height: var(--calendar-cell-size);
}
.calendarMonth {
display: flex;
flex-direction: column;
gap: var(--calendar-gap);
}
.calendarMonthName {
text-align: center;
}
.calendarWeeks {
display: flex;
gap: var(--calendar-gap);
}
.calendarWeek {
display: flex;
flex-direction: column;
gap: var(--calendar-gap);
}
.calendarCell {
width: 18px;
height: 18px;
width: var(--calendar-cell-size, 18px);
height: var(--calendar-cell-size, 18px);
border-radius: 3px;
background-color: var(--activity-none);

View File

@@ -8,6 +8,7 @@ import {
} from "date-fns";
import * as React from "react";
import { useTranslation } from "react-i18next";
import * as R from "remeda";
import { Avatar } from "~/components/Avatar";
import { Flag } from "~/components/Flag";
import { TierImage, WeaponImage } from "~/components/Image";
@@ -43,6 +44,11 @@ const CHART_MARGIN = { top: 26, right: 14, bottom: 22, left: 14 };
const CHART_POINTS_NEEDED = 2;
const CHART_PEAK_LABEL_CLAMP = 48;
const TOP_MATES_COUNT = 3;
const CALENDAR_WEEK_LENGTH = 7;
/** Thursday, the day that decides which month a week column belongs to */
const CALENDAR_WEEK_MONTH_DAY_INDEX = 3;
/** Monday and Friday, the only rows the calendar names */
const CALENDAR_NAMED_WEEKDAY_INDICES = [0, 4];
/** Without weapons the teammates box is alone next to the activity calendar, so it has room for more */
const TOP_MATES_COUNT_WITHOUT_WEAPONS = 6;
@@ -533,36 +539,119 @@ function ActivityCalendar({
seasonDateRange: { starts: Date; ends: Date };
activeDays: Array<{ date: string; activity: SeasonSummaryGraphicActivity }>;
}) {
const { formatter } = useDateTimeFormat({ month: "long" });
const activityByDay = new Map(
activeDays.map((day) => [day.date, day.activity]),
);
const seasonFirstDay = startOfDay(seasonDateRange.starts);
const days = eachDayOfInterval({
start: startOfWeek(seasonFirstDay, { weekStartsOn: 1 }),
end: seasonDateRange.ends,
const weeks = seasonWeeks({
seasonFirstDay,
seasonLastDay: seasonDateRange.ends,
});
const months = calendarMonths(weeks);
return (
<div className={styles.calendar}>
{days.map((day) => {
const key = format(day, "yyyy-MM-dd");
const beforeSeason = day.getTime() < seasonFirstDay.getTime();
<CalendarWeekdays firstWeek={weeks[0]} />
{months.map((month) => (
<div key={month.key} className={styles.calendarMonth}>
<GraphicBoxLabel className={styles.calendarMonthName}>
{formatter.format(month.month)}
</GraphicBoxLabel>
<div className={styles.calendarWeeks}>
{month.weeks.map((week) => (
<div
key={format(week[0], "yyyy-MM-dd")}
className={styles.calendarWeek}
>
{week.map((day) => {
const key = format(day, "yyyy-MM-dd");
const beforeSeason = day.getTime() < seasonFirstDay.getTime();
return (
<div
key={key}
className={clsx(
styles.calendarCell,
activityClass(activityByDay.get(key)),
{ [styles.calendarCellHidden]: beforeSeason },
)}
/>
);
})}
return (
<div
key={key}
className={clsx(
styles.calendarCell,
activityClass(activityByDay.get(key)),
{ [styles.calendarCellHidden]: beforeSeason },
)}
/>
);
})}
</div>
))}
</div>
</div>
))}
</div>
);
}
function CalendarWeekdays({ firstWeek }: { firstWeek: Date[] }) {
const { formatter } = useDateTimeFormat({ weekday: "short" });
return (
<GraphicBoxLabel className={styles.calendarWeekdays}>
{firstWeek.map((day, dayIndex) => (
<div key={format(day, "yyyy-MM-dd")} className={styles.calendarWeekday}>
{CALENDAR_NAMED_WEEKDAY_INDICES.includes(dayIndex)
? formatter.format(day)
: null}
</div>
))}
</GraphicBoxLabel>
);
}
/**
* The season's Monday to Sunday week columns. A season ending mid-week (its last hours can fall on the
* next day depending on the time zone) would leave a ragged column, so an incomplete last week is left out.
*/
function seasonWeeks({
seasonFirstDay,
seasonLastDay,
}: {
seasonFirstDay: Date;
seasonLastDay: Date;
}): Date[][] {
const weeks: Date[][] = R.chunk(
eachDayOfInterval({
start: startOfWeek(seasonFirstDay, { weekStartsOn: 1 }),
end: seasonLastDay,
}),
CALENDAR_WEEK_LENGTH,
);
const lastWeek = weeks[weeks.length - 1];
if (weeks.length > 1 && lastWeek.length < CALENDAR_WEEK_LENGTH) {
weeks.pop();
}
return weeks;
}
/** Groups the week columns under the month that holds most of the week */
function calendarMonths(weeks: Date[][]) {
const months: Array<{ key: string; month: Date; weeks: Date[][] }> = [];
for (const week of weeks) {
const monthDay =
week[CALENDAR_WEEK_MONTH_DAY_INDEX] ?? week[week.length - 1];
const key = format(monthDay, "yyyy-MM");
const latestMonth = months[months.length - 1];
if (latestMonth?.key === key) {
latestMonth.weeks.push(week);
} else {
months.push({ key, month: monthDay, weeks: [week] });
}
}
return months;
}
function ActivityLegend() {
const { t } = useTranslation(["user"]);