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,264 @@
|
||||
import React from "react";
|
||||
import { connect, DispatchProp } from "react-redux";
|
||||
|
||||
import {
|
||||
AlignmentContract,
|
||||
characterActions,
|
||||
CharacterLifestyleContract,
|
||||
HtmlSelectOption,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
rulesEngineSelectors,
|
||||
SizeContract,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
|
||||
import { SharedAppState } from "../../../stores/typings";
|
||||
import { TypeScriptUtils } from "../../../utils";
|
||||
import DescriptionPaneEntry from "./DescriptionPaneEntry";
|
||||
import DescriptionPaneEntryContent from "./DescriptionPaneEntryContent";
|
||||
import DescriptionPaneNumberEditor from "./DescriptionPaneNumberEditor";
|
||||
import DescriptionPaneSelectEditor from "./DescriptionPaneSelectEditor";
|
||||
import DescriptionPaneTextEditor from "./DescriptionPaneTextEditor";
|
||||
|
||||
interface Props extends DispatchProp {
|
||||
height: string | null;
|
||||
weight: number | null;
|
||||
size: SizeContract | null;
|
||||
faith: string | null;
|
||||
skin: string | null;
|
||||
eyes: string | null;
|
||||
hair: string | null;
|
||||
age: number | null;
|
||||
gender: string | null;
|
||||
alignment: AlignmentContract | null;
|
||||
lifestyle: CharacterLifestyleContract | null;
|
||||
|
||||
ruleData: RuleData;
|
||||
}
|
||||
class DescriptionPane extends React.PureComponent<Props> {
|
||||
handleAlignmentUpdate = (propertyKey: string, value: number | null): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.alignmentSet(value));
|
||||
};
|
||||
|
||||
handleLifestyleUpdate = (propertyKey: string, value: number | null): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.lifestyleSet(value));
|
||||
};
|
||||
|
||||
handleFaithChange = (propertyKey: string, value: string): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.faithSet(value));
|
||||
};
|
||||
|
||||
handleHairChange = (propertyKey: string, value: string): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.hairSet(value));
|
||||
};
|
||||
|
||||
handleSkinChange = (propertyKey: string, value: string): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.skinSet(value));
|
||||
};
|
||||
|
||||
handleEyesChange = (propertyKey: string, value: string): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.eyesSet(value));
|
||||
};
|
||||
|
||||
handleHeightChange = (propertyKey: string, value: string): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.heightSet(value));
|
||||
};
|
||||
|
||||
handleWeightChange = (propertyKey: string, value: number | null): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.weightSet(value));
|
||||
};
|
||||
|
||||
handleAgeChange = (propertyKey: string, value: number | null): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.ageSet(value));
|
||||
};
|
||||
|
||||
handleGenderChange = (propertyKey: string, value: string): void => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(characterActions.genderSet(value));
|
||||
};
|
||||
|
||||
renderCharacterDetails = (): React.ReactNode => {
|
||||
const { alignment, lifestyle, ruleData, faith } = this.props;
|
||||
|
||||
const alignmentData = RuleDataUtils.getAlignments(ruleData);
|
||||
const lifestyleData = RuleDataUtils.getLifestyles(ruleData);
|
||||
const alignmentOptions = RuleDataUtils.getAlignmentOptions(ruleData);
|
||||
|
||||
let alignmentDescriptionNode: React.ReactNode;
|
||||
if (alignment !== null && alignment.description) {
|
||||
alignmentDescriptionNode = (
|
||||
<HtmlContent html={alignment.description} withoutTooltips />
|
||||
);
|
||||
}
|
||||
|
||||
const lifestyleOptions: Array<HtmlSelectOption> = lifestyleData
|
||||
.map((lifestyle) => {
|
||||
if (lifestyle.id === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: `${lifestyle.name} ${
|
||||
lifestyle.cost === "-" ? "" : `(${lifestyle.cost})`
|
||||
}`,
|
||||
value: lifestyle.id,
|
||||
};
|
||||
})
|
||||
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
||||
|
||||
let lifestyleDescriptionNode: React.ReactNode;
|
||||
if (lifestyle !== null) {
|
||||
lifestyleDescriptionNode = lifestyle.description;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneSelectEditor
|
||||
label="Alignment"
|
||||
options={alignmentOptions}
|
||||
defaultValue={alignment === null ? null : alignment.id}
|
||||
propertyKey="alignmentId"
|
||||
onUpdate={this.handleAlignmentUpdate}
|
||||
/>
|
||||
<DescriptionPaneEntryContent>
|
||||
{alignmentDescriptionNode}
|
||||
</DescriptionPaneEntryContent>
|
||||
</DescriptionPaneEntry>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneTextEditor
|
||||
label="Faith"
|
||||
defaultValue={faith}
|
||||
propertyKey="faith"
|
||||
onUpdate={this.handleFaithChange}
|
||||
maxLength={512}
|
||||
/>
|
||||
</DescriptionPaneEntry>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneSelectEditor
|
||||
label="Lifestyle"
|
||||
options={lifestyleOptions}
|
||||
defaultValue={lifestyle === null ? null : lifestyle.id}
|
||||
propertyKey="lifestyleId"
|
||||
onUpdate={this.handleLifestyleUpdate}
|
||||
/>
|
||||
<DescriptionPaneEntryContent>
|
||||
{lifestyleDescriptionNode}
|
||||
</DescriptionPaneEntryContent>
|
||||
</DescriptionPaneEntry>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
renderPhysicalCharacteristics = (): React.ReactNode => {
|
||||
const { height, weight, skin, eyes, hair, age, gender } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneTextEditor
|
||||
label="Hair"
|
||||
defaultValue={hair}
|
||||
propertyKey="hair"
|
||||
onUpdate={this.handleHairChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
</DescriptionPaneEntry>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneTextEditor
|
||||
label="Skin"
|
||||
defaultValue={skin}
|
||||
propertyKey="skin"
|
||||
onUpdate={this.handleSkinChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
</DescriptionPaneEntry>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneTextEditor
|
||||
label="Eyes"
|
||||
defaultValue={eyes}
|
||||
propertyKey="eyes"
|
||||
onUpdate={this.handleEyesChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
</DescriptionPaneEntry>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneTextEditor
|
||||
label="Height"
|
||||
defaultValue={height}
|
||||
propertyKey="height"
|
||||
onUpdate={this.handleHeightChange}
|
||||
maxLength={50}
|
||||
/>
|
||||
</DescriptionPaneEntry>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneNumberEditor
|
||||
label="Weight (lbs)"
|
||||
defaultValue={weight}
|
||||
propertyKey="weight"
|
||||
onUpdate={this.handleWeightChange}
|
||||
/>
|
||||
</DescriptionPaneEntry>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneNumberEditor
|
||||
label="Age (Years)"
|
||||
defaultValue={age}
|
||||
propertyKey="age"
|
||||
onUpdate={this.handleAgeChange}
|
||||
/>
|
||||
</DescriptionPaneEntry>
|
||||
<DescriptionPaneEntry>
|
||||
<DescriptionPaneTextEditor
|
||||
label="Gender"
|
||||
defaultValue={gender}
|
||||
propertyKey="gender"
|
||||
onUpdate={this.handleGenderChange}
|
||||
maxLength={128}
|
||||
/>
|
||||
</DescriptionPaneEntry>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ct-description-pane">
|
||||
<Header>Characteristics and Details</Header>
|
||||
<div className="ct-description-pane__entries">
|
||||
{this.renderCharacterDetails()}
|
||||
{this.renderPhysicalCharacteristics()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: SharedAppState) {
|
||||
return {
|
||||
ruleData: rulesEngineSelectors.getRuleData(state),
|
||||
alignment: rulesEngineSelectors.getAlignment(state),
|
||||
height: rulesEngineSelectors.getHeight(state),
|
||||
weight: rulesEngineSelectors.getWeight(state),
|
||||
size: rulesEngineSelectors.getSize(state),
|
||||
faith: rulesEngineSelectors.getFaith(state),
|
||||
skin: rulesEngineSelectors.getSkin(state),
|
||||
eyes: rulesEngineSelectors.getEyes(state),
|
||||
hair: rulesEngineSelectors.getHair(state),
|
||||
age: rulesEngineSelectors.getAge(state),
|
||||
gender: rulesEngineSelectors.getGender(state),
|
||||
lifestyle: rulesEngineSelectors.getLifestyle(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(DescriptionPane);
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
interface Props {
|
||||
propertyKey: string;
|
||||
className: string;
|
||||
}
|
||||
export default class DescriptionPaneEditor extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { propertyKey, className } = this.props;
|
||||
|
||||
let classNames: Array<string> = [
|
||||
"ct-description-pane__editor",
|
||||
`ct-description-pane__editor--${FormatUtils.slugify(propertyKey)}`,
|
||||
className,
|
||||
];
|
||||
|
||||
return <div className={classNames.join(" ")}>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import DescriptionPaneEditor from "./DescriptionPaneEditor";
|
||||
|
||||
export default DescriptionPaneEditor;
|
||||
export { DescriptionPaneEditor };
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
export default class DescriptionPaneEditorLabel extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="ct-description-pane__editor-label">
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import DescriptionPaneEditorLabel from "./DescriptionPaneEditorLabel";
|
||||
|
||||
export default DescriptionPaneEditorLabel;
|
||||
export { DescriptionPaneEditorLabel };
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
export default class DescriptionPaneEditorValue extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="ct-description-pane__editor-value">
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import DescriptionPaneEditorValue from "./DescriptionPaneEditorValue";
|
||||
|
||||
export default DescriptionPaneEditorValue;
|
||||
export { DescriptionPaneEditorValue };
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
export default class DescriptionPaneEntry extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="ct-description-pane__entry">{this.props.children}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import DescriptionPaneEntry from "./DescriptionPaneEntry";
|
||||
|
||||
export default DescriptionPaneEntry;
|
||||
export { DescriptionPaneEntry };
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
export default class DescriptionPaneEntryContent extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<div className="ct-description-pane__entry-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import DescriptionPaneEntryContent from "./DescriptionPaneEntryContent";
|
||||
|
||||
export default DescriptionPaneEntryContent;
|
||||
export { DescriptionPaneEntryContent };
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
|
||||
import { HelperUtils } from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { CHARACTER_DESCRIPTION_NUMBER_VALUE } from "../../../../constants/App";
|
||||
import DescriptionPaneEditor from "../DescriptionPaneEditor";
|
||||
import DescriptionPaneEditorLabel from "../DescriptionPaneEditorLabel";
|
||||
import DescriptionPaneEditorValue from "../DescriptionPaneEditorValue";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
propertyKey: string;
|
||||
defaultValue: number | null;
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
onUpdate?: (propertyKey: string, value: number | null) => void;
|
||||
}
|
||||
interface State {
|
||||
value: number | null;
|
||||
}
|
||||
export default class DescriptionPaneNumberEditor extends React.PureComponent<
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
static defaultProps = {
|
||||
minValue: CHARACTER_DESCRIPTION_NUMBER_VALUE.MIN,
|
||||
maxValue: CHARACTER_DESCRIPTION_NUMBER_VALUE.MAX,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: props.defaultValue,
|
||||
};
|
||||
}
|
||||
|
||||
handleBlur = (evt: React.FocusEvent<HTMLInputElement>): void => {
|
||||
const { propertyKey, onUpdate, maxValue, minValue } = this.props;
|
||||
|
||||
let parsedValue = HelperUtils.parseInputInt(evt.target.value);
|
||||
let clampedValue: number | null =
|
||||
parsedValue === null
|
||||
? null
|
||||
: HelperUtils.clampInt(parsedValue, minValue, maxValue);
|
||||
if (onUpdate) {
|
||||
onUpdate(propertyKey, clampedValue);
|
||||
}
|
||||
this.setState({
|
||||
value: clampedValue,
|
||||
});
|
||||
};
|
||||
|
||||
handleChange = (evt: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
value: HelperUtils.parseInputInt(evt.target.value),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value } = this.state;
|
||||
const { propertyKey, label, maxValue, minValue } = this.props;
|
||||
|
||||
return (
|
||||
<DescriptionPaneEditor
|
||||
propertyKey={propertyKey}
|
||||
className="ct-description-pane__editor--number"
|
||||
>
|
||||
<DescriptionPaneEditorLabel>{label}</DescriptionPaneEditorLabel>
|
||||
<DescriptionPaneEditorValue>
|
||||
<input
|
||||
className="ct-description-pane__editor-input"
|
||||
type="number"
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
value={value === null ? "" : value}
|
||||
onBlur={this.handleBlur}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</DescriptionPaneEditorValue>
|
||||
</DescriptionPaneEditor>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import DescriptionPaneNumberEditor from "./DescriptionPaneNumberEditor";
|
||||
|
||||
export default DescriptionPaneNumberEditor;
|
||||
export { DescriptionPaneNumberEditor };
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
|
||||
import { Select } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
HelperUtils,
|
||||
HtmlSelectOption,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import DescriptionPaneEditor from "../DescriptionPaneEditor";
|
||||
import DescriptionPaneEditorLabel from "../DescriptionPaneEditorLabel";
|
||||
import DescriptionPaneEditorValue from "../DescriptionPaneEditorValue";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
propertyKey: string;
|
||||
defaultValue: number | null;
|
||||
options: Array<HtmlSelectOption>;
|
||||
onUpdate?: (propertyKey: string, value: number | null) => void;
|
||||
}
|
||||
interface State {
|
||||
value: number | null;
|
||||
}
|
||||
export default class DescriptionPaneSelectEditor extends React.PureComponent<
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
value: props.defaultValue,
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = (value: string): void => {
|
||||
const { propertyKey, onUpdate } = this.props;
|
||||
|
||||
if (onUpdate) {
|
||||
const parsedValue = HelperUtils.parseInputInt(value);
|
||||
onUpdate(propertyKey, parsedValue);
|
||||
this.setState({
|
||||
value: parsedValue,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value } = this.state;
|
||||
const { propertyKey, label, options } = this.props;
|
||||
|
||||
return (
|
||||
<DescriptionPaneEditor
|
||||
propertyKey={propertyKey}
|
||||
className="ct-description-pane__editor--select"
|
||||
>
|
||||
<DescriptionPaneEditorLabel>{label}</DescriptionPaneEditorLabel>
|
||||
<DescriptionPaneEditorValue>
|
||||
<Select
|
||||
className="ct-description-pane__editor-input"
|
||||
placeholder="--"
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</DescriptionPaneEditorValue>
|
||||
</DescriptionPaneEditor>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import DescriptionPaneSelectEditor from "./DescriptionPaneSelectEditor";
|
||||
|
||||
export default DescriptionPaneSelectEditor;
|
||||
export { DescriptionPaneSelectEditor };
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import DescriptionPaneEditor from "../DescriptionPaneEditor";
|
||||
import DescriptionPaneEditorLabel from "../DescriptionPaneEditorLabel";
|
||||
import DescriptionPaneEditorValue from "../DescriptionPaneEditorValue";
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
propertyKey: string;
|
||||
defaultValue: string | null;
|
||||
onUpdate?: (propertyKey: string, value: string) => void;
|
||||
maxLength: number | null;
|
||||
}
|
||||
export default class DescriptionPaneTextEditor extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
maxLength: null,
|
||||
};
|
||||
|
||||
handleBlur = (evt: React.FocusEvent<HTMLInputElement>): void => {
|
||||
const { propertyKey, onUpdate } = this.props;
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate(propertyKey, evt.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { propertyKey, label, defaultValue, maxLength } = this.props;
|
||||
|
||||
return (
|
||||
<DescriptionPaneEditor
|
||||
propertyKey={propertyKey}
|
||||
className="ct-description-pane__editor--text"
|
||||
>
|
||||
<DescriptionPaneEditorLabel>{label}</DescriptionPaneEditorLabel>
|
||||
<DescriptionPaneEditorValue>
|
||||
<input
|
||||
className="ct-description-pane__editor-input"
|
||||
type="text"
|
||||
defaultValue={defaultValue === null ? "" : defaultValue}
|
||||
onBlur={this.handleBlur}
|
||||
maxLength={maxLength === null ? undefined : maxLength}
|
||||
/>
|
||||
</DescriptionPaneEditorValue>
|
||||
</DescriptionPaneEditor>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import DescriptionPaneTextEditor from "./DescriptionPaneTextEditor";
|
||||
|
||||
export default DescriptionPaneTextEditor;
|
||||
export { DescriptionPaneTextEditor };
|
||||
@@ -0,0 +1,22 @@
|
||||
import DescriptionPane from "./DescriptionPane";
|
||||
import DescriptionPaneEditor from "./DescriptionPaneEditor";
|
||||
import DescriptionPaneEditorLabel from "./DescriptionPaneEditorLabel";
|
||||
import DescriptionPaneEditorValue from "./DescriptionPaneEditorValue";
|
||||
import DescriptionPaneEntry from "./DescriptionPaneEntry";
|
||||
import DescriptionPaneEntryContent from "./DescriptionPaneEntryContent";
|
||||
import DescriptionPaneNumberEditor from "./DescriptionPaneNumberEditor";
|
||||
import DescriptionPaneSelectEditor from "./DescriptionPaneSelectEditor";
|
||||
import DescriptionPaneTextEditor from "./DescriptionPaneTextEditor";
|
||||
|
||||
export default DescriptionPane;
|
||||
export {
|
||||
DescriptionPane,
|
||||
DescriptionPaneEditor,
|
||||
DescriptionPaneEditorLabel,
|
||||
DescriptionPaneEditorValue,
|
||||
DescriptionPaneEntry,
|
||||
DescriptionPaneEntryContent,
|
||||
DescriptionPaneNumberEditor,
|
||||
DescriptionPaneSelectEditor,
|
||||
DescriptionPaneTextEditor,
|
||||
};
|
||||
Reference in New Issue
Block a user