Group notes feature

This commit is contained in:
Kalle 2023-10-09 17:36:22 +03:00
parent f8f221708f
commit bfb764f238
15 changed files with 203 additions and 31 deletions

View File

@ -13,7 +13,7 @@ export interface ButtonProps
| "minimal"
| "minimal-success"
| "minimal-destructive";
size?: "tiny" | "big";
size?: "miniscule" | "tiny" | "big";
loading?: boolean;
loadingText?: string;
icon?: JSX.Element;
@ -42,6 +42,7 @@ export function Button(props: ButtonProps) {
loading,
tiny: size === "tiny",
big: size === "big",
miniscule: size === "miniscule",
},
className,
)}

View File

@ -535,6 +535,7 @@ export interface GroupMember {
groupId: number;
userId: number;
role: "OWNER" | "MANAGER" | "REGULAR";
note: string | null;
createdAt: number;
}

View File

@ -2,7 +2,6 @@ import { Link, useFetcher } from "@remix-run/react";
import clsx from "clsx";
import { Avatar } from "~/components/Avatar";
import { Button } from "~/components/Button";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { Image, TierImage, WeaponImage } from "~/components/Image";
import { Popover } from "~/components/Popover";
import { SubmitButton } from "~/components/SubmitButton";
@ -21,6 +20,7 @@ import type { LookingGroup } from "../q-types";
import { StarIcon } from "~/components/icons/Star";
import { StarFilledIcon } from "~/components/icons/StarFilled";
import { inGameNameWithoutDiscriminator } from "~/utils/strings";
import * as React from "react";
export function GroupCard({
group,
@ -31,6 +31,7 @@ export function GroupCard({
displayOnly = false,
hideVc = false,
hideWeapons = false,
hideNote: _hidenote = false,
}: {
group: LookingGroup;
action?: "LIKE" | "UNLIKE" | "GROUP_UP" | "MATCH_UP";
@ -40,9 +41,15 @@ export function GroupCard({
displayOnly?: boolean;
hideVc?: boolean;
hideWeapons?: boolean;
hideNote?: boolean;
}) {
const fetcher = useFetcher();
const leaveQFetcher = useFetcher();
const hideNote =
displayOnly ||
!group.members ||
group.members.length === FULL_GROUP_SIZE ||
_hidenote;
return (
<section
@ -62,6 +69,7 @@ export function GroupCard({
displayOnly={displayOnly}
hideVc={hideVc}
hideWeapons={hideWeapons}
hideNote={hideNote}
/>
);
})}
@ -122,33 +130,6 @@ export function GroupCard({
</SubmitButton>
</fetcher.Form>
) : null}
{ownGroup && group.members!.length > 1 ? (
<FormWithConfirm
dialogHeading="Leave this group?"
fields={[["_action", "LEAVE_GROUP"]]}
deleteButtonText="Leave"
action={SENDOUQ_LOOKING_PAGE}
>
<Button variant="minimal-destructive" size="tiny">
Leave group
</Button>
</FormWithConfirm>
) : null}
{/* Leave without confirm if alone */}
{ownGroup && group.members!.length === 1 ? (
<leaveQFetcher.Form method="POST" action={SENDOUQ_LOOKING_PAGE}>
<SubmitButton
_action="LEAVE_GROUP"
variant="minimal-destructive"
size="tiny"
state={fetcher.state}
className="mx-auto"
>
Leave queue
</SubmitButton>
</leaveQFetcher.Form>
) : null}
</section>
);
}
@ -159,13 +140,17 @@ function GroupMember({
displayOnly,
hideVc,
hideWeapons,
hideNote,
}: {
member: NonNullable<LookingGroup["members"]>[number];
showActions: boolean;
displayOnly?: boolean;
hideVc?: boolean;
hideWeapons?: boolean;
hideNote?: boolean;
}) {
const user = useUser();
return (
<div className="stack xxs">
<div className="q__group-member">
@ -225,10 +210,83 @@ function GroupMember({
<MemberSkillDifference skillDifference={member.skillDifference} />
) : null}
</div>
{!hideNote ? (
<MemberNote note={member.note} editable={user?.id === member.id} />
) : null}
</div>
);
}
function MemberNote({
note,
editable,
}: {
note?: string | null;
editable: boolean;
}) {
const fetcher = useFetcher();
const [editing, setEditing] = React.useState(false);
React.useEffect(() => {
setEditing(false);
}, [note]);
if (editing) {
return (
<fetcher.Form method="post" action={SENDOUQ_LOOKING_PAGE}>
<textarea
defaultValue={note ?? ""}
rows={2}
className="q__group-member__note-textarea mt-1"
name="value"
/>
<div className="stack horizontal justify-between">
<Button
variant="minimal-destructive"
size="miniscule"
onClick={() => setEditing(false)}
>
Cancel
</Button>
<SubmitButton
_action="UPDATE_NOTE"
variant="minimal"
size="miniscule"
>
Save
</SubmitButton>
</div>
</fetcher.Form>
);
}
if (note) {
return (
<div className="text-lighter text-center text-xs mt-1">
{note}{" "}
{editable ? (
<Button
size="miniscule"
variant="minimal"
onClick={() => setEditing(true)}
className="mt-2 ml-auto"
>
Edit note
</Button>
) : null}
</div>
);
}
if (!editable) return null;
return (
<Button variant="minimal" size="miniscule" onClick={() => setEditing(true)}>
Add note
</Button>
);
}
function GroupSkillDifference({
skillDifference,
}: {

View File

@ -0,0 +1,46 @@
import { useFetcher } from "@remix-run/react";
import { Button } from "~/components/Button";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { SubmitButton } from "~/components/SubmitButton";
import { SENDOUQ_LOOKING_PAGE } from "~/utils/urls";
export function GroupLeaver({
type,
}: {
type: "LEAVE_GROUP" | "LEAVE_Q" | "GO_BACK";
}) {
const fetcher = useFetcher();
if (type === "LEAVE_GROUP") {
return (
<FormWithConfirm
dialogHeading="Leave this group?"
fields={[["_action", "LEAVE_GROUP"]]}
deleteButtonText="Leave"
action={SENDOUQ_LOOKING_PAGE}
>
<Button
variant="minimal-destructive"
size="tiny"
className="self-start"
>
Leave group
</Button>
</FormWithConfirm>
);
}
// leave without confirm if alone
return (
<fetcher.Form method="POST" action={SENDOUQ_LOOKING_PAGE}>
<SubmitButton
_action="LEAVE_GROUP"
variant="minimal-destructive"
size="tiny"
state={fetcher.state}
>
{type === "LEAVE_Q" ? "Leave queue" : "Go back"}
</SubmitButton>
</fetcher.Form>
);
}

View File

@ -1,3 +1,4 @@
import { TWEET_LENGTH_MAX_LENGTH } from "~/constants";
import type { Group } from "~/db/types";
import { assertType } from "~/utils/types";
@ -21,6 +22,7 @@ export const SENDOUQ = {
SZ_MAP_COUNT: 6,
OTHER_MODE_MAP_COUNT: 3,
MAX_STAGE_REPEAT_COUNT: 2,
NOTE_MAX_LENGTH: TWEET_LENGTH_MAX_LENGTH / 2,
} as const;
export const FULL_GROUP_SIZE = 4;

View File

@ -2,6 +2,7 @@ import { z } from "zod";
import {
FULL_GROUP_SIZE,
MAP_LIST_PREFERENCE_OPTIONS,
SENDOUQ,
SENDOUQ_BEST_OF,
} from "./q-constants";
import {
@ -14,6 +15,7 @@ import {
weaponSplId,
stageId,
modeShort,
falsyToNull,
} from "~/utils/zod";
import { matchEndedAtIndex } from "./core/match";
import { languagesUnified } from "~/modules/i18n/config";
@ -88,6 +90,13 @@ export const lookingSchema = z.union([
z.object({
_action: _action("REFRESH_GROUP"),
}),
z.object({
_action: _action("UPDATE_NOTE"),
value: z.preprocess(
falsyToNull,
z.string().max(SENDOUQ.NOTE_MAX_LENGTH).nullable(),
),
}),
]);
const winners = z.preprocess(

View File

@ -25,6 +25,7 @@ export type LookingGroup = {
customUrl?: User["customUrl"];
plusTier?: PlusTier["tier"];
role: GroupMember["role"];
note?: GroupMember["note"];
weapons?: MainWeaponId[];
skill?: TieredSkill;
vc?: User["vc"];

View File

@ -175,6 +175,10 @@
font-weight: var(--semi-bold);
}
.q__group-member__note-textarea {
height: 4rem !important;
}
.q__member-placeholder {
border-radius: 100%;
background-color: var(--bg-lightest);

View File

@ -14,6 +14,7 @@ const stm = sql.prepare(/* sql */ `
"User"."discordName",
"User"."discordAvatar",
"GroupMember"."role",
"GroupMember"."note",
json_group_array("UserWeapon"."weaponSplId") as "weapons"
from
"Group"
@ -40,6 +41,7 @@ const stm = sql.prepare(/* sql */ `
'discordName', "q1"."discordName",
'discordAvatar', "q1"."discordAvatar",
'role', "q1"."role",
'note', "q1"."note",
'weapons', "q1"."weapons"
)
) as "members"

View File

@ -23,6 +23,7 @@ const stm = sql.prepare(/* sql */ `
"User"."languages",
"PlusTier"."tier" as "plusTier",
"GroupMember"."role",
"GroupMember"."note",
json_group_array("UserWeapon"."weaponSplId") as "weapons"
from
"Group"
@ -55,6 +56,7 @@ const stm = sql.prepare(/* sql */ `
'customUrl', "q1"."customUrl",
'plusTier', "q1"."plusTier",
'role', "q1"."role",
'note', "q1"."note",
'weapons', "q1"."weapons",
'vc', "q1"."vc",
'languages', "q1"."languages"

View File

@ -0,0 +1,16 @@
import { sql } from "~/db/sql";
import type { GroupMember } from "~/db/types";
const stm = sql.prepare(/* sql */ `
update "GroupMember"
set "note" = @note
where "groupId" = @groupId and "userId" = @userId
`);
export function updateNote(args: {
note: GroupMember["note"];
groupId: GroupMember["groupId"];
userId: GroupMember["userId"];
}) {
stm.run(args);
}

View File

@ -70,6 +70,8 @@ import { currentOrPreviousSeason } from "~/features/mmr/season";
import { Chat, useChat } from "~/components/Chat";
import { NewTabs } from "~/components/NewTabs";
import { useWindowSize } from "~/hooks/useWindowSize";
import { updateNote } from "../queries/updateNote.server";
import { GroupLeaver } from "../components/GroupLeaver";
export const handle: SendouRouteHandle = {
i18n: ["q"],
@ -272,6 +274,16 @@ export const action: ActionFunction = async ({ request }) => {
break;
}
case "UPDATE_NOTE": {
updateNote({
note: data.value,
groupId: currentGroup.id,
userId: user.id,
});
refreshGroup(currentGroup.id);
break;
}
default: {
assertUnreachable(data);
}
@ -499,6 +511,9 @@ function Groups() {
trustedPlayers={data.trustedPlayers}
/>
) : null}
<GroupLeaver
type={ownGroup.members.length === 1 ? "LEAVE_Q" : "LEAVE_GROUP"}
/>
</div>
);

View File

@ -36,6 +36,7 @@ import { trustedPlayersAvailableToPlay } from "../queries/usersInActiveGroup.ser
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
import { userHasSkill } from "../queries/userHasSkill.server";
import { currentSeason } from "~/features/mmr";
import { GroupLeaver } from "../components/GroupLeaver";
export const handle: SendouRouteHandle = {
i18n: ["q"],
@ -144,7 +145,7 @@ export default function QPreparingPage() {
return (
<Main className="stack lg items-center">
<div className="q-preparing__card-container">
<GroupCard group={data.group} ownRole={data.role} ownGroup />
<GroupCard group={data.group} ownRole={data.role} ownGroup hideNote />
</div>
{data.group.members.length < FULL_GROUP_SIZE &&
hasGroupManagerPerms(data.role) ? (
@ -162,6 +163,9 @@ export default function QPreparingPage() {
Join the queue
</SubmitButton>
</joinQFetcher.Form>
<GroupLeaver
type={data.group.members.length === 1 ? "GO_BACK" : "LEAVE_GROUP"}
/>
</Main>
);
}

View File

@ -77,6 +77,12 @@ a {
padding-inline: var(--s-2);
}
:is(button, .button).miniscule {
font-size: var(--fonts-xxs);
padding-block: var(--s-1);
padding-inline: var(--s-2);
}
:is(button, .button).big {
font-size: var(--fonts-md);
padding-block: var(--s-2-5);

View File

@ -0,0 +1,5 @@
module.exports.up = function (db) {
db.transaction(() => {
db.prepare(/* sql */ `alter table "GroupMember" add "note" text`).run();
})();
};