mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-28 22:31:22 -05:00
Can like groups and move them to another column
This commit is contained in:
@@ -12,7 +12,7 @@ export function Avatar({
|
||||
"--_avatar-size": tiny ? "2rem" : undefined,
|
||||
};
|
||||
return (
|
||||
<div style={style} className="avatar__placeholder">
|
||||
<div style={style} className={clsx("avatar__placeholder", { tiny })}>
|
||||
{user.discordAvatar && (
|
||||
<img
|
||||
alt=""
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
export function isGroupAdmin({group, user}: {group: {members: {captain: boolean, memberId: string}[]}, user: {id: string}}) {
|
||||
return group.members.some(
|
||||
(member) => member.captain && member.memberId === user.id
|
||||
)
|
||||
}
|
||||
export function isGroupAdmin({
|
||||
group,
|
||||
user,
|
||||
}: {
|
||||
group: { members: { captain: boolean; memberId: string }[] };
|
||||
user: { id: string };
|
||||
}) {
|
||||
return group.members.some(
|
||||
(member) => member.captain && member.memberId === user.id
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,38 @@ export function create({
|
||||
});
|
||||
}
|
||||
|
||||
export function like({
|
||||
likerId,
|
||||
targetId,
|
||||
}: {
|
||||
likerId: string;
|
||||
targetId: string;
|
||||
}) {
|
||||
return db.lfgGroupLike.create({
|
||||
data: {
|
||||
likerId,
|
||||
targetId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function unlike({
|
||||
likerId,
|
||||
targetId,
|
||||
}: {
|
||||
likerId: string;
|
||||
targetId: string;
|
||||
}) {
|
||||
return db.lfgGroupLike.delete({
|
||||
where: {
|
||||
likerId_targetId: {
|
||||
likerId,
|
||||
targetId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function findActiveByMember(user: { id: string }) {
|
||||
return db.lfgGroup.findFirst({
|
||||
where: {
|
||||
@@ -39,6 +71,16 @@ export function findActiveByMember(user: { id: string }) {
|
||||
},
|
||||
include: {
|
||||
members: true,
|
||||
likedGroups: {
|
||||
select: {
|
||||
targetId: true,
|
||||
},
|
||||
},
|
||||
likesReceived: {
|
||||
select: {
|
||||
likerId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,31 +1,78 @@
|
||||
import {
|
||||
ActionFunction,
|
||||
Form,
|
||||
json,
|
||||
LinksFunction,
|
||||
LoaderFunction,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
} from "remix";
|
||||
import { LFG_GROUP_FULL_SIZE } from "~/constants";
|
||||
import * as LFGGroup from "~/models/LFGGroup.server";
|
||||
import { requireUser, Unpacked } from "~/utils";
|
||||
import styles from "~/styles/play-looking.css";
|
||||
import { z } from "zod";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Button } from "~/components/Button";
|
||||
import { LFG_GROUP_FULL_SIZE } from "~/constants";
|
||||
import { 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";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
interface LookingLoaderData {
|
||||
groups: {
|
||||
const actionSchema = z.object({
|
||||
_action: z.enum(["LIKE", "UNLIKE", "UNITE_GROUP", "MATCH_UP"]),
|
||||
targetGroupId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request, context }) => {
|
||||
const data = await parseRequestFormData({
|
||||
request,
|
||||
schema: actionSchema,
|
||||
});
|
||||
const user = requireUser(context);
|
||||
|
||||
const group = await LFGGroup.findActiveByMember(user);
|
||||
validate(group, "No active group");
|
||||
validate(group.looking, "Group is not looking");
|
||||
validate(isGroupAdmin({ group, user }), "Not group admin");
|
||||
|
||||
switch (data._action) {
|
||||
case "UNITE_GROUP":
|
||||
case "MATCH_UP":
|
||||
case "UNLIKE": {
|
||||
await LFGGroup.unlike({
|
||||
likerId: group.id,
|
||||
targetId: data.targetGroupId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "LIKE": {
|
||||
await LFGGroup.like({ likerId: group.id, targetId: data.targetGroupId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: data._action };
|
||||
|
||||
// TODO: notify watchers
|
||||
};
|
||||
|
||||
type LookingLoaderDataGroup = {
|
||||
id: string;
|
||||
members?: {
|
||||
id: string;
|
||||
members?: {
|
||||
id: string;
|
||||
discordId: string;
|
||||
discordAvatar: string | null;
|
||||
discordName: string;
|
||||
discordDiscriminator: string;
|
||||
}[];
|
||||
discordId: string;
|
||||
discordAvatar: string | null;
|
||||
discordName: string;
|
||||
discordDiscriminator: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
interface LookingLoaderData {
|
||||
likedGroups: LookingLoaderDataGroup[];
|
||||
neutralGroups: LookingLoaderDataGroup[];
|
||||
likerGroups: LookingLoaderDataGroup[];
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ context }) => {
|
||||
@@ -46,10 +93,18 @@ export const loader: LoaderFunction = async ({ context }) => {
|
||||
|
||||
return LFG_GROUP_FULL_SIZE - ownGroup.members.length;
|
||||
})();
|
||||
|
||||
const lookingForMatch = ownGroup.members.length === LFG_GROUP_FULL_SIZE;
|
||||
return json<LookingLoaderData>({
|
||||
groups: groups
|
||||
const likesGiven = ownGroup.likedGroups.reduce(
|
||||
(acc, lg) => acc.add(lg.targetId),
|
||||
new Set<string>()
|
||||
);
|
||||
const likesReceived = ownGroup.likesReceived.reduce(
|
||||
(acc, lg) => acc.add(lg.likerId),
|
||||
new Set<string>()
|
||||
);
|
||||
|
||||
return json<LookingLoaderData>(
|
||||
groups
|
||||
.filter((group) => group.members.length <= maxGroupSizeToConsider)
|
||||
.filter((group) => group.id !== ownGroup.id)
|
||||
.map((group) => ({
|
||||
@@ -60,8 +115,24 @@ export const loader: LoaderFunction = async ({ context }) => {
|
||||
group.ranked && lookingForMatch
|
||||
? undefined
|
||||
: group.members.map((member) => member.user),
|
||||
})),
|
||||
});
|
||||
}))
|
||||
.reduce(
|
||||
(acc: LookingLoaderData, group) => {
|
||||
// likesReceived first so that if both received like and
|
||||
// given like then handle this edge case by just displaying the
|
||||
// group as waiting like back
|
||||
if (likesReceived.has(group.id)) {
|
||||
acc.likerGroups.push(group);
|
||||
} else if (likesGiven.has(group.id)) {
|
||||
acc.likedGroups.push(group);
|
||||
} else {
|
||||
acc.neutralGroups.push(group);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ likedGroups: [], neutralGroups: [], likerGroups: [] }
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default function LookingPage() {
|
||||
@@ -72,15 +143,48 @@ export default function LookingPage() {
|
||||
<div className="play-looking__columns">
|
||||
<div>
|
||||
<h2 className="play-looking__column-header">You want to play with</h2>
|
||||
<div className="play-looking__cards">
|
||||
{data.likedGroups.map((group) => {
|
||||
return (
|
||||
<GroupCard
|
||||
key={group.id}
|
||||
group={group}
|
||||
isGroupAdmin
|
||||
type="LIKES_GIVEN"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="play-looking__column-header invisible">Groups</h2>
|
||||
{data.groups.map((group) => {
|
||||
return <GroupCard key={group.id} group={group} />;
|
||||
})}
|
||||
<div className="play-looking__cards">
|
||||
{data.neutralGroups.map((group) => {
|
||||
return (
|
||||
<GroupCard
|
||||
key={group.id}
|
||||
group={group}
|
||||
isGroupAdmin
|
||||
type="NEUTRAL"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="play-looking__column-header">Want to play with you</h2>
|
||||
<div className="play-looking__cards">
|
||||
{data.likerGroups.map((group) => {
|
||||
return (
|
||||
<GroupCard
|
||||
key={group.id}
|
||||
group={group}
|
||||
isGroupAdmin
|
||||
type="LIKES_RECEIVED"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,21 +193,42 @@ export default function LookingPage() {
|
||||
|
||||
function GroupCard({
|
||||
group,
|
||||
isGroupAdmin = false,
|
||||
type,
|
||||
}: {
|
||||
group: Unpacked<LookingLoaderData["groups"]>;
|
||||
group: LookingLoaderDataGroup;
|
||||
isGroupAdmin?: boolean;
|
||||
type: "LIKES_GIVEN" | "NEUTRAL" | "LIKES_RECEIVED";
|
||||
}) {
|
||||
const buttonText = () => {
|
||||
if (type === "LIKES_GIVEN") return "idk";
|
||||
if (type === "NEUTRAL") return "Let's play!";
|
||||
|
||||
return "Group up";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="play-looking__card">
|
||||
{group.members?.map((member) => {
|
||||
return (
|
||||
<div key={member.id} className="play-looking__member-card">
|
||||
<Avatar tiny user={member} />
|
||||
<span className="play-looking__member-name">
|
||||
{member.discordName}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Form method="post">
|
||||
<div className="play-looking__card">
|
||||
<div className="play-looking__card__members">
|
||||
{group.members?.map((member) => {
|
||||
return (
|
||||
<div key={member.id} className="play-looking__member-card">
|
||||
<Avatar tiny user={member} />
|
||||
<span className="play-looking__member-name">
|
||||
{member.discordName}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<input type="hidden" name="targetGroupId" value={group.id} />
|
||||
{isGroupAdmin && (
|
||||
<Button type="submit" name="_action" value="LIKE" tiny>
|
||||
{buttonText()}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -446,6 +446,10 @@ select::selection {
|
||||
inline-size: var(--_avatar-size, 2.5rem);
|
||||
}
|
||||
|
||||
.avatar__placeholder.tiny {
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
|
||||
.mt-2 {
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
.play-looking__columns {
|
||||
display: grid;
|
||||
column-gap: var(--s-5);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.play-looking__cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.play-looking__column-header {
|
||||
font-size: var(--fonts-md);
|
||||
text-align: center;
|
||||
@@ -10,16 +17,26 @@
|
||||
|
||||
.play-looking__card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-evenly;
|
||||
padding: var(--s-4);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
gap: var(--s-4);
|
||||
padding-block: var(--s-3);
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.play-looking__card__members {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.play-looking__member-card {
|
||||
display: flex;
|
||||
min-width: calc(50% - var(--s-4));
|
||||
max-width: calc(50% - var(--s-4));
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -412,6 +412,7 @@ export async function seed(variation?: SeedVariations) {
|
||||
data: [
|
||||
{
|
||||
memberId: ADMIN_TEST_UUID,
|
||||
captain: true,
|
||||
},
|
||||
{ memberId: NZAP_TEST_UUID },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user