diff --git a/app/components/Button.tsx b/app/components/Button.tsx index 206a03acd..3c00ba529 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -60,7 +60,7 @@ type LinkButtonProps = Pick< ButtonProps, "variant" | "children" | "className" | "tiny" > & - Pick & { "data-cy"?: string } & { + Pick & { "data-cy"?: string } & { isExternal?: boolean; }; @@ -72,6 +72,7 @@ export function LinkButton({ to, prefetch, isExternal, + state, "data-cy": testId, }: LinkButtonProps) { if (isExternal) { @@ -92,6 +93,7 @@ export function LinkButton({ to={to} data-cy={testId} prefetch={prefetch} + state={state} > {children} diff --git a/app/components/ConditionalScrollRestoration.tsx b/app/components/ConditionalScrollRestoration.tsx index 5f630c000..f38ce9ae0 100644 --- a/app/components/ConditionalScrollRestoration.tsx +++ b/app/components/ConditionalScrollRestoration.tsx @@ -2,15 +2,24 @@ // https://github.com/remix-run/remix/issues/186#issuecomment-1178395835 import { ScrollRestoration, useLocation } from "@remix-run/react"; +import * as React from "react"; export function ConditionalScrollRestoration() { + const isFirstRenderRef = React.useRef(true); const location = useLocation(); + + React.useEffect(() => { + isFirstRenderRef.current = false; + }, []); + if ( + !isFirstRenderRef.current && location.state != null && typeof location.state === "object" && (location.state as { scroll: boolean }).scroll === false ) { return null; } + return ; } diff --git a/app/constants.ts b/app/constants.ts index 442b909e7..4e6db3697 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -36,6 +36,9 @@ export const BUILD = { MAX_COUNT: 250, } as const; +export const BUILDS_PAGE_BATCH_SIZE = 24; +export const BUILDS_PAGE_MAX_BUILDS = 240; + export const EMPTY_BUILD: BuildAbilitiesTupleWithUnknown = [ ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"], ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"], diff --git a/app/db/models/builds/buildsByUserId.sql b/app/db/models/builds/buildsByUserId.sql index 30150d30b..c0986e10a 100644 --- a/app/db/models/builds/buildsByUserId.sql +++ b/app/db/models/builds/buildsByUserId.sql @@ -35,4 +35,4 @@ from group by "BuildWithWeapon"."id" order by - "BuildWithWeapon"."updatedAt" desc \ No newline at end of file + "BuildWithWeapon"."updatedAt" desc diff --git a/app/db/models/builds/buildsByWeaponId.sql b/app/db/models/builds/buildsByWeaponId.sql index 6c721e48a..3dac56287 100644 --- a/app/db/models/builds/buildsByWeaponId.sql +++ b/app/db/models/builds/buildsByWeaponId.sql @@ -56,3 +56,5 @@ order by else "PlusTier"."tier" end asc, "BuildWithWeapon"."updatedAt" desc +limit + @limit diff --git a/app/db/models/builds/queries.server.ts b/app/db/models/builds/queries.server.ts index bb55946ad..f9a16f058 100644 --- a/app/db/models/builds/queries.server.ts +++ b/app/db/models/builds/queries.server.ts @@ -109,11 +109,18 @@ export function buildsByUserId(userId: Build["ownerId"]) { type BuildsByWeaponIdRow = BuildsByUserRow & Pick; -export function buildsByWeaponId(weaponId: BuildWeapon["weaponSplId"]) { +export function buildsByWeaponId({ + weaponId, + limit, +}: { + weaponId: BuildWeapon["weaponSplId"]; + limit: number; +}) { const rows = buildsByWeaponIdStm.all({ weaponId, // default to impossible weapon id so we can always have same amount of placeholder values altWeaponId: weaponIdToAltId.get(weaponId) ?? -1, + limit, }) as Array; return rows.map(augmentBuild); diff --git a/app/db/seed.ts b/app/db/seed.ts index 1342dfac6..41056f8a6 100644 --- a/app/db/seed.ts +++ b/app/db/seed.ts @@ -46,6 +46,7 @@ const basicSeeds = [ calendarEventBadges, calendarEventResults, adminBuilds, + manySplattershotBuilds, ]; export function seed() { @@ -498,6 +499,12 @@ function calendarEventResults() { } } +const randomAbility = (legalTypes: AbilityType[]) => { + const randomOrderAbilities = shuffle([...abilities]); + + return randomOrderAbilities.find((a) => legalTypes.includes(a.type))!.name; +}; + function adminBuilds() { for (let i = 0; i < 50; i++) { const randomOrderHeadGear = shuffle(headGearIds.slice()); @@ -505,13 +512,6 @@ function adminBuilds() { const randomOrderShoesGear = shuffle(shoesGearIds.slice()); const randomOrderWeaponIds = shuffle(mainWeaponIds.slice()); - const randomAbility = (legalTypes: AbilityType[]) => { - const randomOrderAbilities = shuffle([...abilities]); - - return randomOrderAbilities.find((a) => legalTypes.includes(a.type))! - .name; - }; - db.builds.create({ title: `${capitalize(faker.word.adjective())} ${capitalize( faker.word.noun() @@ -553,3 +553,59 @@ function adminBuilds() { }); } } + +function manySplattershotBuilds() { + for (let i = 0; i < 500; i++) { + const SPLATTERSHOT_ID = 40; + + const randomOrderHeadGear = shuffle(headGearIds.slice()); + const randomOrderClothesGear = shuffle(clothesGearIds.slice()); + const randomOrderShoesGear = shuffle(shoesGearIds.slice()); + const randomOrderWeaponIds = shuffle(mainWeaponIds.slice()).filter( + (id) => id !== SPLATTERSHOT_ID + ); + const users = userIdsInRandomOrder(); + + db.builds.create({ + title: `${capitalize(faker.word.adjective())} ${capitalize( + faker.word.noun() + )}`, + ownerId: users.pop()!, + description: Math.random() < 0.75 ? faker.lorem.paragraph() : null, + headGearSplId: randomOrderHeadGear[0]!, + clothesGearSplId: randomOrderClothesGear[0]!, + shoesGearSplId: randomOrderShoesGear[0]!, + weaponSplIds: new Array( + faker.helpers.arrayElement([1, 1, 1, 2, 2, 3, 4, 5]) + ) + .fill(null) + .map((_, i) => + i === 0 ? SPLATTERSHOT_ID : randomOrderWeaponIds.pop()! + ), + modes: + Math.random() < 0.75 + ? modesShort.filter(() => Math.random() < 0.5) + : null, + abilities: [ + [ + randomAbility(["HEAD_MAIN_ONLY", "STACKABLE"]), + randomAbility(["STACKABLE"]), + randomAbility(["STACKABLE"]), + randomAbility(["STACKABLE"]), + ], + [ + randomAbility(["CLOTHES_MAIN_ONLY", "STACKABLE"]), + randomAbility(["STACKABLE"]), + randomAbility(["STACKABLE"]), + randomAbility(["STACKABLE"]), + ], + [ + randomAbility(["SHOES_MAIN_ONLY", "STACKABLE"]), + randomAbility(["STACKABLE"]), + randomAbility(["STACKABLE"]), + randomAbility(["STACKABLE"]), + ], + ], + }); + } +} diff --git a/app/routes/builds/$slug.tsx b/app/routes/builds/$slug.tsx index 756272e27..fd69d3796 100644 --- a/app/routes/builds/$slug.tsx +++ b/app/routes/builds/$slug.tsx @@ -4,7 +4,10 @@ import { type SerializeFrom, } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; +import { useTranslation } from "react-i18next"; import { BuildCard } from "~/components/BuildCard"; +import { LinkButton } from "~/components/Button"; +import { BUILDS_PAGE_BATCH_SIZE, BUILDS_PAGE_MAX_BUILDS } from "~/constants"; import { db } from "~/db"; import { i18next } from "~/modules/i18n"; import { mainWeaponIds, weaponIdIsNotAlt } from "~/modules/in-game-lists"; @@ -33,28 +36,53 @@ export const loader = async ({ request, params }: LoaderArgs) => { throw new Response(null, { status: 404 }); } + const url = new URL(request.url); + const limit = Math.min( + Number(url.searchParams.get("limit") ?? BUILDS_PAGE_BATCH_SIZE), + BUILDS_PAGE_MAX_BUILDS + ); + return { weaponId, title: makeTitle([t(`weapons:MAIN_${weaponId}`), t("common:pages.builds")]), - builds: db.builds.buildsByWeaponId(weaponId), + builds: db.builds.buildsByWeaponId({ + weaponId, + limit, + }), + limit, }; }; export default function WeaponsBuildsPage() { const data = useLoaderData(); + const { t } = useTranslation(["common"]); return ( -
- {data.builds.map((build) => { - return ( - - ); - })} +
+
+ {data.builds.map((build) => { + return ( + + ); + })} +
+ {data.limit < BUILDS_PAGE_MAX_BUILDS && + // not considering edge case where there are amount of builds equal to current limit + data.builds.length === data.limit && ( + + {t("common:actions.loadMore")} + + )}
); } diff --git a/app/styles/utils.css b/app/styles/utils.css index d93191d18..8a580823f 100644 --- a/app/styles/utils.css +++ b/app/styles/utils.css @@ -86,6 +86,10 @@ margin-block: var(--s-4); } +.m-0-auto { + margin: 0 auto; +} + .hidden { display: none; } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 336b95066..6fbb39f64 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -25,6 +25,7 @@ "actions.add": "Add", "actions.remove": "Remove", "actions.delete": "Delete", + "actions.loadMore": "Load more", "results": "Results", diff --git a/translation-progress.md b/translation-progress.md index e0ae2c246..aa949da46 100644 --- a/translation-progress.md +++ b/translation-progress.md @@ -18,9 +18,27 @@ **44/44** -### 🟢 common.json +### 🟡 common.json -**36/36** +**36/48** + +
+Missing + +- actions.loadMore +- weapon.category.SHOOTERS +- weapon.category.BLASTERS +- weapon.category.ROLLERS +- weapon.category.BRUSHES +- weapon.category.CHARGERS +- weapon.category.SLOSHERS +- weapon.category.SPLATLINGS +- weapon.category.DUALIES +- weapon.category.BRELLAS +- weapon.category.STRINGERS +- weapon.category.SPLATANAS + +
### 🟢 contributions.json @@ -40,7 +58,61 @@ --- -## /de (🟢 Done) +## /de (🟡 In progress) + +### 🟢 analyzer.json + +**91/91** + +### 🟢 badges.json + +**7/7** + +### 🟢 builds.json + +**11/11** + +### 🟢 calendar.json + +**44/44** + +### 🟡 common.json + +**36/48** + +
+Missing + +- actions.loadMore +- weapon.category.SHOOTERS +- weapon.category.BLASTERS +- weapon.category.ROLLERS +- weapon.category.BRUSHES +- weapon.category.CHARGERS +- weapon.category.SLOSHERS +- weapon.category.SPLATLINGS +- weapon.category.DUALIES +- weapon.category.BRELLAS +- weapon.category.STRINGERS +- weapon.category.SPLATANAS + +
+ +### 🟢 contributions.json + +**6/6** + +### 🟢 faq.json + +**6/6** + +### 🟢 front.json + +**9/9** + +### 🟢 user.json + +**7/7** --- @@ -96,9 +168,27 @@ **44/44** -### 🟢 common.json +### 🟡 common.json -**36/36** +**36/48** + +
+Missing + +- actions.loadMore +- weapon.category.SHOOTERS +- weapon.category.BLASTERS +- weapon.category.ROLLERS +- weapon.category.BRUSHES +- weapon.category.CHARGERS +- weapon.category.SLOSHERS +- weapon.category.SPLATLINGS +- weapon.category.DUALIES +- weapon.category.BRELLAS +- weapon.category.STRINGERS +- weapon.category.SPLATANAS + +
### 🟡 contributions.json @@ -152,12 +242,24 @@ ### 🟡 common.json -**35/36** +**35/48**
Missing - pages.analyzer +- actions.loadMore +- weapon.category.SHOOTERS +- weapon.category.BLASTERS +- weapon.category.ROLLERS +- weapon.category.BRUSHES +- weapon.category.CHARGERS +- weapon.category.SLOSHERS +- weapon.category.SPLATLINGS +- weapon.category.DUALIES +- weapon.category.BRELLAS +- weapon.category.STRINGERS +- weapon.category.SPLATANAS
@@ -218,12 +320,24 @@ ### 🟡 common.json -**35/36** +**35/48**
Missing - pages.analyzer +- actions.loadMore +- weapon.category.SHOOTERS +- weapon.category.BLASTERS +- weapon.category.ROLLERS +- weapon.category.BRUSHES +- weapon.category.CHARGERS +- weapon.category.SLOSHERS +- weapon.category.SPLATLINGS +- weapon.category.DUALIES +- weapon.category.BRELLAS +- weapon.category.STRINGERS +- weapon.category.SPLATANAS
@@ -279,12 +393,24 @@ ### 🟡 common.json -**35/36** +**35/48**
Missing - pages.analyzer +- actions.loadMore +- weapon.category.SHOOTERS +- weapon.category.BLASTERS +- weapon.category.ROLLERS +- weapon.category.BRUSHES +- weapon.category.CHARGERS +- weapon.category.SLOSHERS +- weapon.category.SPLATLINGS +- weapon.category.DUALIES +- weapon.category.BRELLAS +- weapon.category.STRINGERS +- weapon.category.SPLATANAS
@@ -365,9 +491,27 @@ **44/44** -### 🟢 common.json +### 🟡 common.json -**36/36** +**36/48** + +
+Missing + +- actions.loadMore +- weapon.category.SHOOTERS +- weapon.category.BLASTERS +- weapon.category.ROLLERS +- weapon.category.BRUSHES +- weapon.category.CHARGERS +- weapon.category.SLOSHERS +- weapon.category.SPLATLINGS +- weapon.category.DUALIES +- weapon.category.BRELLAS +- weapon.category.STRINGERS +- weapon.category.SPLATANAS + +
### 🟡 contributions.json @@ -421,12 +565,24 @@ ### 🟡 common.json -**35/36** +**35/48**
Missing - pages.analyzer +- actions.loadMore +- weapon.category.SHOOTERS +- weapon.category.BLASTERS +- weapon.category.ROLLERS +- weapon.category.BRUSHES +- weapon.category.CHARGERS +- weapon.category.SLOSHERS +- weapon.category.SPLATLINGS +- weapon.category.DUALIES +- weapon.category.BRELLAS +- weapon.category.STRINGERS +- weapon.category.SPLATANAS