From a39213ecc6b47dcd2f1f1339b5e59bfe9ab58db4 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 29 Aug 2026 11:17:20 +0300 Subject: [PATCH] Refactor --- .../availability/components/MySchedule.tsx | 45 +++------ .../RegistrationAvailabilityPanel.tsx | 64 ++++--------- .../components/ScheduleDayCell.tsx | 2 +- .../components/ScheduleTracks.module.css | 41 ++++++++ .../components/ScheduleWeekDialog.tsx | 28 ++---- .../WeekAvailabilityEditor.module.css | 41 -------- .../components/WeekAvailabilityEditor.tsx | 10 +- .../availability/components/WeekToggle.tsx | 54 +++++++++++ .../availability/core/Availability.ts | 32 +++++++ .../availability/core/Commitments.server.ts | 12 ++- .../availability/core/MySchedule.server.ts | 59 ++++-------- .../core/RegistrationAvailability.server.ts | 13 +-- .../core/RosterSchedule.server.ts | 18 ++-- .../availability/core/ScheduleWeek.ts | 20 ++-- .../core/TournamentDuration.server.ts | 8 +- .../scrims/components/ScrimAvailability.tsx | 6 +- .../components/ScrimSchedulePicker.module.css | 36 ------- .../scrims/components/ScrimSchedulePicker.tsx | 70 +++++--------- app/features/sidebar/core/sidebar.server.ts | 4 +- .../tournament/routes/to.$id.register.tsx | 95 ++++++------------- 20 files changed, 288 insertions(+), 370 deletions(-) create mode 100644 app/features/availability/components/WeekToggle.tsx diff --git a/app/features/availability/components/MySchedule.tsx b/app/features/availability/components/MySchedule.tsx index f905873df..dc89b2611 100644 --- a/app/features/availability/components/MySchedule.tsx +++ b/app/features/availability/components/MySchedule.tsx @@ -3,10 +3,6 @@ import { useTranslation } from "react-i18next"; import type { FetcherWithComponents } from "react-router"; import * as R from "remeda"; import { SendouButton } from "~/components/elements/Button"; -import { - SendouChipRadio, - SendouChipRadioGroup, -} from "~/components/elements/ChipRadio"; import { toastQueue } from "~/components/elements/Toast"; import { useUnsavedChangesChecker } from "~/form/UnsavedChangesGuard"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; @@ -18,8 +14,7 @@ import type { AvailabilityEditorWeek } from "../availability-types"; import type { MyScheduleData } from "../core/MySchedule.server"; import styles from "./MySchedule.module.css"; import { WeekAvailabilityEditor } from "./WeekAvailabilityEditor"; - -const WEEK_VALUES = ["current", "next"] as const; +import { WeekToggle } from "./WeekToggle"; /** * The "My schedule" section of the events page: the schedule editor with a @@ -95,31 +90,21 @@ export function MySchedule({ data }: { data: MyScheduleData }) {

{t("schedule:editor.title")}

- - {WEEK_VALUES.map((value, index) => ( - setParams({ week: value })} - > - - {index === 0 - ? t("schedule:team.currentWeek") - : t("schedule:team.nextWeek")} - {!data.weeks[index].submitted ? ( - - • {t("schedule:editor.notFilled")} - - ) : null} + setParams({ week: value })} + renderExtra={(value) => + !data.weeks[value === "next" ? 1 : 0].submitted ? ( + + • {t("schedule:editor.notFilled")} - - ))} - + ) : null + } + />

{t("schedule:team.weekHeading", { diff --git a/app/features/availability/components/RegistrationAvailabilityPanel.tsx b/app/features/availability/components/RegistrationAvailabilityPanel.tsx index 905fd3c11..f45fc0887 100644 --- a/app/features/availability/components/RegistrationAvailabilityPanel.tsx +++ b/app/features/availability/components/RegistrationAvailabilityPanel.tsx @@ -12,11 +12,11 @@ import type * as React from "react"; import { useTranslation } from "react-i18next"; import { Avatar } from "~/components/Avatar"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; -import { databaseTimestampToDate } from "~/utils/dates"; import type { SerializeFrom } from "~/utils/remix"; import type { TimeRange, WindowAvailabilityEntry } from "../availability-types"; import type { RegistrationAvailability } from "../core/RegistrationAvailability.server"; import styles from "./RegistrationAvailabilityPanel.module.css"; +import { useRangeText } from "./ScheduleDayCell"; export interface AvailabilityPanelUser { id: number; @@ -34,15 +34,6 @@ export type AvailabilityRowStatus = /** On the roster, but their schedule is not visible to the viewer (neither a teammate nor a friend). */ | "hidden"; -const STATUS_ORDER: Array = [ - "available", - "partial", - "unavailable", - "busy", - "unknown", - "hidden", -]; - /** * The tournament registration page's availability panel: how each member of * the roster relates to the event's estimated window, plus the friends who @@ -88,6 +79,18 @@ export function RegistrationAvailabilityPanel({ if (roster.length === 0 && freeSubs.length === 0) return null; + const freeSubRows = ( +
    + {freeSubs.map((user) => ( + + ))} +
+ ); + return (

@@ -118,26 +121,10 @@ export function RegistrationAvailabilityPanel({

{t("schedule:registration.friends")}
-
    - {freeSubs.map((user) => ( - - ))} -
+ {freeSubRows} ) : ( -
    - {freeSubs.map((user) => ( - - ))} -
+ freeSubRows ) ) : null}
@@ -311,9 +298,10 @@ export function AvailabilityStatusDots({ }: { statuses: Array; }) { - const shown = statuses - .filter((status) => status === "available" || status === "partial") - .sort((a, b) => STATUS_ORDER.indexOf(a) - STATUS_ORDER.indexOf(b)); + const shown = [ + ...statuses.filter((status) => status === "available"), + ...statuses.filter((status) => status === "partial"), + ]; if (shown.length === 0) return null; return ( @@ -326,19 +314,7 @@ export function AvailabilityStatusDots({ } function RangesText({ ranges }: { ranges: Array }) { - const { formatter: timeFormatter } = useDateTimeFormat({ - hour: "numeric", - minute: "2-digit", - }); - - // formatRange expands to full dates when the ends fall on different - // calendar days, so a range crossing midnight formats its ends separately - // to stay times-only - const rangeText = (range: TimeRange) => - databaseTimestampToDate(range.startsAt).getDate() === - databaseTimestampToDate(range.endsAt).getDate() - ? timeFormatter.formatRange(range.startsAt, range.endsAt) - : `${timeFormatter.format(range.startsAt)} – ${timeFormatter.format(range.endsAt)}`; + const rangeText = useRangeText(); return ( {ranges.map(rangeText).join(" · ")} diff --git a/app/features/availability/components/ScheduleDayCell.tsx b/app/features/availability/components/ScheduleDayCell.tsx index 8f5167f7f..f256e1749 100644 --- a/app/features/availability/components/ScheduleDayCell.tsx +++ b/app/features/availability/components/ScheduleDayCell.tsx @@ -78,7 +78,7 @@ export function ScheduleDayCell({ * ends fall on different calendar days, so a range crossing (or ending exactly * at) midnight formats its ends separately. */ -function useRangeText() { +export function useRangeText() { const { formatter } = useDateTimeFormat({ hour: "numeric", minute: "2-digit", diff --git a/app/features/availability/components/ScheduleTracks.module.css b/app/features/availability/components/ScheduleTracks.module.css index 5f8d41b4e..64f74a5c7 100644 --- a/app/features/availability/components/ScheduleTracks.module.css +++ b/app/features/availability/components/ScheduleTracks.module.css @@ -159,3 +159,44 @@ font-size: var(--font-xs); color: var(--color-text-high); } + +.listDay { + display: flex; + flex-direction: column; + gap: var(--s-1-5); + padding-block: var(--s-2); + border-bottom: 1px solid var(--color-bg-higher); + + &:last-child { + border-bottom: none; + } +} + +.listDayHeader { + display: flex; + align-items: center; + gap: var(--s-1); + font-size: var(--font-xs); + font-weight: var(--weight-semi); +} + +.listDayBody { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--s-1-5); +} + +.timeChip { + padding: var(--s-0-5) var(--s-2); + background-color: var(--color-success-low); + border: 1px solid var(--color-success); + border-radius: var(--radius-full); + font-size: var(--font-xs); + color: var(--color-text); + cursor: pointer; + + &:focus-visible { + outline: var(--focus-ring); + } +} diff --git a/app/features/availability/components/ScheduleWeekDialog.tsx b/app/features/availability/components/ScheduleWeekDialog.tsx index d49256741..e8861d25b 100644 --- a/app/features/availability/components/ScheduleWeekDialog.tsx +++ b/app/features/availability/components/ScheduleWeekDialog.tsx @@ -1,8 +1,4 @@ import { useTranslation } from "react-i18next"; -import { - SendouChipRadio, - SendouChipRadioGroup, -} from "~/components/elements/ChipRadio"; import { SendouDialog } from "~/components/elements/Dialog"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; import { useSearchParamsTyped } from "~/modules/search-params/hooks"; @@ -10,6 +6,7 @@ import { scheduleWeekSearchParams } from "../availability-search-params"; import type { ScheduleWeekView } from "../availability-types"; import { ScheduleDayCell } from "./ScheduleDayCell"; import styles from "./ScheduleWeekDialog.module.css"; +import { WeekToggle } from "./WeekToggle"; /** * One person's reportable weeks as a read-only day-by-day list of the time @@ -49,24 +46,11 @@ export function ScheduleWeekDialog({ shownWeek.days[6].noonAt, )} - - setParams({ week: "current" })} - > - {t("schedule:team.currentWeek")} - - setParams({ week: "next" })} - > - {t("schedule:team.nextWeek")} - - + setParams({ week: value })} + /> {shownWeek.reported ? ( diff --git a/app/features/availability/components/WeekAvailabilityEditor.module.css b/app/features/availability/components/WeekAvailabilityEditor.module.css index 6c79e1c51..dfde1ea51 100644 --- a/app/features/availability/components/WeekAvailabilityEditor.module.css +++ b/app/features/availability/components/WeekAvailabilityEditor.module.css @@ -133,47 +133,6 @@ color: var(--color-text-high); } -.listDay { - display: flex; - flex-direction: column; - gap: var(--s-1-5); - padding-block: var(--s-2); - border-bottom: 1px solid var(--color-bg-higher); - - &:last-child { - border-bottom: none; - } -} - -.listDayHeader { - display: flex; - align-items: center; - gap: var(--s-1); - font-size: var(--font-xs); - font-weight: var(--weight-semi); -} - -.listDayBody { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--s-1-5); -} - -.timeChip { - padding: var(--s-0-5) var(--s-2); - background-color: var(--color-success-low); - border: 1px solid var(--color-success); - border-radius: var(--radius-full); - font-size: var(--font-xs); - color: var(--color-text); - cursor: pointer; - - &:focus-visible { - outline: var(--focus-ring); - } -} - .addChip { display: inline-flex; align-items: center; diff --git a/app/features/availability/components/WeekAvailabilityEditor.tsx b/app/features/availability/components/WeekAvailabilityEditor.tsx index 972cc6e6b..9aab7fc4c 100644 --- a/app/features/availability/components/WeekAvailabilityEditor.tsx +++ b/app/features/availability/components/WeekAvailabilityEditor.tsx @@ -561,14 +561,16 @@ export function WeekAvailabilityEditor({ ); return ( -
-
{dayLabelText(day)}
-
+
+
+ {dayLabelText(day)} +
+
{day.ranges.map((range) => (
); } diff --git a/app/features/scrims/components/ScrimSchedulePicker.module.css b/app/features/scrims/components/ScrimSchedulePicker.module.css index 481b7ed3d..fdfb884ad 100644 --- a/app/features/scrims/components/ScrimSchedulePicker.module.css +++ b/app/features/scrims/components/ScrimSchedulePicker.module.css @@ -114,43 +114,7 @@ color: var(--color-text-high); } -.listDay { - display: flex; - flex-direction: column; - gap: var(--s-1-5); - padding-block: var(--s-2); - border-bottom: 1px solid var(--color-bg-higher); - - &:last-child { - border-bottom: none; - } -} - -.listDayHeader { - font-size: var(--font-xs); - font-weight: var(--weight-semi); -} - -.listDayBody { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--s-1-5); -} - .slotChip { - padding: var(--s-0-5) var(--s-2); - background-color: var(--color-success-low); - border: 1px solid var(--color-success); - border-radius: var(--radius-full); - font-size: var(--font-xs); - color: var(--color-text); - cursor: pointer; - - &:focus-visible { - outline: var(--focus-ring); - } - &.oneShort { border-style: dashed; opacity: 0.85; diff --git a/app/features/scrims/components/ScrimSchedulePicker.tsx b/app/features/scrims/components/ScrimSchedulePicker.tsx index 401a83d2a..4aa116b37 100644 --- a/app/features/scrims/components/ScrimSchedulePicker.tsx +++ b/app/features/scrims/components/ScrimSchedulePicker.tsx @@ -2,10 +2,6 @@ import clsx from "clsx"; import * as React from "react"; import { useTranslation } from "react-i18next"; import * as R from "remeda"; -import { - SendouChipRadio, - SendouChipRadioGroup, -} from "~/components/elements/ChipRadio"; import { useUser } from "~/features/auth/core/user"; import type { DayTimeRange, @@ -18,6 +14,7 @@ import { useClockWindow, } from "~/features/availability/components/ScheduleTracks"; import trackStyles from "~/features/availability/components/ScheduleTracks.module.css"; +import { WeekToggle } from "~/features/availability/components/WeekToggle"; import * as Availability from "~/features/availability/core/Availability"; import type { RosterScheduleData } from "~/features/availability/core/RosterSchedule.server"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; @@ -147,23 +144,17 @@ function RosterTimeline({ }); const nameById = new Map(names.map((member) => [member.id, member.username])); - const freeNames = (userIds: Array) => - userIds - .flatMap((userId) => { - const username = nameById.get(userId); + const namesOf = (userIds: Array) => + userIds.flatMap((userId) => { + const username = nameById.get(userId); - return username ? [username] : []; - }) - .join(", "); + return username ? [username] : []; + }); const unknownUserIds = roster.filter( (userId) => !memberById.get(userId)?.reportedWeekStarts.includes(week.startsAt), ); - const unknownNamed = unknownUserIds.flatMap((userId) => { - const username = nameById.get(userId); - - return username ? [username] : []; - }); + const unknownNamed = namesOf(unknownUserIds); const unknownUnnamed = unknownUserIds.length - unknownNamed.length; const pickedAt = at ? dateToDatabaseTimestamp(at) : null; @@ -193,7 +184,7 @@ function RosterTimeline({ label={`${rangeText(slot)} · ${t("schedule:picker.free", { amount: slot.userIds.length, })}`} - members={freeNames(slot.userIds)} + members={namesOf(slot.userIds).join(", ")} isPicked={slot.pick.startsAt === pickedAt} onPick={() => pick(slot)} /> @@ -209,24 +200,11 @@ function RosterTimeline({

{t("schedule:picker.title")}

- - setWeekIndex(0)} - > - {t("schedule:team.currentWeek")} - - setWeekIndex(1)} - > - {t("schedule:team.nextWeek")} - - + setWeekIndex(value === "next" ? 1 : 0)} + />
@@ -239,23 +217,23 @@ function RosterTimeline({
{dayRows.map(({ day, slots: daySlots }) => { return ( -
-
+
+
{dayFormatter.format(day.noonAt)}
{daySlots.length === 0 ? ( ) : ( -
+
{daySlots.map((slot) => (