mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Build cards initial
This commit is contained in:
parent
b8b84dd7f9
commit
0f0542f2e9
33
app/components/Ability.tsx
Normal file
33
app/components/Ability.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
122
app/components/BuildCard.tsx
Normal file
122
app/components/BuildCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
select
|
||||
count(*)
|
||||
count(*) as "count"
|
||||
from
|
||||
"Build"
|
||||
where
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%);
|
||||
|
|
|
|||
|
|
@ -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
BIN
public/img/modes/CB.avif
Normal file
Binary file not shown.
BIN
public/img/modes/CB.png
Normal file
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
BIN
public/img/modes/RM.avif
Normal file
Binary file not shown.
BIN
public/img/modes/RM.png
Normal file
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
BIN
public/img/modes/SZ.avif
Normal file
Binary file not shown.
BIN
public/img/modes/SZ.png
Normal file
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
BIN
public/img/modes/TC.avif
Normal file
Binary file not shown.
BIN
public/img/modes/TC.png
Normal file
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
BIN
public/img/modes/TW.avif
Normal file
Binary file not shown.
BIN
public/img/modes/TW.png
Normal file
BIN
public/img/modes/TW.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
Loading…
Reference in New Issue
Block a user