Grabbed dndbeyond's source code

```
~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js
```
This commit is contained in:
2025-05-28 11:50:03 -07:00
commit 8df9031d27
3592 changed files with 319051 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
// GA Docs for using multiple accounts on the same page
// https://developers.google.com/analytics/devguides/collection/analyticsjs/creating-trackers#working_with_multiple_trackers
import debounce from "debounce";
import config from "../../config";
import {
SessionTrackingIdByName,
SessionNames,
EventCategories,
EventActions,
EventLabels,
} from "../../constants";
import {
getSearchableTerms,
getSearchableTermsAnalyticsLabel,
} from "../../state/selectors/characterUtils";
import { CharacterData, LogEventOptions } from "../../types";
let { debug } = config;
const tryGa = (...params) => {
if (typeof window.ga === "function") {
window.ga(...params);
return true;
}
return false;
};
// eslint-disable-next-line max-params
const logGa = (sessionName: string, eventName: string, ...rest: any) =>
tryGa(`${sessionName}.send`, eventName, ...rest);
export const logEvent = (
sessionName: string,
{ category, action, label, value }: LogEventOptions
) => {
const success = logGa(sessionName, "event", category, action, label, value);
if (success && debug) {
// eslint-disable-next-line no-console
console.log(
"Logged analytics event:",
"\nsession:",
sessionName,
"\ncategory:",
category,
"\naction:",
action,
"\nlabel:",
label,
"\nvalue:",
value
);
}
};
// eslint-disable-next-line max-params
export const logPageView = (sessionName, pageName, ...rest) => {
tryGa(`${sessionName}.set`, "page", pageName, ...rest);
const success = tryGa(`${sessionName}.send`, "pageview");
if (success && debug) {
// eslint-disable-next-line no-console
console.log(
"Logged page view:",
"\nsession:",
sessionName,
"\npage:",
pageName,
"\nrest:",
...rest
);
}
};
export const initAnalytics = (sessionName: string, forceDebug?: boolean) => {
/* eslint-disable @typescript-eslint/no-unused-expressions, no-sequences */
(function (
i: Window,
s: Document,
o: string,
g: string,
r: string,
a?: HTMLScriptElement,
m?: Element
) {
i["GoogleAnalyticsObject"] = r;
(i[r] =
i[r] ||
function () {
(i[r].q = i[r].q || []).push(arguments);
}),
(i[r].l = 1 * (new Date() as any));
(a = s.createElement(o) as HTMLScriptElement),
(m = s.getElementsByTagName(o)[0]);
a.async = true;
a.src = g;
m.parentNode?.insertBefore(a, m);
})(
window,
document,
"script",
"https://www.google-analytics.com/analytics.js",
"ga"
);
/* eslint-enable */
tryGa("create", SessionTrackingIdByName[sessionName], "auto", sessionName);
if (forceDebug) {
debug = forceDebug;
}
};
/**
* Converts an array to a string that can be used for an event label
* @param {array} array The array to convert
* @param {object} valueToLabelMap An optional value to label mapping object
* @returns {string} A string representation of the array
*/
export const arrayToLabel = (array, valueToLabelMap = {}) =>
array
.sort()
.map((value) => valueToLabelMap[value] || value)
.join(", ");
/**
* Reduces an array of filter options ({ label, value }) to a lookup object of value to label
* @param {array} filterOptions The list of options to reduce
* @returns {object} A lookup object with filter values as the keys and filter labels as the values
*/
export const filterOptionsToLookup = (filterOptions = []) =>
filterOptions.reduce((lookup, { label, value }) => {
lookup[value] = label;
return lookup;
}, {});
export const logCharacterCampaignClicked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_Campaign,
label: EventLabels.Clicked,
});
};
export const logCharacterCopyCancelled = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_Copy,
label: EventLabels.Cancelled,
});
};
export const logCharacterCopyClicked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_Copy,
label: EventLabels.Clicked,
});
};
export const logCharacterCopyConfirmed = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_Copy,
label: EventLabels.Confirmed,
});
};
export const logCharacterDeleteCancelled = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_Delete,
label: EventLabels.Cancelled,
});
};
export const logCharacterDeleteClicked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_Delete,
label: EventLabels.Clicked,
});
};
export const logCharacterDeleteConfirmed = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_Delete,
label: EventLabels.Confirmed,
});
};
export const logCharacterEditClicked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_Edit,
label: EventLabels.Clicked,
});
};
export const logCharacterLeaveCampaignCancelled = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_LeaveCampaign,
label: EventLabels.Cancelled,
});
};
export const logCharacterLeaveCampaignClicked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_LeaveCampaign,
label: EventLabels.Clicked,
});
};
export const logCharacterLeaveCampaignConfirmed = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_LeaveCampaign,
label: EventLabels.Confirmed,
});
};
export const logCharacterViewClicked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Character,
action: EventActions.DDB_Character_View,
label: EventLabels.Clicked,
});
};
export const logListingSearchChanged = debounce(
(characters: Array<CharacterData>, matchesSearch: () => any) => {
const matchedLabels = characters
.map(
(character) =>
getSearchableTermsAnalyticsLabel(character)[
getSearchableTerms(character).findIndex(matchesSearch)
]
)
.filter(Boolean)
.filter(
(character, index, characters) =>
characters.indexOf(character) === index
);
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_ListingFilter,
action: EventActions.DDB_ListingFilter_SearchChanged,
label: arrayToLabel(matchedLabels) || "no match",
});
},
500
);
export const logListingSearchCleared = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_ListingFilter,
action: EventActions.DDB_ListingFilter_SearchCleared,
});
};
export const logListingSortChanged = (label) => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_ListingSort,
action: EventActions.DDB_ListingSort_Changed,
label,
});
};
export const logPlayerAppBannerDismissed = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_PlayerAppBanner,
action: EventActions.DDB_PlayerAppBanner_Dismissed,
});
};
export const logPlayerAppBannerClickedCta = (label) => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_PlayerAppBanner,
action: EventActions.DDB_PlayerAppBanner_ClickedCta,
label,
});
};
export const logUnlockCharacterLocked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Unlock,
action: EventActions.DDB_Unlock_CharacterLocked,
});
};
export const logUnlockCharacterUnlocked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Unlock,
action: EventActions.DDB_Unlock_CharacterUnlocked,
});
};
export const logUnlockFinishUnlockingClicked = (
unlockedCharacterCount,
maxSlots
) => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Unlock,
action: EventActions.DDB_Unlock_FinishUnlockingClicked,
label: `Unlocked Characters: ${unlockedCharacterCount} / ${maxSlots}`,
});
};
export const logUnlockFinishUnlockingCancelled = (
unlockedCharacterCount,
maxSlots
) => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Unlock,
action: EventActions.DDB_Unlock_FinishUnlockingCancelled,
label: `Unlocked Characters: ${unlockedCharacterCount} / ${maxSlots}`,
});
};
export const logUnlockFinishUnlockingConfirmed = (
unlockedCharacterCount,
maxSlots
) => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Unlock,
action: EventActions.DDB_Unlock_FinishUnlockingConfirmed,
label: `Unlocked Characters: ${unlockedCharacterCount} / ${maxSlots}`,
});
};
export const logUnlockSubscribeClicked = () => {
logEvent(SessionNames.DDB, {
category: EventCategories.DDB_Unlock,
action: EventActions.DDB_Unlock_SubscribeClicked,
});
};
+11
View File
@@ -0,0 +1,11 @@
export const toCamelCase = (str: string) =>
str
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index === 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\W/g, "");
export const toPascalCase = (str: string) =>
str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase())
.replace(/\W/g, "");
+189
View File
@@ -0,0 +1,189 @@
import { CharacterData, UserPreferences } from "~/types";
import config, {
characterServiceBaseUrl,
ddbBaseUrl,
userServiceBaseUrl,
} from "../config";
import { getId, getCampaignId } from "../state/selectors/characterUtils";
import { applyQueriesClientSide, queryListToMap } from "./queryUtils";
import { summon } from "./summon";
import { getUserId } from "./userApi";
export const getCharacters = async () => {
try {
const userId = await getUserId();
const req = await summon(
`${characterServiceBaseUrl}/characters/list?userId=${userId}`
);
return req.json();
} catch {
throw new Error("Non-OK response from GET characters");
}
};
export const getItems = async ({ queries }) => {
const characters = await getCharacters();
applyQueriesClientSide(queryListToMap(queries));
return new Promise((resolve) =>
resolve({
json: () => new Promise((resolve2) => resolve2(characters)),
})
);
};
export const copyCharacter = (character: CharacterData) => {
try {
const characterId = getId(character);
return summon(`${characterServiceBaseUrl}/character/copy`, {
method: "POST",
body: JSON.stringify({ characterId }),
});
} catch (err) {
throw err;
}
};
export const claimCharacter = (characterId: number, isAssigned: boolean) => {
try {
return summon(`${characterServiceBaseUrl}/premade/claim`, {
method: "POST",
body: JSON.stringify({ characterId, isAssigned }),
});
} catch (err) {
throw err;
}
};
export const getCharacterSlots = async () => {
try {
const req = await summon(`${config.userServiceBaseUrl}/slot-limit`);
const res = await req.json();
return res.data;
} catch (err) {
throw err;
}
};
export const deleteCharacter = (character: CharacterData) => {
try {
const characterId = getId(character);
return summon(`${characterServiceBaseUrl}/character`, {
method: "DELETE",
body: JSON.stringify({ characterId }),
});
} catch (err) {
throw err;
}
};
export const leaveCampaign = (character: CharacterData) => {
try {
const campaignId = getCampaignId(character);
const characterId = getId(character);
return summon(
`${ddbBaseUrl}/api/campaign/${campaignId}/character/${characterId}`,
{
method: "DELETE",
}
);
} catch (err) {
throw err;
}
};
export const joinCampaign = (campaignJoinCode: string, characterId: number) => {
try {
return summon(`${ddbBaseUrl}/api/campaign/join/${campaignJoinCode}`, {
method: "POST",
body: JSON.stringify({ characterId }),
});
} catch (err) {
throw err;
}
};
export const unlockCharacters = (characterIds: Array<number>) => {
try {
return summon(`${userServiceBaseUrl}/unlock`, {
method: "POST",
body: JSON.stringify({ characterIds }),
});
} catch (err) {
throw err;
}
};
export const activateCharacter = (character: CharacterData) => {
try {
const characterId = getId(character);
return summon(
`${characterServiceBaseUrl}/characters/${characterId}/status/activate`,
{
method: "PUT",
}
);
} catch (err) {
throw err;
}
};
export const getFeatureFlagsFromCharacterService = (flags: Array<string>) => {
try {
return summon(`${characterServiceBaseUrl}/featureflag`, {
method: "POST",
body: JSON.stringify({ flags }),
});
} catch (err) {
throw err;
}
};
/**
* Get all sources that the user is able to use.
*/
export const getAllEntitledSources = (campaignId?: number) => {
try {
return summon(
`${userServiceBaseUrl}/entitled-sources${
campaignId ? `?campaignId=${campaignId}` : ""
}`
);
} catch (err) {
throw err;
}
};
export const getUserPreferences = () => {
try {
return summon(`${config.userServiceBaseUrl}/preferences`);
} catch (err) {
throw err;
}
};
export const updateUserPreferences = (preferences: UserPreferences) => {
try {
return summon(`${config.userServiceBaseUrl}/preferences`, {
method: "POST",
body: JSON.stringify(preferences),
});
} catch (err) {
throw err;
}
};
export const getRulesData = () => {
try {
return summon(`${characterServiceBaseUrl}/rule-data`, {
method: "GET",
});
} catch (err) {
throw err;
}
};
@@ -0,0 +1,52 @@
import {
MovementTypeEnum,
PreferenceAbilityScoreDisplayTypeEnum as AbilityScoreDisplayType,
PreferenceEncumbranceTypeEnum,
PreferenceHitPointTypeEnum,
PreferencePrivacyTypeEnum,
PreferenceProgressionTypeEnum,
PreferenceSharingTypeEnum,
SenseTypeEnum,
} from "~/constants";
import { FeatureFlags } from "~/contexts/FeatureFlag";
/**
* This is a copy of the `generateCharacterPreferences` function from the rules
* engine package. This is necessary because we have restructured the
* featureFlagContext, so the `featureFlagInfo` variable is no longer
* available.
**/
export const generateCharacterPreferences = (featureFlags: FeatureFlags) => {
// Default values
const abilityScoreDisplayType = AbilityScoreDisplayType.MODIFIERS_TOP;
const encumbranceType = PreferenceEncumbranceTypeEnum.ENCUMBRANCE;
const hitPointType = PreferenceHitPointTypeEnum.FIXED;
const primaryMovement = MovementTypeEnum.WALK;
const primarySense = SenseTypeEnum.PASSIVE_PERCEPTION;
const progressionType = PreferenceProgressionTypeEnum.MILESTONE;
const sharingType = PreferenceSharingTypeEnum.LIMITED;
const privacyType = PreferencePrivacyTypeEnum.CAMPAIGN_ONLY;
return {
abilityScoreDisplayType,
enableContainerCurrency: false,
enableDarkMode: false,
enableOptionalClassFeatures: false,
enableOptionalOrigins: false,
encumbranceType,
enforceFeatRules: true,
enforceMulticlassRules: true,
hitPointType,
ignoreCoinWeight: true,
primaryMovement,
primarySense,
privacyType,
progressionType,
sharingType,
showCompanions: false,
showScaledSpells: true,
showUnarmedStrike: true,
showWildShape: false,
useHomebrewContent: true,
};
};
+33
View File
@@ -0,0 +1,33 @@
export const tryGet = (key: string) => {
try {
return window.localStorage.getItem(key);
} catch (exception) {
// If 3rd party cookies are turned off even though window.localStorage
// is accessible or local storage is full this will error
// https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js
return null;
}
};
export const trySet = (key: string, value: string) => {
try {
window.localStorage.setItem(key, value);
return true;
} catch (exception) {
// Safari can throw an exception when calling localStorage.setItem in a private browsing tab.
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions
return false;
}
};
export const tryRemove = (key: string) => {
try {
window.localStorage.removeItem(key);
return true;
} catch (exception) {
// Get and trySet can throw exceptions, so this probably can too...
return false;
}
};
+13
View File
@@ -0,0 +1,13 @@
import { ReportHandler } from "web-vitals";
export const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
+141
View File
@@ -0,0 +1,141 @@
import { SortOrderEnum, SortTypeEnum, CharacterData } from "../types";
// Creates a sorting predicate to sort for some property
export const byProp =
(propSelector: Function) =>
(a: CharacterData, b: CharacterData): number => {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
const aProp = propSelector(a);
const bProp = propSelector(b);
if (aProp < bProp) {
return -1;
}
if (aProp > bProp) {
return 1;
}
return 0;
};
// Creates a value to be used for sorting
export const createSortValue = (
sortBy: SortTypeEnum,
sortOrder: SortOrderEnum
): string => {
return `${sortBy}-${sortOrder}`;
};
/**
* Sorts an array of objects by a given key. If a drilled-down key is needed,
* simply provide the path to the key as a string (ex. "key.subkey") and the
* function will handle the rest.
**/
export const orderBy = (
arr: Array<any>,
key?: string,
sort: "asc" | "desc" = "asc"
) => {
const getKey = (obj: any) => {
if (key) {
if (key.includes(".")) {
const arr = key.split(".");
return arr.reduce((acc, curr) => {
return acc[curr];
}, obj);
} else {
return obj[key];
}
} else {
return;
}
};
const handleSort = (valA: object, valB: object) => {
// If a key is provided, sort by that
if (key) {
// Get values to compare
const a = getKey(valA);
const b = getKey(valB);
// If the values are strings, compare them alphabetically
if (a < b) return sort === "asc" ? -1 : 1;
if (b < a) return sort === "asc" ? 1 : -1;
}
// Otherwise, sort by the object itself
return 0;
};
// Return a new array with the sorted values
return arr.concat().sort(handleSort);
};
/**
* Separates a single array of objects into two separate arrays based on
* whether or not they include a query or not. If a key is provided, the
* function will look for the query by that key. If the key is nested, it will
* handle that as well. The function will return both the items which match and
* the ones that don't as separate arrays to be used as needed.
**/
export const sortByMatch = (
arr: Array<any>,
query: string,
key: string = "name",
exact: boolean = false
) => {
// If a key is provided, drill down until that key is found
const getKey = (obj: any) => {
if (key) {
if (key.includes(".")) {
const arr = key.split(".");
return arr.reduce((acc, curr) => {
return acc[curr];
}, obj);
} else {
return obj[key];
}
} else {
return;
}
};
// Create empty arrays to store the objects
let arrA: Array<any> = [];
let arrB: Array<any> = [];
// Loop through the provided array and separate the objects
arr.forEach((item) => {
if (exact) {
if (getKey(item) === query) {
arrA.push(item);
} else {
arrB.push(item);
}
} else {
if (getKey(item).includes(query)) {
arrA.push(item);
} else {
arrB.push(item);
}
}
});
// Return a new array with the sorted values
return [arrA, arrB];
};
export const sortObjectByKeys = (val: object, dir: "asc" | "desc" = "asc") => {
const sortFn = (a, b) => {
const numA = parseInt(a);
const numB = parseInt(b);
return dir === "asc" ? numA - numB : numB - numA;
};
const sorted = Object.keys(val)
.sort(sortFn)
.reduce((obj, key) => {
obj[key] = val[key];
return obj;
}, {});
return sorted;
};
+39
View File
@@ -0,0 +1,39 @@
import AuthUtils from "@dndbeyond/authentication-lib-js";
import { getStt } from "./tokenUtils";
export const getAuthHeaders = AuthUtils.makeGetAuthorizationHeaders({
madeGetShortTermToken: getStt,
});
/**
* A wrapper around fetch that adds the Authorization header and anything else
* that may need to be included in every request. If withCookies is true, the
* request will add the credentials: "include" option. Otherwise, this function
* should work exactly like the native fetch function.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
*/
export const summon = async (
input: RequestInfo | URL,
init?: RequestInit,
withCookies?: boolean
) => {
// Get the Authorization headers from the auth token
const authHeaders = await getAuthHeaders();
// If withCookies is true, set the credentials to include
const credentials = withCookies && { credentials: "include" };
// If there is a body, set the Content-Type to application/json
const contentType = init?.body && { "Content-Type": "application/json" };
return window.fetch(input, {
...(credentials as { credentials: RequestCredentials } | undefined),
...init,
headers: {
...authHeaders,
...init?.headers,
...contentType,
Accept: "application/json",
},
});
};
+15
View File
@@ -0,0 +1,15 @@
import jwtDecode from "jwt-decode";
import { AuthUtils } from "@dndbeyond/authentication-lib-js";
import { authEndpoint as authUrl } from "../config";
export const getStt = AuthUtils.makeGetShortTermToken({
authUrl,
throwOnHttpStatusError: false,
});
export const getDecodedStt = async () => {
const stt = await getStt();
return stt ? jwtDecode(stt) : null;
};
+36
View File
@@ -0,0 +1,36 @@
import { UserUtils } from "@dndbeyond/authentication-lib-js";
import { getDecodedStt } from "./tokenUtils";
export const getUserToken = async () => {
const stt = await getDecodedStt();
return stt;
};
export const getUser = async () => {
const token = await getUserToken();
return {
...UserUtils.jwtToUser(token),
displayName: token.displayName,
};
};
export const getUserDisplayName = async () => {
const user = await getUser();
return user.displayName;
};
export const getUserId = async () => {
const user = await getUser();
return user.id;
};
export const getSubscriptionTier = async () => {
const user = await getUser();
return user.subscriptionTier;
};
export const getRoles = async () => {
const user = await getUser();
return user.roles;
};
+11
View File
@@ -0,0 +1,11 @@
/**
* https://github.com/Microsoft/TypeScript/issues/16069#issuecomment-369374214
* Used to get around array filter function that errors because it doesn't recognize the filter
* is removing nulls
* @param input
*/
export const isNotNullOrUndefined = <T extends Object>(
input: null | undefined | T
): input is T => {
return input !== null;
};