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": "",