diff --git a/app/components/play/GroupCard.tsx b/app/components/play/GroupCard.tsx new file mode 100644 index 000000000..f4c40d6e7 --- /dev/null +++ b/app/components/play/GroupCard.tsx @@ -0,0 +1,68 @@ +import { Form } from "remix"; +import { Avatar } from "~/components/Avatar"; +import { Button } from "~/components/Button"; +import type { + LookingActionSchema, + LookingLoaderDataGroup, +} from "~/routes/play/looking"; + +export function GroupCard({ + group, + isGroupAdmin = false, + type, +}: { + group: LookingLoaderDataGroup; + isGroupAdmin?: boolean; + type: "LIKES_GIVEN" | "NEUTRAL" | "LIKES_RECEIVED"; +}) { + const buttonText = () => { + if (type === "LIKES_GIVEN") return "Undo"; + if (type === "NEUTRAL") return "Let's play!"; + + return "Group up"; + }; + const buttonValue = (): LookingActionSchema["_action"] => { + if (type === "LIKES_GIVEN") return "UNLIKE"; + if (type === "NEUTRAL") return "LIKE"; + + return "UNITE_GROUP"; + }; + + return ( +
+ ); +} diff --git a/app/core/play/utils.test.ts b/app/core/play/utils.test.ts new file mode 100644 index 000000000..97136ac1b --- /dev/null +++ b/app/core/play/utils.test.ts @@ -0,0 +1,32 @@ +import { suite } from "uvu"; +import * as assert from "uvu/assert"; +import { uniteGroupInfo, UniteGroupInfoArg } from "./utils"; + +const UniteGroupInfo = suite("uniteGroupInfo()"); + +const SMALL_GROUP: UniteGroupInfoArg = { id: "small", memberCount: 1 }; +const BIG_GROUP: UniteGroupInfoArg = { id: "big", memberCount: 3 }; + +UniteGroupInfo("Removes captain if other group is smaller", () => { + const { removeCaptainsFromOther } = uniteGroupInfo(SMALL_GROUP, BIG_GROUP); + + assert.ok(removeCaptainsFromOther); +}); + +UniteGroupInfo("Doesn't remove captain if groups are same size", () => { + const { removeCaptainsFromOther } = uniteGroupInfo(SMALL_GROUP, SMALL_GROUP); + + assert.not.ok(removeCaptainsFromOther); +}); + +UniteGroupInfo("Bigger group survives", () => { + let { otherGroupId, survivingGroupId } = uniteGroupInfo( + BIG_GROUP, + SMALL_GROUP + ); + + assert.equal(survivingGroupId, "big"); + assert.equal(otherGroupId, "small"); +}); + +UniteGroupInfo.run(); diff --git a/app/core/play/utils.ts b/app/core/play/utils.ts new file mode 100644 index 000000000..50e454a3f --- /dev/null +++ b/app/core/play/utils.ts @@ -0,0 +1,20 @@ +import type { UniteGroupsArgs } from "~/models/LFGGroup.server"; + +export interface UniteGroupInfoArg { + id: string; + memberCount: number; +} +export function uniteGroupInfo( + groupA: UniteGroupInfoArg, + groupB: UniteGroupInfoArg +): UniteGroupsArgs { + const survivingGroupId = + groupA.memberCount > groupB.memberCount ? groupA.id : groupB.id; + const otherGroupId = survivingGroupId === groupA.id ? groupB.id : groupA.id; + + return { + survivingGroupId, + otherGroupId, + removeCaptainsFromOther: groupA.memberCount !== groupB.memberCount, + }; +} diff --git a/app/core/play/validators.ts b/app/core/play/validators.ts index 8cbcad294..db7775f21 100644 --- a/app/core/play/validators.ts +++ b/app/core/play/validators.ts @@ -1,3 +1,6 @@ +import { LfgGroupType } from "@prisma/client"; +import { LFG_GROUP_FULL_SIZE } from "~/constants"; + export function isGroupAdmin({ group, user, @@ -9,3 +12,25 @@ export function isGroupAdmin({ (member) => member.captain && member.memberId === user.id ); } + +/** + * Checks that group size is suitable to be united with. E.g. if type of group + * is QUAD and your group has 2 members then a legal group to unite with would have + * 1 or 2 members. + */ +export function canUniteWithGroup({ + ownGroupType, + ownGroupSize, + otherGroupSize, +}: { + ownGroupType: LfgGroupType; + ownGroupSize: number; + otherGroupSize: number; +}): boolean { + const maxGroupSizeToConsider = + ownGroupType === "TWIN" + ? 2 - ownGroupSize + : LFG_GROUP_FULL_SIZE - ownGroupSize; + + return maxGroupSizeToConsider >= otherGroupSize; +} diff --git a/app/models/LFGGroup.server.ts b/app/models/LFGGroup.server.ts index de5b0960f..ecc6bf98d 100644 --- a/app/models/LFGGroup.server.ts +++ b/app/models/LFGGroup.server.ts @@ -59,6 +59,38 @@ export function unlike({ }); } +export interface UniteGroupsArgs { + survivingGroupId: string; + otherGroupId: string; + removeCaptainsFromOther: boolean; +} +export function uniteGroups({ + survivingGroupId, + otherGroupId, + removeCaptainsFromOther, +}: UniteGroupsArgs) { + return db.$transaction([ + db.lfgGroupMember.updateMany({ + where: { groupId: otherGroupId }, + data: { + groupId: survivingGroupId, + captain: removeCaptainsFromOther ? false : undefined, + // TODO: also reset message + }, + }), + db.lfgGroup.delete({ where: { id: otherGroupId } }), + db.lfgGroupLike.deleteMany({ + where: { + OR: [{ likerId: survivingGroupId }, { targetId: survivingGroupId }], + }, + }), + ]); +} + +export function findById(id: string) { + return db.lfgGroup.findUnique({ where: { id }, include: { members: true } }); +} + export function findActiveByMember(user: { id: string }) { return db.lfgGroup.findFirst({ where: { diff --git a/app/routes/play/looking.tsx b/app/routes/play/looking.tsx index d1a8a3af7..b85a3f03b 100644 --- a/app/routes/play/looking.tsx +++ b/app/routes/play/looking.tsx @@ -1,17 +1,17 @@ import { ActionFunction, - Form, json, LinksFunction, LoaderFunction, redirect, useLoaderData, } from "remix"; +import invariant from "tiny-invariant"; import { z } from "zod"; -import { Avatar } from "~/components/Avatar"; -import { Button } from "~/components/Button"; +import { GroupCard } from "~/components/play/GroupCard"; import { LFG_GROUP_FULL_SIZE } from "~/constants"; -import { isGroupAdmin } from "~/core/play/validators"; +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"; @@ -20,36 +20,86 @@ export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; }; -type ActionSchema = z.infer