mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-28 22:31:22 -05:00
LFG Match page initial
This commit is contained in:
8
app/models/LFGMatch.server.ts
Normal file
8
app/models/LFGMatch.server.ts
Normal file
@@ -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 } } } } },
|
||||
});
|
||||
}
|
||||
@@ -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<LookingLoaderData>({
|
||||
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<LookingLoaderData, "ownGroup" | "type" | "userIsCaptain">,
|
||||
acc: Omit<LookingLoaderData, "ownGroup" | "type" | "isCaptain">,
|
||||
group
|
||||
) => {
|
||||
// likesReceived first so that if both received like and
|
||||
@@ -292,7 +287,7 @@ export default function LookingPage() {
|
||||
</div>
|
||||
<div className="play-looking__waves-button">
|
||||
<Form method="post">
|
||||
{data.userIsCaptain && (
|
||||
{data.isCaptain && (
|
||||
<Button
|
||||
type="submit"
|
||||
name="_action"
|
||||
@@ -327,7 +322,7 @@ export default function LookingPage() {
|
||||
<GroupCard
|
||||
key={group.id}
|
||||
group={group}
|
||||
isCaptain={data.userIsCaptain}
|
||||
isCaptain={data.isCaptain}
|
||||
type="LIKES_GIVEN"
|
||||
ranked={group.ranked}
|
||||
lookingForMatch={lookingForMatch}
|
||||
@@ -344,7 +339,7 @@ export default function LookingPage() {
|
||||
<GroupCard
|
||||
key={group.id}
|
||||
group={group}
|
||||
isCaptain={data.userIsCaptain}
|
||||
isCaptain={data.isCaptain}
|
||||
type="NEUTRAL"
|
||||
ranked={group.ranked}
|
||||
lookingForMatch={lookingForMatch}
|
||||
@@ -361,7 +356,7 @@ export default function LookingPage() {
|
||||
<GroupCard
|
||||
key={group.id}
|
||||
group={group}
|
||||
isCaptain={data.userIsCaptain}
|
||||
isCaptain={data.isCaptain}
|
||||
type="LIKES_RECEIVED"
|
||||
ranked={data.ownGroup.ranked}
|
||||
lookingForMatch={lookingForMatch}
|
||||
|
||||
@@ -1,3 +1,119 @@
|
||||
export default function MatchPage() {
|
||||
return <div>match :D</div>;
|
||||
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<LFGMatchLoaderData>({
|
||||
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<LFGMatchLoaderData>();
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="play-match__waves">
|
||||
<div className="play-match__teams">
|
||||
{data.groups.map((g, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="play-match__waves-section play-match__players"
|
||||
>
|
||||
{g.map((user) => (
|
||||
<div key={user.id} className="play-match__player">
|
||||
<Avatar user={user} />
|
||||
<span className="play-match__player-name">
|
||||
{user.discordName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!data.isRanked && (
|
||||
<div className="play-match__waves-section play-match__info">
|
||||
This is your match! You can reach out to your opponents{" "}
|
||||
<a href={DISCORD_URL}>our Discord</a> in the{" "}
|
||||
<code>#match-meetup</code> channel.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="play-match__waves-button">
|
||||
<Form method="post">
|
||||
{data.isCaptain && (
|
||||
<Button
|
||||
type="submit"
|
||||
name="_action"
|
||||
value="LOOK_AGAIN"
|
||||
tiny
|
||||
variant="outlined"
|
||||
>
|
||||
Look again
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
53
app/styles/play-match.css
Normal file
53
app/styles/play-match.css
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user