Build cards initial

This commit is contained in:
Kalle 2022-08-31 17:26:24 +03:00
parent b8b84dd7f9
commit 0f0542f2e9
20 changed files with 298 additions and 29 deletions

View File

@ -0,0 +1,33 @@
import type { Ability as AbilityType } from "~/modules/in-game-lists";
import { abilityImageUrl } from "~/utils/urls";
import { Image } from "./Image";
const sizeMap = {
MAIN: 42,
SUB: 32,
TINY: 22,
} as const;
export function Ability({
ability,
size,
}: {
ability: AbilityType;
size: keyof typeof sizeMap;
}) {
const sizeNumber = sizeMap[size];
return (
<div
className="build__ability"
style={
{
"--ability-size": `${sizeNumber}px`,
} as any
}
>
{/* xxx: make ticket for this or fix */}
<Image alt="" path={abilityImageUrl(ability)} />
</div>
);
}

View File

@ -0,0 +1,122 @@
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import type { AllAbilitiesTuple } from "~/db/models/builds/queries.server";
import type { Build, BuildWeapon } from "~/db/types";
import { useIsMounted } from "~/hooks/useIsMounted";
import type { ModeShort } from "~/modules/in-game-lists";
import { databaseTimestampToDate } from "~/utils/dates";
import { gearImageUrl, modeImageUrl, weaponImageUrl } from "~/utils/urls";
import { Ability } from "./Ability";
import { Image } from "./Image";
type BuildProps = Pick<
Build,
| "title"
| "description"
| "clothesGearSplId"
| "headGearSplId"
| "shoesGearSplId"
| "updatedAt"
| "modes"
> & {
abilities: AllAbilitiesTuple;
modes: ModeShort[] | null;
weapons: Array<BuildWeapon["weaponSplId"]>;
};
export function BuildCard({
title,
weapons,
updatedAt,
headGearSplId,
clothesGearSplId,
shoesGearSplId,
abilities,
modes,
}: BuildProps) {
const { i18n } = useTranslation();
const isMounted = useIsMounted();
return (
<div className="build">
<div>
<div className="build__top-row">
<h2 className="build__title">{title}</h2>
{modes && modes.length > 0 && (
<div className="build__modes">
{modes.map((mode) => (
<Image
// xxx: alt to translated name + title
// xxx: maybe border same as gear img?
alt=""
path={modeImageUrl(mode)}
width={18}
height={18}
/>
))}
</div>
)}
</div>
<time className={clsx("build__date", { invisible: !isMounted })}>
{isMounted
? databaseTimestampToDate(updatedAt).toLocaleDateString(
i18n.language,
{
day: "numeric",
month: "numeric",
year: "numeric",
}
)
: "t"}
</time>
</div>
<div className="build__weapons">
{weapons.map((weaponSplId) => (
<Image
key={weaponSplId}
path={weaponImageUrl(weaponSplId)}
alt=""
height={36}
width={36}
/>
))}
</div>
<div className="build__gear-abilities">
{/* xxx: extract this to component instead of duplicating */}
<Image
height={64}
width={64}
/* xxx: make ticket for this or fix */
alt=""
path={gearImageUrl("head", headGearSplId)}
className="build__gear"
/>
{abilities[0].map((ability, i) => (
<Ability key={i} ability={ability} size={i === 0 ? "MAIN" : "SUB"} />
))}
<Image
height={64}
width={64}
/* xxx: make ticket for this or fix */
alt=""
path={gearImageUrl("clothes", clothesGearSplId)}
className="build__gear"
/>
{abilities[1].map((ability, i) => (
<Ability key={i} ability={ability} size={i === 0 ? "MAIN" : "SUB"} />
))}
<Image
height={64}
width={64}
/* xxx: make ticket for this or fix */
alt=""
path={gearImageUrl("shoes", shoesGearSplId)}
className="build__gear"
/>
{abilities[2].map((ability, i) => (
<Ability key={i} ability={ability} size={i === 0 ? "MAIN" : "SUB"} />
))}
</div>
</div>
);
}

View File

@ -1,5 +1,5 @@
select
count(*)
count(*) as "count"
from
"Build"
where

View File

