Repository: plohkoon/wow-random-spec
Pull request #1: WIP: Adding Mythic Run Overview to Event Overview Page
Author: login: DendyA
Created: 2025-05-09T23:37:18Z | Merged: 2025-05-13T01:37:55Z

The changes included in this PR will show info on all team's best keys for each mythic dungeon on the event overview page. Furthermore, a couple of the functions that were to be duplicated were instead made into utility functions. These are in ```/lib/mythics.ts```

There are still two changes that I need to make. 

---

The first is to fix the following error that shows up in the console:

> Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously later calls tries to update the component. Move this work to useEffect instead.

This occurs when the user navigates to any specific team's overview page. I suspect the issue occurs because of the moving of the clientLoader code to a utility function so that it could be used in the Event Overview page as well. I don't know React / React Remix enough to understand why moving this code causes the error. And why this does not occur when navigating to the event overview page. The data still loads fine.

---

The second change is in the teams map lambda when creating each team's player table, in ```app/routes/event/show/route.tsx```. 

The parsedMythicData, which is an array of each team's mythic data, is currently indexed by using the index of the map function. This was done during testing since that was the easiest way to make sure what I was doing worked. Now I would like to instead combine the team object with the mythic data so that the teams array is still enumerated but the mythic data is correctly included and the array doesn't need to be indexed.

---

A further addition will be to also include the overall team's score, best single dungeon etc. to the Event Overview page as well.


Diff:
diff --git a/app/lib/mythics.ts b/app/lib/mythics.ts
new file mode 100644
index 0000000..b8f4aa9
--- /dev/null
+++ b/app/lib/mythics.ts
@@ -0,0 +1,205 @@
+import type { CharacterNS } from "~/lib/raiderIO/characters";
+import {RaiderIOClient} from "~/lib/raiderIO";
+
+export type DBPlayerInternalTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+};
+
+export type DBPlayerType = {
+    team?: DBPlayerInternalTeamType | null
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    nickname: string
+    main?: string | null
+    teamId?: string | null
+    spec?: string | null
+    playerName?: string | null
+    playerServer?: string | null
+    eventId?: string | null
+    assignedRole?: string | null
+};
+
+export type DBTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+    players: DBPlayerType[]
+};
+
+export type MythicData = CharacterNS.MythicPlusRun & {
+    participants: DBPlayerType[]
+};
+
+export function getPlayersPromises(team: DBTeamType | null, client: RaiderIOClient) {
+    if(!team) {
+        const playersPromises: Promise<null>[] = [Promise.resolve(null)];
+
+        return playersPromises;
+    }
+
+    const playersPromises: (Promise<null> | Promise<CharacterNS.CharacterProfilePayload<CharacterNS.OptionalCharacterProfilePayloadOptions>>)[] =
+        team.players.map((player) =>
+            player.playerServer && player.playerName
+                ? client.character.getCharacterProfile({
+                    region: "us",
+                    realm: player.playerServer,
+                    name: player.playerName,
+                    fields: {
+                        gear: true,
+                        mythic_plus_best_runs: true,
+                        mythic_plus_alternate_runs: true,
+                        mythic_plus_highest_level_runs: true,
+                        mythic_plus_recent_runs: true,
+                        mythic_plus_previous_weekly_highest_level_runs: true,
+                        mythic_plus_weekly_highest_level_runs: true,
+                    },
+                })
+                : Promise.resolve(null)
+        );
+
+    return playersPromises;
+}
+
+export async function parseMythicDataPerTeam(team: DBTeamType | null, playersPromises: (Promise<null> | Promise<CharacterNS.CharacterProfilePayload<CharacterNS.OptionalCharacterProfilePayloadOptions>>)[]) {
+
+    if(!team) {
+        const promiseOfAllMythicData: Promise<null> = Promise.resolve(null);
+
+        return promiseOfAllMythicData;
+    }
+
+    const playersOnTeam = team.players;
+
+    const playersMap = new Map<string, DBPlayerType>();
+    playersOnTeam.forEach((p: DBPlayerType) => playersMap.set(p.id, p));
+
+    // Set these promises up in the client loader so they are stable in the react application.
+    const allPlayersPromise = Promise.allSettled(playersPromises);
+    const promiseOfAllMythicData: Promise<MythicData[]> = allPlayersPromise.then((res) => {
+        // console.log("Mythics Promise", res);
+        if (!res) return res;
+
+        const succeededRes: ((CharacterNS.CharacterPayloadBase & CharacterNS.OptionalCharacterProfilePayloadFields) | null)[] = res
+            .filter((r) => r.status === "fulfilled")
+            .map((r) => r.value);
+
+        const mythicMap = new Map<number, CharacterNS.MythicPlusRun>();
+        const participantMap = new Map<number, string[]>();
+
+        succeededRes.forEach((player: ((CharacterNS.CharacterPayloadBase & CharacterNS.OptionalCharacterProfilePayloadFields) | null)) => {
+            if (!player) return;
+            // We just want to resolve via the dbPlayer ID so we find and ensure we have that.
+            const dbPlayer: DBPlayerType | undefined = playersOnTeam.find(({ playerName, playerServer }) => {
+                // Strip whitespace and replace lowercase to slugify everything for more accurate
+                // matching. This should be fine since names are not case-sensitive.
+                const strip = (str: string | undefined | null) =>
+                    str?.toLocaleLowerCase()?.replace(/\s/, "");
+
+                return (
+                    strip(playerName) === strip(player.name) &&
+                    strip(playerServer) === strip(player.realm)
+                );
+            });
+            // console.log("DB Player", dbPlayer, player.name, player.realm);
+            if (!dbPlayer) return;
+
+            const allMythics = [
+                ...(player.mythic_plus_best_runs ?? []),
+                ...(player.mythic_plus_alternate_runs ?? []),
+                ...(player.mythic_plus_highest_level_runs ?? []),
+                ...(player.mythic_plus_recent_runs ?? []),
+                ...(player.mythic_plus_previous_weekly_highest_level_runs ?? []),
+                ...(player.mythic_plus_weekly_highest_level_runs ?? []),
+            ];
+
+            allMythics.forEach((run) => {
+                mythicMap.set(run.keystone_run_id, run);
+
+                const participantsList = participantMap.get(run.keystone_run_id);
+
+                // Ensure this player is in the participant list
+                if (!participantsList) {
+                    participantMap.set(run.keystone_run_id, [dbPlayer.id]);
+                } else if (participantsList.findIndex((id) => id === dbPlayer.id) < 0) {
+                    participantsList.push(dbPlayer.id);
+                }
+            });
+        });
+
+        // A mythic counts if at least 4 players of the team are in it.
+        const mythics: MythicData[]  = Array.from(mythicMap.values())
+            .flatMap((m) => {
+                const participants = participantMap.get(m.keystone_run_id);
+
+                // If there were no participants or not enough participants this does not count as a key for this team.
+                if (!participants || participants.length < 4) {
+                  return [];
+                }
+
+                return [
+                    {
+                        ...m,
+                        participants: participants
+                            .map((p) => playersMap.get(p))
+                            .filter((p) => p !== undefined),
+                    },
+                ];
+            })
+            .sort(
+                (mythicA, mythicB) =>
+                    // First sort descending on the keystone level so higher keystones are at the top.
+                    mythicB.mythic_level - mythicA.mythic_level ||
+                    // Next descending by score.
+                    mythicB.score - mythicA.score ||
+                    // Finally sort ascending within the score and mythic level category by string.
+                    mythicA.short_name.localeCompare(mythicB.short_name)
+            );
+
+        return mythics;
+    });
+
+    return promiseOfAllMythicData;
+}
+
+export function calculateBestMythicsAndTotalScore(mythics: MythicData[]) {
+    const bestMythicsMap = {} as Record<string, (typeof mythics)[number]>;
+
+    mythics.forEach((m: MythicData) => {
+        const existing = bestMythicsMap[m.dungeon];
+
+        // TODO: Maybe incorporate the most recent run into this somehow?
+        if (!existing || existing.score < m.score) {
+            bestMythicsMap[m.dungeon] = m;
+        }
+    });
+
+    const bestMythics = Object.values(bestMythicsMap);
+    const totalScore = bestMythics.reduce((acc, m) => acc + m.score, 0);
+
+    return [Object.values(bestMythicsMap), totalScore] as const;
+}
+
+export function calculateBestScoreAndBestUnderTime(mythics: MythicData[]) {
+    const bestSingleScore = mythics.reduce(
+        (score: number, m: MythicData) => (score > m.score ? score : m.score),
+        0
+    );
+
+    const mostUnderTime = mythics.reduce((curr: number, m: MythicData) => {
+        const percentUnder = (m.clear_time_ms - m.par_time_ms) / m.par_time_ms;
+
+        if (percentUnder > curr) {
+            return percentUnder;
+        } else {
+            return curr;
+        }
+    }, 0);
+    return [bestSingleScore, mostUnderTime] as const;
+}
diff --git a/app/lib/raiderIO/characters.ts b/app/lib/raiderIO/characters.ts
index 8f7aea8..52ca1c1 100644
--- a/app/lib/raiderIO/characters.ts
+++ b/app/lib/raiderIO/characters.ts
@@ -2,8 +2,9 @@ import type { RaiderIOClient } from ".";
 import type { RootNS } from "./index";
 import { fieldsString, HasChildBool } from "./utils";
 
