feat : server side
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { PublicGuide } from "@/lib/guides/queries";
|
||||
import { filterPublicTeamGroups, resolvePublicGuideFilters } from "./public-filters";
|
||||
|
||||
function team(
|
||||
id: string,
|
||||
title: string,
|
||||
character: string,
|
||||
alias: string,
|
||||
damage: number,
|
||||
rotationSeconds: number,
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
rotationSeconds,
|
||||
members: [{
|
||||
damage,
|
||||
character: { name: character, aliases: [alias] },
|
||||
weapon: null,
|
||||
artifact: null,
|
||||
secondArtifact: null,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const guide = {
|
||||
weaponGroups: [{ rows: [{}] }, { rows: [] }],
|
||||
constellationGroups: [{ rows: [] }, { rows: [{}] }],
|
||||
artifactGroups: [{ name: "Sets one" }, { name: "Sets two" }],
|
||||
artifactProfile: {
|
||||
recommendedConditions: [{ name: "Stats one" }, { name: "Stats two" }],
|
||||
targetConditions: [
|
||||
{ name: "Empty", rows: [] },
|
||||
{ name: "Targets", rows: [{ stat: "ATK", value: "100" }] },
|
||||
],
|
||||
},
|
||||
teamGroups: [
|
||||
{
|
||||
id: "first",
|
||||
name: "First condition",
|
||||
note: "First note",
|
||||
teams: [team("first-team", "First team", "Lyney", "The Knave", 100, 10)],
|
||||
},
|
||||
{
|
||||
id: "second",
|
||||
name: "Second condition",
|
||||
note: "Second note",
|
||||
teams: [
|
||||
team("lower", "Lower DPS", "Mualani", "Shark Surfer", 100, 20),
|
||||
team("higher", "Higher DPS", "Mualani", "Shark Surfer", 200, 10),
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as PublicGuide;
|
||||
|
||||
describe("public guide filters", () => {
|
||||
it("resolves one-based condition and artifact selections with safe defaults", () => {
|
||||
const filters = resolvePublicGuideFilters(guide, "artifact", {
|
||||
recommended: ["2", "1"],
|
||||
sets: "99",
|
||||
targets: "1",
|
||||
field: "unknown",
|
||||
sort: "unknown",
|
||||
});
|
||||
|
||||
expect(filters.recommendedIndex).toBe(1);
|
||||
expect(filters.setsIndex).toBe(0);
|
||||
expect(filters.targetsIndex).toBe(0);
|
||||
expect(filters.field).toBe("all");
|
||||
expect(filters.sort).toBeNull();
|
||||
expect(filters.recommendedLabels).toEqual(["Stats one", "Stats two"]);
|
||||
expect(filters.targetsLabels).toEqual(["Targets"]);
|
||||
});
|
||||
|
||||
it("filters the selected team condition on the server, including fuzzy aliases and DPS order", () => {
|
||||
const filters = resolvePublicGuideFilters(guide, "team", {
|
||||
condition: "2",
|
||||
q: "mulani",
|
||||
field: "character",
|
||||
sort: "best",
|
||||
});
|
||||
const groups = filterPublicTeamGroups(guide, filters);
|
||||
|
||||
expect(groups[0].teams).toEqual([]);
|
||||
expect(groups[0].note).toBeNull();
|
||||
expect(groups[1].teams.map(({ id }) => id)).toEqual(["higher", "lower"]);
|
||||
expect(groups[1].note).toBe("Second note");
|
||||
expect(groups[1].teams[0].members[0].character).not.toHaveProperty("aliases");
|
||||
});
|
||||
|
||||
it("defaults invalid team condition indexes to the first condition", () => {
|
||||
const filters = resolvePublicGuideFilters(guide, "team", { condition: "0" });
|
||||
|
||||
expect(filters.conditionIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { PublicGuide } from "@/lib/guides/queries";
|
||||
import {
|
||||
searchTeams,
|
||||
sortTeamsByDps,
|
||||
type DpsOrder,
|
||||
type TeamSearchTarget,
|
||||
} from "@/lib/guides/team-search";
|
||||
|
||||
export type GuideSearchParams = Record<string, string | string[] | undefined>;
|
||||
|
||||
export interface PublicGuideFilters {
|
||||
conditionIndex: number;
|
||||
recommendedIndex: number;
|
||||
setsIndex: number;
|
||||
targetsIndex: number;
|
||||
query: string;
|
||||
field: TeamSearchTarget;
|
||||
sort: DpsOrder;
|
||||
recommendedLabels: string[];
|
||||
setsLabels: string[];
|
||||
targetsLabels: string[];
|
||||
}
|
||||
|
||||
type GuideTeam = PublicGuide["teams"][number];
|
||||
type GuideMember = GuideTeam["members"][number];
|
||||
type GuideItem = NonNullable<GuideMember["weapon"]>;
|
||||
|
||||
export type PublicTeamCardData = Pick<GuideTeam, "id" | "title" | "rotationSeconds" | "combo"> & {
|
||||
members: Array<Pick<GuideMember, "position" | "damage" | "constellation" | "refinement"> & {
|
||||
character: Pick<GuideMember["character"], "key" | "name" | "imageUrl" | "element" | "rarity"> & { aliases?: string[] };
|
||||
weapon: (Pick<GuideItem, "key" | "name" | "imageUrl" | "rarity"> & { aliases?: string[] }) | null;
|
||||
artifact: (Pick<GuideItem, "key" | "name" | "imageUrl" | "rarity"> & { aliases?: string[] }) | null;
|
||||
secondArtifact: (Pick<GuideItem, "key" | "name" | "imageUrl" | "rarity"> & { aliases?: string[] }) | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type PublicTeamConditionGroup = Pick<PublicGuide["teamGroups"][number], "id" | "name" | "note"> & {
|
||||
teams: PublicTeamCardData[];
|
||||
};
|
||||
|
||||
export function firstSearchValue(value: string | string[] | undefined): string | undefined {
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
export function resolveOneBasedIndex(value: string | undefined, length: number): number {
|
||||
if (length <= 0 || !value || !/^\d+$/u.test(value)) return 0;
|
||||
const index = Number(value) - 1;
|
||||
return Number.isSafeInteger(index) && index >= 0 && index < length ? index : 0;
|
||||
}
|
||||
|
||||
export function resolvePublicGuideFilters(
|
||||
guide: PublicGuide,
|
||||
section: string,
|
||||
params: GuideSearchParams,
|
||||
): PublicGuideFilters {
|
||||
const profile = guide.artifactProfile;
|
||||
const recommended = profile?.recommendedConditions ?? [];
|
||||
const sets = guide.artifactGroups;
|
||||
const targets = profile?.targetConditions.filter((condition) => condition.rows.length) ?? [];
|
||||
const fieldValue = firstSearchValue(params.field);
|
||||
const sortValue = firstSearchValue(params.sort);
|
||||
|
||||
return {
|
||||
conditionIndex: resolveOneBasedIndex(
|
||||
firstSearchValue(params.condition),
|
||||
section === "weapon"
|
||||
? guide.weaponGroups.filter((group) => group.rows.length > 0).length
|
||||
: section === "constellations"
|
||||
? guide.constellationGroups.filter((group) => group.rows.length > 0).length
|
||||
: guide.teamGroups.length,
|
||||
),
|
||||
recommendedIndex: resolveOneBasedIndex(firstSearchValue(params.recommended), recommended.length),
|
||||
setsIndex: resolveOneBasedIndex(firstSearchValue(params.sets), sets.length),
|
||||
targetsIndex: resolveOneBasedIndex(firstSearchValue(params.targets), targets.length),
|
||||
query: firstSearchValue(params.q)?.trim() ?? "",
|
||||
field: fieldValue === "team" || fieldValue === "character" || fieldValue === "weapon" || fieldValue === "artifact"
|
||||
? fieldValue
|
||||
: "all",
|
||||
sort: sortValue === "best" || sortValue === "worst" ? sortValue : null,
|
||||
recommendedLabels: recommended.map((condition, index) => condition.name || `เงื่อนไข ${index + 1}`),
|
||||
setsLabels: sets.map((condition, index) => condition.name || `เงื่อนไข ${index + 1}`),
|
||||
targetsLabels: targets.map((condition, index) => condition.name || `เงื่อนไข ${index + 1}`),
|
||||
};
|
||||
}
|
||||
|
||||
export function filterPublicTeamGroups(
|
||||
guide: PublicGuide,
|
||||
filters: PublicGuideFilters,
|
||||
): PublicTeamConditionGroup[] {
|
||||
const activeGroup = guide.teamGroups[filters.conditionIndex] ?? guide.teamGroups[0];
|
||||
if (!activeGroup) return [];
|
||||
|
||||
const matches = searchTeams(activeGroup.teams, (team) => ({
|
||||
team: [team.title],
|
||||
character: team.members.flatMap((member) => [member.character.name, ...member.character.aliases]),
|
||||
weapon: team.members.flatMap((member) => member.weapon ? [member.weapon.name, ...member.weapon.aliases] : []),
|
||||
artifact: team.members.flatMap((member) => [member.artifact, member.secondArtifact].flatMap((artifact) => artifact ? [artifact.name, ...artifact.aliases] : [])),
|
||||
}), filters.query, filters.field);
|
||||
const teams = sortTeamsByDps(matches.items, filters.sort).map((team) => ({
|
||||
id: team.id,
|
||||
title: team.title,
|
||||
rotationSeconds: team.rotationSeconds,
|
||||
combo: team.combo,
|
||||
members: team.members.map((member) => ({
|
||||
position: member.position,
|
||||
damage: member.damage,
|
||||
constellation: member.constellation,
|
||||
refinement: member.refinement,
|
||||
character: {
|
||||
key: member.character.key,
|
||||
name: member.character.name,
|
||||
imageUrl: member.character.imageUrl,
|
||||
element: member.character.element,
|
||||
rarity: member.character.rarity,
|
||||
},
|
||||
weapon: member.weapon ? {
|
||||
key: member.weapon.key,
|
||||
name: member.weapon.name,
|
||||
imageUrl: member.weapon.imageUrl,
|
||||
rarity: member.weapon.rarity,
|
||||
} : null,
|
||||
artifact: member.artifact ? {
|
||||
key: member.artifact.key,
|
||||
name: member.artifact.name,
|
||||
imageUrl: member.artifact.imageUrl,
|
||||
rarity: member.artifact.rarity,
|
||||
} : null,
|
||||
secondArtifact: member.secondArtifact ? {
|
||||
key: member.secondArtifact.key,
|
||||
name: member.secondArtifact.name,
|
||||
imageUrl: member.secondArtifact.imageUrl,
|
||||
rarity: member.secondArtifact.rarity,
|
||||
} : null,
|
||||
})),
|
||||
}));
|
||||
|
||||
return guide.teamGroups.map((group, index) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
note: index === filters.conditionIndex ? group.note : null,
|
||||
teams: index === filters.conditionIndex ? teams : [],
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user