@ -35,6 +35,7 @@ export const create = sql.transaction((build: CreateArgs) => {
ownerId: build.ownerId,
title: build.title,
description: build.description,
// xxx: sort these
modes: build.modes ? JSON.stringify(build.modes) : null,
headGearSplId: build.headGearSplId,
clothesGearSplId: build.clothesGearSplId,
@ -98,14 +99,16 @@ function augmentBuild<T>(
};
}
const gearOrder: Array<BuildAbility["gearType"]> = ["HEAD", "CLOTHES", "SHOES"];
function dbAbilitiesToArrayOfArrays(
abilities: Array<Pick<BuildAbility, "ability" | "gearType" | "slotIndex">>
): [
export type AllAbilitiesTuple = [
head: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
clothes: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
shoes: [main: Ability, s1: Ability, s2: Ability, s3: Ability]
] {
];
const gearOrder: Array<BuildAbility["gearType"]> = ["HEAD", "CLOTHES", "SHOES"];
function dbAbilitiesToArrayOfArrays(
abilities: Array<Pick<BuildAbility, "ability" | "gearType" | "slotIndex">>
): AllAbilitiesTuple {
const sorted = abilities
.slice()
.sort((a, b) => {

View File

@ -57,6 +57,7 @@ export function seed() {
function wipeDB() {
const tablesToDelete = [
"Build",
"CalendarEventDate",
"CalendarEventResultPlayer",
"CalendarEventResultTeam",
@ -509,8 +510,8 @@ function adminBuilds() {
)}`,
ownerId: 1,
description: Math.random() < 0.75 ? faker.lorem.paragraph() : null,
clothesGearSplId: randomOrderHeadGear[0]!,
headGearSplId: randomOrderClothesGear[0]!,
headGearSplId: randomOrderHeadGear[0]!,
clothesGearSplId: randomOrderClothesGear[0]!,
shoesGearSplId: randomOrderShoesGear[0]!,
weaponSplIds: new Array(
faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5, 6])
@ -523,18 +524,28 @@ function adminBuilds() {
: null,
abilities: new Array(12).fill(null).map((_, i) => {
const gearType = i < 4 ? "HEAD" : i < 8 ? "CLOTHES" : "SHOES";
const isMain = i === 0 || i === 4 || i === 8;
const randomOrderAbilities = shuffle([...abilityCodes]);
const getAbility = () => {
const legalAbilityForSlot = randomOrderAbilities.find((ability) => {
if (ability.type === "HEAD_ONLY" && gearType !== "HEAD") {
if (
ability.type === "HEAD_MAIN_ONLY" &&
(gearType !== "HEAD" || !isMain)
) {
return false;
}
if (ability.type === "CLOTHES_ONLY" && gearType !== "CLOTHES") {
if (
ability.type === "CLOTHES_MAIN_ONLY" &&
(gearType !== "CLOTHES" || !isMain)
) {
return false;
}
if (ability.type === "SHOES_ONLY" && gearType !== "SHOES") {
if (
ability.type === "SHOES_MAIN_ONLY" &&
(gearType !== "SHOES" || !isMain)
) {
return false;
}

View File

@ -1,30 +1,30 @@
export const abilityCodes = [
{ name: "AD", type: "CLOTHES_ONLY" },
{ name: "AD", type: "CLOTHES_MAIN_ONLY" },
{ name: "BRU", type: "STACKABLE" },
{ name: "CB", type: "HEAD_ONLY" },
{ name: "DR", type: "SHOES_ONLY" },
{ name: "H", type: "CLOTHES_ONLY" },
{ name: "CB", type: "HEAD_MAIN_ONLY" },
{ name: "DR", type: "SHOES_MAIN_ONLY" },
{ name: "H", type: "CLOTHES_MAIN_ONLY" },
{ name: "IA", type: "STACKABLE" },
{ name: "IRU", type: "STACKABLE" },
{ name: "ISM", type: "STACKABLE" },
{ name: "ISS", type: "STACKABLE" },
{ name: "LDE", type: "HEAD_ONLY" },
{ name: "NS", type: "CLOTHES_ONLY" },
{ name: "OG", type: "HEAD_ONLY" },
{ name: "OS", type: "SHOES_ONLY" },
{ name: "LDE", type: "HEAD_MAIN_ONLY" },
{ name: "NS", type: "CLOTHES_MAIN_ONLY" },
{ name: "OG", type: "HEAD_MAIN_ONLY" },
{ name: "OS", type: "SHOES_MAIN_ONLY" },
{ name: "QR", type: "STACKABLE" },
{ name: "QSJ", type: "STACKABLE" },
{ name: "RES", type: "STACKABLE" },
{ name: "RP", type: "CLOTHES_ONLY" },
{ name: "RP", type: "CLOTHES_MAIN_ONLY" },
{ name: "RSU", type: "STACKABLE" },
{ name: "SCU", type: "STACKABLE" },
{ name: "SJ", type: "SHOES_ONLY" },
{ name: "SJ", type: "SHOES_MAIN_ONLY" },
{ name: "SPU", type: "STACKABLE" },
{ name: "SRU", type: "STACKABLE" },
{ name: "SS", type: "STACKABLE" },
{ name: "SSU", type: "STACKABLE" },
{ name: "T", type: "CLOTHES_ONLY" },
{ name: "TI", type: "CLOTHES_ONLY" },
{ name: "T", type: "HEAD_MAIN_ONLY" },
{ name: "TI", type: "CLOTHES_MAIN_ONLY" },
] as const;
export const abilitiesShort = abilityCodes.map((ability) => ability.name);

View File

@ -1,5 +1,6 @@
import { json, type LoaderArgs } from "@remix-run/node";
// import { useLoaderData } from "@remix-run/react";
import { useLoaderData } from "@remix-run/react";
import { BuildCard } from "~/components/BuildCard";
import { Main } from "~/components/Main";
import { db } from "~/db";
import { notFoundIfFalsy } from "~/utils/remix";
@ -15,9 +16,26 @@ export const loader = ({ params }: LoaderArgs) => {
};
export default function UserBuildsPage() {
// const data = useLoaderData<typeof loader>();
const data = useLoaderData<typeof loader>();
// console.log({ data });
return <Main>hey</Main>;
return (
<Main>
<div className="builds-container">
{data.builds.map((build) => (
<BuildCard
key={build.id}
title={build.title}
description={build.description}
headGearSplId={build.headGearSplId}
clothesGearSplId={build.clothesGearSplId}
shoesGearSplId={build.shoesGearSplId}
modes={build.modes}
updatedAt={build.updatedAt}
abilities={build.abilities}
weapons={build.weapons}
/>
))}
</div>
</Main>
);
}

View File

@ -647,3 +647,72 @@ dialog::backdrop {
margin-inline-end: 0 !important;
margin-inline-start: var(--s-1);
}
.builds-container {
display: grid;
justify-content: center;
gap: var(--s-3);
grid-template-columns: repeat(auto-fit, 15rem);
}
.build {
display: flex;
flex-direction: column;
padding: var(--s-2-5);
background-color: var(--bg-lighter);
border-radius: var(--rounded);
gap: var(--s-3);
}
.build__title {
font-size: var(--fonts-sm);
word-wrap: break-word;
}
.build__top-row {
display: flex;
justify-content: space-between;
}
.build__date {
display: block;
font-size: var(--fonts-xxxs);
}
.build__modes {
display: flex;
min-width: max-content;
gap: var(--s-1);
}
.build__weapons {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.build__gear-abilities {
display: grid;
column-gap: var(--s-1);
grid-template-columns: repeat(5, max-content);
place-items: center;
row-gap: var(--s-2);
}
.build__gear {
background-color: var(--bg-darker-very-transparent);
border-radius: 50%;
}
.build__ability {
width: var(--ability-size);
height: var(--ability-size);
border: 2px solid var(--theme-transparent);
border-right: 0;
border-bottom: 0;
background: var(--bg-ability);
background-size: 100%;
border-radius: 50%;
box-shadow: 0 0 0 1px var(--bg-ability);
user-select: none;
}

View File

@ -5,6 +5,7 @@
--bg-lighter-transparent: hsla(225deg 100% 88% / 50%);
--bg-darker-very-transparent: hsl(202deg 90% 90% / 50%);
--bg-darker-transparent: hsla(202deg 90% 90% / 65%);
--bg-ability: rgb(3 6 7);
--bg-badge: #000;
--badge-text: rgb(255 255 255 / 95%);
--border: hsl(237deg 100% 86%);
@ -80,6 +81,7 @@
--bg-lighter-transparent: rgb(64 67 108 / 50%);
--bg-darker-very-transparent: hsla(237.3deg 42.3% 26.6% / 50%);
--bg-darker-transparent: hsla(237.3deg 42.3% 26.6% / 90%);
--bg-ability: rgb(17 19 43);
--bg-badge: #000;
--border: hsl(237.3deg 42.3% 45.6%);
--button-text: rgb(0 0 0 / 85%);

View File

@ -1,4 +1,5 @@
import type { Badge } from "~/db/types";
import type { Ability, ModeShort } from "~/modules/in-game-lists";
export const SENDOU_INK_DISCORD_URL = "https://discord.gg/sendou";
export const SENDOU_TWITTER_URL = "https://twitter.com/sendouc";
@ -40,10 +41,20 @@ export const badgeUrl = ({
code: Badge["code"];
extension?: "gif";
}) => `/badges/${code}${extension ? `.${extension}` : ""}`;
export const navIconUrl = (navItem: string) => `/img/layout/${navItem}`;
export const articlePreviewUrl = (slug: string) =>
`/img/article-previews/${slug}.png`;
export const navIconUrl = (navItem: string) => `/img/layout/${navItem}`;
export const gearImageUrl = (
gearType: "head" | "clothes" | "shoes",
gearSplId: number
) => `/img/gear/${gearType}/${gearSplId}`;
export const weaponImageUrl = (weaponSplId: number) =>
`/img/weapons/${weaponSplId}`;
export const abilityImageUrl = (ability: Ability) =>
`/img/abilities/${ability}`;
export const modeImageUrl = (mode: ModeShort) => `/img/modes/${mode}`;
export function resolveBaseUrl(url: string) {
return new URL(url).host;
}

BIN
public/img/modes/CB.avif Normal file

Binary file not shown.

BIN
public/img/modes/CB.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
public/img/modes/RM.avif Normal file

Binary file not shown.

BIN
public/img/modes/RM.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/img/modes/SZ.avif Normal file

Binary file not shown.

BIN
public/img/modes/SZ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/modes/TC.avif Normal file

Binary file not shown.

BIN
public/img/modes/TC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/modes/TW.avif Normal file

Binary file not shown.

BIN
public/img/modes/TW.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB