/events page

This commit is contained in:
Kalle
2026-03-08 14:00:27 +02:00
parent bb195c2aad
commit 421d06113d
29 changed files with 491 additions and 144 deletions

View File

@@ -0,0 +1,8 @@
.dayHeader {
padding: var(--s-2) var(--s-2) var(--s-1);
font-size: var(--font-2xs);
font-weight: var(--weight-bold);
color: var(--color-text-high);
text-transform: uppercase;
letter-spacing: 0.05em;
}

View File

@@ -0,0 +1,101 @@
import { isToday, isTomorrow } from "date-fns";
import { useTranslation } from "react-i18next";
import type { SidebarEvent } from "~/features/sidebar/core/sidebar.server";
import styles from "./EventsList.module.css";
import { ListLink } from "./SideNav";
export function EventsList({
events,
onClick,
}: {
events: SidebarEvent[];
onClick?: () => void;
}) {
const { t, i18n } = useTranslation(["front"]);
if (events.length === 0) {
return (
<div className="text-lighter text-sm p-2">
{t("front:sideNav.noEvents")}
</div>
);
}
const getDayKey = (timestamp: number) => {
const date = new Date(timestamp * 1000);
return date.toDateString();
};
const formatDayHeader = (date: Date) => {
if (isToday(date)) {
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
numeric: "auto",
});
const str = rtf.format(0, "day");
return str.charAt(0).toUpperCase() + str.slice(1);
}
if (isTomorrow(date)) {
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
numeric: "auto",
});
const str = rtf.format(1, "day");
return str.charAt(0).toUpperCase() + str.slice(1);
}
return date.toLocaleDateString(i18n.language, {
weekday: "long",
month: "short",
day: "numeric",
});
};
const formatTime = (date: Date) => {
return date.toLocaleTimeString(i18n.language, {
hour: "numeric",
minute: "2-digit",
});
};
const groupedEvents = events.reduce<Record<string, typeof events>>(
(acc, event) => {
const key = getDayKey(event.startTime);
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(event);
return acc;
},
{},
);
const dayKeys = Object.keys(groupedEvents);
return (
<>
{dayKeys.map((dayKey) => {
const dayEvents = groupedEvents[dayKey];
const firstDate = new Date(dayEvents[0].startTime * 1000);
return (
<div key={dayKey}>
<div className={styles.dayHeader}>{formatDayHeader(firstDate)}</div>
{dayEvents.map((event) => (
<ListLink
key={`${event.type}-${event.id}`}
to={event.url}
imageUrl={event.logoUrl ?? undefined}
subtitle={formatTime(new Date(event.startTime * 1000))}
onClick={onClick}
>
{event.scrimStatus === "booked"
? t("front:sideNav.scrimVs", { opponent: event.name })
: event.scrimStatus === "looking"
? t("front:sideNav.lookingForScrim")
: event.name}
</ListLink>
))}
</div>
);
})}
</>
);
}

View File

@@ -299,24 +299,24 @@
}
.panelSectionLink {
display: block;
display: flex;
align-items: center;
gap: 2px;
width: fit-content;
margin: var(--s-4) auto;
font-size: var(--font-2xs);
color: var(--color-text-high);
text-decoration: none;
padding: var(--s-3) var(--s-2);
text-align: center;
padding: var(--s-1) var(--s-3);
background-color: var(--color-bg-higher);
border-radius: var(--radius-field);
& svg {
stroke-width: 3;
}
}
.panelSectionLink:hover {
color: var(--color-text);
text-decoration: underline;
}
.dayHeader {
padding: var(--s-2) var(--s-2) var(--s-1);
font-size: var(--font-2xs);
font-weight: var(--weight-bold);
color: var(--color-text-high);
text-transform: uppercase;
letter-spacing: 0.05em;
background-color: var(--color-bg-high);
}

View File

