diff --git a/app/components/Ability.tsx b/app/components/Ability.tsx
new file mode 100644
index 000000000..83acb2967
--- /dev/null
+++ b/app/components/Ability.tsx
@@ -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 (
+
+ {/* xxx: make ticket for this or fix */}
+
+
+ );
+}
diff --git a/app/components/BuildCard.tsx b/app/components/BuildCard.tsx
new file mode 100644
index 000000000..ca504ffa2
--- /dev/null
+++ b/app/components/BuildCard.tsx
@@ -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;
+};
+
+export function BuildCard({
+ title,
+ weapons,
+ updatedAt,
+ headGearSplId,
+ clothesGearSplId,
+ shoesGearSplId,
+ abilities,
+ modes,
+}: BuildProps) {
+ const { i18n } = useTranslation();
+ const isMounted = useIsMounted();
+
+ return (
+
+
+
+
{title}
+ {modes && modes.length > 0 && (
+
+ {modes.map((mode) => (
+
+ ))}
+
+ )}
+
+
+
+
+ {weapons.map((weaponSplId) => (
+
+ ))}
+
+
+ {/* xxx: extract this to component instead of duplicating */}
+
+ {abilities[0].map((ability, i) => (
+
+ ))}
+
+ {abilities[1].map((ability, i) => (
+
+ ))}
+
+ {abilities[2].map((ability, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/app/db/models/builds/countByUserId.sql b/app/db/models/builds/countByUserId.sql
index 9cc575a4d..d21ec67b4 100644
--- a/app/db/models/builds/countByUserId.sql
+++ b/app/db/models/builds/countByUserId.sql
@@ -1,5 +1,5 @@
select
- count(*)
+ count(*) as "count"
from
"Build"
where
diff --git a/app/db/models/builds/queries.server.ts b/app/db/models/builds/queries.server.ts
index 0d447ad97..5a2d81578 100644
--- a/app/db/models/builds/queries.server.ts
+++ b/app/db/models/builds/queries.server.ts
@@ -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(
};
}
-const gearOrder: Array = ["HEAD", "CLOTHES", "SHOES"];
-function dbAbilitiesToArrayOfArrays(
- abilities: Array>
-): [
+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 = ["HEAD", "CLOTHES", "SHOES"];
+function dbAbilitiesToArrayOfArrays(
+ abilities: Array>
+): AllAbilitiesTuple {
const sorted = abilities
.slice()
.sort((a, b) => {
diff --git a/app/db/seed.ts b/app/db/seed.ts
index a4c72e523..902f503cd 100644
--- a/app/db/seed.ts
+++ b/app/db/seed.ts
@@ -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;
}
diff --git a/app/modules/in-game-lists/abilities.ts b/app/modules/in-game-lists/abilities.ts
index 18c4856ef..1e4f963d0 100644
--- a/app/modules/in-game-lists/abilities.ts
+++ b/app/modules/in-game-lists/abilities.ts
@@ -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);
diff --git a/app/routes/u.$identifier/builds.tsx b/app/routes/u.$identifier/builds.tsx
index 041c6b298..5f6198164 100644
--- a/app/routes/u.$identifier/builds.tsx
+++ b/app/routes/u.$identifier/builds.tsx
@@ -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();
+ const data = useLoaderData();
- // console.log({ data });
-
- return hey;
+ return (
+
+
+ {data.builds.map((build) => (
+
+ ))}
+
+
+ );
}
diff --git a/app/styles/common.css b/app/styles/common.css
index c790917db..54503b663 100644
--- a/app/styles/common.css
+++ b/app/styles/common.css
@@ -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;
+}
diff --git a/app/styles/vars.css b/app/styles/vars.css
index 5689b9f8b..928f29a2e 100644
--- a/app/styles/vars.css
+++ b/app/styles/vars.css
@@ -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%);
diff --git a/app/utils/urls.ts b/app/utils/urls.ts
index 96c31a8b4..169638b50 100644
--- a/app/utils/urls.ts
+++ b/app/utils/urls.ts
@@ -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;
}
diff --git a/public/img/modes/CB.avif b/public/img/modes/CB.avif
new file mode 100644
index 000000000..0f65d3bcb
Binary files /dev/null and b/public/img/modes/CB.avif differ
diff --git a/public/img/modes/CB.png b/public/img/modes/CB.png
new file mode 100644
index 000000000..4166cb35d
Binary files /dev/null and b/public/img/modes/CB.png differ
diff --git a/public/img/modes/RM.avif b/public/img/modes/RM.avif
new file mode 100644
index 000000000..636c09c75
Binary files /dev/null and b/public/img/modes/RM.avif differ
diff --git a/public/img/modes/RM.png b/public/img/modes/RM.png
new file mode 100644
index 000000000..f27aae65b
Binary files /dev/null and b/public/img/modes/RM.png differ
diff --git a/public/img/modes/SZ.avif b/public/img/modes/SZ.avif
new file mode 100644
index 000000000..927def58d
Binary files /dev/null and b/public/img/modes/SZ.avif differ
diff --git a/public/img/modes/SZ.png b/public/img/modes/SZ.png
new file mode 100644
index 000000000..b511034be
Binary files /dev/null and b/public/img/modes/SZ.png differ
diff --git a/public/img/modes/TC.avif b/public/img/modes/TC.avif
new file mode 100644
index 000000000..3febe9559
Binary files /dev/null and b/public/img/modes/TC.avif differ
diff --git a/public/img/modes/TC.png b/public/img/modes/TC.png
new file mode 100644
index 000000000..0289fdf9b
Binary files /dev/null and b/public/img/modes/TC.png differ
diff --git a/public/img/modes/TW.avif b/public/img/modes/TW.avif
new file mode 100644
index 000000000..811505d22
Binary files /dev/null and b/public/img/modes/TW.avif differ
diff --git a/public/img/modes/TW.png b/public/img/modes/TW.png
new file mode 100644
index 000000000..780bb8aa3
Binary files /dev/null and b/public/img/modes/TW.png differ