From 4ed1cbc23fba5699a13cc6e1e836699c79f237ec Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 25 Jan 2023 00:52:24 +0200 Subject: [PATCH] Team custom colors --- app/components/CustomizedColorsInput.tsx | 2 +- app/features/img-upload/routes/upload.tsx | 8 +- app/features/team/queries/edit.server.ts | 7 +- .../team/queries/findByIdentifier.server.ts | 75 +++++++++++-------- .../team/routes/t.$customUrl.edit.tsx | 18 +++-- .../team/routes/t.$customUrl.join.tsx | 4 +- .../team/routes/t.$customUrl.roster.tsx | 4 +- app/features/team/routes/t.$customUrl.tsx | 12 ++- app/features/team/team-schemas.server.ts | 3 +- app/features/team/team-types.ts | 1 + app/features/team/team-utils.ts | 6 ++ 11 files changed, 84 insertions(+), 56 deletions(-) diff --git a/app/components/CustomizedColorsInput.tsx b/app/components/CustomizedColorsInput.tsx index 2add341d8..37ea6ef9e 100644 --- a/app/components/CustomizedColorsInput.tsx +++ b/app/components/CustomizedColorsInput.tsx @@ -19,7 +19,7 @@ type CustomColorsRecord = Partial< export function CustomizedColorsInput({ initialColors, }: { - initialColors?: Record; + initialColors?: Record | null; }) { const { t } = useTranslation(); const [colors, setColors] = React.useState( diff --git a/app/features/img-upload/routes/upload.tsx b/app/features/img-upload/routes/upload.tsx index e04837423..8240e5c95 100644 --- a/app/features/img-upload/routes/upload.tsx +++ b/app/features/img-upload/routes/upload.tsx @@ -37,8 +37,8 @@ export const action = async ({ request }: ActionArgs) => { validate(validatedType); validate(user.team); - const detailedTeam = findByIdentifier(user.team.customUrl); - validate(detailedTeam && isTeamOwner({ team: detailedTeam, user })); + const detailed = findByIdentifier(user.team.customUrl); + validate(detailed && isTeamOwner({ team: detailed.team, user })); // TODO: graceful error handling when uploading many images validate(countUnvalidatedImg(user.id) < MAX_UNVALIDATED_IMG_COUNT); @@ -82,9 +82,9 @@ export const loader = async ({ request }: LoaderArgs) => { throw redirect("/"); } - const detailedTeam = findByIdentifier(user.team.customUrl); + const detailed = findByIdentifier(user.team.customUrl); - if (!detailedTeam || !isTeamOwner({ team: detailedTeam, user })) { + if (!detailed || !isTeamOwner({ team: detailed.team, user })) { throw redirect("/"); } diff --git a/app/features/team/queries/edit.server.ts b/app/features/team/queries/edit.server.ts index 3dad8e9ea..48cb7ef5c 100644 --- a/app/features/team/queries/edit.server.ts +++ b/app/features/team/queries/edit.server.ts @@ -7,7 +7,8 @@ const stm = sql.prepare(/*sql*/ ` "name" = @name, "customUrl" = @customUrl, "bio" = @bio, - "twitter" = @twitter + "twitter" = @twitter, + "css" = @css where "id" = @id returning * `); @@ -18,12 +19,14 @@ export function edit({ customUrl, bio, twitter, -}: Pick) { + css, +}: Pick) { return stm.get({ id, name, customUrl, bio, twitter, + css, }) as Team; } diff --git a/app/features/team/queries/findByIdentifier.server.ts b/app/features/team/queries/findByIdentifier.server.ts index a6f870440..71eaf3023 100644 --- a/app/features/team/queries/findByIdentifier.server.ts +++ b/app/features/team/queries/findByIdentifier.server.ts @@ -10,6 +10,7 @@ const teamStm = sql.prepare(/*sql*/ ` "t"."twitter", "t"."bio", "t"."customUrl", + "t"."css", "ia"."url" as "avatarSrc", "ib"."url" as "bannerSrc", json_group_array("User"."country") as "countries" @@ -29,6 +30,7 @@ const membersStm = sql.prepare(/*sql*/ ` "User"."discordAvatar", "User"."discordId", "User"."discordDiscriminator", + "User"."patronTier", "TeamMember"."role", "TeamMember"."isOwner", json_group_array("UserWeapon"."weaponSplId") as "weapons" @@ -40,7 +42,7 @@ const membersStm = sql.prepare(/*sql*/ ` `); type TeamRow = - | (Pick & { + | (Pick & { avatarSrc: string; bannerSrc: string; countries: string; @@ -55,11 +57,14 @@ type MemberRows = Array< | "discordAvatar" | "discordId" | "discordDiscriminator" + | "patronTier" > & Pick & { weapons: string } >; -export function findByIdentifier(customUrl: string): DetailedTeam | null { +export function findByIdentifier( + customUrl: string +): { team: DetailedTeam; css: Record | null } | null { const team = teamStm.get({ customUrl: customUrl.toLowerCase() }) as TeamRow; if (!team) return null; @@ -67,36 +72,40 @@ export function findByIdentifier(customUrl: string): DetailedTeam | null { const members = membersStm.all({ teamId: team.id }) as MemberRows; return { - id: team.id, - name: team.name, - customUrl: team.customUrl, - twitter: team.twitter ?? undefined, - bio: team.bio ?? undefined, - avatarSrc: team.avatarSrc, - bannerSrc: team.bannerSrc, - countries: removeDuplicates(JSON.parse(team.countries).filter(Boolean)), - members: members.map((member) => ({ - id: member.id, - discordAvatar: member.discordAvatar, - discordId: member.discordId, - discordName: member.discordName, - discordDiscriminator: member.discordDiscriminator, - role: member.role ?? undefined, - isOwner: Boolean(member.isOwner), - weapons: JSON.parse(member.weapons).filter(Boolean), - })), - // results: { - // count: 23, - // placements: [ - // { - // count: 10, - // placement: 1, - // }, - // { - // count: 5, - // placement: 2, - // }, - // ], - // }, + css: team.css ? (JSON.parse(team.css) as Record) : null, + team: { + id: team.id, + name: team.name, + customUrl: team.customUrl, + twitter: team.twitter ?? undefined, + bio: team.bio ?? undefined, + avatarSrc: team.avatarSrc, + bannerSrc: team.bannerSrc, + countries: removeDuplicates(JSON.parse(team.countries).filter(Boolean)), + members: members.map((member) => ({ + id: member.id, + discordAvatar: member.discordAvatar, + discordId: member.discordId, + discordName: member.discordName, + discordDiscriminator: member.discordDiscriminator, + patronTier: member.patronTier, + role: member.role ?? undefined, + isOwner: Boolean(member.isOwner), + weapons: JSON.parse(member.weapons).filter(Boolean), + })), + // results: { + // count: 23, + // placements: [ + // { + // count: 10, + // placement: 1, + // }, + // { + // count: 5, + // placement: 2, + // }, + // ], + // }, + }, }; } diff --git a/app/features/team/routes/t.$customUrl.edit.tsx b/app/features/team/routes/t.$customUrl.edit.tsx index 1181e0b7b..1150143df 100644 --- a/app/features/team/routes/t.$customUrl.edit.tsx +++ b/app/features/team/routes/t.$customUrl.edit.tsx @@ -11,6 +11,7 @@ import { import { Form, Link, useLoaderData } from "@remix-run/react"; import * as React from "react"; import { Button } from "~/components/Button"; +import { CustomizedColorsInput } from "~/components/CustomizedColorsInput"; import { FormErrors } from "~/components/FormErrors"; import { FormMessage } from "~/components/FormMessage"; import { FormWithConfirm } from "~/components/FormWithConfirm"; @@ -39,7 +40,7 @@ import { edit } from "../queries/edit.server"; import { findByIdentifier } from "../queries/findByIdentifier.server"; import { TEAM } from "../team-constants"; import { editTeamSchema, teamParamsSchema } from "../team-schemas.server"; -import { isTeamOwner } from "../team-utils"; +import { canAddCustomizedColors, isTeamOwner } from "../team-utils"; import styles from "../team.css"; export const links: LinksFunction = () => { @@ -84,7 +85,7 @@ export const action: ActionFunction = async ({ request, params }) => { const user = await requireUserId(request); const { customUrl } = teamParamsSchema.parse(params); - const team = notFoundIfFalsy(findByIdentifier(customUrl)); + const { team } = notFoundIfFalsy(findByIdentifier(customUrl)); validate(isTeamOwner({ team, user })); @@ -101,10 +102,10 @@ export const action: ActionFunction = async ({ request, params }) => { } case "EDIT": { const newCustomUrl = mySlugify(data.name); - const existingTeam = findByIdentifier(newCustomUrl); + const existing = findByIdentifier(newCustomUrl); // can't take someone else's custom url - if (existingTeam && existingTeam.id !== team.id) { + if (existing && existing.team.id !== team.id) { return { errors: ["forms.errors.duplicateName"], }; @@ -128,18 +129,18 @@ export const loader = async ({ request, params }: LoaderArgs) => { const user = await requireUserId(request); const { customUrl } = teamParamsSchema.parse(params); - const team = notFoundIfFalsy(findByIdentifier(customUrl)); + const { team, css } = notFoundIfFalsy(findByIdentifier(customUrl)); if (!isTeamOwner({ team, user })) { throw redirect(teamPage(customUrl)); } - return { team }; + return { team, css }; }; export default function EditTeamPage() { const { t } = useTranslation(["common", "team"]); - const { team } = useLoaderData(); + const { team, css } = useLoaderData(); return (
@@ -157,6 +158,9 @@ export default function EditTeamPage() {
+ {canAddCustomizedColors(team) ? ( + + ) : null} diff --git a/app/features/team/routes/t.$customUrl.join.tsx b/app/features/team/routes/t.$customUrl.join.tsx index 44a601aad..84b492f5f 100644 --- a/app/features/team/routes/t.$customUrl.join.tsx +++ b/app/features/team/routes/t.$customUrl.join.tsx @@ -32,7 +32,7 @@ export const action: ActionFunction = async ({ request, params }) => { const user = await requireUser(request); const { customUrl } = teamParamsSchema.parse(params); - const team = notFoundIfFalsy(findByIdentifier(customUrl)); + const { team } = notFoundIfFalsy(findByIdentifier(customUrl)); const inviteCode = new URL(request.url).searchParams.get("code") ?? ""; const realInviteCode = inviteCodeById(team.id)!; @@ -59,7 +59,7 @@ export const loader = async ({ request, params }: LoaderArgs) => { const user = await requireUser(request); const { customUrl } = teamParamsSchema.parse(params); - const team = notFoundIfFalsy(findByIdentifier(customUrl)); + const { team } = notFoundIfFalsy(findByIdentifier(customUrl)); const inviteCode = new URL(request.url).searchParams.get("code") ?? ""; const realInviteCode = inviteCodeById(team.id)!; diff --git a/app/features/team/routes/t.$customUrl.roster.tsx b/app/features/team/routes/t.$customUrl.roster.tsx index bde3b316d..b63036331 100644 --- a/app/features/team/routes/t.$customUrl.roster.tsx +++ b/app/features/team/routes/t.$customUrl.roster.tsx @@ -63,7 +63,7 @@ export const action: ActionFunction = async ({ request, params }) => { const user = await requireUserId(request); const { customUrl } = teamParamsSchema.parse(params); - const team = notFoundIfFalsy(findByIdentifier(customUrl)); + const { team } = notFoundIfFalsy(findByIdentifier(customUrl)); validate(isTeamOwner({ team, user })); const data = await parseRequestFormData({ @@ -132,7 +132,7 @@ export const loader = async ({ request, params }: LoaderArgs) => { const user = await requireUserId(request); const { customUrl } = teamParamsSchema.parse(params); - const team = notFoundIfFalsy(findByIdentifier(customUrl)); + const { team } = notFoundIfFalsy(findByIdentifier(customUrl)); if (!isTeamOwner({ team, user })) { throw redirect(teamPage(customUrl)); diff --git a/app/features/team/routes/t.$customUrl.tsx b/app/features/team/routes/t.$customUrl.tsx index 11039cfa7..e97e9138f 100644 --- a/app/features/team/routes/t.$customUrl.tsx +++ b/app/features/team/routes/t.$customUrl.tsx @@ -41,7 +41,11 @@ import { findByIdentifier } from "../queries/findByIdentifier.server"; import { leaveTeam } from "../queries/leaveTeam.server"; import { teamParamsSchema } from "../team-schemas.server"; import type { DetailedTeamMember, TeamResultPeek } from "../team-types"; -import { isTeamMember, isTeamOwner } from "../team-utils"; +import { + canAddCustomizedColors, + isTeamMember, + isTeamOwner, +} from "../team-utils"; import styles from "../team.css"; export const meta: MetaFunction = ({ @@ -65,7 +69,7 @@ export const action: ActionFunction = async ({ request, params }) => { const user = await requireUserId(request); const { customUrl } = teamParamsSchema.parse(params); - const team = notFoundIfFalsy(findByIdentifier(customUrl)); + const { team } = notFoundIfFalsy(findByIdentifier(customUrl)); validate(isTeamMember({ user, team }) && !isTeamOwner({ user, team })); @@ -99,9 +103,9 @@ export const handle: SendouRouteHandle = { export const loader = ({ params }: LoaderArgs) => { const { customUrl } = teamParamsSchema.parse(params); - const team = notFoundIfFalsy(findByIdentifier(customUrl)); + const { team, css } = notFoundIfFalsy(findByIdentifier(customUrl)); - return { team }; + return { team, css: canAddCustomizedColors(team) ? css : null }; }; export default function TeamPage() { diff --git a/app/features/team/team-schemas.server.ts b/app/features/team/team-schemas.server.ts index a98ce8142..36b8bd930 100644 --- a/app/features/team/team-schemas.server.ts +++ b/app/features/team/team-schemas.server.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { falsyToNull, id } from "~/utils/zod"; +import { falsyToNull, id, jsonParseable } from "~/utils/zod"; import { TEAM, TEAM_MEMBER_ROLES } from "./team-constants"; export const teamParamsSchema = z.object({ customUrl: z.string() }); @@ -23,6 +23,7 @@ export const editTeamSchema = z.union([ falsyToNull, z.string().max(TEAM.TWITTER_MAX_LENGTH).nullable() ), + css: z.preprocess(falsyToNull, z.string().refine(jsonParseable).nullable()), }), ]); diff --git a/app/features/team/team-types.ts b/app/features/team/team-types.ts index 941539f7e..ef26605f5 100644 --- a/app/features/team/team-types.ts +++ b/app/features/team/team-types.ts @@ -23,6 +23,7 @@ export interface DetailedTeamMember { isOwner: boolean; weapons: MainWeaponId[]; role?: MemberRole; + patronTier: number | null; } export interface TeamResultPeek { diff --git a/app/features/team/team-utils.ts b/app/features/team/team-utils.ts index b747db2b5..96c6f7463 100644 --- a/app/features/team/team-utils.ts +++ b/app/features/team/team-utils.ts @@ -28,3 +28,9 @@ export function isTeamMember({ export function isTeamFull(team: DetailedTeam) { return team.members.length >= TEAM.MAX_MEMBER_COUNT; } + +export function canAddCustomizedColors(team: DetailedTeam) { + return team.members.some( + (member) => member.patronTier && member.patronTier >= 2 + ); +}