From 0122a6c3f026c78de844a8bfeac47ba329acefd2 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:51:49 +0200 Subject: [PATCH] Progress --- app/components/MobileNav.tsx | 2 +- app/components/layout/ChatSidebar.tsx | 25 ++- app/components/layout/index.tsx | 7 +- app/db/seed/index.ts | 85 ++------ .../AssociationRepository.server.ts | 18 +- .../associations/associations-constants.ts | 2 +- .../associations/core/Association.test.ts | 57 ++++++ app/features/associations/core/Association.ts | 10 + app/features/calendar/routes/events.tsx | 4 +- app/features/chat/ChatProvider.tsx | 106 +++++++--- app/features/chat/chat-provider-types.ts | 3 + .../friends/FriendRepository.server.ts | 187 +++++++++++------- app/features/friends/friends-utils.server.ts | 4 +- .../friends/loaders/friends.server.ts | 14 +- app/features/friends/routes/friends.tsx | 24 +-- app/features/scrims/loaders/scrims.server.ts | 1 + app/features/scrims/routes/scrims.new.tsx | 4 +- .../sendouq-match/routes/q.match.$id.tsx | 6 +- .../components/ModeMapPoolPicker.module.css | 2 +- .../sendouq/components/GroupCard.module.css | 2 +- app/features/sendouq/components/GroupCard.tsx | 12 +- app/features/sendouq/q-constants.ts | 3 + app/features/sendouq/routes/q.looking.tsx | 15 +- app/features/sidebar/core/sidebar.server.ts | 14 +- .../components/LFGGroupCard.module.css | 2 +- .../components/LFGGroupCard.tsx | 114 ++++++----- .../tournament-lfg/routes/to.$id.looking.tsx | 11 +- .../queries/deleteSub.server.ts | 15 -- .../tournament/actions/to.$id.admin.server.ts | 11 +- .../actions/to.$id.register.server.ts | 2 - .../queries/joinLeaveTeam.server.ts | 8 +- ...seWindowSize.ts => useMainContentWidth.ts} | 33 +++- app/styles/common.css | 2 + app/styles/utils.css | 11 ++ locales/da/scrims.json | 1 + locales/de/scrims.json | 1 + locales/en/scrims.json | 1 + locales/es-ES/scrims.json | 1 + locales/es-US/scrims.json | 1 + locales/fr-CA/scrims.json | 1 + locales/fr-EU/scrims.json | 1 + locales/he/scrims.json | 1 + locales/it/scrims.json | 1 + locales/ja/scrims.json | 1 + locales/ko/scrims.json | 1 + locales/nl/scrims.json | 1 + locales/pl/scrims.json | 1 + locales/pt-BR/scrims.json | 1 + locales/ru/scrims.json | 1 + locales/zh/scrims.json | 1 + 50 files changed, 533 insertions(+), 299 deletions(-) delete mode 100644 app/features/tournament-subs/queries/deleteSub.server.ts rename app/hooks/{useWindowSize.ts => useMainContentWidth.ts} (67%) diff --git a/app/components/MobileNav.tsx b/app/components/MobileNav.tsx index 729124379..7b9a377a8 100644 --- a/app/components/MobileNav.tsx +++ b/app/components/MobileNav.tsx @@ -20,7 +20,7 @@ import { useUser } from "~/features/auth/core/user"; import { useChatContext } from "~/features/chat/useChatContext"; import { FriendMenu } from "~/features/friends/components/FriendMenu"; import { SENDOUQ_ACTIVITY_LABEL } from "~/features/friends/friends-constants"; -import { useLayoutSize } from "~/hooks/useWindowSize"; +import { useLayoutSize } from "~/hooks/useMainContentWidth"; import type { RootLoaderData } from "~/root"; import { EVENTS_PAGE, diff --git a/app/components/layout/ChatSidebar.tsx b/app/components/layout/ChatSidebar.tsx index fc41443ca..fec9d3716 100644 --- a/app/components/layout/ChatSidebar.tsx +++ b/app/components/layout/ChatSidebar.tsx @@ -62,9 +62,12 @@ function RoomList({ onClose }: { onClose?: () => void }) { const chatContext = useChatContext()!; const { formatDateTime } = useTimeFormat(); - const nonExpiredRooms = chatContext.rooms.filter( - (room) => room.expiresAt > Date.now(), - ); + const nonExpiredRooms = chatContext.rooms + .filter((room) => room.expiresAt > Date.now()) + .sort((a, b) => { + if (a.isObsolete !== b.isObsolete) return a.isObsolete ? 1 : -1; + return 0; + }); return (
@@ -81,7 +84,11 @@ function RoomList({ onClose }: { onClose?: () => void }) { return (
- {unread > 0 ? ( + {unread > 0 && !room.isObsolete ? ( {unread} ) : room.lastMessageTimestamp > 0 ? ( @@ -170,7 +178,12 @@ function ChatView({ onClose }: { onClose?: () => void }) { /> ) : null}
- + {resolveDatePlaceholders(room?.header ?? activeRoom, (d) => formatDateTime(d, { month: "short", diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx index 8a7c97158..e23b7a3d3 100644 --- a/app/components/layout/index.tsx +++ b/app/components/layout/index.tsx @@ -202,6 +202,12 @@ export function Layout({ return () => window.removeEventListener("resize", handleResize); }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally close modals on navigation + React.useEffect(() => { + setSideNavModalOpen(false); + setChatSidebarModalOpen(false); + }, [location.pathname]); + const user = useUser(); const { unseenIds } = useNotifications(); const sidebarData = data?.sidebar; @@ -316,7 +322,6 @@ export function Layout({ diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 2a49a50df..993f64346 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -56,7 +56,6 @@ import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes"; import { stagesObj as s, stageIds } from "~/modules/in-game-lists/stage-ids"; import type { AbilityType, - MainWeaponId, ModeShort, StageId, } from "~/modules/in-game-lists/types"; @@ -220,7 +219,6 @@ const basicSeeds = (variation?: SeedVariation | null) => [ calendarEventWithToToolsTeamsDepths, calendarEventWithToToolsLUTI, calendarEventWithToToolsTeamsLUTI, - tournamentSubs, variation === "NO_TOURNAMENT_TEAMS" ? undefined : tournamentLfgGroups, adminBuilds, manySplattershotBuilds, @@ -1565,80 +1563,19 @@ function calendarEventWithToToolsTeams( } } -function tournamentSubs() { - for (let id = 100; id < 120; id++) { - const includedWeaponIds: MainWeaponId[] = []; - - sql - .prepare( - /* sql */ ` - insert into "TournamentSub" ( - "userId", - "tournamentId", - "canVc", - "bestWeapons", - "okWeapons", - "message", - "visibility" - ) values ( - @userId, - @tournamentId, - @canVc, - @bestWeapons, - @okWeapons, - @message, - @visibility - ) - `, - ) - .run({ - userId: id, - tournamentId: 1, - canVc: Number(faker.number.float(1) > 0.5), - bestWeapons: nullFilledArray( - faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5]), - ) - // biome-ignore lint/suspicious/useIterableCallbackReturn: Biome 2.3.1 upgrade - .map(() => { - while (true) { - const weaponId = R.sample(mainWeaponIds, 1)[0]!; - if (!includedWeaponIds.includes(weaponId)) { - includedWeaponIds.push(weaponId); - return weaponId; - } - } - }) - .join(","), - okWeapons: - faker.number.float(1) > 0.5 - ? null - : nullFilledArray( - faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5]), - ) - // biome-ignore lint/suspicious/useIterableCallbackReturn: Biome 2.3.1 upgrade - .map(() => { - while (true) { - const weaponId = R.sample(mainWeaponIds, 1)[0]!; - if (!includedWeaponIds.includes(weaponId)) { - includedWeaponIds.push(weaponId); - return weaponId; - } - } - }) - .join(","), - message: faker.number.float(1) > 0.5 ? null : faker.lorem.paragraph(), - visibility: id < 105 ? "+1" : id < 110 ? "+2" : id < 115 ? "+2" : "ALL", - }); - } - - return null; -} - async function tournamentLfgGroups() { const availableUsers = userIdsInAscendingOrderById().slice(300); const MAX_GROUP_SIZE = 6; + // Add admin's friends to tournament LFG so sidebar shows tournament friends + for (const friendId of SENDOU_FRIEND_IDS_IN_TOURNAMENT_LFG) { + await TournamentLFGRepository.createPlaceholderTeam({ + tournamentId: 1, + userId: friendId, + }); + } + const tournaments = [1, 2, 3]; let userIndex = 0; @@ -2895,12 +2832,14 @@ async function organization() { } const SENDOU_FRIEND_IDS_IN_LOOKING_GROUPS = [150, 151, 152, 153]; -const SENDOU_FRIEND_IDS_AS_TOURNAMENT_SUBS = [100, 101, 102, 103]; +const SENDOU_FRIEND_IDS_IN_TOURNAMENT_LFG = [100, 101]; +const SENDOU_FRIEND_IDS_OTHER = [102, 103]; async function friendships(variation?: SeedVariation | null) { const allFriendIds = [ ...SENDOU_FRIEND_IDS_IN_LOOKING_GROUPS, - ...SENDOU_FRIEND_IDS_AS_TOURNAMENT_SUBS, + ...SENDOU_FRIEND_IDS_IN_TOURNAMENT_LFG, + ...SENDOU_FRIEND_IDS_OTHER, ]; for (const friendId of allFriendIds) { diff --git a/app/features/associations/AssociationRepository.server.ts b/app/features/associations/AssociationRepository.server.ts index 5dd0b5095..b9d121195 100644 --- a/app/features/associations/AssociationRepository.server.ts +++ b/app/features/associations/AssociationRepository.server.ts @@ -3,6 +3,7 @@ import { db } from "~/db/sql"; import type { TablesInsertable } from "~/db/tables"; import type { AssociationVirtualIdentifier } from "~/features/associations/associations-constants"; import { ASSOCIATION } from "~/features/associations/associations-constants"; +import * as FriendRepository from "~/features/friends/FriendRepository.server"; import { LimitReachedError } from "~/utils/errors"; import { shortNanoid } from "~/utils/id"; import { COMMON_USER_FIELDS } from "~/utils/kysely.server"; @@ -28,6 +29,7 @@ export async function findByMemberUserId( return { actual: await findBy({ type: "user", userId }, options), virtual: await virtualAssociationsByUserId(userId), + friendIds: await FriendRepository.findFriendIds(userId), }; } @@ -94,6 +96,10 @@ async function findBy( })); } +const DEFAULT_VIRTUAL_ASSOCIATIONS: Array = [ + "FRIENDS", +]; + async function virtualAssociationsByUserId( userId: number, ): Promise> { @@ -103,14 +109,16 @@ async function virtualAssociationsByUserId( .select(["PlusTier.tier as plusTier"]) .where("userId", "=", userId) .executeTakeFirst()) ?? {}; - if (!plusTier) return []; + if (!plusTier) return [...DEFAULT_VIRTUAL_ASSOCIATIONS]; - if (plusTier === 1) return ["+1", "+2", "+3"] as const; - if (plusTier === 2) return ["+2", "+3"] as const; - if (plusTier === 3) return ["+3"] as const; + if (plusTier === 1) + return [...DEFAULT_VIRTUAL_ASSOCIATIONS, "+1", "+2", "+3"] as const; + if (plusTier === 2) + return [...DEFAULT_VIRTUAL_ASSOCIATIONS, "+2", "+3"] as const; + if (plusTier === 3) return [...DEFAULT_VIRTUAL_ASSOCIATIONS, "+3"] as const; logger.error("Invalid plusTier", { plusTier }); - return []; + return [...DEFAULT_VIRTUAL_ASSOCIATIONS]; } type InsertArgs = Omit & { diff --git a/app/features/associations/associations-constants.ts b/app/features/associations/associations-constants.ts index bf3bc6dad..363b32a49 100644 --- a/app/features/associations/associations-constants.ts +++ b/app/features/associations/associations-constants.ts @@ -1,5 +1,5 @@ export const ASSOCIATION = { - VIRTUAL_IDENTIFIERS: ["+1", "+2", "+3"] as const, + VIRTUAL_IDENTIFIERS: ["+1", "+2", "+3", "FRIENDS"] as const, MAX_COUNT_REGULAR_USER: 3, MAX_COUNT_SUPPORTER: 6, MAX_ASSOCIATION_MEMBER_COUNT: 300, diff --git a/app/features/associations/core/Association.test.ts b/app/features/associations/core/Association.test.ts index 288b259f1..3919056a3 100644 --- a/app/features/associations/core/Association.test.ts +++ b/app/features/associations/core/Association.test.ts @@ -103,6 +103,63 @@ describe("isVisible", () => { expect(Association.isVisible(args)).toBe(true); }); + it("should return true if viewer is a friend of the content owner", () => { + const args: Association.IsVisibleArgs = { + visibility: { forAssociation: "FRIENDS" }, + time: new Date(), + associations: { + actual: [], + virtual: [], + friendIds: [42], + }, + contentOwnerUserId: 42, + }; + expect(Association.isVisible(args)).toBe(true); + }); + + it("should return false if viewer is not a friend of the content owner", () => { + const args: Association.IsVisibleArgs = { + visibility: { forAssociation: "FRIENDS" }, + time: new Date(), + associations: { + actual: [], + virtual: [], + friendIds: [99], + }, + contentOwnerUserId: 42, + }; + expect(Association.isVisible(args)).toBe(false); + }); + + it("should return false for FRIENDS visibility when not logged in", () => { + const args: Association.IsVisibleArgs = { + visibility: { forAssociation: "FRIENDS" }, + time: new Date(), + associations: null, + }; + expect(Association.isVisible(args)).toBe(false); + }); + + it("should return true when FRIENDS visibility becomes public via notFoundInstructions", () => { + const visibleAt = add(new Date(), { days: 1 }); + + const args: Association.IsVisibleArgs = { + visibility: { + forAssociation: "FRIENDS", + notFoundInstructions: [ + { at: dateToDatabaseTimestamp(visibleAt), forAssociation: null }, + ], + }, + time: add(new Date(), { days: 2 }), + associations: { + actual: [], + virtual: [], + friendIds: [], + }, + }; + expect(Association.isVisible(args)).toBe(true); + }); + it("should return true if has become public (no associations)", () => { const visibleAt = add(new Date(), { days: 1 }); diff --git a/app/features/associations/core/Association.ts b/app/features/associations/core/Association.ts index cf684e13d..2d2ef3bac 100644 --- a/app/features/associations/core/Association.ts +++ b/app/features/associations/core/Association.ts @@ -8,7 +8,9 @@ export interface IsVisibleArgs { associations: { virtual: Array; actual: Array<{ id: number }>; + friendIds?: Array; } | null; + contentOwnerUserId?: number; } export function isVisible(args: IsVisibleArgs) { @@ -29,6 +31,14 @@ export function isVisible(args: IsVisibleArgs) { if (isPublic) return true; + if ( + currentVisibility.includes("FRIENDS") && + args.contentOwnerUserId && + args.associations?.friendIds?.includes(args.contentOwnerUserId) + ) { + return true; + } + return ( args.associations?.actual.some((association) => currentVisibility.includes(association.id), diff --git a/app/features/calendar/routes/events.tsx b/app/features/calendar/routes/events.tsx index 2f9b215ce..36923be08 100644 --- a/app/features/calendar/routes/events.tsx +++ b/app/features/calendar/routes/events.tsx @@ -72,12 +72,12 @@ export default function EventsPage() { )}
{hasNoEventsAtAll ? ( -

+

{t("calendar:events.emptyAll")}{" "} {t("calendar:events.findOnCalendar")}

) : shownEvents.length === 0 ? ( -

{t("calendar:events.empty")}

+

{t("calendar:events.empty")}

) : ( )} diff --git a/app/features/chat/ChatProvider.tsx b/app/features/chat/ChatProvider.tsx index 10b9edad5..d9dec87ed 100644 --- a/app/features/chat/ChatProvider.tsx +++ b/app/features/chat/ChatProvider.tsx @@ -32,9 +32,49 @@ function flattenServerRoom(serverRoom: ServerRoomInfo): RoomInfo { expiresAt: serverRoom.metadata.expiresAt, lastMessageTimestamp: serverRoom.lastMessageTimestamp ?? 0, totalMessageCount: serverRoom.totalMessageCount, + createdAt: serverRoom.metadata.createdAt ?? 0, }; } +const SENDOUQ_GROUP_HEADER_PATTERN = /^Group\b/; + +function isSendouQGroupRoom(room: RoomInfo) { + return ( + room.subtitle === "SendouQ" && + SENDOUQ_GROUP_HEADER_PATTERN.test(room.header) + ); +} + +function resolveObsoleteGroupRooms(rooms: RoomInfo[]): RoomInfo[] { + const groupRooms = rooms.filter(isSendouQGroupRoom); + + if (groupRooms.length <= 1) return rooms; + + const newestCreatedAt = Math.max(...groupRooms.map((r) => r.createdAt)); + + // If no room has createdAt set, skip resolution + if (newestCreatedAt === 0) return rooms; + + const obsoleteChatCodes = new Set(); + const removedChatCodes = new Set(); + + for (const room of groupRooms) { + if (room.createdAt === newestCreatedAt) continue; + + if (room.totalMessageCount === 0) { + removedChatCodes.add(room.chatCode); + } else { + obsoleteChatCodes.add(room.chatCode); + } + } + + return rooms + .filter((r) => !removedChatCodes.has(r.chatCode)) + .map((r) => + obsoleteChatCodes.has(r.chatCode) ? { ...r, isObsolete: true } : r, + ); +} + function localStorageKey(chatCode: string) { return `${LOCAL_STORAGE_PREFIX}${chatCode}`; } @@ -116,7 +156,9 @@ function ChatProviderInner({ if (parsed.rooms && Array.isArray(parsed.rooms)) { logger.debug("WS initial rooms payload, count:", parsed.rooms.length); const serverRooms = parsed.rooms as ServerRoomInfo[]; - const roomList = serverRooms.map(flattenServerRoom); + const roomList = resolveObsoleteGroupRooms( + serverRooms.map(flattenServerRoom), + ); setRooms(roomList); setIsLoading(false); @@ -157,13 +199,20 @@ function ChatProviderInner({ setRooms((prev) => { const exists = prev.some((r) => r.chatCode === newRoom.chatCode); if (exists) return prev; - return [...prev, newRoom]; + return resolveObsoleteGroupRooms([...prev, newRoom]); }); setChatUsersCache((prev) => ({ ...prev, ...serverRoom.metadata.chatUsers, })); + + const lastRead = readLastReadCount(serverRoom.chatCode); + const unread = Math.max(0, serverRoom.totalMessageCount - lastRead); + setUnreadCounts((prev) => ({ + ...prev, + [serverRoom.chatCode]: unread, + })); return; } @@ -194,6 +243,7 @@ function ChatProviderInner({ expiresAt: metadata.expiresAt, lastMessageTimestamp: messages.at(-1)?.timestamp ?? 0, totalMessageCount: messages.length, + createdAt: metadata.createdAt ?? 0, }; setRooms((prev) => { const exists = prev.some((r) => r.chatCode === chatCode); @@ -212,7 +262,7 @@ function ChatProviderInner({ // Regular message(s) const messageArr = ( Array.isArray(parsed) ? parsed : [parsed] - ) as (ChatMessage & { totalMessageCount?: number })[]; + ) as (ChatMessage & { totalMessageCount: number })[]; const isSystemMessage = Boolean(messageArr[0].type); logger.debug( @@ -253,30 +303,31 @@ function ChatProviderInner({ return { ...prev, [roomCode]: [...existing, msg] }; }); - if (msg.totalMessageCount) { - setRooms((prev) => - prev.map((r) => - r.chatCode === roomCode - ? { - ...r, - lastMessageTimestamp: msg.timestamp, - totalMessageCount: Math.max( - r.totalMessageCount, - msg.totalMessageCount!, - ), - } - : r, - ), - ); + setRooms((prev) => + prev.map((r) => + r.chatCode === roomCode + ? { + ...r, + lastMessageTimestamp: msg.timestamp, + totalMessageCount: Math.max( + r.totalMessageCount, + msg.totalMessageCount!, + ), + } + : r, + ), + ); - if (roomCode === activeRoom && chatOpen) { - writeLastReadCount(roomCode, msg.totalMessageCount); - } else { - setUnreadCounts((prev) => ({ - ...prev, - [roomCode]: (prev[roomCode] ?? 0) + 1, - })); - } + if (roomCode === activeRoom && chatOpen) { + writeLastReadCount(roomCode, msg.totalMessageCount); + } else { + setUnreadCounts((prev) => ({ + ...prev, + [roomCode]: Math.max( + 0, + msg.totalMessageCount - readLastReadCount(roomCode), + ), + })); } } }); @@ -323,7 +374,8 @@ function ChatProviderInner({ const handleStorage = (e: StorageEvent) => { if (!e.key?.startsWith(LOCAL_STORAGE_PREFIX)) return; const chatCode = e.key.slice(LOCAL_STORAGE_PREFIX.length); - const newCount = e.newValue ? Number(e.newValue) : 0; + const parsed = Number(e.newValue); + const newCount = Number.isFinite(parsed) ? parsed : 0; setUnreadCounts((prev) => { const room = rooms.find((r) => r.chatCode === chatCode); diff --git a/app/features/chat/chat-provider-types.ts b/app/features/chat/chat-provider-types.ts index b132ace83..59c3a5cd4 100644 --- a/app/features/chat/chat-provider-types.ts +++ b/app/features/chat/chat-provider-types.ts @@ -9,6 +9,7 @@ export interface RoomMetadata { subtitle?: string; url?: string; imageUrl?: string; + createdAt?: number; } /** Room info as returned by Skalop on connect and ROOM_JOINED events */ @@ -30,6 +31,8 @@ export interface RoomInfo { expiresAt: number; lastMessageTimestamp: number; totalMessageCount: number; + createdAt: number; + isObsolete?: boolean; } export interface ChatContextValue { diff --git a/app/features/friends/FriendRepository.server.ts b/app/features/friends/FriendRepository.server.ts index 093722dde..08197386a 100644 --- a/app/features/friends/FriendRepository.server.ts +++ b/app/features/friends/FriendRepository.server.ts @@ -1,87 +1,52 @@ +import { type SelectQueryBuilder, sql } from "kysely"; import { db } from "~/db/sql"; +import type { DB } from "~/db/tables"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; +import { COMMON_USER_FIELDS } from "~/utils/kysely.server"; export async function findByUserIdWithActivity(userId: number) { const [friendRows, teamMemberRows] = await Promise.all([ - db - .selectFrom("Friendship") - .innerJoin("User", (join) => - join.on((eb) => + withLfgJoins( + db + .selectFrom("Friendship") + .innerJoin("User", (join) => + join.on((eb) => + eb.or([ + eb.and([ + eb("Friendship.userOneId", "=", userId), + eb("User.id", "=", eb.ref("Friendship.userTwoId")), + ]), + eb.and([ + eb("Friendship.userTwoId", "=", userId), + eb("User.id", "=", eb.ref("Friendship.userOneId")), + ]), + ]), + ), + ) + .where((eb) => eb.or([ - eb.and([ - eb("Friendship.userOneId", "=", userId), - eb("User.id", "=", eb.ref("Friendship.userTwoId")), - ]), - eb.and([ - eb("Friendship.userTwoId", "=", userId), - eb("User.id", "=", eb.ref("Friendship.userOneId")), - ]), + eb("Friendship.userOneId", "=", userId), + eb("Friendship.userTwoId", "=", userId), ]), ), - ) - .leftJoin("TournamentSub", "TournamentSub.userId", "User.id") - .leftJoin( - "CalendarEvent", - "CalendarEvent.tournamentId", - "TournamentSub.tournamentId", - ) - .leftJoin( - "CalendarEventDate", - "CalendarEventDate.eventId", - "CalendarEvent.id", - ) + ) .select([ "Friendship.id as friendshipId", - "User.id", - "User.username", - "User.discordId", - "User.discordAvatar", - "User.customUrl", - "CalendarEvent.name as tournamentName", - "TournamentSub.tournamentId", - "CalendarEventDate.startTime as tournamentStartTime", "Friendship.createdAt as friendshipCreatedAt", ]) - .where((eb) => - eb.or([ - eb("Friendship.userOneId", "=", userId), - eb("Friendship.userTwoId", "=", userId), - ]), - ) .orderBy("Friendship.createdAt", "desc") .execute(), - db - .selectFrom("TeamMemberWithSecondary as myMembership") - .innerJoin("TeamMemberWithSecondary as otherMembership", (join) => - join - .onRef("otherMembership.teamId", "=", "myMembership.teamId") - .on("otherMembership.userId", "!=", userId), - ) - .innerJoin("User", "User.id", "otherMembership.userId") - .leftJoin("TournamentSub", "TournamentSub.userId", "User.id") - .leftJoin( - "CalendarEvent", - "CalendarEvent.tournamentId", - "TournamentSub.tournamentId", - ) - .leftJoin( - "CalendarEventDate", - "CalendarEventDate.eventId", - "CalendarEvent.id", - ) - .select([ - "User.id", - "User.username", - "User.discordId", - "User.discordAvatar", - "User.customUrl", - "CalendarEvent.name as tournamentName", - "TournamentSub.tournamentId", - "CalendarEventDate.startTime as tournamentStartTime", - ]) - .where("myMembership.userId", "=", userId) - .where("myMembership.leftAt", "is", null) - .where("otherMembership.leftAt", "is", null) - .execute(), + withLfgJoins( + db + .selectFrom("TeamMemberWithSecondary as myMembership") + .innerJoin("TeamMemberWithSecondary as otherMembership", (join) => + join + .onRef("otherMembership.teamId", "=", "myMembership.teamId") + .on("otherMembership.userId", "!=", userId), + ) + .innerJoin("User", "User.id", "otherMembership.userId") + .where("myMembership.userId", "=", userId), + ).execute(), ]); return [ @@ -94,6 +59,63 @@ export async function findByUserIdWithActivity(userId: number) { ]; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function withLfgJoins>(qb: QB) { + const nowTimestamp = dateToDatabaseTimestamp(new Date()); + + return (qb as SelectQueryBuilder>) + .leftJoin("TournamentTeamMember", (join) => + join + .onRef("TournamentTeamMember.userId", "=", "User.id") + .on("TournamentTeamMember.isLooking", "=", 1), + ) + .leftJoin( + "TournamentTeam", + "TournamentTeam.id", + "TournamentTeamMember.tournamentTeamId", + ) + .leftJoin("Tournament", (join) => + join + .onRef("Tournament.id", "=", "TournamentTeam.tournamentId") + .on((eb) => + eb.or([ + eb( + sql`json_extract("Tournament"."settings", '$.regClosesAt')`, + "is", + null, + ), + eb( + sql`json_extract("Tournament"."settings", '$.regClosesAt')`, + ">", + nowTimestamp, + ), + ]), + ), + ) + .leftJoin("CalendarEvent", "CalendarEvent.tournamentId", "Tournament.id") + .leftJoin( + "CalendarEventDate", + "CalendarEventDate.eventId", + "CalendarEvent.id", + ) + .select([ + ...COMMON_USER_FIELDS, + "CalendarEvent.name as tournamentName", + "TournamentTeam.tournamentId", + "CalendarEventDate.startTime as tournamentStartTime", + sql< + number | null + >`(SELECT COUNT(*) FROM "TournamentTeamMember" "ttm" WHERE "ttm"."tournamentTeamId" = "TournamentTeam"."id")`.as( + "teamMemberCount", + ), + sql< + number | null + >`json_extract("Tournament"."settings", '$.minMembersPerTeam')`.as( + "tournamentMinTeamSize", + ), + ]); +} + export async function findPendingSentRequests(senderId: number) { return db .selectFrom("FriendRequest") @@ -333,6 +355,29 @@ export async function countPendingSentRequests( return result.count; } +export async function findFriendIds(userId: number): Promise { + const rows = await db + .selectFrom("Friendship") + .select((eb) => + eb + .case() + .when("Friendship.userOneId", "=", userId) + .then(eb.ref("Friendship.userTwoId")) + .else(eb.ref("Friendship.userOneId")) + .end() + .as("friendId"), + ) + .where((eb) => + eb.or([ + eb("Friendship.userOneId", "=", userId), + eb("Friendship.userTwoId", "=", userId), + ]), + ) + .execute(); + + return rows.map((row) => row.friendId); +} + export async function findFriendship({ userOneId, userTwoId, diff --git a/app/features/friends/friends-utils.server.ts b/app/features/friends/friends-utils.server.ts index 7f882ffd9..df1c99212 100644 --- a/app/features/friends/friends-utils.server.ts +++ b/app/features/friends/friends-utils.server.ts @@ -5,6 +5,8 @@ import { SENDOUQ_ACTIVITY_LABEL } from "./friends-constants"; export function resolveFriendActivity( friendId: number, tournamentName: string | null, + teamMemberCount: number | null, + tournamentMinTeamSize: number | null, ) { const ownGroup = SendouQ.findOwnGroup(friendId); @@ -18,7 +20,7 @@ export function resolveFriendActivity( if (tournamentName) { return { subtitle: tournamentName, - badge: `1/${FULL_GROUP_SIZE}`, + badge: `${teamMemberCount ?? 1}/${tournamentMinTeamSize ?? FULL_GROUP_SIZE}`, }; } diff --git a/app/features/friends/loaders/friends.server.ts b/app/features/friends/loaders/friends.server.ts index 041723a44..d67a68e38 100644 --- a/app/features/friends/loaders/friends.server.ts +++ b/app/features/friends/loaders/friends.server.ts @@ -21,7 +21,12 @@ export const loader = async () => { const friends = unique .filter((f) => f.friendshipId !== null) .map((friend) => { - const activity = resolveFriendActivity(friend.id, friend.tournamentName); + const activity = resolveFriendActivity( + friend.id, + friend.tournamentName, + friend.teamMemberCount, + friend.tournamentMinTeamSize, + ); return { id: friend.id, @@ -51,7 +56,12 @@ export const loader = async () => { const teamMembers = unique .filter((f) => f.friendshipId === null) .map((tm) => { - const activity = resolveFriendActivity(tm.id, tm.tournamentName); + const activity = resolveFriendActivity( + tm.id, + tm.tournamentName, + tm.teamMemberCount, + tm.tournamentMinTeamSize, + ); return { id: tm.id, diff --git a/app/features/friends/routes/friends.tsx b/app/features/friends/routes/friends.tsx index 600bc2970..a67c1803b 100644 --- a/app/features/friends/routes/friends.tsx +++ b/app/features/friends/routes/friends.tsx @@ -170,9 +170,9 @@ function FriendsListSection() { : "friends:friendsList.empty"; return ( -
+
-

{t("friends:friendsList.title")}

+

{t("friends:friendsList.title")}

{VIEW_FILTERS.map((value) => (
- {shownItems.length === 0 ? ( -

{t(emptyKey)}

- ) : ( -
- {shownItems.map((item) => ( - - ))} -
- )} +
+ {shownItems.length === 0 ? ( +

{t(emptyKey)}

+ ) : ( +
+ {shownItems.map((item) => ( + + ))} +
+ )} +
); } diff --git a/app/features/scrims/loaders/scrims.server.ts b/app/features/scrims/loaders/scrims.server.ts index 0bf599868..a18bf8817 100644 --- a/app/features/scrims/loaders/scrims.server.ts +++ b/app/features/scrims/loaders/scrims.server.ts @@ -34,6 +34,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { associations, time: now, visibility: post.visibility, + contentOwnerUserId: post.users.find((u) => u.isOwner)?.id, }), ) .map((post) => ({ diff --git a/app/features/scrims/routes/scrims.new.tsx b/app/features/scrims/routes/scrims.new.tsx index d09104a05..8787359ff 100644 --- a/app/features/scrims/routes/scrims.new.tsx +++ b/app/features/scrims/routes/scrims.new.tsx @@ -257,7 +257,9 @@ function AssociationSelect({ {associations.virtual.map((association) => ( ))} {associations.actual.map((association) => ( diff --git a/app/features/sendouq-match/routes/q.match.$id.tsx b/app/features/sendouq-match/routes/q.match.$id.tsx index e7c9b2cf5..6fce94cf9 100644 --- a/app/features/sendouq-match/routes/q.match.$id.tsx +++ b/app/features/sendouq-match/routes/q.match.$id.tsx @@ -34,8 +34,8 @@ import { AddPrivateNoteDialog } from "~/features/sendouq-match/components/AddPri import type { ReportedWeaponForMerging } from "~/features/sendouq-match/core/reported-weapons.server"; import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useMainContentWidth } from "~/hooks/useMainContentWidth"; import { useTimeFormat } from "~/hooks/useTimeFormat"; -import { useWindowSize } from "~/hooks/useWindowSize"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids"; import { useHasRole } from "~/modules/permissions/hooks"; @@ -650,8 +650,8 @@ function BottomSection({ participatingInTheMatch: boolean; }) { const { t } = useTranslation(["q", "common"]); - const { width } = useWindowSize(); - const isMobile = width < 750; + const width = useMainContentWidth(); + const isMobile = width < 650; const isMounted = useIsMounted(); const user = useUser(); const isStaff = useHasRole("STAFF"); diff --git a/app/features/sendouq-settings/components/ModeMapPoolPicker.module.css b/app/features/sendouq-settings/components/ModeMapPoolPicker.module.css index 7050261f6..332d90140 100644 --- a/app/features/sendouq-settings/components/ModeMapPoolPicker.module.css +++ b/app/features/sendouq-settings/components/ModeMapPoolPicker.module.css @@ -27,7 +27,7 @@ .mapButton { background-image: var(--map-image-url); - background-size: contain; + background-size: cover; height: var(--map-height); width: var(--map-width); border: none; diff --git a/app/features/sendouq/components/GroupCard.module.css b/app/features/sendouq/components/GroupCard.module.css index d2d63d2ae..a30c90c6f 100644 --- a/app/features/sendouq/components/GroupCard.module.css +++ b/app/features/sendouq/components/GroupCard.module.css @@ -25,7 +25,7 @@ gap: var(--s-2); align-items: center; background-color: var(--color-bg); - border-radius: var(--radius-avatar); + border-radius: calc(var(--radius-box) + 6px); font-size: var(--font-sm); font-weight: var(--weight-semi); padding-inline-end: var(--s-3); diff --git a/app/features/sendouq/components/GroupCard.tsx b/app/features/sendouq/components/GroupCard.tsx index 9317615a6..0d1fcc02b 100644 --- a/app/features/sendouq/components/GroupCard.tsx +++ b/app/features/sendouq/components/GroupCard.tsx @@ -16,6 +16,7 @@ import { useUser } from "~/features/auth/core/user"; import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "~/features/leaderboards/leaderboards-constants"; import { ordinalToRoundedSp } from "~/features/mmr/mmr-utils"; import type { TieredSkill } from "~/features/mmr/tiered.server"; +import { useMainContentWidth } from "~/hooks/useMainContentWidth"; import { useTimeFormat } from "~/hooks/useTimeFormat"; import { languagesUnified } from "~/modules/i18n/config"; import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids"; @@ -36,7 +37,11 @@ import type { SQMatchGroupMember, SQOwnGroup, } from "../core/SendouQ.server"; -import { FULL_GROUP_SIZE, SENDOUQ } from "../q-constants"; +import { + FULL_GROUP_SIZE, + IS_Q_LOOKING_MOBILE_BREAKPOINT, + SENDOUQ, +} from "../q-constants"; import { resolveFutureMatchModes } from "../q-utils"; import styles from "./GroupCard.module.css"; @@ -248,10 +253,13 @@ function GroupCardContainer({ groupId: number; children: React.ReactNode; }) { + const width = useMainContentWidth(); + const layout = width < IS_Q_LOOKING_MOBILE_BREAKPOINT ? "mobile" : "desktop"; + // we don't want it to animate if (isOwnGroup) return <>{children}; - return {children}; + return {children}; } function GroupMember({ diff --git a/app/features/sendouq/q-constants.ts b/app/features/sendouq/q-constants.ts index def6ce44f..b2744b353 100644 --- a/app/features/sendouq/q-constants.ts +++ b/app/features/sendouq/q-constants.ts @@ -17,3 +17,6 @@ export const SENDOUQ_BEST_OF = 7; export const JOIN_CODE_SEARCH_PARAM_KEY = "join"; export const USER_SKILLS_CACHE_KEY = "user-skills"; + +/** Breakpoint (in px) for the main container after which we switch from 3-column layout to tabs */ +export const IS_Q_LOOKING_MOBILE_BREAKPOINT = 700; diff --git a/app/features/sendouq/routes/q.looking.tsx b/app/features/sendouq/routes/q.looking.tsx index 9b1953e05..1ce7f7717 100644 --- a/app/features/sendouq/routes/q.looking.tsx +++ b/app/features/sendouq/routes/q.looking.tsx @@ -19,8 +19,8 @@ import { SubmitButton } from "~/components/SubmitButton"; import { useUser } from "~/features/auth/core/user"; import { useAutoRefresh } from "~/hooks/useAutoRefresh"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useMainContentWidth } from "~/hooks/useMainContentWidth"; import { useTimeFormat } from "~/hooks/useTimeFormat"; -import { useWindowSize } from "~/hooks/useWindowSize"; import { metaTags } from "~/utils/remix"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { @@ -36,7 +36,10 @@ import { GroupLeaver } from "../components/GroupLeaver"; import { MemberAdder } from "../components/MemberAdder"; import { groupExpiryStatus } from "../core/groups"; import { loader } from "../loaders/q.looking.server"; -import { FULL_GROUP_SIZE } from "../q-constants"; +import { + FULL_GROUP_SIZE, + IS_Q_LOOKING_MOBILE_BREAKPOINT, +} from "../q-constants"; export { action, loader }; import styles from "./q.looking.module.css"; @@ -209,11 +212,11 @@ function Groups() { const data = useLoaderData(); const isMounted = useIsMounted(); - const { width } = useWindowSize(); + const width = useMainContentWidth(); if (!isMounted) return null; - const isMobile = width < 750; + const isMobile = width < IS_Q_LOOKING_MOBILE_BREAKPOINT; const isFullGroup = data.ownGroup && data.ownGroup.members.length === FULL_GROUP_SIZE; @@ -410,9 +413,9 @@ function Groups() { } function ColumnHeader({ children }: { children: React.ReactNode }) { - const { width } = useWindowSize(); + const width = useMainContentWidth(); - const isMobile = width < 750; + const isMobile = width < IS_Q_LOOKING_MOBILE_BREAKPOINT; if (isMobile) return null; diff --git a/app/features/sidebar/core/sidebar.server.ts b/app/features/sidebar/core/sidebar.server.ts index ed5ebe274..86c6432d8 100644 --- a/app/features/sidebar/core/sidebar.server.ts +++ b/app/features/sidebar/core/sidebar.server.ts @@ -248,7 +248,12 @@ function resolveFriends(friendsWithActivity: FriendWithActivity[]) { const inactiveFriends: FriendWithActivity[] = []; for (const friend of friendRows) { - const activity = resolveFriendActivity(friend.id, friend.tournamentName); + const activity = resolveFriendActivity( + friend.id, + friend.tournamentName, + friend.teamMemberCount, + friend.tournamentMinTeamSize, + ); if (!activity.subtitle) { inactiveFriends.push(friend); @@ -291,7 +296,12 @@ function resolveFriends(friendsWithActivity: FriendWithActivity[]) { if (result.length >= MAX_FRIENDS_VISIBLE) break; if (shownIds.has(tm.id)) continue; - const activity = resolveFriendActivity(tm.id, tm.tournamentName); + const activity = resolveFriendActivity( + tm.id, + tm.tournamentName, + tm.teamMemberCount, + tm.tournamentMinTeamSize, + ); if (!activity.subtitle) { inactiveTeamMembers.push(tm); continue; diff --git a/app/features/tournament-lfg/components/LFGGroupCard.module.css b/app/features/tournament-lfg/components/LFGGroupCard.module.css index 36df807e3..150c0ac1a 100644 --- a/app/features/tournament-lfg/components/LFGGroupCard.module.css +++ b/app/features/tournament-lfg/components/LFGGroupCard.module.css @@ -15,7 +15,7 @@ gap: var(--s-2); align-items: center; background-color: var(--color-bg); - border-radius: var(--radius-box); + border-radius: calc(var(--radius-box) + 6px); font-size: var(--font-sm); font-weight: var(--weight-semi); padding-inline-end: var(--s-3); diff --git a/app/features/tournament-lfg/components/LFGGroupCard.tsx b/app/features/tournament-lfg/components/LFGGroupCard.tsx index 29f2d2588..e2ca2d571 100644 --- a/app/features/tournament-lfg/components/LFGGroupCard.tsx +++ b/app/features/tournament-lfg/components/LFGGroupCard.tsx @@ -1,6 +1,7 @@ import clsx from "clsx"; import { Mic, Star, Volume2, VolumeX } from "lucide-react"; import * as React from "react"; +import { Flipped } from "react-flip-toolkit"; import { useTranslation } from "react-i18next"; import { Link, useFetcher } from "react-router"; import { Avatar } from "~/components/Avatar"; @@ -11,7 +12,9 @@ import { Image, WeaponImage } from "~/components/Image"; import { SubmitButton } from "~/components/SubmitButton"; import type { Pronouns } from "~/db/tables"; import { useUser } from "~/features/auth/core/user"; +import { IS_Q_LOOKING_MOBILE_BREAKPOINT } from "~/features/sendouq/q-constants"; import { SendouForm } from "~/form/SendouForm"; +import { useMainContentWidth } from "~/hooks/useMainContentWidth"; import { languagesUnified } from "~/modules/i18n/config"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { navIconUrl, userPage } from "~/utils/urls"; @@ -64,54 +67,74 @@ export function LFGGroupCard({ : undefined; return ( -
- {group.teamName ? ( - - {group.teamAvatarUrl ? ( - - ) : null} -
{group.teamName}
-
- ) : null} -
- {group.members.map((member) => ( - +
+ {group.teamName ? ( + + {group.teamAvatarUrl ? ( + + ) : null} +
{group.teamName}
+
+ ) : null} +
+ {group.members.map((member) => ( + + ))} +
+ {isOwnGroup ? ( + - ))} -
- {isOwnGroup ? ( - - ) : null} - {action && - (ownGroup?.usersRole === "OWNER" || ownGroup?.usersRole === "MANAGER") ? ( - - - - {action === "LIKE" - ? t("q:looking.groups.actions.invite") - : action === "ACCEPT" - ? t("common:actions.accept") - : t("q:looking.groups.actions.undo")} - - - ) : null} -
+ ) : null} + {action && + (ownGroup?.usersRole === "OWNER" || + ownGroup?.usersRole === "MANAGER") ? ( + + + + {action === "LIKE" + ? t("q:looking.groups.actions.invite") + : action === "ACCEPT" + ? t("common:actions.accept") + : t("q:looking.groups.actions.undo")} + + + ) : null} +
+ ); } +function LFGGroupCardContainer({ + isOwnGroup, + groupId, + children, +}: { + isOwnGroup: boolean; + groupId: number; + children: React.ReactNode; +}) { + const width = useMainContentWidth(); + const layout = width < IS_Q_LOOKING_MOBILE_BREAKPOINT ? "mobile" : "desktop"; + + if (isOwnGroup) return <>{children}; + + return {children}; +} + function LFGGroupMemberRow({ member, showActions, @@ -284,6 +307,7 @@ function LFGMemberRoleManager({ trigger={ } /> } diff --git a/app/features/tournament-lfg/routes/to.$id.looking.tsx b/app/features/tournament-lfg/routes/to.$id.looking.tsx index 4f772b075..c1b0bc625 100644 --- a/app/features/tournament-lfg/routes/to.$id.looking.tsx +++ b/app/features/tournament-lfg/routes/to.$id.looking.tsx @@ -19,10 +19,11 @@ import { WeaponImage } from "~/components/Image"; import { Placeholder } from "~/components/Placeholder"; import { SubmitButton } from "~/components/SubmitButton"; import { useUser } from "~/features/auth/core/user"; +import { IS_Q_LOOKING_MOBILE_BREAKPOINT } from "~/features/sendouq/q-constants"; import { useTournament } from "~/features/tournament/routes/to.$id"; import { SendouForm } from "~/form/SendouForm"; import { useIsMounted } from "~/hooks/useIsMounted"; -import { useWindowSize } from "~/hooks/useWindowSize"; +import { useMainContentWidth } from "~/hooks/useMainContentWidth"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { userPage } from "~/utils/urls"; import { LFGGroupCard } from "../components/LFGGroupCard"; @@ -69,9 +70,9 @@ function GroupsView({ }) { const { t } = useTranslation(["q"]); const tournament = useTournament(); - const { width } = useWindowSize(); + const width = useMainContentWidth(); - const isMobile = width < 750; + const isMobile = width < IS_Q_LOOKING_MOBILE_BREAKPOINT; const ownMemberCount = data.ownGroup?.members.length ?? data.ownTeam?.members.length ?? 0; @@ -373,9 +374,9 @@ function SubCard({ sub }: { sub: SubEntry }) { } function ColumnHeader({ children }: { children: React.ReactNode }) { - const { width } = useWindowSize(); + const width = useMainContentWidth(); - const isMobile = width < 750; + const isMobile = width < IS_Q_LOOKING_MOBILE_BREAKPOINT; if (isMobile) return null; diff --git a/app/features/tournament-subs/queries/deleteSub.server.ts b/app/features/tournament-subs/queries/deleteSub.server.ts deleted file mode 100644 index 65a08d050..000000000 --- a/app/features/tournament-subs/queries/deleteSub.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { sql } from "~/db/sql"; -import type { Tables } from "~/db/tables"; - -const stm = sql.prepare(/* sql */ ` - delete from "TournamentSub" - where - "userId" = @userId and - "tournamentId" = @tournamentId -`); - -export function deleteSub( - args: Pick, -) { - stm.run(args); -} diff --git a/app/features/tournament/actions/to.$id.admin.server.ts b/app/features/tournament/actions/to.$id.admin.server.ts index 0cb2c3a3b..824d08402 100644 --- a/app/features/tournament/actions/to.$id.admin.server.ts +++ b/app/features/tournament/actions/to.$id.admin.server.ts @@ -12,7 +12,7 @@ import { clearTournamentDataCache, tournamentFromDB, } from "~/features/tournament-bracket/core/Tournament.server"; -import { deleteSub } from "~/features/tournament-subs/queries/deleteSub.server"; +import * as TournamentLFGRepository from "~/features/tournament-lfg/TournamentLFGRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import invariant from "~/utils/invariant"; import { logger } from "~/utils/logger"; @@ -83,7 +83,10 @@ export const action: ActionFunction = async ({ request, params }) => { userId: data.userId, tournamentId, }); - deleteSub({ tournamentId, userId: data.userId }); + await TournamentLFGRepository.leaveLfg({ + userId: data.userId, + tournamentId, + }); ShowcaseTournaments.addToCached({ tournamentId, @@ -247,6 +250,10 @@ export const action: ActionFunction = async ({ request, params }) => { "User has no friend code set", ); + await TournamentLFGRepository.leaveLfg({ + userId: data.userId, + tournamentId, + }); joinTeam({ userId: data.userId, newTeamId: team.id, diff --git a/app/features/tournament/actions/to.$id.register.server.ts b/app/features/tournament/actions/to.$id.register.server.ts index a13201a3f..77ac0a1fd 100644 --- a/app/features/tournament/actions/to.$id.register.server.ts +++ b/app/features/tournament/actions/to.$id.register.server.ts @@ -12,7 +12,6 @@ import { tournamentFromDB, } from "~/features/tournament-bracket/core/Tournament.server"; import * as TournamentLFGRepository from "~/features/tournament-lfg/TournamentLFGRepository.server"; -import { deleteSub } from "~/features/tournament-subs/queries/deleteSub.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import { logger } from "~/utils/logger"; import { @@ -139,7 +138,6 @@ export const action: ActionFunction = async ({ request, params }) => { tournamentId, avatarFileName, }); - deleteSub({ tournamentId, userId: user.id }); await SavedCalendarEventRepository.unsave({ userId: user.id, tournamentId, diff --git a/app/features/tournament/queries/joinLeaveTeam.server.ts b/app/features/tournament/queries/joinLeaveTeam.server.ts index 7e3bfebb0..89d01603f 100644 --- a/app/features/tournament/queries/joinLeaveTeam.server.ts +++ b/app/features/tournament/queries/joinLeaveTeam.server.ts @@ -1,5 +1,4 @@ import { sql } from "~/db/sql"; -import { deleteSub } from "~/features/tournament-subs/queries/deleteSub.server"; import invariant from "~/utils/invariant"; import { checkOut } from "./checkOut.server"; @@ -26,6 +25,7 @@ const deleteMemberStm = sql.prepare(/*sql*/ ` and "userId" = @userId `); +// xxx: migrate to kysely. note: deleteSub was deleted from here export const joinTeam = sql.transaction( ({ previousTeamId, @@ -33,7 +33,7 @@ export const joinTeam = sql.transaction( newTeamId, userId, inGameName, - tournamentId, + tournamentId: _tournamentId, checkOutTeam = false, }: { previousTeamId?: number; @@ -50,10 +50,6 @@ export const joinTeam = sql.transaction( deleteMemberStm.run({ tournamentTeamId: previousTeamId ?? null, userId }); } - if (!previousTeamId) { - deleteSub({ tournamentId, userId }); - } - if (checkOutTeam) { invariant( previousTeamId, diff --git a/app/hooks/useWindowSize.ts b/app/hooks/useMainContentWidth.ts similarity index 67% rename from app/hooks/useWindowSize.ts rename to app/hooks/useMainContentWidth.ts index f7895bcfc..086cca936 100644 --- a/app/hooks/useWindowSize.ts +++ b/app/hooks/useMainContentWidth.ts @@ -2,16 +2,16 @@ import { useState } from "react"; import { useEventListener } from "./useEventListener"; import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; -interface WindowSize { - width: number; - height: number; -} - const MOBILE_BREAKPOINT = 600; const DESKTOP_BREAKPOINT = 1000; type LayoutSize = "mobile" | "tablet" | "desktop"; +interface WindowSize { + width: number; + height: number; +} + export function useLayoutSize(): LayoutSize { const { width } = useWindowSize(); @@ -21,7 +21,28 @@ export function useLayoutSize(): LayoutSize { return "desktop"; } -export function useWindowSize(): WindowSize { +export function useMainContentWidth() { + const [width, setWidth] = useState(0); + + useIsomorphicLayoutEffect(() => { + const main = document.querySelector("main"); + if (!main) return; + + setWidth(main.clientWidth); + + const observer = new ResizeObserver((entries) => { + setWidth(entries[0]?.contentRect.width); + }); + + observer.observe(main); + + return () => observer.disconnect(); + }, []); + + return width; +} + +function useWindowSize(): WindowSize { const [windowSize, setWindowSize] = useState({ width: 0, height: 0, diff --git a/app/styles/common.css b/app/styles/common.css index b8240c52d..cec4dd3b8 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -593,6 +593,8 @@ .top-leaderboard { min-height: 100px; margin-top: 10px; + display: grid; + place-items: center; } @media screen and (max-width: 599px) { diff --git a/app/styles/utils.css b/app/styles/utils.css index 9690b7b68..20aa5682a 100644 --- a/app/styles/utils.css +++ b/app/styles/utils.css @@ -83,6 +83,10 @@ text-decoration: underline; } + .line-through { + text-decoration: line-through; + } + .color-success { color: var(--color-success); } @@ -620,4 +624,11 @@ .clickable { cursor: pointer; } + + .no-results { + text-align: center; + color: var(--color-text-high); + font-style: italic; + font-size: var(--font-xs); + } } diff --git a/locales/da/scrims.json b/locales/da/scrims.json index f18239f52..5390e098b 100644 --- a/locales/da/scrims.json +++ b/locales/da/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/de/scrims.json b/locales/de/scrims.json index deef767d6..0c2c191da 100644 --- a/locales/de/scrims.json +++ b/locales/de/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "Text", "forms.visibility.title": "Sichtbarkeit", "forms.visibility.public": "Öffentlich", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "Keine Assoziationen verfügkar. Erstelle eine, um in einer kleineren Gruppe nach einem Scrim zu suchen.", "forms.notFoundVisibility.title": "Wenn kein Scrim gefunden wurde bis", "forms.notFoundVisibility.explanation": "Leer lassen, wenn sich die Sichtbarkeit deines Posts nicht mit der Zeit ändern soll.", diff --git a/locales/en/scrims.json b/locales/en/scrims.json index 9943a9ccf..c35b2fdaf 100644 --- a/locales/en/scrims.json +++ b/locales/en/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "Text", "forms.visibility.title": "Visibility", "forms.visibility.public": "Public", + "forms.visibility.friends": "Friends only", "forms.visibility.noneAvailable": "No associations available. Create one to look for a scrim in a smaller group.", "forms.notFoundVisibility.title": "If not found by", "forms.notFoundVisibility.explanation": "Leave blank if you don't want the visibility of your post to change over time", diff --git a/locales/es-ES/scrims.json b/locales/es-ES/scrims.json index 9d2b9f057..efd0b87c6 100644 --- a/locales/es-ES/scrims.json +++ b/locales/es-ES/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "Texto", "forms.visibility.title": "Visibilidad", "forms.visibility.public": "Pública", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "No hay asociaciones disponibles. Crea una para buscar un scrim en un grupo más reducido.", "forms.notFoundVisibility.title": "Si no se encuentra antes de", "forms.notFoundVisibility.explanation": "Déjalo en blanco si no quieres que la visibilidad de tu publicación cambie con el tiempo", diff --git a/locales/es-US/scrims.json b/locales/es-US/scrims.json index f18239f52..5390e098b 100644 --- a/locales/es-US/scrims.json +++ b/locales/es-US/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/fr-CA/scrims.json b/locales/fr-CA/scrims.json index f18239f52..5390e098b 100644 --- a/locales/fr-CA/scrims.json +++ b/locales/fr-CA/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/fr-EU/scrims.json b/locales/fr-EU/scrims.json index c5aff3f54..45a31f30d 100644 --- a/locales/fr-EU/scrims.json +++ b/locales/fr-EU/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "Texte", "forms.visibility.title": "Visibilité", "forms.visibility.public": "Public", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "Aucun association disponible. Créez-en un pour rechercher un scrim dans un groupe plus petit.", "forms.notFoundVisibility.title": "S'il n'est pas trouvé par", "forms.notFoundVisibility.explanation": "Laissez vide si vous ne souhaitez pas que la visibilité de votre post change au fil du temps", diff --git a/locales/he/scrims.json b/locales/he/scrims.json index f18239f52..5390e098b 100644 --- a/locales/he/scrims.json +++ b/locales/he/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/it/scrims.json b/locales/it/scrims.json index f18239f52..5390e098b 100644 --- a/locales/it/scrims.json +++ b/locales/it/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/ja/scrims.json b/locales/ja/scrims.json index f18239f52..5390e098b 100644 --- a/locales/ja/scrims.json +++ b/locales/ja/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/ko/scrims.json b/locales/ko/scrims.json index f18239f52..5390e098b 100644 --- a/locales/ko/scrims.json +++ b/locales/ko/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/nl/scrims.json b/locales/nl/scrims.json index f18239f52..5390e098b 100644 --- a/locales/nl/scrims.json +++ b/locales/nl/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/pl/scrims.json b/locales/pl/scrims.json index f18239f52..5390e098b 100644 --- a/locales/pl/scrims.json +++ b/locales/pl/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/pt-BR/scrims.json b/locales/pt-BR/scrims.json index f18239f52..5390e098b 100644 --- a/locales/pt-BR/scrims.json +++ b/locales/pt-BR/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/ru/scrims.json b/locales/ru/scrims.json index f18239f52..5390e098b 100644 --- a/locales/ru/scrims.json +++ b/locales/ru/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "", diff --git a/locales/zh/scrims.json b/locales/zh/scrims.json index f18239f52..5390e098b 100644 --- a/locales/zh/scrims.json +++ b/locales/zh/scrims.json @@ -58,6 +58,7 @@ "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", + "forms.visibility.friends": "", "forms.visibility.noneAvailable": "", "forms.notFoundVisibility.title": "", "forms.notFoundVisibility.explanation": "",