@@ -1,7 +1,7 @@
import clsx from "clsx";
import { isToday, isTomorrow } from "date-fns";
import {
Calendar,
ChevronRight,
LogIn,
Menu,
Settings,
@@ -18,12 +18,14 @@ import { useUser } from "~/features/auth/core/user";
import { FriendMenu } from "~/features/friends/components/FriendMenu";
import type { RootLoaderData } from "~/root";
import {
EVENTS_PAGE,
FRIENDS_PAGE,
navIconUrl,
SETTINGS_PAGE,
userPage,
} from "~/utils/urls";
import { Avatar } from "./Avatar";
import { EventsList } from "./EventsList";
import { SendouButton } from "./elements/Button";
import { Image } from "./Image";
import { LogInButtonContainer } from "./layout/LogInButtonContainer";
@@ -34,7 +36,6 @@ import {
import { navItems } from "./layout/nav-items";
import styles from "./MobileNav.module.css";
import { NotificationDot } from "./NotificationDot";
import { ListLink } from "./SideNav";
import { StreamListItems } from "./StreamListItems";
type SidebarData = RootLoaderData["sidebar"] | undefined;
@@ -302,6 +303,7 @@ function FriendsPanel({
onClick={onClose}
>
{t("common:actions.viewAll")}
<ChevronRight size={14} />
</Link>
</MobilePanel>
);
@@ -314,91 +316,19 @@ function TourneysPanel({
events: NonNullable<SidebarData>["events"];
onClose: () => void;
}) {
const { t, i18n } = useTranslation(["front"]);
const formatDayHeader = (date: Date) => {
if (isToday(date)) {
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
numeric: "auto",
});
const str = rtf.format(0, "day");
return str.charAt(0).toUpperCase() + str.slice(1);
}
if (isTomorrow(date)) {
const rtf = new Intl.RelativeTimeFormat(i18n.language, {
numeric: "auto",
});
const str = rtf.format(1, "day");
return str.charAt(0).toUpperCase() + str.slice(1);
}
return date.toLocaleDateString(i18n.language, {
weekday: "long",
month: "short",
day: "numeric",
});
};
const formatTime = (date: Date) => {
return date.toLocaleTimeString(i18n.language, {
hour: "numeric",
minute: "2-digit",
});
};
const getDayKey = (timestamp: number) => {
const date = new Date(timestamp * 1000);
return date.toDateString();
};
const groupedEvents = events.reduce<Record<string, typeof events>>(
(acc, event) => {
const key = getDayKey(event.startTime);
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(event);
return acc;
},
{},
);
const dayKeys = Object.keys(groupedEvents);
const { t } = useTranslation(["front", "common"]);
return (
<MobilePanel title={t("front:sideNav.myCalendar")} onClose={onClose}>
{events.length > 0 ? (
dayKeys.map((dayKey) => {
const dayEvents = groupedEvents[dayKey];
const firstDate = new Date(dayEvents[0].startTime * 1000);
return (
<div key={dayKey}>
<div className={styles.dayHeader}>
{formatDayHeader(firstDate)}
</div>
{dayEvents.map((event) => (
<ListLink
key={`${event.type}-${event.id}`}
to={event.url}
imageUrl={event.logoUrl ?? undefined}
subtitle={formatTime(new Date(event.startTime * 1000))}
onClick={onClose}
>
{event.scrimStatus === "booked"
? t("front:sideNav.scrimVs", { opponent: event.name })
: event.scrimStatus === "looking"
? t("front:sideNav.lookingForScrim")
: event.name}
</ListLink>
))}
</div>
);
})
) : (
<div className="text-lighter text-sm p-2">
{t("front:sideNav.noEvents")}
</div>
)}
<EventsList events={events} onClick={onClose} />
<Link
to={EVENTS_PAGE}
className={styles.panelSectionLink}
onClick={onClose}
>
{t("common:actions.viewAll")}
<ChevronRight size={14} />
</Link>
</MobilePanel>
);
}

View File

@@ -44,3 +44,32 @@
.divider {
border-color: var(--color-border);
}
.viewAllLink {
display: flex;
align-items: center;
gap: 2px;
width: fit-content;
margin: var(--s-2) auto;
font-size: var(--font-2xs);
color: var(--color-text-high);
text-decoration: none;
padding: var(--s-1) var(--s-3);
background-color: var(--color-bg-higher);
border-radius: var(--radius-field);
& svg {
stroke-width: 3;
}
}
@media screen and (max-width: 599px) {
.viewAllLink {
margin-top: var(--s-4);
}
}
.viewAllLink:hover {
color: var(--color-text);
background-color: var(--color-bg-high);
}

View File

@@ -1,7 +1,7 @@
import { Bell, RefreshCcw } from "lucide-react";
import { Bell, ChevronRight, RefreshCcw } from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMatches, useRevalidator } from "react-router";
import { Link, useMatches, useRevalidator } from "react-router";
import {
NotificationItem,
NotificationItemDivider,
@@ -11,7 +11,7 @@ import { NOTIFICATIONS } from "~/features/notifications/notifications-contants";
import type { RootLoaderData } from "~/root";
import { NOTIFICATIONS_URL } from "~/utils/urls";
import { useMarkNotificationsAsSeen } from "../../features/notifications/notifications-hooks";
import { LinkButton, SendouButton } from "../elements/Button";
import { SendouButton } from "../elements/Button";
import styles from "./NotificationPopover.module.css";
@@ -95,15 +95,15 @@ function NotificationsFooter({ onClose }: { onClose?: () => void }) {
return (
<div>
<hr className={styles.divider} />
<LinkButton
variant="minimal"
size="small"
<Link
to={NOTIFICATIONS_URL}
testId="notifications-see-all-button"
className={styles.viewAllLink}
data-testid="notifications-see-all-button"
onClick={onClose}
>
{t("common:notifications.seeAll")}
</LinkButton>
{t("common:actions.viewAll")}
<ChevronRight size={14} />
</Link>
</div>
);
}

View File

@@ -25,7 +25,12 @@ import { useUser } from "~/features/auth/core/user";
import { FriendMenu } from "~/features/friends/components/FriendMenu";
import type { RootLoaderData } from "~/root";
import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix.server";
import { FRIENDS_PAGE, SETTINGS_PAGE, userPage } from "~/utils/urls";
import {
EVENTS_PAGE,
FRIENDS_PAGE,
SETTINGS_PAGE,
userPage,
} from "~/utils/urls";
import { Avatar } from "../Avatar";
import { SendouButton } from "../elements/Button";
import { SendouPopover } from "../elements/Popover";
@@ -210,7 +215,17 @@ export function Layout({
const sideNavChildren = (
<>
<SideNavHeader icon={<Calendar />}>
<SideNavHeader
icon={<Calendar />}
action={
user ? (
<Link to={EVENTS_PAGE} className={styles.viewAllLink}>
{t("common:actions.viewAll")}
<ChevronRight size={14} />
</Link>
) : null
}
>
{t("front:sideNav.myCalendar")}
</SideNavHeader>
{events.length > 0 ? (

View File

@@ -0,0 +1,32 @@
import { requireUser } from "~/features/auth/core/user.server";
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
import {
scrimToSidebarEvent,
tournamentToSidebarEvent,
} from "~/features/sidebar/core/sidebar.server";
export type EventsLoaderData = typeof loader;
export const loader = async () => {
const user = requireUser();
const [tournamentsData, scrimsData] = await Promise.all([
ShowcaseTournaments.frontPageTournamentsByUserId(user.id),
ScrimPostRepository.findUserScrims(user.id),
]);
const registered = tournamentsData.participatingFor
.map(tournamentToSidebarEvent)
.sort((a, b) => a.startTime - b.startTime);
const hosting = tournamentsData.organizingFor
.map(tournamentToSidebarEvent)
.sort((a, b) => a.startTime - b.startTime);
const scrims = scrimsData
.map(scrimToSidebarEvent)
.sort((a, b) => a.startTime - b.startTime);
return { registered, hosting, scrims };
};

View File

@@ -0,0 +1,22 @@
.eventsListHeader {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--s-2);
margin-block-end: var(--s-2);
}
.filterRadio {
padding: var(--s-1) var(--s-2);
border-radius: var(--radius-field);
cursor: pointer;
color: var(--color-text-high);
font-weight: var(--weight-semi);
font-size: var(--font-3xs);
}
.filterRadioSelected {
background-color: var(--color-bg-higher);
color: var(--color-text);
}

View File

@@ -0,0 +1,83 @@
import clsx from "clsx";
import { useState } from "react";
import { Radio, RadioGroup } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { Link, useLoaderData } from "react-router";
import { EventsList } from "~/components/EventsList";
import { Main } from "~/components/Main";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { CALENDAR_PAGE } from "~/utils/urls";
import type { EventsLoaderData } from "../loaders/events.server";
import styles from "./events.module.css";
export { loader } from "../loaders/events.server";
export const handle: SendouRouteHandle = {
i18n: ["calendar"],
};
type ViewFilter = "registered" | "hosting" | "scrims";
export default function EventsPage() {
const { t } = useTranslation(["calendar"]);
const data = useLoaderData<EventsLoaderData>();
const [filter, setFilter] = useState<ViewFilter>("registered");
const viewLabels: Record<ViewFilter, string> = {
registered: t("calendar:events.view.registered"),
hosting: t("calendar:events.view.hosting"),
scrims: t("calendar:events.view.scrims"),
};
const shownEvents =
filter === "registered"
? data.registered
: filter === "hosting"
? data.hosting
: data.scrims;
const hasNoEventsAtAll =
data.registered.length === 0 &&
data.hosting.length === 0 &&
data.scrims.length === 0;
return (
<Main halfWidth>
<div className={styles.eventsListHeader}>
<h2 className="text-lg mx-2">{t("calendar:events.title")}</h2>
{hasNoEventsAtAll ? null : (
<RadioGroup
value={filter}
onChange={(v) => setFilter(v as ViewFilter)}
orientation="horizontal"
className="stack horizontal xs"
>
{(["registered", "hosting", "scrims"] as const).map((value) => (
<Radio key={value} value={value}>
{({ isSelected }) => (
<span
className={clsx(styles.filterRadio, {
[styles.filterRadioSelected]: isSelected,
})}
>
{viewLabels[value]}
</span>
)}
</Radio>
))}
</RadioGroup>
)}
</div>
{hasNoEventsAtAll ? (
<p className="text-lighter text-sm">
{t("calendar:events.emptyAll")}{" "}
<Link to={CALENDAR_PAGE}>{t("calendar:events.findOnCalendar")}</Link>
</p>
) : shownEvents.length === 0 ? (
<p className="text-lighter text-sm">{t("calendar:events.empty")}</p>
) : (
<EventsList events={shownEvents} />
)}
</Main>
);
}

View File

@@ -12,6 +12,7 @@ import * as FriendRepository from "~/features/friends/FriendRepository.server";
import { resolveFriendActivity } from "~/features/friends/friends-utils.server";
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server";
import type { SidebarScrim } from "~/features/scrims/ScrimPostRepository.server";
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
import { getSendouQSidebarStreams } from "~/features/sendouq-streams/core/streams.server";
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
@@ -81,27 +82,9 @@ export async function resolveSidebarData(userId: number | null) {
seenTournamentIds.add(t.id);
return true;
})
.map((t) => ({
id: t.id,
name: t.name,
url: t.url,
logoUrl: t.logoUrl,
startTime: t.startTime,
type: "tournament" as const,
}));
.map(tournamentToSidebarEvent);
const scrimsIconUrl = `${navIconUrl("scrims")}.png`;
const scrimEvents: SidebarEvent[] = scrimsData.map((s) => ({
id: s.id,
name: s.opponentName ?? "Scrim",
url: s.isAccepted
? href("/scrims/:id", { id: String(s.id) })
: href("/scrims"),
logoUrl: s.opponentAvatarUrl ?? scrimsIconUrl,
startTime: s.at,
type: "scrim" as const,
scrimStatus: s.isAccepted ? ("booked" as const) : ("looking" as const),
}));
const scrimEvents: SidebarEvent[] = scrimsData.map(scrimToSidebarEvent);
const personalEvents = [...tournamentEvents, ...scrimEvents].sort(
(a, b) => a.startTime - b.startTime,
@@ -347,3 +330,32 @@ function rowToSidebarFriend(
tournamentId: row.tournamentId,
};
}
export function tournamentToSidebarEvent(
t: ShowcaseCalendarEvent,
): SidebarEvent {
return {
id: t.id,
name: t.name,
url: t.url,
logoUrl: t.logoUrl,
startTime: t.startTime,
type: "tournament" as const,
};
}
const SCRIMS_ICON_URL = `${navIconUrl("scrims")}.png`;
export function scrimToSidebarEvent(s: SidebarScrim): SidebarEvent {
return {
id: s.id,
name: s.opponentName ?? "Scrim",
url: s.isAccepted
? href("/scrims/:id", { id: String(s.id) })
: href("/scrims"),
logoUrl: s.opponentAvatarUrl ?? SCRIMS_ICON_URL,
startTime: s.at,
type: "scrim" as const,
scrimStatus: s.isAccepted ? ("booked" as const) : ("looking" as const),
};
}

View File

@@ -44,6 +44,8 @@ export default [
route("/friends", "features/friends/routes/friends.tsx"),
route("/events", "features/calendar/routes/events.tsx"),
route("/suspended", "features/ban/routes/suspended.tsx"),
route("/u", "features/user-search/routes/u.tsx"),

View File

@@ -120,6 +120,7 @@ export const SENDOUQ_STREAMS_PAGE = "/q/streams";
export const TIERS_PAGE = "/tiers";
export const SUSPENDED_PAGE = "/suspended";
export const LFG_PAGE = "/lfg";
export const EVENTS_PAGE = "/events";
export const FRIENDS_PAGE = "/friends";
export const SETTINGS_PAGE = "/settings";
export const LUTI_PAGE = "/luti";

View File

@@ -79,5 +79,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -79,5 +79,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -79,5 +79,12 @@
"filter.applyAndDefault": "Apply & make default",
"forms.draft": "Draft",
"forms.draftInfo": "Draft tournaments are hidden and only visible to organizers. The tournament must be opened (by disabling this toggle) before any bracket can be started.",
"forms.draftBracketStartBlocked": "Tournament is in draft mode. Edit the tournament and disable the draft toggle before starting the bracket."
"forms.draftBracketStartBlocked": "Tournament is in draft mode. Edit the tournament and disable the draft toggle before starting the bracket.",
"events.title": "My Events",
"events.view.registered": "Registered",
"events.view.hosting": "Hosting",
"events.view.scrims": "Scrims",
"events.empty": "No events in this category",
"events.emptyAll": "You have no upcoming events.",
"events.findOnCalendar": "Find an event to join on the calendar!"
}

View File

@@ -81,5 +81,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -81,5 +81,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -81,5 +81,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -81,5 +81,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -81,5 +81,12 @@
"filter.applyAndDefault": "החל והפוך לברירת מחדל",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -81,5 +81,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -75,5 +75,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -75,5 +75,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -79,5 +79,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -83,5 +83,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -81,5 +81,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -83,5 +83,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}

View File

@@ -75,5 +75,12 @@
"filter.applyAndDefault": "",
"forms.draft": "",
"forms.draftInfo": "",
"forms.draftBracketStartBlocked": ""
"forms.draftBracketStartBlocked": "",
"events.title": "",
"events.view.registered": "",
"events.view.hosting": "",
"events.view.scrims": "",
"events.empty": "",
"events.emptyAll": "",
"events.findOnCalendar": ""
}