diff --git a/app/models/LFGMatch.server.ts b/app/models/LFGMatch.server.ts new file mode 100644 index 000000000..350b36c28 --- /dev/null +++ b/app/models/LFGMatch.server.ts @@ -0,0 +1,8 @@ +import { db } from "~/utils/db.server"; + +export function findById(id: string) { + return db.lfgGroupMatch.findUnique({ + where: { id }, + select: { groups: { include: { members: { include: { user: true } } } } }, + }); +} diff --git a/app/routes/play/looking.tsx b/app/routes/play/looking.tsx index 254f72365..7f9db804a 100644 --- a/app/routes/play/looking.tsx +++ b/app/routes/play/looking.tsx @@ -17,7 +17,7 @@ import { uniteGroupInfo } from "~/core/play/utils"; import { canUniteWithGroup, isGroupAdmin } from "~/core/play/validators"; import * as LFGGroup from "~/models/LFGGroup.server"; import styles from "~/styles/play-looking.css"; -import { parseRequestFormData, requireUser, validate } from "~/utils"; +import { parseRequestFormData, requireUser, UserLean, validate } from "~/utils"; import { skillToMMR, teamSkillToApproximateMMR, @@ -143,14 +143,9 @@ export const action: ActionFunction = async ({ request, context }) => { export type LookingLoaderDataGroup = { id: string; - members?: { - id: string; - discordId: string; - discordAvatar: string | null; - discordName: string; - discordDiscriminator: string; + members?: (UserLean & { MMR?: number; - }[]; + })[]; teamMMR?: { exact: boolean; value: number; @@ -164,7 +159,7 @@ interface LookingLoaderData { likerGroups: LookingLoaderDataGroup[]; ownGroup: LookingLoaderDataGroup; type: LfgGroupType; - userIsCaptain: boolean; + isCaptain: boolean; } export const loader: LoaderFunction = async ({ context }) => { @@ -223,7 +218,7 @@ export const loader: LoaderFunction = async ({ context }) => { return json({ ownGroup: ownGroupForResponse, type: ownGroup.type, - userIsCaptain: isGroupAdmin({ group: ownGroup, user }), + isCaptain: isGroupAdmin({ group: ownGroup, user }), ...groups .filter( (group) => @@ -257,7 +252,7 @@ export const loader: LoaderFunction = async ({ context }) => { })) .reduce( ( - acc: Omit, + acc: Omit, group ) => { // likesReceived first so that if both received like and @@ -292,7 +287,7 @@ export default function LookingPage() {
- {data.userIsCaptain && ( + {data.isCaptain && (
; +import { + Form, + json, + LinksFunction, + LoaderFunction, + useLoaderData, +} from "remix"; +import invariant from "tiny-invariant"; +import { Button } from "~/components/Button"; +import { DISCORD_URL } from "~/constants"; +import * as LFGMatch from "~/models/LFGMatch.server"; +import { getUser, UserLean } from "~/utils"; +import styles from "~/styles/play-match.css"; +import { Avatar } from "~/components/Avatar"; + +export const links: LinksFunction = () => { + return [{ rel: "stylesheet", href: styles }]; +}; + +interface LFGMatchLoaderData { + /** Can the user counterpick and report scores? */ + isCaptain: boolean; + isRanked: boolean; + groups: UserLean[][]; +} + +export const loader: LoaderFunction = async ({ params, context }) => { + invariant(typeof params.id === "string", "Expected params.bid to be string"); + const user = getUser(context); + + const match = await LFGMatch.findById(params.id); + if (!match) return new Response(null, { status: 404 }); + + //const isRanked = match.groups.every((g) => g.ranked); + const isRanked = false; + const isOwnMatch = match.groups.some((g) => + g.members.some((m) => user?.id === m.user.id) + ); + // Non-ranked matches are only of interest to participants + if (!isRanked && !isOwnMatch) { + return new Response(null, { status: 404 }); + } + + const isCaptain = match.groups.some((g) => + g.members.some((m) => m.user.id === user?.id && m.captain) + ); + return json({ + isCaptain, + isRanked, + groups: match.groups + .sort((a, b) => { + const aIsOwnGroup = a.members.some((m) => user?.id === m.user.id); + const bIsOwnGroup = b.members.some((m) => user?.id === m.user.id); + + return Number(bIsOwnGroup) - Number(aIsOwnGroup); + }) + .map((g) => { + return g.members.map((g) => ({ + id: g.user.id, + discordId: g.user.discordId, + discordAvatar: g.user.discordAvatar, + discordName: g.user.discordName, + discordDiscriminator: g.user.discordDiscriminator, + })); + }), + }); +}; + +export default function LFGMatchPage() { + const data = useLoaderData(); + + return ( +
+
+
+ {data.groups.map((g, i) => { + return ( +
+ {g.map((user) => ( +
+ + + {user.discordName} + +
+ ))} +
+ ); + })} +
+ {!data.isRanked && ( +
+ This is your match! You can reach out to your opponents{" "} + our Discord in the{" "} + #match-meetup channel. +
+ )} +
+
+ + {data.isCaptain && ( + + )} + +
+
+ ); } diff --git a/app/styles/play-match.css b/app/styles/play-match.css new file mode 100644 index 000000000..5122ba244 --- /dev/null +++ b/app/styles/play-match.css @@ -0,0 +1,53 @@ +.play-match__waves { + padding: var(--s-8); + background: url("/svg/background-pattern.svg"); + background-color: var(--bg-lighter); + border-radius: var(--rounded); +} + +.play-match__teams { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: var(--s-4); +} + +.play-match__waves-section { + padding: var(--s-4); + background-color: var(--bg-darker); + border-radius: var(--rounded); + font-size: var(--fonts-sm); + text-align: center; +} + +.play-match__info { + margin-block-start: var(--s-8); +} + +.play-match__players { + display: grid; + max-width: 14rem; + flex: 1; + gap: var(--s-4); + grid-template-columns: 1fr 1fr; +} + +.play-match__player-name { + font-size: var(--fonts-xs); + font-weight: var(--semi-bold); + margin-block-start: 1px; + white-space: nowrap; +} + +.play-match__player { + display: flex; + flex-direction: column; + align-items: center; + font-size: var(--fonts-xs); +} + +.play-match__waves-button { + display: flex; + justify-content: center; + margin-block-start: var(--s-4); +} diff --git a/app/utils/index.ts b/app/utils/index.ts index cd87bc646..aab5b9a0f 100644 --- a/app/utils/index.ts +++ b/app/utils/index.ts @@ -150,3 +150,12 @@ export interface MyCSSProperties extends CSSProperties { "--brackets-column-matches"?: number; "--tabs-count"?: number; } + +/** Minimal information on user to show their name and avatar */ +export interface UserLean { + id: string; + discordId: string; + discordAvatar: string | null; + discordName: string; + discordDiscriminator: string; +}