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