Favorite weapons in user weapon pool Closes #1346

This commit is contained in:
Kalle
2023-06-17 16:56:51 +03:00
parent 600c67f765
commit 2c6bd22249
173 changed files with 152 additions and 44 deletions

View File

@@ -3,6 +3,7 @@ import type { MainWeaponId, ModeShort, StageId } from "~/modules/in-game-lists";
import {
mainWeaponImageUrl,
modeImageUrl,
outlinedFiveStarMainWeaponImageUrl,
outlinedMainWeaponImageUrl,
stageImageUrl,
} from "~/utils/urls";
@@ -59,7 +60,7 @@ export function Image({
type WeaponImageProps = {
weaponSplId: MainWeaponId;
variant: "badge" | "build";
variant: "badge" | "badge-5-star" | "build";
} & Omit<ImageProps, "path" | "alt" | "title">;
export function WeaponImage({
@@ -79,6 +80,8 @@ export function WeaponImage({
path={
variant === "badge"
? outlinedMainWeaponImageUrl(weaponSplId)
: variant == "badge-5-star"
? outlinedFiveStarMainWeaponImageUrl(weaponSplId)
: mainWeaponImageUrl(weaponSplId)
}
/>

View File

@@ -0,0 +1,18 @@
export function StarIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
/>
</svg>
);
}

View File

@@ -0,0 +1,16 @@
export function StarFilledIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={className}
>
<path
fillRule="evenodd"
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@@ -1,4 +1,4 @@
insert into
"UserWeapon" ("userId", "weaponSplId", "order")
"UserWeapon" ("userId", "weaponSplId", "order", "isFavorite")
values
(@userId, @weaponSplId, @order);
(@userId, @weaponSplId, @order, @isFavorite);

View File

@@ -5,7 +5,14 @@ select
"Team"."id" as "teamId",
"UserSubmittedImage"."url" as "teamAvatarUrl",
"PlusTier"."tier" as "plusTier",
json_group_array("UserWeapon"."weaponSplId") as "weapons"
json_group_array(
json_object(
'weaponSplId',
"UserWeapon"."weaponSplId",
'isFavorite',
"UserWeapon"."isFavorite"
)
) as "weapons"
from
"User"
left join "PlusTier" on "PlusTier"."userId" = "User"."id"

View File

@@ -3,9 +3,9 @@ import type {
CalendarEventResultTeam,
SplatoonPlayer,
User,
UserWeapon,
UserWithPlusTier,
} from "../../types";
import type { MainWeaponId } from "~/modules/in-game-lists";
import { parseDBArray } from "~/utils/sql";
import { dateToDatabaseTimestamp } from "~/utils/dates";
@@ -68,10 +68,15 @@ export const updateProfile = sql.transaction(
| "css"
| "favoriteBadgeId"
| "showDiscordUniqueName"
> & { weapons: MainWeaponId[] }) => {
> & { weapons: Pick<UserWeapon, "weaponSplId" | "isFavorite">[] }) => {
deleteUserWeaponsStm.run({ userId: rest.id });
for (const [i, weaponSplId] of weapons.entries()) {
addUserWeaponStm.run({ userId: rest.id, weaponSplId, order: i + 1 });
for (const [i, weapon] of weapons.entries()) {
addUserWeaponStm.run({
userId: rest.id,
weaponSplId: weapon.weaponSplId,
isFavorite: weapon.isFavorite,
order: i + 1,
});
}
return updateProfileStm.get(rest) as User;
@@ -151,7 +156,7 @@ export function findByIdentifier(identifier: string | number) {
} as
| (Omit<UserWithPlusTier, "css"> & {
css: Record<string, string>;
weapons: MainWeaponId[];
weapons: Pick<UserWeapon, "weaponSplId" | "isFavorite">[];
team?: {
name: string;
customUrl: string;

View File

@@ -46,6 +46,7 @@ export interface UserWeapon {
weaponSplId: MainWeaponId;
createdAt: number;
order: number;
isFavorite: number;
}
export interface PlusSuggestion {

View File

@@ -22,12 +22,13 @@ import { Label } from "~/components/Label";
import { SubmitButton } from "~/components/SubmitButton";
import { USER } from "~/constants";
import { db } from "~/db";
import type { UserWeapon } from "~/db/types";
import { type User } from "~/db/types";
import { useTranslation } from "~/hooks/useTranslation";
import { useUser } from "~/modules/auth";
import { requireUser, requireUserId } from "~/modules/auth/user.server";
import { i18next } from "~/modules/i18n";
import { mainWeaponIds, type MainWeaponId } from "~/modules/in-game-lists";
import { type MainWeaponId } from "~/modules/in-game-lists";
import { canAddCustomizedColorsToUserProfile } from "~/permissions";
import styles from "~/styles/u-edit.css";
import { translatedCountry } from "~/utils/i18n.server";
@@ -43,12 +44,14 @@ import {
id,
jsonParseable,
processMany,
removeDuplicates,
weaponSplId,
safeJSONParse,
undefinedToNull,
} from "~/utils/zod";
import { userParamsSchema, type UserPageLoaderData } from "../u.$identifier";
import { Toggle } from "~/components/Toggle";
import { StarIcon } from "~/components/icons/Star";
import { StarFilledIcon } from "~/components/icons/StarFilled";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
@@ -114,14 +117,13 @@ const userEditActionSchema = z
),
css: z.preprocess(falsyToNull, z.string().refine(jsonParseable).nullable()),
weapons: z.preprocess(
processMany(safeJSONParse, removeDuplicates),
safeJSONParse,
z
.array(
z
.number()
.refine((val) =>
mainWeaponIds.includes(val as (typeof mainWeaponIds)[number])
)
z.object({
weaponSplId,
isFavorite: dbBoolean,
})
)
.max(USER.WEAPON_POOL_MAX_SIZE)
),
@@ -163,7 +165,9 @@ export const action: ActionFunction = async ({ request }) => {
try {
const editedUser = db.users.updateProfile({
...data,
weapons: data.weapons as MainWeaponId[],
weapons: data.weapons as Array<
Pick<UserWeapon, "weaponSplId" | "isFavorite">
>,
inGameName:
inGameNameText && inGameNameDiscriminator
? `${inGameNameText}#${inGameNameDiscriminator}`
@@ -389,11 +393,11 @@ function WeaponPoolSelect({
}: {
parentRouteData: UserPageLoaderData;
}) {
const [weapons, setWeapons] = React.useState<Array<MainWeaponId>>(
parentRouteData.weapons
);
const [weapons, setWeapons] = React.useState(parentRouteData.weapons);
const { t } = useTranslation(["user"]);
const latestWeapon = weapons[weapons.length - 1];
return (
<div className="stack md u-edit__weapon-pool">
<input type="hidden" name="weapons" value={JSON.stringify(weapons)} />
@@ -405,11 +409,17 @@ function WeaponPoolSelect({
id="weapon"
onChange={(weapon) => {
if (!weapon) return;
setWeapons([...weapons, Number(weapon.value) as MainWeaponId]);
setWeapons([
...weapons,
{
weaponSplId: Number(weapon.value) as MainWeaponId,
isFavorite: 0,
},
]);
}}
// empty on selection
key={weapons[weapons.length - 1]}
weaponIdsToOmit={new Set(weapons)}
key={latestWeapon?.weaponSplId ?? "empty"}
weaponIdsToOmit={new Set(weapons.map((w) => w.weaponSplId))}
fullWidth
/>
) : (
@@ -421,22 +431,48 @@ function WeaponPoolSelect({
<div className="stack horizontal sm justify-center">
{weapons.map((weapon) => {
return (
<div key={weapon} className="stack xs">
<div key={weapon.weaponSplId} className="stack xs">
<div className="u__weapon">
<WeaponImage
weaponSplId={weapon}
variant="badge"
weaponSplId={weapon.weaponSplId}
variant={weapon.isFavorite ? "badge-5-star" : "badge"}
width={38}
height={38}
/>
</div>
<Button
icon={<TrashIcon />}
variant="minimal-destructive"
aria-label="Delete weapon"
onClick={() => setWeapons(weapons.filter((w) => w !== weapon))}
testId={`delete-weapon-${weapon}`}
/>
<div className="stack sm horizontal items-center justify-center">
<Button
icon={weapon.isFavorite ? <StarFilledIcon /> : <StarIcon />}
variant="minimal"
aria-label="Favorite weapon"
onClick={() =>
setWeapons(
weapons.map((w) =>
w.weaponSplId === weapon.weaponSplId
? {
...weapon,
isFavorite: weapon.isFavorite === 1 ? 0 : 1,
}
: w
)
)
}
/>
<Button
icon={<TrashIcon />}
variant="minimal-destructive"
aria-label="Delete weapon"
onClick={() =>
setWeapons(
weapons.filter(
(w) => w.weaponSplId !== weapon.weaponSplId
)
)
}
testId={`delete-weapon-${weapon.weaponSplId}`}
size="tiny"
/>
</div>
</div>
);
})}

View File

@@ -216,11 +216,11 @@ function WeaponPool() {
<div className="stack horizontal sm justify-center">
{data.weapons.map((weapon, i) => {
return (
<div key={weapon} className="u__weapon">
<div key={weapon.weaponSplId} className="u__weapon">
<WeaponImage
testId={`${weapon}-${i + 1}`}
weaponSplId={weapon}
variant="badge"
testId={`${weapon.weaponSplId}-${i + 1}`}
weaponSplId={weapon.weaponSplId}
variant={weapon.isFavorite ? "badge-5-star" : "badge"}
width={38}
height={38}
/>

View File

@@ -56,6 +56,10 @@
border-radius: 50%;
}
.u__extra-info__heading > svg {
width: 0.8rem;
}
.u__social-link > svg {
width: 0.9rem;
}
@@ -110,10 +114,6 @@
font-weight: var(--bold);
}
.u__extra-info__heading > svg {
width: 0.8rem;
}
.u__badges {
display: flex;
max-width: 24rem;

View File

@@ -265,6 +265,9 @@ export const mainWeaponImageUrl = (mainWeaponSplId: MainWeaponId) =>
`/static-assets/img/main-weapons/${mainWeaponSplId}`;
export const outlinedMainWeaponImageUrl = (mainWeaponSplId: MainWeaponId) =>
`/static-assets/img/main-weapons-outlined/${mainWeaponSplId}`;
export const outlinedFiveStarMainWeaponImageUrl = (
mainWeaponSplId: MainWeaponId
) => `/static-assets/img/main-weapons-outlined-2/${mainWeaponSplId}`;
export const subWeaponImageUrl = (subWeaponSplId: SubWeaponId) =>
`/static-assets/img/sub-weapons/${subWeaponSplId}`;
export const specialWeaponImageUrl = (specialWeaponSplId: SpecialWeaponId) =>

View File

@@ -3,4 +3,7 @@ module.exports.up = function (db) {
db.prepare(
/* sql */ `alter table "User" add "showDiscordUniqueName" integer not null default 1`
).run();
db.prepare(
/* sql */ `alter table "UserWeapon" add "isFavorite" integer not null default 0`
).run();
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Some files were not shown because too many files have changed in this diff Show More