From d42e6f9bad8f37970932242690dd887843445dc4 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:58:40 +0300 Subject: [PATCH] Scrims routes, chat rebuild UI and match-page components for apps/web --- apps/web-react/locales/en/common.json | 2 + apps/web/package.json | 3 +- .../src/lib/components/ConfirmDialog.svelte | 83 ++ apps/web/src/lib/components/Divider.svelte | 44 + apps/web/src/lib/components/LocaleTime.svelte | 40 + apps/web/src/lib/components/NoteAvatar.svelte | 148 +++ apps/web/src/lib/components/StageImage.svelte | 31 + apps/web/src/lib/components/Table.svelte | 58 ++ .../web/src/lib/components/TimePopover.svelte | 104 ++ .../components/filter-bar/FilterBar.svelte | 297 ++++++ .../components/filter-bar/filter-bar-types.ts | 18 + .../src/lib/components/layout/Layout.svelte | 99 +- .../lib/components/layout/MobileNav.svelte | 26 +- .../components/match-page/IconBanner.svelte | 81 ++ .../match-page/MatchActionTab.svelte | 545 ++++++++++ .../components/match-page/MatchBanner.svelte | 232 +++++ .../match-page/MatchBannerContainer.svelte | 22 + .../MatchBannerScheduledTime.svelte | 20 + .../match-page/MatchBannerTopRow.svelte | 63 ++ .../components/match-page/MatchPage.svelte | 20 + .../match-page/MatchPageHeader.svelte | 36 + .../match-page/MatchResultTab.svelte | 19 + .../match-page/MatchRosterTab.svelte | 275 +++++ .../components/match-page/MatchTabs.svelte | 72 ++ .../match-page/MatchTimeline.svelte | 458 +++++++++ .../match-page/SecondaryAction.svelte | 113 ++ .../match-page/match-page-constants.ts | 17 + .../match-page-search-params.test.ts | 19 + .../match-page/match-page-search-params.ts | 8 + .../match-page/match-page-utils.test.ts | 142 +++ .../components/match-page/match-page-utils.ts | 83 ++ apps/web/src/lib/db/tables-json.ts | 3 + .../lib/features/chat/chat-state.svelte.ts | 24 + apps/web/src/lib/features/chat/chat.remote.ts | 152 +++ .../lib/features/chat/components/Chat.svelte | 318 ++++++ .../chat/components/ChatSidebar.svelte | 397 ++++++++ .../friends/FriendRepository.server.ts | 109 ++ .../components/AssociationSelect.svelte | 34 + .../components/BaseVisibilityFormField.svelte | 45 + .../components/DivsFilterPopover.svelte | 43 + .../components/MapsTournamentFormField.svelte | 12 + .../scrims/components/NewScrimForm.svelte | 90 ++ .../NotFoundVisibilityFormField.svelte | 108 ++ .../scrims/components/PickMapDialog.svelte | 33 + .../scrims/components/ScrimCardShell.svelte | 145 +++ .../components/ScrimExpandableText.svelte | 70 ++ .../scrims/components/ScrimMapListForm.svelte | 59 ++ .../components/ScrimMapListManager.svelte | 130 +++ .../components/ScrimMatchActionTab.svelte | 167 +++ .../scrims/components/ScrimMatchBanner.svelte | 79 ++ .../scrims/components/ScrimMatchHeader.svelte | 71 ++ .../components/ScrimMatchStatsTab.svelte | 222 ++++ .../scrims/components/ScrimMatchTabs.svelte | 109 ++ .../scrims/components/ScrimPage.svelte | 48 + .../scrims/components/ScrimPostCard.svelte | 419 ++++++++ .../scrims/components/ScrimRequestCard.svelte | 100 ++ .../components/ScrimRequestMembersList.svelte | 43 + .../components/ScrimRequestModal.svelte | 101 ++ .../scrims/components/ScrimTeamAvatar.svelte | 28 + .../components/ScrimTeamMembersPopover.svelte | 65 ++ .../scrims/components/ScrimsDaySection.svelte | 148 +++ .../scrims/components/ScrimsFilters.svelte | 112 ++ .../scrims/components/ScrimsPage.svelte | 245 +++++ .../components/SourceDependentFields.svelte | 14 + .../components/TimeRangeFilterPopover.svelte | 42 + .../scrims/components/WithFormField.svelte | 254 +++++ .../src/lib/features/scrims/scrims.remote.ts | 963 ++++++++++++++++++ .../user-page/components/MutualFriends.svelte | 75 ++ .../user-page/components/UserCard.svelte | 722 +++++++++++++ .../features/user-page/user-card-context.ts | 24 + .../features/user-page/user-card.remote.ts | 56 + apps/web/src/lib/modules/i18n/messages.ts | 12 +- .../src/lib/modules/intl/date-time-format.ts | 94 ++ .../lib/modules/permissions/guards.server.ts | 19 +- apps/web/src/lib/utils/respond.server.ts | 30 + apps/web/src/lib/utils/sql.ts | 5 + apps/web/src/lib/utils/urls.ts | 21 + apps/web/src/params.ts | 8 + apps/web/src/routes/run-routine/+server.ts | 5 +- apps/web/src/routes/scrims/+page.svelte | 12 + apps/web/src/routes/scrims/+page.ts | 14 + .../routes/scrims/[id=integer]/+page.svelte | 10 + .../src/routes/scrims/[id=integer]/+page.ts | 14 + apps/web/src/routes/scrims/new/+page.svelte | 8 + packages/components/src/LinkButton.svelte | 214 ++++ packages/components/src/MenuItem.svelte | 4 + packages/components/src/index.ts | 1 + pnpm-lock.yaml | 3 + 88 files changed, 9515 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/lib/components/ConfirmDialog.svelte create mode 100644 apps/web/src/lib/components/Divider.svelte create mode 100644 apps/web/src/lib/components/LocaleTime.svelte create mode 100644 apps/web/src/lib/components/NoteAvatar.svelte create mode 100644 apps/web/src/lib/components/StageImage.svelte create mode 100644 apps/web/src/lib/components/Table.svelte create mode 100644 apps/web/src/lib/components/TimePopover.svelte create mode 100644 apps/web/src/lib/components/filter-bar/FilterBar.svelte create mode 100644 apps/web/src/lib/components/filter-bar/filter-bar-types.ts create mode 100644 apps/web/src/lib/components/match-page/IconBanner.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchActionTab.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchBanner.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchBannerContainer.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchBannerScheduledTime.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchBannerTopRow.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchPage.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchPageHeader.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchResultTab.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchRosterTab.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchTabs.svelte create mode 100644 apps/web/src/lib/components/match-page/MatchTimeline.svelte create mode 100644 apps/web/src/lib/components/match-page/SecondaryAction.svelte create mode 100644 apps/web/src/lib/components/match-page/match-page-constants.ts create mode 100644 apps/web/src/lib/components/match-page/match-page-search-params.test.ts create mode 100644 apps/web/src/lib/components/match-page/match-page-search-params.ts create mode 100644 apps/web/src/lib/components/match-page/match-page-utils.test.ts create mode 100644 apps/web/src/lib/components/match-page/match-page-utils.ts create mode 100644 apps/web/src/lib/features/chat/chat-state.svelte.ts create mode 100644 apps/web/src/lib/features/chat/chat.remote.ts create mode 100644 apps/web/src/lib/features/chat/components/Chat.svelte create mode 100644 apps/web/src/lib/features/chat/components/ChatSidebar.svelte create mode 100644 apps/web/src/lib/features/scrims/components/AssociationSelect.svelte create mode 100644 apps/web/src/lib/features/scrims/components/BaseVisibilityFormField.svelte create mode 100644 apps/web/src/lib/features/scrims/components/DivsFilterPopover.svelte create mode 100644 apps/web/src/lib/features/scrims/components/MapsTournamentFormField.svelte create mode 100644 apps/web/src/lib/features/scrims/components/NewScrimForm.svelte create mode 100644 apps/web/src/lib/features/scrims/components/NotFoundVisibilityFormField.svelte create mode 100644 apps/web/src/lib/features/scrims/components/PickMapDialog.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimCardShell.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimExpandableText.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimMapListForm.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimMapListManager.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimMatchActionTab.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimMatchBanner.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimMatchHeader.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimMatchStatsTab.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimMatchTabs.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimPage.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimPostCard.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimRequestCard.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimRequestMembersList.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimRequestModal.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimTeamAvatar.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimTeamMembersPopover.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimsDaySection.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimsFilters.svelte create mode 100644 apps/web/src/lib/features/scrims/components/ScrimsPage.svelte create mode 100644 apps/web/src/lib/features/scrims/components/SourceDependentFields.svelte create mode 100644 apps/web/src/lib/features/scrims/components/TimeRangeFilterPopover.svelte create mode 100644 apps/web/src/lib/features/scrims/components/WithFormField.svelte create mode 100644 apps/web/src/lib/features/scrims/scrims.remote.ts create mode 100644 apps/web/src/lib/features/user-page/components/MutualFriends.svelte create mode 100644 apps/web/src/lib/features/user-page/components/UserCard.svelte create mode 100644 apps/web/src/lib/features/user-page/user-card-context.ts create mode 100644 apps/web/src/lib/features/user-page/user-card.remote.ts create mode 100644 apps/web/src/lib/modules/intl/date-time-format.ts create mode 100644 apps/web/src/lib/utils/respond.server.ts create mode 100644 apps/web/src/lib/utils/sql.ts create mode 100644 apps/web/src/params.ts create mode 100644 apps/web/src/routes/scrims/+page.svelte create mode 100644 apps/web/src/routes/scrims/+page.ts create mode 100644 apps/web/src/routes/scrims/[id=integer]/+page.svelte create mode 100644 apps/web/src/routes/scrims/[id=integer]/+page.ts create mode 100644 apps/web/src/routes/scrims/new/+page.svelte create mode 100644 packages/components/src/LinkButton.svelte diff --git a/apps/web-react/locales/en/common.json b/apps/web-react/locales/en/common.json index 2e4e21590..55d48b227 100644 --- a/apps/web-react/locales/en/common.json +++ b/apps/web-react/locales/en/common.json @@ -372,6 +372,8 @@ "chat.newMessages": "New messages", "chat.sidebar.title": "Chat", "chat.sidebar.noActiveChats": "No active chats", + "chat.sidebar.inactive": "Inactive", + "chat.room.scrim": "Scrim", "fc.title": "Friend code", "fc.whereToFind": "Find your friend code on your Nintendo Switch (2)", "fc.onceSetStaffOnly": "Once set, only a member of staff can change it", diff --git a/apps/web/package.json b/apps/web/package.json index 27a2285f1..bbc2c8eb0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,7 +34,8 @@ "nprogress": "^0.2.0", "remeda": "2.39.0", "sql-formatter": "^15.8.2", - "valibot": "^1.4.2" + "valibot": "^1.4.2", + "web-haptics": "0.0.6" }, "devDependencies": { "@inlang/paraglide-js": "^2.24.1", diff --git a/apps/web/src/lib/components/ConfirmDialog.svelte b/apps/web/src/lib/components/ConfirmDialog.svelte new file mode 100644 index 000000000..ef940e968 --- /dev/null +++ b/apps/web/src/lib/components/ConfirmDialog.svelte @@ -0,0 +1,83 @@ + + + +{@render trigger({ + onclick: () => { + open = true; + }, + type: "button", +})} + +{#if open} + { + open = false; + }} + > +
+

{dialogHeading}

+ {#if description} + {description} + {/if} +
+ +
+
+
+{/if} diff --git a/apps/web/src/lib/components/Divider.svelte b/apps/web/src/lib/components/Divider.svelte new file mode 100644 index 000000000..a2a9a00b2 --- /dev/null +++ b/apps/web/src/lib/components/Divider.svelte @@ -0,0 +1,44 @@ + + +
{@render children?.()}
+ + diff --git a/apps/web/src/lib/components/LocaleTime.svelte b/apps/web/src/lib/components/LocaleTime.svelte new file mode 100644 index 000000000..ff892ecf2 --- /dev/null +++ b/apps/web/src/lib/components/LocaleTime.svelte @@ -0,0 +1,40 @@ + + + + diff --git a/apps/web/src/lib/components/NoteAvatar.svelte b/apps/web/src/lib/components/NoteAvatar.svelte new file mode 100644 index 000000000..e198b90c8 --- /dev/null +++ b/apps/web/src/lib/components/NoteAvatar.svelte @@ -0,0 +1,148 @@ + + + + + + + + {@render children()} + {#if sentiment} + + {/if} + + + diff --git a/apps/web/src/lib/components/StageImage.svelte b/apps/web/src/lib/components/StageImage.svelte new file mode 100644 index 000000000..64dc38d52 --- /dev/null +++ b/apps/web/src/lib/components/StageImage.svelte @@ -0,0 +1,31 @@ + + +{label} diff --git a/apps/web/src/lib/components/Table.svelte b/apps/web/src/lib/components/Table.svelte new file mode 100644 index 000000000..c45c903eb --- /dev/null +++ b/apps/web/src/lib/components/Table.svelte @@ -0,0 +1,58 @@ + + +
+ + {@render children()} +
+
+ + diff --git a/apps/web/src/lib/components/TimePopover.svelte b/apps/web/src/lib/components/TimePopover.svelte new file mode 100644 index 000000000..a60a9672d --- /dev/null +++ b/apps/web/src/lib/components/TimePopover.svelte @@ -0,0 +1,104 @@ + + + +
+ + {#snippet trigger(triggerProps)} + + {/snippet} +
+
+ +
+ + {#if footerText} +
+ {footerText} +
+ {/if} +
+
+
+ + diff --git a/apps/web/src/lib/components/filter-bar/FilterBar.svelte b/apps/web/src/lib/components/filter-bar/FilterBar.svelte new file mode 100644 index 000000000..85a069a79 --- /dev/null +++ b/apps/web/src/lib/components/filter-bar/FilterBar.svelte @@ -0,0 +1,297 @@ + + + +
+ {#each visiblePills as pill (pill.key)} + {@render filterPill(pill)} + {/each} + {#if hiddenPills.length > 0} + {@render addFilterMenu()} + {/if} + {#if onReset || actions} +
+ {#if onReset} + + {/if} + {#if actions}{@render actions()}{/if} +
+ {/if} +
+ +{#snippet filterPill(pill: FilterBarPill)} +
+ { + openPillKey = isOpen ? pill.key : null; + }} + popoverClass={["filterPopover", pill.popoverClass] + .filter(Boolean) + .join(" ")} + > + {#snippet trigger(triggerProps)} + + {/snippet} + {@render pill.popover()} + + {#if pill.onRemove} + + {/if} +
+{/snippet} + +{#snippet addFilterMenu()} + + {#snippet trigger(triggerProps)} +
+ +
+ {/snippet} + {#each hiddenPills as pill (pill.key)} + addPill(pill)} + testId={pill.testId ? `menu-item-${pill.testId}` : undefined} + > + {pill.name} + + {/each} +
+{/snippet} + + diff --git a/apps/web/src/lib/components/filter-bar/filter-bar-types.ts b/apps/web/src/lib/components/filter-bar/filter-bar-types.ts new file mode 100644 index 000000000..2ac915d12 --- /dev/null +++ b/apps/web/src/lib/components/filter-bar/filter-bar-types.ts @@ -0,0 +1,18 @@ +import type { Snippet } from "svelte"; + +export interface FilterBarPill { + key: string; + /** Translated filter name shown on the pill and in the add filter menu. */ + name: string; + /** Translated current value shown on the pill. Null when the filter is at its default. */ + formattedValue: string | null; + /** Popover content. Inputs inside write search params directly (instant apply). */ + popover: Snippet; + /** Resets the pill's param(s) to defaults. Renders the remove button. */ + onRemove?: () => void; + /** Writes a starting value when the pill is added from the menu. */ + onAdd?: () => void; + icon?: Snippet; + popoverClass?: string; + testId?: string; +} diff --git a/apps/web/src/lib/components/layout/Layout.svelte b/apps/web/src/lib/components/layout/Layout.svelte index 93307d327..edd4e9bd5 100644 --- a/apps/web/src/lib/components/layout/Layout.svelte +++ b/apps/web/src/lib/components/layout/Layout.svelte @@ -6,6 +6,9 @@ import Image from "#lib/components/Image.svelte"; import NotificationDot from "#lib/components/NotificationDot.svelte"; import { loggedInUser } from "#lib/features/auth/user-state.ts"; import { getPatrons } from "#lib/features/front-page/front-page.remote.ts"; +import ChatSidebar from "#lib/features/chat/components/ChatSidebar.svelte"; +import { chatUi } from "#lib/features/chat/chat-state.svelte.ts"; +import { getChatRooms } from "#lib/features/chat/chat.remote.ts"; import { getNotifications } from "#lib/features/notifications/notifications.remote.ts"; import { UnseenNotificationsDot } from "#lib/features/notifications/notifications-state.svelte.ts"; import { toNotificationRows } from "#lib/features/notifications/notifications-utils.ts"; @@ -79,9 +82,18 @@ const unseenFriendRequests = $derived( const isFrontPage = $derived(page.url.pathname === "/"); -// xxx: wire to the chat sidebar when the chat rebuild lands (svelte-big-bang -// phase 3); until then the button renders for layout parity with the React app -const noopUntilChatRebuild = () => {}; +// chat data streams lazily after hydration, like the bell +const chatRoomsQuery = $derived( + mounted.current && user ? getChatRooms() : null, +); +const chatUnreadCount = $derived( + chatRoomsQuery?.current?.rooms.reduce( + (total, room) => total + room.unseenCount, + 0, + ) ?? 0, +); + +let chatModalOpen = $state(false); const breadcrumbs = $derived(page.data.breadcrumbs ?? []); const currentPageText = $derived(breadcrumbs.at(-1)?.text); @@ -155,9 +167,10 @@ function handleScroll() { lastScrollY = currentScrollY; } -// the tablet-only sidenav modal closes on navigation +// the tablet-only modals close on navigation afterNavigate(() => { sideNavModalOpen = false; + chatModalOpen = false; }); function showModal(dialog: HTMLDialogElement) { @@ -419,9 +432,15 @@ function formatRelativeDate(timestamp: number) { showSupport={Boolean(!user?.roles.includes("MINOR_SUPPORT"))} showSearch={Boolean(user)} isLoggedIn={Boolean(user)} - onChatToggle={user ? noopUntilChatRebuild : undefined} - onChatModalToggle={user ? noopUntilChatRebuild : undefined} - chatUnreadCount={0} + onChatToggle={user && !chatUi.sidebarOpen + ? () => chatUi.toggleSidebar() + : undefined} + onChatModalToggle={user + ? () => { + chatModalOpen = !chatModalOpen; + } + : undefined} + {chatUnreadCount} /> @@ -429,6 +448,34 @@ function formatRelativeDate(timestamp: number) {