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
@@ -0,0 +1,21 @@
import { useContext } from "react";
import { createContext } from "react";
import useUser from "~/hooks/useUser";
import type { AppUserState } from "~/hooks/useUser";
export let AuthContext = createContext<AppUserState>(undefined);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const user = useUser();
if (user === undefined) {
return null;
}
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
return useContext(AuthContext);
};
@@ -0,0 +1,85 @@
import { useContext, useEffect } from "react";
import { createContext } from "react";
import { useSelector } from "react-redux";
import {
rulesEngineSelectors,
CharacterTheme,
} from "@dndbeyond/character-rules-engine";
export let CharacterThemeContext = createContext<CharacterTheme>({
isDefault: false,
themeColorId: 0,
name: "",
themeColor: "",
backgroundColor: "",
isDarkMode: false,
});
export const CharacterThemeProvider = ({ children }) => {
const {
isDefault,
themeColorId,
name,
themeColor,
backgroundColor,
isDarkMode,
} = useSelector(rulesEngineSelectors.getCharacterTheme);
const solidBackground =
backgroundColor.length > 7 ? backgroundColor.slice(0, -2) : backgroundColor;
const isThemeColorDark = () => {
// Remove #
const c = themeColor.substring(1);
// convert rrggbb to decimal
const rgb = parseInt(c, 16);
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = (rgb >> 0) & 0xff;
// Get luma from rgb colors
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return luma < 40;
};
useEffect(() => {
document.body.dataset.theme = isDarkMode ? "dark" : "light";
}, [isDarkMode]);
return (
<CharacterThemeContext.Provider
value={{
isDefault,
themeColorId,
name,
themeColor,
backgroundColor,
isDarkMode,
}}
>
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--theme-color: ${themeColor};
--theme-background: ${backgroundColor};
--theme-background-solid: ${solidBackground};
--theme-contrast: var(--ttui_grey-${isDarkMode ? 50 : 900});
--theme-transparent: color-mix(in srgb, var(--theme-color), transparent 60%);
/* Update the character muted color for readability on dark mode */
${
isDarkMode ? "--character-muted-color: var(--ttui_grey-400);" : ""
}
}`,
}}
/>
{children}
</CharacterThemeContext.Provider>
);
};
export const useCharacterTheme = () => {
return useContext(CharacterThemeContext);
};
@@ -0,0 +1,84 @@
import { FC, createContext, useContext, useEffect, useState } from "react";
import { toCamelCase } from "~/helpers/casing";
import { getFeatureFlagsFromCharacterService } from "~/helpers/characterServiceApi";
const featureFlagsToGet = [
"release-gate-ims",
"release-gate-gfs-blessings-ui",
"release-gate-character-sheet-tour",
"release-gate-premade-characters",
];
export const defaultFlags = {
imsFlag: false,
gfsBlessingsUiFlag: false,
characterSheetTourFlag: false,
premadeCharactersFlag: false,
};
export type FeatureFlags = typeof defaultFlags;
export interface FeatureFlagContextType extends FeatureFlags {
featureFlags: FeatureFlags;
}
export const FeatureFlagContext = createContext<FeatureFlagContextType>(null!);
interface ProviderProps {
defaultOverrides?: Partial<FeatureFlags>;
}
export const FeatureFlagProvider: FC<ProviderProps> = ({
children,
defaultOverrides,
}) => {
const [featureFlags, setFeatureFlags] = useState({
...defaultFlags,
...defaultOverrides,
});
const convertFlagsToKeys = (flags: Object) => {
return Object.keys(flags).reduce((acc, flag) => {
// Perform any manipulations to remove unwanted characters
const formattedFlag = flag
// Remove `character-app` from the flag name
.replace(/character-app\./i, "")
// Remove `release-gate` from the flag name
.replace(/release-gate\W/i, "")
// Remove `character-sheet` from flag name if not the tour flag
.replace(/character-sheet\W(?!tour)/i, "")
// Change `2d` to `tactical` since var can't start with number
.replace(/2d/i, "tactical");
const key = toCamelCase(`${formattedFlag}Flag`);
return { ...acc, [key]: flags[flag] };
}, {});
};
const getFeatureFlags = async () => {
try {
const req = await getFeatureFlagsFromCharacterService(featureFlagsToGet);
const res = await req.json();
const flags = convertFlagsToKeys(res.data) as FeatureFlags;
setFeatureFlags(flags);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
if (!defaultOverrides) getFeatureFlags();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultOverrides]);
return (
<FeatureFlagContext.Provider value={{ ...featureFlags, featureFlags }}>
{children}
</FeatureFlagContext.Provider>
);
};
export const useFeatureFlags = () => {
return useContext(FeatureFlagContext);
};
+45
View File
@@ -0,0 +1,45 @@
import { createContext, FC, useContext, useState } from "react";
export interface FiltersContextType {
showItemTypes: boolean;
setShowItemTypes: (show: boolean) => void;
showItemSourceCategories: boolean;
setShowItemSourceCategories: (show: boolean) => void;
showSpellLevels: boolean;
setShowSpellLevels: (show: boolean) => void;
showSpellSourceCategories: boolean;
setShowSpellSourceCategories: (show: boolean) => void;
}
export const FiltersContext = createContext<FiltersContextType>(null!);
export const FiltersProvider: FC = ({ children }) => {
const [showItemTypes, setShowItemTypes] = useState(true);
const [showItemSourceCategories, setShowItemSourceCategories] =
useState(true);
const [showSpellLevels, setShowSpellLevels] = useState(true);
const [showSpellSourceCategories, setShowSpellSourceCategories] =
useState(true);
return (
<FiltersContext.Provider
value={{
showItemTypes,
setShowItemTypes,
showItemSourceCategories,
setShowItemSourceCategories,
showSpellLevels,
setShowSpellLevels,
showSpellSourceCategories,
setShowSpellSourceCategories,
}}
>
{children}
</FiltersContext.Provider>
);
};
export const useFiltersContext = () => {
return useContext(FiltersContext);
};
+36
View File
@@ -0,0 +1,36 @@
import { createContext, useContext, useState } from "react";
import { Helmet } from "react-helmet";
interface HeadContextProps {
title: string;
setTitle: (title: string) => void;
}
export const HeadContext = createContext<HeadContextProps>(null!);
export const HeadContextProvider = ({ children }) => {
const [title, setTitle] = useState("Character App");
const setNewTitle = (title) => {
setTitle(`${title} - D&D Beyond`);
};
return (
<HeadContext.Provider value={{ title, setTitle: setNewTitle }}>
<Helmet>
<title>{title}</title>
</Helmet>
{children}
</HeadContext.Provider>
);
};
export const useHeadContext = () => {
const context = useContext(HeadContext);
if (!context) {
throw new Error("useHead must be used within a HeadProvider");
}
return context;
};
+174
View File
@@ -0,0 +1,174 @@
import { isEqual } from "lodash";
import { createContext, FC, useContext, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
PaneComponentEnum,
PaneComponentInfo,
PaneIdentifiers,
SidebarAlignmentEnum,
SidebarPlacementEnum,
} from "~/subApps/sheet/components/Sidebar/types";
import { SIDEBAR_FIXED_POSITION_START_WIDTH } from "~/tools/js/CharacterSheet/config";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
export interface SidebarInfo {
isVisible: boolean;
setIsVisible: (isVisible: boolean) => void;
isLocked: boolean;
setIsLocked: (isLocked: boolean) => void;
placement: SidebarPlacementEnum;
setPlacement: (placement: SidebarPlacementEnum) => void;
alignment: SidebarAlignmentEnum;
setAlignment: (alignment: SidebarAlignmentEnum) => void;
width: number;
}
export interface PaneInfo {
activePane: PaneComponentInfo | null;
showControls: boolean;
isAtStart: boolean;
isAtEnd: boolean;
paneHistoryStart: (
componentType: PaneComponentEnum,
componentIdentifiers?: PaneIdentifiers | null
) => void;
paneHistoryPush: (
componentType: PaneComponentEnum,
componentIdentifiers?: PaneIdentifiers | null
) => void;
paneHistoryPrevious: () => void;
paneHistoryNext: () => void;
}
export interface SidebarContextType {
sidebar: SidebarInfo;
pane: PaneInfo;
}
export const SidebarContext = createContext<SidebarContextType>(null!);
export const SidebarProvider: FC = ({ children }) => {
const isMobile = useSelector(appEnvSelectors.getIsMobile);
const sheetDimensions = useSelector(appEnvSelectors.getDimensions);
const [isVisible, setIsVisible] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [placement, setPlacement] = useState(SidebarPlacementEnum.OVERLAY);
const [alignment, setAlignment] = useState(SidebarAlignmentEnum.RIGHT);
const [paneIdx, setPaneIdx] = useState<number>(0);
const [paneHistory, setPaneHistory] = useState<Array<PaneComponentInfo>>([]);
const [activePane, setActivePane] = useState<PaneComponentInfo | null>(null);
const width: number = 340;
//Create a new pane history
const paneHistoryStart = (
type: PaneComponentEnum,
identifiers: PaneIdentifiers | null = null
) => {
setIsVisible(true);
setPaneIdx(0);
setPaneHistory([
{
type,
identifiers,
},
]);
};
//Add a new pane to the history
const paneHistoryPush = (
type: PaneComponentEnum,
identifiers: PaneIdentifiers | null = null
) => {
if (paneIdx === null) {
paneHistoryStart(type, identifiers);
} else {
// get current active pane (last in history)
const activeHistoryElement = {
identifiers: activePane?.identifiers,
type: activePane?.type,
};
// create a new node for the history
const newHistoryElement = { identifiers, type };
// compare the current active pane with the new one (last element in history with new one)
if (!isEqual(activeHistoryElement, newHistoryElement)) {
// Check if new history element is different than current - otherwise skip to avoid duplicates
let newHistory = [
...paneHistory.slice(0, paneIdx + 1),
newHistoryElement,
];
setPaneHistory(newHistory);
setPaneIdx(newHistory.length - 1);
}
}
setIsVisible(true);
};
//Go back to the previous pane in the history
const paneHistoryPrevious = () => {
setPaneIdx(Math.max(0, paneIdx - 1));
};
//Go to the next pane in the history
const paneHistoryNext = () => {
setPaneIdx(Math.min(paneHistory.length - 1, paneIdx + 1));
};
useEffect(() => {
//If the window less than the fixed position start width (1600px), set the placement to overlay
if (
isMobile ||
sheetDimensions.window.width < SIDEBAR_FIXED_POSITION_START_WIDTH
) {
setPlacement(SidebarPlacementEnum.OVERLAY);
}
//set Right alignment if mobile
setAlignment(isMobile ? SidebarAlignmentEnum.RIGHT : alignment);
}, [isMobile, sheetDimensions]);
useEffect(() => {
const activePane = paneHistory[paneIdx];
setActivePane(activePane);
}, [paneIdx, paneHistory]);
return (
<SidebarContext.Provider
value={{
sidebar: {
isVisible,
setIsVisible,
isLocked,
setIsLocked,
placement,
setPlacement,
alignment,
setAlignment,
width,
},
pane: {
activePane,
showControls: paneHistory.length > 1,
isAtStart: paneIdx === 0,
isAtEnd: paneIdx === paneHistory.length - 1,
paneHistoryStart,
paneHistoryPush,
paneHistoryPrevious,
paneHistoryNext,
},
}}
>
{children}
</SidebarContext.Provider>
);
};
export const useSidebar = () => {
return useContext(SidebarContext);
};
@@ -0,0 +1,88 @@
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { ThemeProvider } from "@mui/material/styles";
import { useState, createContext, useEffect, useMemo } from "react";
import { prefixer } from "stylis";
import { getTheme } from "~/theme";
type PaletteOpt = "light" | "dark";
interface Manager {
lightOrDark: PaletteOpt;
primary?: string;
}
interface ThemeManagerContext {
manager: Manager;
setManager: (manager: Manager) => void;
}
export const ThemeManager = createContext<ThemeManagerContext>(null!);
export const ThemeManagerProvider = ({
children,
manager: propsManager,
}: {
children: React.ReactNode;
manager?: Manager;
}) => {
const [manager, setManager] = useState<Manager>(
propsManager || {
lightOrDark: "light",
}
);
useEffect(() => {
if (propsManager) {
setManager(propsManager);
}
}, [propsManager]);
// HACK
/**
* We are doing some hacky things to
* win is styling conflicts this can
* go away once we no longer need
* waterdeep styles
*/
function hack__createExtraScopePlugin(...extra) {
const scopes = extra.map((scope) => `${scope.trim()} `);
return (element) => {
if (element.type !== "rule") {
return;
}
if (element.root?.type === "@keyframes") {
return;
}
element.props = element.props
.map((prop) => scopes.map((scope) => scope + prop))
.reduce((scopesArray, scope) => scopesArray.concat(scope), []);
};
}
const hack__cache = useMemo(
() =>
createCache({
key: "ddb-character-app",
container: document.body,
prepend: true,
stylisPlugins: [
hack__createExtraScopePlugin("body"),
// has to be included manually when customizing `stylisPlugins` if you want to have vendor prefixes added automatically
prefixer,
],
}),
[]
);
/**
* End the hack
*/
const theme = getTheme(manager.lightOrDark, manager.primary);
return (
<ThemeManager.Provider value={{ manager, setManager }}>
<CacheProvider value={hack__cache}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</CacheProvider>
</ThemeManager.Provider>
);
};