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:
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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, "");
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user