Link between own/main team schedule

This commit is contained in:
Kalle
2026-09-05 12:39:10 +03:00
parent 13a0e24b7e
commit fac0f67eab
24 changed files with 137 additions and 9 deletions

View File

@@ -6,6 +6,13 @@
flex-wrap: wrap;
}
.title {
display: flex;
align-items: center;
gap: var(--s-1);
flex-wrap: wrap;
}
.weekHeading {
font-size: var(--font-xs);
font-weight: var(--weight-semi);

View File

@@ -1,13 +1,16 @@
import { Users } from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import type { FetcherWithComponents } from "react-router";
import * as R from "remeda";
import { SendouButton } from "~/components/elements/Button";
import { LinkButton, SendouButton } from "~/components/elements/Button";
import { toastQueue } from "~/components/elements/Toast";
import { useUser } from "~/features/auth/core/user";
import { useUnsavedChangesChecker } from "~/form/UnsavedChangesGuard";
import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat";
import { useActionSubmit } from "~/hooks/useActionSubmit";
import { useSearchParamsTyped } from "~/modules/search-params/hooks";
import { teamSchedulePage } from "~/utils/urls";
import { saveWeekSchema } from "../availability-schemas";
import { scheduleWeekSearchParams } from "../availability-search-params";
import type { AvailabilityEditorWeek } from "../availability-types";
@@ -19,6 +22,7 @@ import { WeekToggle } from "./WeekToggle";
/** The events page's "My schedule": editor with current/next week toggle, "Copy last week" prefill and save. */
export function MySchedule({ data }: { data: MyScheduleData }) {
const { t } = useTranslation(["schedule"]);
const user = useUser();
const [{ week }, setParams] = useSearchParamsTyped(scheduleWeekSearchParams);
const [weeks, setWeeks] = React.useState<Array<AvailabilityEditorWeek>>(() =>
data.weeks.map((editorWeek) => editorWeek.days),
@@ -87,7 +91,23 @@ export function MySchedule({ data }: { data: MyScheduleData }) {
return (
<section className="stack sm" data-testid="my-schedule">
<div className={styles.header}>
<h2 className="text-lg mx-2">{t("schedule:editor.title")}</h2>
<div className={styles.title}>
<h2 className="text-lg mx-2">{t("schedule:editor.title")}</h2>
{user?.team ? (
<LinkButton
to={scheduleWeekSearchParams.href(
teamSchedulePage(user.team.customUrl),
{ week },
)}
variant="minimal"
size="miniscule"
icon={<Users />}
testId="team-schedule-link"
>
{t("schedule:editor.teamSchedule")}
</LinkButton>
) : null}
</div>
<WeekToggle
name="my-schedule-week"
value={week}

View File

@@ -6,6 +6,20 @@
gap: var(--s-2);
}
/* narrow: the actions take a line of their own, so they split to the edges of it */
.headerActions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--s-2);
flex-grow: 1;
@container (width >= 640px) {
flex-grow: 0;
}
}
.heading {
font-size: var(--font-md);
color: var(--color-text);

View File

@@ -1,6 +1,6 @@
import clsx from "clsx";
import { isSameDay } from "date-fns";
import { Flag, Pencil, Plus, Trash } from "lucide-react";
import { CalendarClock, Flag, Pencil, Plus, Trash } from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useLoaderData, useMatches } from "react-router";
@@ -8,7 +8,7 @@ import * as R from "remeda";
import type * as v from "valibot";
import { ActionButton } from "~/components/ActionButton";
import { Alert } from "~/components/Alert";
import { SendouButton } from "~/components/elements/Button";
import { LinkButton, SendouButton } from "~/components/elements/Button";
import { SendouDialog } from "~/components/elements/Dialog";
import { FormMessage } from "~/components/FormMessage";
import { UserLink } from "~/components/UserLink";
@@ -24,6 +24,7 @@ import { useSearchParamsTyped } from "~/modules/search-params/hooks";
import { databaseTimestampToDate } from "~/utils/dates";
import invariant from "~/utils/invariant";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { EVENTS_PAGE } from "~/utils/urls";
import { action } from "../actions/t.$customUrl.schedule.server";
import {
addTeamEventSchema,
@@ -91,11 +92,22 @@ function ScheduleWeeks({ weeks }: { weeks: Array<WeekData> }) {
shownWeek.days[6].noonAt,
)}
</h2>
<WeekToggle
name="schedule-week"
value={week}
onChange={(value) => setParams({ week: value })}
/>
<div className={styles.headerActions}>
<LinkButton
to={scheduleWeekSearchParams.href(EVENTS_PAGE, { week })}
variant="minimal"
size="miniscule"
icon={<CalendarClock />}
testId="edit-availability-link"
>
{t("schedule:team.editAvailability")}
</LinkButton>
<WeekToggle
name="schedule-week"
value={week}
onChange={(value) => setParams({ week: value })}
/>
</div>
</div>
<TeamEvents week={shownWeek} />
<ScheduleGrid week={shownWeek} />

View File

@@ -0,0 +1,5 @@
---
navItem: calendar
type: feature
---
The availability editor links to your team's schedule and the team schedule links back, both keeping the week you are looking at

View File

@@ -12,6 +12,7 @@ import {
test,
} from "./helpers/playwright";
import { EventsPage } from "./pages/calendar/events-page";
import { TeamSchedulePage } from "./pages/team/team-schedule-page";
const JOINED_TOURNAMENT_NAME = "Joined Tournament";
const ORGANIZED_TOURNAMENT_NAME = "Organized Tournament";
@@ -120,6 +121,37 @@ test.describe("My schedule", () => {
await isNotVisible(events.weekNotFilledMarker("current"));
});
test("crosses over to the team schedule and back, keeping the week", async ({
page,
factories,
}) => {
const { customUrl } = await factories.TeamFactory.create({
name: "Team Olive",
memberUserIds: [ADMIN_ID],
});
await impersonate(page, ADMIN_ID);
await setTimezoneCookie(page);
const events = new EventsPage(page);
await events.goto();
// the week the editor is showing carries over to the team schedule
await events.locators.nextWeekToggle.click();
await events.locators.teamScheduleLink.click();
await expect(page).toHaveURL(
new RegExp(`/t/${customUrl}/schedule\\?week=next`),
);
const schedule = new TeamSchedulePage(page);
await expect(schedule.locators.grid).toBeVisible();
// the week the schedule is showing carries over to the editor
await schedule.locators.editAvailabilityLink.click();
await expect(page).toHaveURL(/\/events\?week=next/);
await expect(events.weekNotFilledMarker("next")).toBeVisible();
});
test("shows a commitment as a locked block on the editor", async ({
page,
factories,

View File

@@ -29,6 +29,11 @@ export class EventsPage {
saveWeekButton: page.getByTestId("save-week-button"),
copyLastWeekButton: page.getByTestId("copy-last-week-button"),
dayEditorPopover: page.getByRole("dialog"),
teamScheduleLink: page.getByTestId("team-schedule-link"),
// the chip radio input is visually hidden, so the label is what clicks
nextWeekToggle: page.locator(
'label[for="chip-radio-my-schedule-week-next"]',
),
};
}

View File

@@ -16,6 +16,7 @@ export class TeamSchedulePage {
notes: page.getByTestId("schedule-note"),
teamEvents: page.getByTestId("schedule-team-event"),
addEventButton: page.getByTestId("add-team-event-button"),
editAvailabilityLink: page.getByTestId("edit-availability-link"),
// the chip radio input is visually hidden, so the label is what clicks
nextWeekToggle: page.locator(
'label[for="chip-radio-schedule-week-next"]',

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "Availability saved",
"editor.saveWeek": "Save week",
"editor.title": "My availability",
"editor.teamSchedule": "Team schedule",
"editor.timesInYourTimezone": "Times in your time zone",
"editor.visibility": "Visible to your teammates and friends",
"events.title": "Team events",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "{{amount}} unknown",
"team.canPlay": "Team can play ({{players}}+)",
"team.currentWeek": "This week",
"team.editAvailability": "Edit my availability",
"team.hidden": "Only team members can see the team schedule",
"team.nextWeek": "Next week",
"team.noSchedule": "No schedule",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",

View File

@@ -10,6 +10,7 @@
"editor.saved": "",
"editor.saveWeek": "",
"editor.title": "",
"editor.teamSchedule": "",
"editor.timesInYourTimezone": "",
"editor.visibility": "",
"events.title": "",
@@ -32,6 +33,7 @@
"registration.summary.unknown": "",
"team.canPlay": "",
"team.currentWeek": "",
"team.editAvailability": "",
"team.hidden": "",
"team.nextWeek": "",
"team.noSchedule": "",