-export namespace CharacterNS {
-  type CharacterPayloadBase = {
+export namespace CharacterNS
+{
+  export type CharacterPayloadBase = {
     name: string;
     race: RootNS.Race;
     class: RootNS.Class;
@@ -240,23 +241,23 @@ export namespace CharacterNS {
   type RaidMetaAchievementType = unknown;
   type RaidAchievementCurveType = unknown;
 
-  type OptionalCharacterProfilePayloadFields = {
-    gear: GearType;
-    talents: TalentType;
-    guild: GuildType;
-    covenant: CovenantType;
-    raid_progression: RaidProgressionType;
-    mythic_plus_scores_by_season: MythicPlusScoresBySeasonType;
-    mythic_plus_ranks: MythicPlusRanksType;
-    mythic_plus_recent_runs: MythicPlusRecentRunsType;
-    mythic_plus_best_runs: MythicPlusBestRunsType;
-    mythic_plus_alternate_runs: MythicPlusAlternateRunsType;
-    mythic_plus_highest_level_runs: MythicPlusHighestLevelRunsType;
-    mythic_plus_weekly_highest_level_runs: MythicPlusWeeklyHighestLevelRunsType;
-    mythic_plus_previous_weekly_highest_level_runs: MythicPlusPreviousWeeklyHighestLevelRunsType;
-    previous_mythic_plus_ranks: PreviousMythicPlusRanksType;
-    raid_achievement_meta: RaidMetaAchievementType;
-    raid_achievement_curve: RaidAchievementCurveType;
+  export type OptionalCharacterProfilePayloadFields = {
+    gear?: GearType;
+    talents?: TalentType;
+    guild?: GuildType;
+    covenant?: CovenantType;
+    raid_progression?: RaidProgressionType;
+    mythic_plus_scores_by_season?: MythicPlusScoresBySeasonType;
+    mythic_plus_ranks?: MythicPlusRanksType;
+    mythic_plus_recent_runs?: MythicPlusRecentRunsType;
+    mythic_plus_best_runs?: MythicPlusBestRunsType;
+    mythic_plus_alternate_runs?: MythicPlusAlternateRunsType;
+    mythic_plus_highest_level_runs?: MythicPlusHighestLevelRunsType;
+    mythic_plus_weekly_highest_level_runs?: MythicPlusWeeklyHighestLevelRunsType;
+    mythic_plus_previous_weekly_highest_level_runs?: MythicPlusPreviousWeeklyHighestLevelRunsType;
+    previous_mythic_plus_ranks?: PreviousMythicPlusRanksType;
+    raid_achievement_meta?: RaidMetaAchievementType;
+    raid_achievement_curve?: RaidAchievementCurveType;
   };
 
   // This entire hash is just for the fact that talents does not match the output key.
@@ -283,17 +284,17 @@ export namespace CharacterNS {
   export type OptionalCharacterProfilePayloadOptions = {
     gear?: boolean;
     talents?:
-      | boolean
-      | {
-          categorized: true;
-        };
+        | boolean
+        | {
+      categorized: true;
+    };
     guild?: boolean;
     covenant?: boolean;
     raid_progression?:
-      | boolean
-      | {
-          [
-            key:
+        | boolean
+        | {
+      [
+          key:
               | string
               | number
               | "current-expansion"
@@ -301,24 +302,24 @@ export namespace CharacterNS {
               | "current-tier"
               | "previous-tier"
           ]: boolean;
-        };
+    };
     mythic_plus_scores_by_season?:
-      | boolean
-      | {
-          [key: "current" | "previous" | `season-${string}` | string]: boolean;
-        };
+        | boolean
+        | {
+      [key: "current" | "previous" | `season-${string}` | string]: boolean;
+    };
     mythic_plus_ranks?: boolean;
     mythic_plus_recent_runs?: boolean;
     mythic_plus_best_runs?:
-      | boolean
-      | {
-          all: boolean;
-        };
+        | boolean
+        | {
+      all: boolean;
+    };
     mythic_plus_alternate_runs?:
-      | boolean
-      | {
-          all: boolean;
-        };
+        | boolean
+        | {
+      all: boolean;
+    };
     mythic_plus_highest_level_runs?: boolean;
     mythic_plus_weekly_highest_level_runs?: boolean;
     mythic_plus_previous_weekly_highest_level_runs?: boolean;
@@ -333,15 +334,15 @@ export namespace CharacterNS {
 
   // Build up the output hash with only the keys that are requested.
   export type CharacterProfilePayload<
-    T extends OptionalCharacterProfilePayloadOptions
+      T extends OptionalCharacterProfilePayloadOptions
   > = CharacterPayloadBase & {
     [key in keyof OptionalCharacterProfilePayloadFields as HasChildBool<
-      T[key]
+        T[key]
     > extends true
-      ? OutputKey[key]
-      : never]: OptionalCharacterProfilePayloadFields[key];
+        ? OutputKey[key]
+        : never]: OptionalCharacterProfilePayloadFields[key];
   };
-}
+};
 
 export class Character {
   private client: RaiderIOClient;
diff --git a/app/lib/session.server.ts b/app/lib/session.server.ts
index 17e8e59..1cea01a 100644
--- a/app/lib/session.server.ts
+++ b/app/lib/session.server.ts
@@ -6,7 +6,7 @@ import {
 } from "react-router";
 import { db, Prisma, User } from "./db.server";
 
-const ADMINS = ["plohkoon", "captainfrogs"];
+const ADMINS = ["plohkoon", "captainfrogs", "pointysalad"];
 
 export const { getSession, commitSession, destroySession } =
   createCookieSessionStorage({
diff --git a/app/routes/event/player/components/playerData.tsx b/app/routes/event/player/components/playerData.tsx
index 9e4d75f..6c6340d 100644
--- a/app/routes/event/player/components/playerData.tsx
+++ b/app/routes/event/player/components/playerData.tsx
@@ -10,15 +10,18 @@ import { ClassDisplay } from "~/components/display/classDisplay";
 import { RoleDisplay } from "~/components/display/roleDisplay";
 import { CharacterName } from "~/components/display/characterName";
 import { TeamDataTable } from "../../show/components/teamDataTable";
+import { MythicData } from "~/lib/mythics";
 
 type Player = Route.ComponentProps["loaderData"]["player"];
 
 export function PlayerData({
   player,
   eventSlug,
+  mythicData
 }: {
   player: Player;
   eventSlug: string;
+  mythicData: MythicData[] | null;
 }) {
   return (
     <section className="grid grid-cols-[2fr_3fr] gap-4">
@@ -51,10 +54,8 @@ export function PlayerData({
         </TableBody>
       </Table>
       {player.team ? (
-        <TeamDataTable team={player.team} slug={eventSlug} />
-      ) : (
-        <div>No Team Assigned Yet</div>
-      )}
+          <TeamDataTable team={player.team} slug={eventSlug} mythicData={mythicData} />
+      ) : <div>No Team Assigned Yet</div>}
     </section>
   );
 }
diff --git a/app/routes/event/player/route.tsx b/app/routes/event/player/route.tsx
index 6f71246..1f580e8 100644
--- a/app/routes/event/player/route.tsx
+++ b/app/routes/event/player/route.tsx
@@ -5,6 +5,7 @@ import { RaiderIOClient } from "~/lib/raiderIO";
 import { Route } from "./+types/route";
 import { CharacterData } from "./components/characterData";
 import { PlayerData } from "./components/playerData";
+import { getPlayersPromises, parseMythicDataPerTeam } from "~/lib/mythics";
 
 export const loader = async ({ params: { id, slug } }: Route.LoaderArgs) => {
   const player = await db.player.findFirst({
@@ -25,7 +26,7 @@ export const loader = async ({ params: { id, slug } }: Route.LoaderArgs) => {
     throw redirect(`/event/${slug}`);
   }
 
-  const client = await RaiderIOClient.getInstance();
+  const client = RaiderIOClient.getInstance();
 
   let playerData =
     player.playerServer && player.playerName
@@ -42,16 +43,34 @@ export const loader = async ({ params: { id, slug } }: Route.LoaderArgs) => {
 
   const scoreTiers = client.mythicPlus.scoreTiers();
 
+  const playersPromises =  getPlayersPromises(player.team, client);
+
   return {
     player,
     playerData,
     scoreTiers,
+    playersPromises,
+    mythicData: null
   };
 };
 export const action = async ({}: Route.ActionArgs) => {};
 
+export const clientLoader = async ({
+    serverLoader,
+}: Route.ClientLoaderArgs) => {
+    const serverRes = await serverLoader();
+
+    const mythicData = await parseMythicDataPerTeam(serverRes.player.team, serverRes.playersPromises);
+
+    return {
+        ...serverRes,
+        mythicData
+    };
+};
+clientLoader.hydrate = true;
+
 export default function PlayerShow({
-  loaderData: { player, playerData, scoreTiers },
+  loaderData: { player, playerData, scoreTiers, mythicData },
   params: { slug },
 }: Route.ComponentProps) {
   return (
@@ -61,7 +80,7 @@ export default function PlayerShow({
       </Link>
       <H2>Player</H2>
 
-      <PlayerData player={player} eventSlug={slug} />
+      <PlayerData player={player} eventSlug={slug} mythicData={mythicData} />
 
       <CharacterData playerData={playerData} scoreTiers={scoreTiers} />
     </article>
diff --git a/app/routes/event/show/components/teamDataTable.tsx b/app/routes/event/show/components/teamDataTable.tsx
index d3b7768..9e4e973 100644
--- a/app/routes/event/show/components/teamDataTable.tsx
+++ b/app/routes/event/show/components/teamDataTable.tsx
@@ -20,8 +20,10 @@ import {
 import { H4 } from "~/components/display/headers";
 import { Button } from "~/components/ui/button";
 import { ArrowUpDown } from "lucide-react";
-import { useState } from "react";
+import { useMemo, useState } from "react";
 import { Link } from "react-router";
+import {ScoreDisplay} from "~/components/display/scoreDisplay";
+import {calculateBestMythicsAndTotalScore, calculateBestScoreAndBestUnderTime, MythicData} from "~/lib/mythics";
 
 type Team = Route.ComponentProps["loaderData"]["event"]["teams"][number];
 
@@ -101,7 +103,112 @@ const columns = [
   },
 ] satisfies ColumnDef<Team["players"][number]>[];
 
-export function TeamDataTable({ team, slug }: { team: Team; slug: string }) {
+function MythicsInfoOverview({ mythics }: { mythics: MythicData[] | null }) {
+
+    if(!mythics) {
+        return (<MissingMythicInfo></MissingMythicInfo>);
+    }
+
+    const [bestMythics, bestMythicsScore] = useMemo(() => { return calculateBestMythicsAndTotalScore(mythics) }, [mythics]);
+
+    const [bestSingleScore, mostUnderTime] = useMemo(() => { return calculateBestScoreAndBestUnderTime(mythics) }, [mythics]);
+
+    return(
+        <div className="flex flex-col space-y-4">
+            <div>
+                <H4>Dungeons</H4>
+
+                <div className="grid md:grid-cols-1 lg:grid-cols-2 3xl:grid-cols-4 6xl:grid-cols-8 gap-2 w-full">
+                    {bestMythics.map((run) => (
+                        <div
+                            key={run.keystone_run_id}
+                            className="grow rounded-lg border-gray-100 border p-4 gap-2 bg-background/60 bg-(image:--bg-image) dark:bg-blend-darken bg-blend-lighten bg-linear-to-b bg-cover bg-no-repeat"
+                            style={{
+                                // @ts-expect-error: Variables are not typed
+                                "--bg-image": `url(${run.background_image_url})`,
+                            }}
+                        >
+                            <div className="flex flex-col items-center gap-y-2">
+                                <div className="flex flex-col justify-start text-3xl">
+                                    <ScoreDisplay
+                                        individual
+                                        score={run.score}
+                                        className=""
+                                    />
+                                </div>
+                                <div className="flex flex-row justify-evenly gap-4">
+                                    <p className="flex text-lg">
+                                        +{run.mythic_level}
+                                    </p>
+                                    <p className="flex text-lg">
+                                        {(
+                                            ((run.clear_time_ms - run.par_time_ms) / run.par_time_ms) *
+                                            100
+                                        ).toFixed(2)}
+                                        %
+                                    </p>
+                                </div>
+                                <p className="text-xl font-bold">
+                                    {run.short_name}
+                                </p>
+                            </div>
+                        </div>
+                    ))}
+                </div>
+            </div>
+            <div>
+                <H4>Total Team Score</H4>
+                <div className="grid md:grid-cols-1 lg:grid-cols-2 3xl:grid-cols-4 gap-2 w-full">
+                    <div className="rounded-lg border border-neutral-100 grow">
+                        <div className="flex flex-col items-center space-around pb-2 pt-2 ps-1 pe-1">
+                            <span className="text-4xl font-semibold">{mythics.length}</span>
+                            <span className="text-md font-bold">Mythics Ran</span>
+                        </div>
+                    </div>
+                    <div className="rounded-lg border border-neutral-100 grow">
+                        <div className="flex flex-col items-center space-around pb-2 pt-2 ps-1 pe-1">
+                            <ScoreDisplay
+                                score={bestMythicsScore}
+                                className="text-4xl font-semibold"
+                            />
+                            <span className="text-md font-bold">Team Score</span>
+                        </div>
+                    </div>
+
+                    <div className="rounded-lg border border-neutral-100 grow">
+                        <div className="flex flex-col items-center space-around pb-2 pt-2 ps-1 pe-1">
+                            <ScoreDisplay
+                                score={bestSingleScore}
+                                individual
+                                className="text-4xl font-semibold"
+                            />
+                            <span className="text-md font-bold">Best Single Score</span>
+                        </div>
+                    </div>
+                    <div className="rounded-lg border border-neutral-100 grow">
+                        <div className="flex flex-col items-center space-around pb-2 pt-2 ps-1 pe-1">
+                            <span className="text-4xl font-semibold">
+                              {(mostUnderTime * 100).toFixed(2)}%
+                            </span>
+                            <span className="text-md font-bold">Best Under Par</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    );
+}
+
+function MissingMythicInfo() {
+    return (
+        <div className="flex flex-col gap-4">
+            <H4>Dungeons</H4>
+            <p>No Mythic data available.</p>
+        </div>
+    );
+}
+
+export function TeamDataTable({ team, slug, mythicData }: { team: Team; slug: string; mythicData: MythicData[] | null }) {
   const [sorting, setSorting] = useState<SortingState>([]);
 
   const table = useReactTable({
@@ -162,6 +269,7 @@ export function TeamDataTable({ team, slug }: { team: Team; slug: string }) {
           )}
         </TableBody>
       </Table>
+        {mythicData && mythicData.length > 0 ? (<MythicsInfoOverview mythics={mythicData} />) : (<MissingMythicInfo />)}
     </div>
   );
 }
diff --git a/app/routes/event/show/route.tsx b/app/routes/event/show/route.tsx
index 1e4087d..45e6644 100644
--- a/app/routes/event/show/route.tsx
+++ b/app/routes/event/show/route.tsx
@@ -1,22 +1,15 @@
 import { db } from "~/lib/db.server";
 import { Route } from "./+types/route";
-import {
-  TableHead,
-  TableHeader,
-  TableRow,
-  Table,
-  TableCell,
-} from "~/components/ui/table";
-import { TableBody } from "~/components/ui/table";
 import { H2, H3 } from "~/components/display/headers";
-import { ClassDisplay } from "~/components/display/classDisplay";
-import { RoleDisplay } from "~/components/display/roleDisplay";
 import { organizeTeams } from "~/lib/teams";
 import { AppSession } from "~/lib/session.server";
 import { Button } from "~/components/ui/button";
-import { Link } from "react-router";
+import {Link} from "react-router";
 import { PlayerDataTable } from "./components/playerDataTable";
 import { TeamDataTable } from "./components/teamDataTable";
+import {RaiderIOClient} from "~/lib/raiderIO";
+import { getPlayersPromises, parseMythicDataPerTeam, MythicData } from "~/lib/mythics"
+import { CharacterNS } from "~/lib/raiderIO/characters";
 
 export async function loader({ request, params: { slug } }: Route.LoaderArgs) {
   const [event, isAdmin] = await Promise.all([
@@ -47,12 +40,43 @@ export async function loader({ request, params: { slug } }: Route.LoaderArgs) {
   }
 
   const teams = organizeTeams(event.teams);
+  const eachTeamsPlayersPromises: (Promise<null> | Promise<CharacterNS.CharacterProfilePayload<CharacterNS.OptionalCharacterProfilePayloadOptions>>)[][] = [];
 
-  return { event, teams, isAdmin };
+  const client = RaiderIOClient.getInstance();
+
+  for(const team of teams) {
+
+    const parsedPlayersPromises = getPlayersPromises(team, client);
+
+    eachTeamsPlayersPromises.push(parsedPlayersPromises);
+  }
+
+  return { event, teams, isAdmin, eachTeamsPlayersPromises, parsedMythicDataArray: null };
 }
 
+export const clientLoader = async ({
+  serverLoader,
+}: Route.ClientLoaderArgs) => {
+  const serverRes = await serverLoader();
+
+  const parsedMythicDataArray: (MythicData[] | null)[] = [];
+  let counter: number = 0;
+
+  for(const team of serverRes.teams) {
+    const mythicData = await parseMythicDataPerTeam(team, serverRes.eachTeamsPlayersPromises[counter]);
+    parsedMythicDataArray.push(mythicData);
+    ++counter;
+  }
+
+  return {
+    ...serverRes,
+    parsedMythicDataArray
+  };
+};
+clientLoader.hydrate = true;
+
 export default function Event({
-  loaderData: { event, teams, isAdmin },
+  loaderData: { event, teams, isAdmin, parsedMythicDataArray },
   params: { slug },
 }: Route.ComponentProps) {
   return (
@@ -75,8 +99,8 @@ export default function Event({
         <H3>Teams</H3>
 
         <div className="grid grid-cols-3">
-          {teams.map((team) => (
-            <TeamDataTable key={team.id} team={team} slug={slug} />
+          {teams.map((team, index) => (
+            <TeamDataTable key={team.id} team={team} slug={slug} mythicData={parsedMythicDataArray ? parsedMythicDataArray[index] : null} />
           ))}
         </div>
       </section>
diff --git a/app/routes/event/team/components/mythicInfo.tsx b/app/routes/event/team/components/mythicInfo.tsx
index 06819ab..b2dbf6a 100644
--- a/app/routes/event/team/components/mythicInfo.tsx
+++ b/app/routes/event/team/components/mythicInfo.tsx
@@ -1,51 +1,19 @@
-import { Suspense, useMemo } from "react";
+import { useMemo } from "react";
 import { Route } from "../+types/route";
-import { Await } from "react-router";
 import { H2, H3, H4, H5 } from "~/components/display/headers";
 import { msToDuration } from "~/lib/time";
 import { ScoreDisplay } from "~/components/display/scoreDisplay";
+import { calculateBestMythicsAndTotalScore, calculateBestScoreAndBestUnderTime, MythicData } from "~/lib/mythics";
 
-type Mythics = NonNullable<
-  Awaited<Route.ComponentProps["loaderData"]["mythicsPromise"]>
->;
-type MythicsPromise = Promise<Mythics | null>;
+function MythicsInfoInternal({ mythics }: { mythics: MythicData[] }) {
 
-function MythicsInfoInternal({ mythics }: { mythics: Mythics }) {
-  const [bestMythics, bestMythicsScore] = useMemo(() => {
-    const bestMythicsMap = {} as Record<string, (typeof mythics)[number]>;
+  if(!mythics) {
+    return (<MissingMythicInfo></MissingMythicInfo>);
+  }
 
-    mythics.forEach((m) => {
-      const existing = bestMythicsMap[m.dungeon];
+  const [bestMythics, bestMythicsScore] = useMemo(() => { return calculateBestMythicsAndTotalScore(mythics) }, [mythics]);
 
-      // TODO: Maybe incorporate the most recent run into this somehow?
-      if (!existing || existing.score < m.score) {
-        bestMythicsMap[m.dungeon] = m;
-      }
-    });
-
-    const bestMythics = Object.values(bestMythicsMap);
-    const totalScore = bestMythics.reduce((acc, m) => acc + m.score, 0);
-
-    return [Object.values(bestMythicsMap), totalScore] as const;
-  }, [mythics]);
-
-  const [bestSingleScore, mostUnderTime] = useMemo(() => {
-    const bestSingleScore = mythics.reduce(
-      (score, m) => (score > m.score ? score : m.score),
-      0
-    );
-
-    const mostUnderTime = mythics.reduce((curr, m) => {
-      const percentUnder = (m.clear_time_ms - m.par_time_ms) / m.par_time_ms;
-
-      if (percentUnder > curr) {
-        return percentUnder;
-      } else {
-        return curr;
-      }
-    }, 0);
-    return [bestSingleScore, mostUnderTime] as const;
-  }, [mythics]);
+  const [bestSingleScore, mostUnderTime] = useMemo(() => { return calculateBestScoreAndBestUnderTime(mythics) }, [mythics]);
 
   return (
     <div className="flex flex-col space-y-2">
@@ -89,7 +57,7 @@ function MythicsInfoInternal({ mythics }: { mythics: Mythics }) {
 
         <H4>Dungeons</H4>
 
-        <div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
+        <div className="grid sm:grid-cols-1 lg:grid-cols-2 2xl:grid-cols-4 gap-2">
           {bestMythics.map((run) => (
             <div
               key={run.keystone_run_id}
@@ -128,7 +96,9 @@ function MythicsInfoInternal({ mythics }: { mythics: Mythics }) {
         <H3>All Dungeons</H3>
 
         <div className="grid grid-cols-1 sm:grid-cols-2">
-          {mythics.map((run) => (
+          {mythics
+              .filter((run) => run !== null)
+              .map((run) => (
             <div
               key={run.keystone_run_id}
               className="flex flex-row sm:even:flex-row-reverse gap-4"
@@ -158,6 +128,7 @@ function MythicsInfoInternal({ mythics }: { mythics: Mythics }) {
 }
 
 function MissingMythicInfo() {
+  console.log("Mythic Info Missing Component");
   return (
     <div className="flex flex-col gap-4">
       <H2>Mythics</H2>
@@ -166,18 +137,10 @@ function MissingMythicInfo() {
   );
 }
 
-export function MythicInfo({ mythics }: { mythics: MythicsPromise | null }) {
+export function MythicInfo({ mythics }: { mythics: MythicData[] | null }) {
   return (
-    <Suspense fallback={<div>loading...</div>}>
-      <Await resolve={mythics} errorElement={<MissingMythicInfo />}>
-        {(mythics) =>
-          mythics ? (
-            <MythicsInfoInternal mythics={mythics} />
-          ) : (
-            <MissingMythicInfo />
-          )
-        }
-      </Await>
-    </Suspense>
+      <div>
+        {mythics ? (<MythicsInfoInternal mythics={mythics} />) : (<MissingMythicInfo />)}
+      </div>
   );
 }
diff --git a/app/routes/event/team/route.tsx b/app/routes/event/team/route.tsx
index 8d28101..3b16cdf 100644
--- a/app/routes/event/team/route.tsx
+++ b/app/routes/event/team/route.tsx
@@ -1,11 +1,11 @@
-import { Await, Link, redirect } from "react-router";
+import { Link, redirect } from "react-router";
 import { H2 } from "~/components/display/headers";
 import { db } from "~/lib/db.server";
 import { Route } from "./+types/route";
 import { RaiderIOClient } from "~/lib/raiderIO";
-import { Suspense } from "react";
 import { PlayerData } from "./components/playerData";
 import { MythicInfo } from "./components/mythicInfo";
+import { getPlayersPromises, parseMythicDataPerTeam } from "~/lib/mythics"
 
 export const loader = async ({ params: { slug, id } }: Route.LoaderArgs) => {
   const team = await db.team.findFirst({
@@ -17,32 +17,14 @@ export const loader = async ({ params: { slug, id } }: Route.LoaderArgs) => {
     return redirect(`/event/${slug}`);
   }
 
-  const client = await RaiderIOClient.getInstance();
+  const client = RaiderIOClient.getInstance();
 
-  const playersPromises = team.players.map((player) =>
-    player.playerServer && player.playerName
-      ? client.character.getCharacterProfile({
-          region: "us",
-          realm: player.playerServer,
-          name: player.playerName,
-          fields: {
-            gear: true,
-            mythic_plus_best_runs: true,
-            mythic_plus_alternate_runs: true,
-            mythic_plus_highest_level_runs: true,
-            mythic_plus_recent_runs: true,
-            mythic_plus_previous_weekly_highest_level_runs: true,
-            mythic_plus_weekly_highest_level_runs: true,
-          },
-        })
-      : Promise.resolve(null)
-  );
+  const playersPromises =  getPlayersPromises(team, client);
 
   return {
     team,
     playersPromises,
-    allPlayersPromise: null,
-    mythicsPromise: null,
+    mythicData: null
   };
 };
 export const action = async ({}: Route.ActionArgs) => ({});
@@ -52,113 +34,121 @@ export const clientLoader = async ({
 }: Route.ClientLoaderArgs) => {
   const serverRes = await serverLoader();
 
-  const players = serverRes.team.players;
-  const playersMap = new Map<string, (typeof players)[number]>();
-  players.forEach((p) => playersMap.set(p.id, p));
-
-  // Set these promises up in the client loader so they are stable in the react application.
-  const allPlayersPromise = Promise.allSettled(serverRes.playersPromises);
-  const mythicsPromise = allPlayersPromise.then((res) => {
-    // console.log("Mythics Promise", res);
-    if (!res) return res;
-
-    const succeededRes = res
-      .filter((r) => r.status === "fulfilled")
-      .map((r) => r.value);
-
-    type MythicType = NonNullable<
-      (typeof succeededRes)[number]
-    >["mythic_plus_best_runs"][number];
-
-    const mythicMap = new Map<number, MythicType>();
-    const participantMap = new Map<number, string[]>();
-    // console.log("DB Players", players);
-
-    succeededRes.forEach((player) => {
-      if (!player) return;
-      // We just want to resolve via the dbPlayer ID so we find and ensure we have that.
-      const dbPlayer = players.find(({ playerName, playerServer }) => {
-        // Stripe whitespace and replace lowercase to slugify everything for more accurate
-        // matching. This should be fine since names are not case sensitive.
-        const strip = (str: string | undefined | null) =>
-          str?.toLocaleLowerCase()?.replace(/\s/, "");
-
-        return (
-          strip(playerName) === strip(player.name) &&
-          strip(playerServer) === strip(player.realm)
-        );
-      });
-      // console.log("DB Player", dbPlayer, player.name, player.realm);
-      if (!dbPlayer) return;
-
-      const allMythics = [
-        ...(player.mythic_plus_best_runs ?? []),
-        ...(player.mythic_plus_alternate_runs ?? []),
-        ...(player.mythic_plus_highest_level_runs ?? []),
-        ...(player.mythic_plus_recent_runs ?? []),
-        ...(player.mythic_plus_previous_weekly_highest_level_runs ?? []),
-        ...(player.mythic_plus_weekly_highest_level_runs ?? []),
-      ];
-
-      allMythics.forEach((run) => {
-        mythicMap.set(run.keystone_run_id, run);
-
-        const participantsList = participantMap.get(run.keystone_run_id);
-
-        // Ensure this player is in the participant list
-        if (!participantsList) {
-          participantMap.set(run.keystone_run_id, [dbPlayer.id]);
-        } else if (participantsList.findIndex((id) => id === dbPlayer.id) < 0) {
-          participantsList.push(dbPlayer.id);
-        }
-      });
-    });
-
-    // console.log("Mythic Map", mythicMap);
-    // console.log("Participant Map", participantMap);
-
-    // A mythic counts if at least 4 players of the team are in it.
-    const mythics = Array.from(mythicMap.values())
-      .flatMap((m) => {
-        const participants = participantMap.get(m.keystone_run_id);
-
-        // If there were no participants or not enough participants this does not count as a key for this team.
-        if (!participants || participants.length < 4) {
-          return [];
-        }
-
-        return [
-          {
-            ...m,
-            participants: participants
-              .map((p) => playersMap.get(p))
-              .filter((p) => p !== undefined),
-          },
-        ];
-      })
-      .sort(
-        (mythicA, mythicB) =>
-          // First sort descending on the keystone level so higher keystones are at the top
-          mythicB.mythic_level - mythicA.mythic_level ||
-          // Next descending by score..
-          mythicB.score - mythicA.score ||
-          // Finally sort ascending within the score and mythic level category by string.
-          mythicA.short_name.localeCompare(mythicB.short_name)
-      );
-
-    return mythics;
-  });
-
+  const mythicData = await parseMythicDataPerTeam(serverRes.team, serverRes.playersPromises);
+
+//   console.log(serverRes);
+//
+//   const parsedMythicData =  await ParseMythicDataPerTeam(serverRes.team.players, serverRes.playersPromises);
+//
+//   return {
+//     ...serverRes,
+//     ...parsedMythicData,
+//   }
+  // const players = serverRes.team.players;
+  // const playersMap = new Map<string, (typeof players)[number]>();
+  // players.forEach((p) => playersMap.set(p.id, p));
+  //
+  // // Set these promises up in the client loader so they are stable in the react application.
+  // const allPlayersPromise = Promise.allSettled(serverRes.playersPromises);
+  // const mythicsPromise = allPlayersPromise.then((res) => { // console.log("Mythics Promise", res);
+  //   if (!res) return res;
+  //
+  //   const succeededRes = res
+  //       .filter((r: any) => r.status === "fulfilled")
+  //       .map((r: any) => r.value);
+  //
+  //   type MythicType = NonNullable<
+  //       (typeof succeededRes)[number]
+  //   >["mythic_plus_best_runs"][number];
+  //
+  //   const mythicMap = new Map<number, MythicType>();
+  //   const participantMap = new Map<number, string[]>();
+  //   // console.log("DB Players", players);
+  //
+  //   succeededRes.forEach((player: any) => {
+  //     if (!player) return;
+  //     // We just want to resolve via the dbPlayer ID so we find and ensure we have that.
+  //     const dbPlayer = players.find(({ playerName, playerServer }) => {
+  //       // Strip whitespace and replace lowercase to slugify everything for more accurate
+  //       // matching. This should be fine since names are not case-sensitive.
+  //       const strip = (str: string | undefined | null) =>
+  //           str?.toLocaleLowerCase()?.replace(/\s/, "");
+  //
+  //       return (
+  //           strip(playerName) === strip(player.name) &&
+  //           strip(playerServer) === strip(player.realm)
+  //       );
+  //     });
+  //     // console.log("DB Player", dbPlayer, player.name, player.realm);
+  //     if (!dbPlayer) return;
+  //
+  //     const allMythics = [
+  //       ...(player.mythic_plus_best_runs ?? []),
+  //       ...(player.mythic_plus_alternate_runs ?? []),
+  //       ...(player.mythic_plus_highest_level_runs ?? []),
+  //       ...(player.mythic_plus_recent_runs ?? []),
+  //       ...(player.mythic_plus_previous_weekly_highest_level_runs ?? []),
+  //       ...(player.mythic_plus_weekly_highest_level_runs ?? []),
+  //     ];
+  //
+  //     allMythics.forEach((run) => {
+  //       mythicMap.set(run.keystone_run_id, run);
+  //
+  //       const participantsList = participantMap.get(run.keystone_run_id);
+  //
+  //       // Ensure this player is in the participant list
+  //       if (!participantsList) {
+  //         participantMap.set(run.keystone_run_id, [dbPlayer.id]);
+  //       } else if (participantsList.findIndex((id) => id === dbPlayer.id) < 0) {
+  //         participantsList.push(dbPlayer.id);
+  //       }
+  //     });
+  //   });
+  //
+  //   // console.log("Mythic Map", mythicMap);
+  //   // console.log("Participant Map", participantMap);
+  //
+  //   // A mythic counts if at least 4 players of the team are in it.
+  //   const mythics = Array.from(mythicMap.values())
+  //       .flatMap((m) => {
+  //         const participants = participantMap.get(m.keystone_run_id);
+  //
+  //         // If there were no participants or not enough participants this does not count as a key for this team.
+  //         // if (!participants || participants.length < 4) {
+  //         //   return [];
+  //         // }
+  //
+  //         return [
+  //           {
+  //             ...m,
+  //             participants: participants
+  //                 .map((p) => playersMap.get(p))
+  //                 .filter((p) => p !== undefined),
+  //           },
+  //         ];
+  //       })
+  //       .sort(
+  //           (mythicA, mythicB) =>
+  //               // First sort descending on the keystone level so higher keystones are at the top.
+  //               mythicB.mythic_level - mythicA.mythic_level ||
+  //               // Next descending by score.
+  //               mythicB.score - mythicA.score ||
+  //               // Finally sort ascending within the score and mythic level category by string.
+  //               mythicA.short_name.localeCompare(mythicB.short_name)
+  //       );
+  //
+  //   return mythics;
+  // });
+  //
   return {
     ...serverRes,
-    allPlayersPromise,
-    mythicsPromise,
+    mythicData
   };
 };
 clientLoader.hydrate = true;
 
 export default function TeamShow({
-  loaderData: { team, playersPromises, allPlayersPromise, mythicsPromise },
+  loaderData: { team, playersPromises, mythicData },
   params: { slug },
 }: Route.ComponentProps) {
   return (
@@ -170,7 +160,7 @@ export default function TeamShow({
       {playersPromises.map((playerPromise, index) => (
         <PlayerData player={playerPromise} key={index} />
       ))}
-      <MythicInfo mythics={mythicsPromise} />
+      <MythicInfo mythics={mythicData} />
     </article>
   );
 }
diff --git a/app/tailwind.css b/app/tailwind.css
index 668980f..3819227 100644
--- a/app/tailwind.css
+++ b/app/tailwind.css
@@ -26,6 +26,9 @@
   --color-rare: #0070dd;
   --color-epic: #a335ee;
   --color-legendary: #ff8000;
+
+  --breakpoint-3xl: 120rem;
+  --breakpoint-6xl: 230rem;
 }
 
 @theme inline {


Reviews:
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T23:48:19Z
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T23:48:43Z
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T23:48:47Z
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T23:49:48Z
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T23:59:57Z
user: login: DendyA
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T04:31:20Z
user: login: DendyA
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-10T18:48:26Z
user: login: DendyA
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T00:18:25Z
user: login: DendyA
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T00:18:39Z
user: login: DendyA
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T00:27:50Z
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-12T06:55:12Z
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-12T06:56:11Z
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-12T06:56:45Z
user: login: plohkoon
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-12T06:57:24Z
user: login: DendyA
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T15:59:29Z
user: login: DendyA
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T01:07:20Z
user: login: DendyA
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T01:10:34Z

Review comments:
diff_hunk: @@ -0,0 +1,138 @@
+export async function ParseMythicDataPerTeam(players: any, playersPromises: any) {
path: app/lib/mythics.ts
user: login: plohkoon
body: Any is sad
created_at: 2025-05-09T23:48:19Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,138 @@
+export async function ParseMythicDataPerTeam(players: any, playersPromises: any) {
+    // const players = serverRes.team.players;
+    const playersMap = new Map<string, (typeof players)[number]>();
+    players.forEach((p: any) => playersMap.set(p.id, p));
+
+    // Set these promises up in the client loader so they are stable in the react application.
+    const allPlayersPromise = Promise.allSettled(playersPromises);
+    const mythicsPromise = allPlayersPromise.then((res) => { // console.log("Mythics Promise", res);
+        if (!res) return res;
+
+        const succeededRes = res
+            .filter((r: any) => r.status === "fulfilled")
+            .map((r: any) => r.value);
+
+        type MythicType = NonNullable<
+            (typeof succeededRes)[number]
+        >["mythic_plus_best_runs"][number];
+
+        const mythicMap = new Map<number, MythicType>();
+        const participantMap = new Map<number, string[]>();
+        // console.log("DB Players", players);
+
+        succeededRes.forEach((player: any) => {
+            if (!player) return;
+            // We just want to resolve via the dbPlayer ID so we find and ensure we have that.
+            const dbPlayer = players.find(({ playerName, playerServer }) => {
+                // Strip whitespace and replace lowercase to slugify everything for more accurate
+                // matching. This should be fine since names are not case-sensitive.
+                const strip = (str: string | undefined | null) =>
+                    str?.toLocaleLowerCase()?.replace(/\s/, "");
+
+                return (
+                    strip(playerName) === strip(player.name) &&
+                    strip(playerServer) === strip(player.realm)
+                );
+            });
+            // console.log("DB Player", dbPlayer, player.name, player.realm);
+            if (!dbPlayer) return;
+
+            const allMythics = [
+                ...(player.mythic_plus_best_runs ?? []),
+                ...(player.mythic_plus_alternate_runs ?? []),
+                ...(player.mythic_plus_highest_level_runs ?? []),
+                ...(player.mythic_plus_recent_runs ?? []),
+                ...(player.mythic_plus_previous_weekly_highest_level_runs ?? []),
+                ...(player.mythic_plus_weekly_highest_level_runs ?? []),
+            ];
+
+            allMythics.forEach((run) => {
+                mythicMap.set(run.keystone_run_id, run);
+
+                const participantsList = participantMap.get(run.keystone_run_id);
+
+                // Ensure this player is in the participant list
+                if (!participantsList) {
+                    participantMap.set(run.keystone_run_id, [dbPlayer.id]);
+                } else if (participantsList.findIndex((id) => id === dbPlayer.id) < 0) {
+                    participantsList.push(dbPlayer.id);
+                }
+            });
+        });
+
+        // console.log("Mythic Map", mythicMap);
+        // console.log("Participant Map", participantMap);
+
+        // A mythic counts if at least 4 players of the team are in it.
+        const mythics = Array.from(mythicMap.values())
+            .flatMap((m) => {
+                const participants = participantMap.get(m.keystone_run_id);
+
+                // If there were no participants or not enough participants this does not count as a key for this team.
+                if (!participants || participants.length < 4) {
+                  return [];
+                }
+
+                return [
+                    {
+                        ...m,
+                        participants: participants
+                            .map((p) => playersMap.get(p))
+                            .filter((p) => p !== undefined),
+                    },
+                ];
+            })
+            .sort(
+                (mythicA, mythicB) =>
+                    // First sort descending on the keystone level so higher keystones are at the top.
+                    mythicB.mythic_level - mythicA.mythic_level ||
+                    // Next descending by score.
+                    mythicB.score - mythicA.score ||
+                    // Finally sort ascending within the score and mythic level category by string.
+                    mythicA.short_name.localeCompare(mythicB.short_name)
+            );
+
+        return mythics;
+    });
+
+    return {
+        allPlayersPromise,
+        mythicsPromise,
+    };
+}
+
+export function CalculateBestMythicsAndTotalScore(mythics: any) {
+    const bestMythicsMap = {} as Record<string, (typeof mythics)[number]>;
+
+    mythics.forEach((m: any) => {
+        const existing = bestMythicsMap[m.dungeon];
+
+        // TODO: Maybe incorporate the most recent run into this somehow?
+        if (!existing || existing.score < m.score) {
+            bestMythicsMap[m.dungeon] = m;
+        }
+    });
+
+    const bestMythics = Object.values(bestMythicsMap);
+    const totalScore = bestMythics.reduce((acc, m) => acc + m.score, 0);
+
+    return [Object.values(bestMythicsMap), totalScore] as const;
+}
+
+export function CalculateBestScoreAndBestUnderTime(mythics: any) {
+    const bestSingleScore = mythics.reduce(
+        (score: any, m: any) => (score > m.score ? score : m.score),
+        0
+    );
+
+    const mostUnderTime = mythics.reduce((curr: any, m: any) => {
+        const percentUnder = (m.clear_time_ms - m.par_time_ms) / m.par_time_ms;
+
+        if (percentUnder > curr) {
+            return percentUnder;
+        } else {
+            return curr;
+        }
+    }, 0);
+    return [bestSingleScore, mostUnderTime] as const;
+}
path: app/lib/mythics.ts
user: login: plohkoon
body: Should configure your editor to append new lines to save
created_at: 2025-05-09T23:48:43Z
author_association: OWNER
diff_hunk: @@ -6,7 +6,7 @@ import {
 } from "react-router";
 import { db, Prisma, User } from "./db.server";
 
-const ADMINS = ["plohkoon", "captainfrogs"];
+const ADMINS = ["plohkoon", "captainfrogs", "pointysalad"];
path: app/lib/session.server.ts
user: login: plohkoon
body: lol
created_at: 2025-05-09T23:48:46Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,138 @@
+export async function ParseMythicDataPerTeam(players: any, playersPromises: any) {
path: app/lib/mythics.ts
user: login: plohkoon
body: Also stylistically this isn't a component so it should theoretically be `parseMythicDataPerTeam`
created_at: 2025-05-09T23:49:48Z
author_association: OWNER
diff_hunk: @@ -47,12 +39,40 @@ export async function loader({ request, params: { slug } }: Route.LoaderArgs) {
   }
 
   const teams = organizeTeams(event.teams);
+  const parsedMythicDataArray: any[] = [];
 
-  return { event, teams, isAdmin };
+  const client = await RaiderIOClient.getInstance();
+
+  for(const team of teams) {
+    const playersPromises = team.players.map((player) =>
+        player.playerServer && player.playerName
+            ? client.character.getCharacterProfile({
+              region: "us",
+              realm: player.playerServer,
+              name: player.playerName,
+              fields: {
+                gear: true,
+                mythic_plus_best_runs: true,
+                mythic_plus_alternate_runs: true,
+                mythic_plus_highest_level_runs: true,
+                mythic_plus_recent_runs: true,
+                mythic_plus_previous_weekly_highest_level_runs: true,
+                mythic_plus_weekly_highest_level_runs: true,
+              },
+            })
+            : Promise.resolve(null)
+    );
+
+    const mythicsParsedPerTeam = await ParseMythicDataPerTeam(team.players, playersPromises);
+
+    parsedMythicDataArray.push(mythicsParsedPerTeam.mythicsPromise);
+  }
path: app/routes/event/show/route.tsx
user: login: plohkoon
body: This is potentially getting really weird with the data loading. I'll have to look on my machine but I'm suspicious of what's a promise here and what's not. You may need to wrap the final `parsedMythicDataArray` with a `Promise.all` or `Promise.allSettled` before returning it to the client.
created_at: 2025-05-09T23:59:57Z
author_association: OWNER
diff_hunk: @@ -6,7 +6,7 @@ import {
 } from "react-router";
 import { db, Prisma, User } from "./db.server";
 
-const ADMINS = ["plohkoon", "captainfrogs"];
+const ADMINS = ["plohkoon", "captainfrogs", "pointysalad"];
path: app/lib/session.server.ts
user: login: DendyA
body: Was hoping to sneak that in! Nothing to see here! lol
created_at: 2025-05-10T04:31:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,138 @@
+export async function ParseMythicDataPerTeam(players: any, playersPromises: any) {
+    // const players = serverRes.team.players;
+    const playersMap = new Map<string, (typeof players)[number]>();
+    players.forEach((p: any) => playersMap.set(p.id, p));
+
+    // Set these promises up in the client loader so they are stable in the react application.
+    const allPlayersPromise = Promise.allSettled(playersPromises);
+    const mythicsPromise = allPlayersPromise.then((res) => { // console.log("Mythics Promise", res);
+        if (!res) return res;
+
+        const succeededRes = res
+            .filter((r: any) => r.status === "fulfilled")
+            .map((r: any) => r.value);
+
+        type MythicType = NonNullable<
+            (typeof succeededRes)[number]
+        >["mythic_plus_best_runs"][number];
+
+        const mythicMap = new Map<number, MythicType>();
+        const participantMap = new Map<number, string[]>();
+        // console.log("DB Players", players);
+
+        succeededRes.forEach((player: any) => {
+            if (!player) return;
+            // We just want to resolve via the dbPlayer ID so we find and ensure we have that.
+            const dbPlayer = players.find(({ playerName, playerServer }) => {
+                // Strip whitespace and replace lowercase to slugify everything for more accurate
+                // matching. This should be fine since names are not case-sensitive.
+                const strip = (str: string | undefined | null) =>
+                    str?.toLocaleLowerCase()?.replace(/\s/, "");
+
+                return (
+                    strip(playerName) === strip(player.name) &&
+                    strip(playerServer) === strip(player.realm)
+                );
+            });
+            // console.log("DB Player", dbPlayer, player.name, player.realm);
+            if (!dbPlayer) return;
+
+            const allMythics = [
+                ...(player.mythic_plus_best_runs ?? []),
+                ...(player.mythic_plus_alternate_runs ?? []),
+                ...(player.mythic_plus_highest_level_runs ?? []),
+                ...(player.mythic_plus_recent_runs ?? []),
+                ...(player.mythic_plus_previous_weekly_highest_level_runs ?? []),
+                ...(player.mythic_plus_weekly_highest_level_runs ?? []),
+            ];
+
+            allMythics.forEach((run) => {
+                mythicMap.set(run.keystone_run_id, run);
+
+                const participantsList = participantMap.get(run.keystone_run_id);
+
+                // Ensure this player is in the participant list
+                if (!participantsList) {
+                    participantMap.set(run.keystone_run_id, [dbPlayer.id]);
+                } else if (participantsList.findIndex((id) => id === dbPlayer.id) < 0) {
+                    participantsList.push(dbPlayer.id);
+                }
+            });
+        });
+
+        // console.log("Mythic Map", mythicMap);
+        // console.log("Participant Map", participantMap);
+
+        // A mythic counts if at least 4 players of the team are in it.
+        const mythics = Array.from(mythicMap.values())
+            .flatMap((m) => {
+                const participants = participantMap.get(m.keystone_run_id);
+
+                // If there were no participants or not enough participants this does not count as a key for this team.
+                if (!participants || participants.length < 4) {
+                  return [];
+                }
+
+                return [
+                    {
+                        ...m,
+                        participants: participants
+                            .map((p) => playersMap.get(p))
+                            .filter((p) => p !== undefined),
+                    },
+                ];
+            })
+            .sort(
+                (mythicA, mythicB) =>
+                    // First sort descending on the keystone level so higher keystones are at the top.
+                    mythicB.mythic_level - mythicA.mythic_level ||
+                    // Next descending by score.
+                    mythicB.score - mythicA.score ||
+                    // Finally sort ascending within the score and mythic level category by string.
+                    mythicA.short_name.localeCompare(mythicB.short_name)
+            );
+
+        return mythics;
+    });
+
+    return {
+        allPlayersPromise,
+        mythicsPromise,
+    };
+}
+
+export function CalculateBestMythicsAndTotalScore(mythics: any) {
+    const bestMythicsMap = {} as Record<string, (typeof mythics)[number]>;
+
+    mythics.forEach((m: any) => {
+        const existing = bestMythicsMap[m.dungeon];
+
+        // TODO: Maybe incorporate the most recent run into this somehow?
+        if (!existing || existing.score < m.score) {
+            bestMythicsMap[m.dungeon] = m;
+        }
+    });
+
+    const bestMythics = Object.values(bestMythicsMap);
+    const totalScore = bestMythics.reduce((acc, m) => acc + m.score, 0);
+
+    return [Object.values(bestMythicsMap), totalScore] as const;
+}
+
+export function CalculateBestScoreAndBestUnderTime(mythics: any) {
+    const bestSingleScore = mythics.reduce(
+        (score: any, m: any) => (score > m.score ? score : m.score),
+        0
+    );
+
+    const mostUnderTime = mythics.reduce((curr: any, m: any) => {
+        const percentUnder = (m.clear_time_ms - m.par_time_ms) / m.par_time_ms;
+
+        if (percentUnder > curr) {
+            return percentUnder;
+        } else {
+            return curr;
+        }
+    }, 0);
+    return [bestSingleScore, mostUnderTime] as const;
+}
path: app/lib/mythics.ts
user: login: DendyA
body: My editor has been updated to append new lines on save.
created_at: 2025-05-10T18:48:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,138 @@
+export async function ParseMythicDataPerTeam(players: any, playersPromises: any) {
path: app/lib/mythics.ts
user: login: DendyA
body: Agreed, any is very sad. Therefore, every any typing from mythics.ts (and files that use mythics.ts) has been replaced with an actual type.
created_at: 2025-05-12T00:18:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,138 @@
+export async function ParseMythicDataPerTeam(players: any, playersPromises: any) {
path: app/lib/mythics.ts
user: login: DendyA
body: The function name has been renamed correctly
created_at: 2025-05-12T00:18:39Z
author_association: COLLABORATOR
diff_hunk: @@ -47,12 +39,40 @@ export async function loader({ request, params: { slug } }: Route.LoaderArgs) {
   }
 
   const teams = organizeTeams(event.teams);
+  const parsedMythicDataArray: any[] = [];
 
-  return { event, teams, isAdmin };
+  const client = await RaiderIOClient.getInstance();
+
+  for(const team of teams) {
+    const playersPromises = team.players.map((player) =>
+        player.playerServer && player.playerName
+            ? client.character.getCharacterProfile({
+              region: "us",
+              realm: player.playerServer,
+              name: player.playerName,
+              fields: {
+                gear: true,
+                mythic_plus_best_runs: true,
+                mythic_plus_alternate_runs: true,
+                mythic_plus_highest_level_runs: true,
+                mythic_plus_recent_runs: true,
+                mythic_plus_previous_weekly_highest_level_runs: true,
+                mythic_plus_weekly_highest_level_runs: true,
+              },
+            })
+            : Promise.resolve(null)
+    );
+
+    const mythicsParsedPerTeam = await ParseMythicDataPerTeam(team.players, playersPromises);
+
+    parsedMythicDataArray.push(mythicsParsedPerTeam.mythicsPromise);
+  }
path: app/routes/event/show/route.tsx
user: login: DendyA
body: The ```team.players.map``` portion of code this comment is referring to has been moved into ```getPlayersPromises()``` in ```mythic.ts```. The return type of the function is an array of promises of  a team's player's data. This can be interspersed with promises of null due to the else clause in the ternary operator.

For example, due to the event's teams having 5 players on them, there will be an array of 5 promises with each being either null or the player's data.

This array of promises is used in ```parseMythicDataPerTeam()``` in the same ```mythics.ts``` file. In this function, it is wrapped in a ```Promise.allSettled```. Essentially it is collapsed into one promise. This function is run in the clientLoader while the ```getPlayersPromises()``` is run in the loader (server side). The reason being that ```getPlayersPromises()``` needs the RaiderIO client, which has to be called server side. The ```parseMythicDataPerTeam()``` function needs to be used in the clientLoader to resolve the early React state update error called out in this PR. The ```parseMythicDataPerTeam()``` function returns a Promise of an array of mythic data or a Promise of null.
created_at: 2025-05-12T00:27:50Z
author_association: COLLABORATOR
diff_hunk: @@ -2,346 +2,344 @@ import type { RaiderIOClient } from ".";
 import type { RootNS } from "./index";
 import { fieldsString, HasChildBool } from "./utils";
 
-export namespace CharacterNS {
path: app/lib/raiderIO/characters.ts
user: login: plohkoon
body: I'm a bit confused why you were having trouble with a NS import. You can also maybe try changing it to `import type {CharacterNS} ...`. The type directive helps sometimes
created_at: 2025-05-12T06:55:12Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,218 @@
+import {
+    OptionalCharacterProfilePayloadOptions,
+    OptionalCharacterProfilePayloadFields,
+    CharacterProfilePayload,
+    CharacterPayloadBase,
+    MythicPlusRun
+} from "~/lib/raiderIO/characters";
+import {RaiderIOClient} from "~/lib/raiderIO";
+
+export type DBPlayerInternalTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+};
+
+export type DBPlayerType = {
+    team?: DBPlayerInternalTeamType | null
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    nickname: string
+    main?: string | null
+    teamId?: string | null
+    spec?: string | null
+    playerName?: string | null
+    playerServer?: string | null
+    eventId?: string | null
+    assignedRole?: string | null
+};
+
+export type DBTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+    players: DBPlayerType[]
+};
+
+export type MythicData = MythicPlusRun & {
+    participants: DBPlayerType[]
+};
+
+export function getPlayersPromises(team: DBTeamType | null, client: RaiderIOClient) {
+    if(!team) {
+        const playersPromises: Promise<null>[] = [Promise.resolve(null)];
+
+        return playersPromises;
+    }
+
+    const playersPromises: (Promise<null> | Promise<CharacterProfilePayload<OptionalCharacterProfilePayloadOptions>>)[] =
+        team.players.map((player) =>
+            player.playerServer && player.playerName
+                ? client.character.getCharacterProfile({
+                    region: "us",
+                    realm: player.playerServer,
+                    name: player.playerName,
+                    fields: {
+                        gear: true,
+                        mythic_plus_best_runs: true,
+                        mythic_plus_alternate_runs: true,
+                        mythic_plus_highest_level_runs: true,
+                        mythic_plus_recent_runs: true,
+                        mythic_plus_previous_weekly_highest_level_runs: true,
+                        mythic_plus_weekly_highest_level_runs: true,
+                    },
+                })
+                : Promise.resolve(null)
+        );
+
+    return playersPromises;
+}
+
+export async function parseMythicDataPerTeam(team: DBTeamType | null, playersPromises: (Promise<null> | Promise<CharacterProfilePayload<OptionalCharacterProfilePayloadOptions>>)[]) {
path: app/lib/mythics.ts
user: login: plohkoon
body: I don't think that this will return the correct type information. You probably need to do something with a generic here. I can try to help with this tomorrow.
created_at: 2025-05-12T06:56:11Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,218 @@
+import {
+    OptionalCharacterProfilePayloadOptions,
+    OptionalCharacterProfilePayloadFields,
+    CharacterProfilePayload,
+    CharacterPayloadBase,
+    MythicPlusRun
+} from "~/lib/raiderIO/characters";
+import {RaiderIOClient} from "~/lib/raiderIO";
+
+export type DBPlayerInternalTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+};
+
+export type DBPlayerType = {
+    team?: DBPlayerInternalTeamType | null
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    nickname: string
+    main?: string | null
+    teamId?: string | null
+    spec?: string | null
+    playerName?: string | null
+    playerServer?: string | null
+    eventId?: string | null
+    assignedRole?: string | null
+};
+
+export type DBTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+    players: DBPlayerType[]
+};
+
+export type MythicData = MythicPlusRun & {
+    participants: DBPlayerType[]
+};
+
+export function getPlayersPromises(team: DBTeamType | null, client: RaiderIOClient) {
+    if(!team) {
+        const playersPromises: Promise<null>[] = [Promise.resolve(null)];
+
+        return playersPromises;
+    }
+
+    const playersPromises: (Promise<null> | Promise<CharacterProfilePayload<OptionalCharacterProfilePayloadOptions>>)[] =
+        team.players.map((player) =>
+            player.playerServer && player.playerName
+                ? client.character.getCharacterProfile({
+                    region: "us",
+                    realm: player.playerServer,
+                    name: player.playerName,
+                    fields: {
+                        gear: true,
+                        mythic_plus_best_runs: true,
+                        mythic_plus_alternate_runs: true,
+                        mythic_plus_highest_level_runs: true,
+                        mythic_plus_recent_runs: true,
+                        mythic_plus_previous_weekly_highest_level_runs: true,
+                        mythic_plus_weekly_highest_level_runs: true,
+                    },
+                })
+                : Promise.resolve(null)
+        );
+
+    return playersPromises;
+}
+
+export async function parseMythicDataPerTeam(team: DBTeamType | null, playersPromises: (Promise<null> | Promise<CharacterProfilePayload<OptionalCharacterProfilePayloadOptions>>)[]) {
path: app/lib/mythics.ts
user: login: plohkoon
body: Presumably the return type gets correctly inferred though?
created_at: 2025-05-12T06:56:45Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,218 @@
+import {
+    OptionalCharacterProfilePayloadOptions,
+    OptionalCharacterProfilePayloadFields,
+    CharacterProfilePayload,
+    CharacterPayloadBase,
+    MythicPlusRun
+} from "~/lib/raiderIO/characters";
+import {RaiderIOClient} from "~/lib/raiderIO";
+
+export type DBPlayerInternalTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+};
+
+export type DBPlayerType = {
+    team?: DBPlayerInternalTeamType | null
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    nickname: string
+    main?: string | null
+    teamId?: string | null
+    spec?: string | null
+    playerName?: string | null
+    playerServer?: string | null
+    eventId?: string | null
+    assignedRole?: string | null
+};
+
+export type DBTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+    players: DBPlayerType[]
+};
+
+export type MythicData = MythicPlusRun & {
+    participants: DBPlayerType[]
+};
+
+export function getPlayersPromises(team: DBTeamType | null, client: RaiderIOClient) {
+    if(!team) {
+        const playersPromises: Promise<null>[] = [Promise.resolve(null)];
+
+        return playersPromises;
+    }
+
+    const playersPromises: (Promise<null> | Promise<CharacterProfilePayload<OptionalCharacterProfilePayloadOptions>>)[] =
+        team.players.map((player) =>
+            player.playerServer && player.playerName
+                ? client.character.getCharacterProfile({
+                    region: "us",
+                    realm: player.playerServer,
+                    name: player.playerName,
+                    fields: {
+                        gear: true,
+                        mythic_plus_best_runs: true,
+                        mythic_plus_alternate_runs: true,
+                        mythic_plus_highest_level_runs: true,
+                        mythic_plus_recent_runs: true,
+                        mythic_plus_previous_weekly_highest_level_runs: true,
+                        mythic_plus_weekly_highest_level_runs: true,
+                    },
+                })
+                : Promise.resolve(null)
+        );
+
+    return playersPromises;
+}
+
+export async function parseMythicDataPerTeam(team: DBTeamType | null, playersPromises: (Promise<null> | Promise<CharacterProfilePayload<OptionalCharacterProfilePayloadOptions>>)[]) {
path: app/lib/mythics.ts
user: login: plohkoon
body: Could also just replace the generic with the actual type object we're using. In this case:

```Typescript
{
                        gear: true,
                        mythic_plus_best_runs: true,
                        mythic_plus_alternate_runs: true,
                        mythic_plus_highest_level_runs: true,
                        mythic_plus_recent_runs: true,
                        mythic_plus_previous_weekly_highest_level_runs: true,
                        mythic_plus_weekly_highest_level_runs: true,
                    }
```
created_at: 2025-05-12T06:57:24Z
author_association: OWNER
diff_hunk: @@ -2,346 +2,344 @@ import type { RaiderIOClient } from ".";
 import type { RootNS } from "./index";
 import { fieldsString, HasChildBool } from "./utils";
 
-export namespace CharacterNS {
path: app/lib/raiderIO/characters.ts
user: login: DendyA
body: I just undid these changes and with both import type { CharacterNS } and just import { CharacterNS }, it doesn't seem to show the issue in console anymore. I will undo this specific change in my next commit. Marking this as resolved.
created_at: 2025-05-12T15:59:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,218 @@
+import {
+    OptionalCharacterProfilePayloadOptions,
+    OptionalCharacterProfilePayloadFields,
+    CharacterProfilePayload,
+    CharacterPayloadBase,
+    MythicPlusRun
+} from "~/lib/raiderIO/characters";
+import {RaiderIOClient} from "~/lib/raiderIO";
+
+export type DBPlayerInternalTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+};
+
+export type DBPlayerType = {
+    team?: DBPlayerInternalTeamType | null
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    nickname: string
+    main?: string | null
+    teamId?: string | null
+    spec?: string | null
+    playerName?: string | null
+    playerServer?: string | null
+    eventId?: string | null
+    assignedRole?: string | null
+};
+
+export type DBTeamType = {
+    name: string
+    id: string
+    createdAt: Date
+    updatedAt: Date
+    eventId: string
+    players: DBPlayerType[]
+};
+
+export type MythicData = MythicPlusRun & {
+    participants: DBPlayerType[]
+};
+
+export function getPlayersPromises(team: DBTeamType | null, client: RaiderIOClient) {
+    if(!team) {
+        const playersPromises: Promise<null>[] = [Promise.resolve(null)];
+
+        return playersPromises;
+    }
+
+    const playersPromises: (Promise<null> | Promise<CharacterProfilePayload<OptionalCharacterProfilePayloadOptions>>)[] =
+        team.players.map((player) =>
+            player.playerServer && player.playerName
+                ? client.character.getCharacterProfile({
+                    region: "us",
+                    realm: player.playerServer,
+                    name: player.playerName,
+                    fields: {
+                        gear: true,
+                        mythic_plus_best_runs: true,
+                        mythic_plus_alternate_runs: true,
+                        mythic_plus_highest_level_runs: true,
+                        mythic_plus_recent_runs: true,
+                        mythic_plus_previous_weekly_highest_level_runs: true,
+                        mythic_plus_weekly_highest_level_runs: true,
+                    },
+                })
+                : Promise.resolve(null)
+        );
+
+    return playersPromises;
+}
+
+export async function parseMythicDataPerTeam(team: DBTeamType | null, playersPromises: (Promise<null> | Promise<CharacterProfilePayload<OptionalCharacterProfilePayloadOptions>>)[]) {
path: app/lib/mythics.ts
user: login: DendyA
body: Upon discussion, the return type defined in the function is correct so I will mark this as resolved.
created_at: 2025-05-13T01:07:19Z
author_association: COLLABORATOR
diff_hunk: @@ -47,12 +39,40 @@ export async function loader({ request, params: { slug } }: Route.LoaderArgs) {
   }
 
   const teams = organizeTeams(event.teams);
+  const parsedMythicDataArray: any[] = [];
 
-  return { event, teams, isAdmin };
+  const client = await RaiderIOClient.getInstance();
+
+  for(const team of teams) {
+    const playersPromises = team.players.map((player) =>
+        player.playerServer && player.playerName
+            ? client.character.getCharacterProfile({
+              region: "us",
+              realm: player.playerServer,
+              name: player.playerName,
+              fields: {
+                gear: true,
+                mythic_plus_best_runs: true,
+                mythic_plus_alternate_runs: true,
+                mythic_plus_highest_level_runs: true,
+                mythic_plus_recent_runs: true,
+                mythic_plus_previous_weekly_highest_level_runs: true,
+                mythic_plus_weekly_highest_level_runs: true,
+              },
+            })
+            : Promise.resolve(null)
+    );
+
+    const mythicsParsedPerTeam = await ParseMythicDataPerTeam(team.players, playersPromises);
+
+    parsedMythicDataArray.push(mythicsParsedPerTeam.mythicsPromise);
+  }
path: app/routes/event/show/route.tsx
user: login: DendyA
body: This will be marked as resolved as the data loads correctly with only one error in the browser's console which is not stemming from this change. More on that in another comment. Marking this as resolved.
created_at: 2025-05-13T01:10:34Z
author_association: COLLABORATOR

PR comments:
user: login: plohkoon
created_at: 2025-05-09T23:56:20Z
body: I like your idea of summary on the overview page. I was actually toying with an idea in my head that the current index page for a route should actually be a subpage, like `/event/slug/details`or something and the index page should actually show like a leaderboard and all the teams scores and stuff. Maybe a really quick summary of which players are on the team and their role icon or something.

I was noticing that error about can't update during transition and I thought it was when navigating off of certain pages. It might have something to do with overwriting the promise that the server loader replies with.
author_association: OWNER
user: login: DendyA
created_at: 2025-05-12T00:33:47Z
body: ~~One larger change that I needed to make was to remove the ```CharacterNS``` namespace in the ```characters.ts``` file. Many of the types defined in this file are used in ```mythics.ts``` but due to them being defined in a namespace, my browser's console was complaining about trying to import a namespace. Essentially, the way the code was structured (i.e. the code in ```mythics.ts``` was defined outside ```characters.ts``` file) caused the mixing of namespace style and module style. According to the documentation, using the modules structure is the preferred method over namespaces now, hence my decision to remove it.~~

See my later comment about undoing this change and not seeing the issue anymore. Therefore this is not relevant anymore and the changes will be reverted in the next commit.
author_association: COLLABORATOR
user: login: DendyA
created_at: 2025-05-12T00:35:39Z
body: The use of the index (detailed in the second point in the PR's description) to extract the relevant mythic data for that team will be a FIXME for a future update. It works fine for now.
author_association: COLLABORATOR
user: login: DendyA
created_at: 2025-05-13T01:13:33Z
body: The error called out in the PR's description does still exist but only when you navigate to a specific player's page. The reason actually stems from devtools and is not related to any of the data the app loads or processes. That's a couple hours of debugging that I would like back, thanks forge42.
author_association: COLLABORATOR