style: add documentation comments for all main window react components

This commit is contained in:
Lukas Obermann
2024-01-08 15:45:28 +01:00
parent 3e30439f47
commit 93a1d5d741
53 changed files with 437 additions and 273 deletions
@@ -28,6 +28,10 @@ import {
} from "../selectors/traditionSelectors.ts"
import { selectTotalAdventurePoints } from "../slices/characterSlice.ts"
/**
* Returns the contents for the adventure points tooltip that displays which
* entry groups adventure points were spent on.
*/
export const AdventurePointsTooltip: FC = () => {
const translate = useTranslate()
+3
View File
@@ -23,6 +23,9 @@ import { NavigationBarWrapper } from "./NavigationBarWrapper.tsx"
const showSettings = () => ExternalAPI.showSettings()
const toggleDevTools = () => ExternalAPI.toggleDevTools()
/**
* Returns a top navigation bar.
*/
export const NavigationBar: FC = () => {
const translate = useTranslate()
const dispatch = useAppDispatch()
@@ -5,6 +5,9 @@ interface Props {
handleSetTab(): void
}
/**
* Returns a back button for the beginning of the navigation bar.
*/
export const NavigationBarBack: FC<Props> = props => {
const { handleSetTab } = props
@@ -1,6 +1,9 @@
import { FCC } from "../../shared/utils/react.js"
import "./NavigationBarLeft.scss"
/**
* Returns a grouping element for the left section of the navigation bar.
*/
export const NavigationBarLeft: FCC = ({ children }) => (
<div className="navigationbar-left">{children}</div>
)
@@ -1,6 +1,9 @@
import { FCC } from "../../shared/utils/react.js"
import "./NavigationBarRight.scss"
/**
* Returns a grouping element for the right section of the navigation bar.
*/
export const NavigationBarRight: FCC = ({ children }) => (
<div className="navigationbar-right">{children}</div>
)
@@ -7,6 +7,10 @@ interface Props {
tabs: RoutePath[]
}
/**
* Returns a section underneath the main navigation tabs section for switching
* between subtabs.
*/
export const NavigationBarSubTabs: FC<Props> = props => {
const { tabs } = props
@@ -16,7 +20,7 @@ export const NavigationBarSubTabs: FC<Props> = props => {
<NavigationBarTab
key={displayRoute.at(-1)}
displayRoute={{ type: "single", route: displayRoute }}
/>
/>
))}
</ul>
)
@@ -97,6 +97,9 @@ type Props = {
displayRoute: DisplayRoute
}
/**
* Returns a single navigation tab.
*/
export const NavigationBarTab: FC<Props> = props => {
const { className, displayRoute } = props
@@ -7,6 +7,9 @@ type Props = {
tabs: DisplayRoute[]
}
/**
* Returns a navigation bar section for displaying navigation tabs.
*/
export const NavigationBarTabs: FC<Props> = props => {
const { tabs } = props
@@ -1,6 +1,9 @@
import { FCC } from "../../shared/utils/react.js"
import "./NavigationBarWrapper.scss"
/**
* Returns a wrapper element for the complete navigation bar.
*/
export const NavigationBarWrapper: FCC = ({ children }) => (
<div className="navigationbar">
<div className="navigationbar-inner">{children}</div>
+3
View File
@@ -16,6 +16,9 @@ const onRestore = () => ExternalAPI.restore()
const onClose = () => ExternalAPI.close()
const isMaximized = () => ExternalAPI.isMaximized()
/**
* Returns the React application for the main window.
*/
export const Root: FC = () => {
const language = useAppSelector(selectLocale)
const areAnimationsEnabled = useAppSelector(selectAreAnimationsEnabled)
+3
View File
@@ -17,6 +17,9 @@ import { Skills } from "./characters/character/skills/Skills.tsx"
import { SpecialAbilities } from "./characters/character/specialAbilities/SpecialAbilities.tsx"
import { Spells } from "./characters/character/spells/Spells.tsx"
/**
* Returns a page based on the currently selected route.
*/
export const Router: FC = () => {
const route = useAppSelector(selectRoute)
+3
View File
@@ -4,6 +4,9 @@ import { Page } from "../../../shared/components/page/Page.tsx"
import { Scroll } from "../../../shared/components/scroll/Scroll.tsx"
import { useTranslate } from "../../../shared/hooks/translate.ts"
/**
* Returns a page containing the imprint.
*/
export const Imprint: FC = () => {
const translate = useTranslate()
return (
@@ -7,6 +7,9 @@ import { ExternalAPI } from "../../external.ts"
let savedText: string | undefined = undefined
/**
* Returns a page that shows the changelog.
*/
export const LastChanges: FC = () => {
const [text, setText] = useState(savedText ?? "...")
@@ -8,6 +8,9 @@ import "./ThirdPartyLicenses.scss"
let savedVersion: string | undefined = undefined
let savedText: string | undefined = undefined
/**
* Returns a page that shows licenses of third-party software.
*/
export const ThirdPartyLicenses: FC = () => {
const [version, setVersion] = useState(savedVersion ?? "0.0.0")
const [text, setText] = useState(savedText ?? "...")
@@ -19,6 +19,9 @@ import { changeCharactersSortOrder, selectCharactersSortOrder } from "../../slic
import "./Characters.scss"
import { CharactersItem } from "./CharactersItem.tsx"
/**
* Returns a page for managing characters.
*/
export const Characters: FC = () => {
const translate = useTranslate()
const localeCompare = useLocaleCompare()
@@ -26,11 +26,14 @@ import {
} from "../../slices/databaseSlice.ts"
import { goToTab } from "../../slices/routeSlice.ts"
export type HerolistItemProps = {
type Props = {
character: CharacterState
}
export const CharactersItem: FC<HerolistItemProps> = props => {
/**
* Returns a single character item.
*/
export const CharactersItem: FC<Props> = props => {
const { character } = props
const dispatch = useAppDispatch()
@@ -24,6 +24,9 @@ import {
} from "../../../../slices/settingsSlice.ts"
import { AdventurePointsSpent } from "./AdventurePointsSpent.tsx"
/**
* Returns a page for managing advantages.
*/
export const Advantages: FC = () => {
const translate = useTranslate()
const dispatch = useAppDispatch()
@@ -12,6 +12,10 @@ import {
} from "../../../../selectors/adventurePointSelectors.ts"
import { selectMaximumAdventurePointsForMagicalAdvantagesAndDisadvantages } from "../../../../selectors/traditionSelectors.ts"
/**
* Returns a sidebar widget for showing spent and available adventure points for
* advantages or disadvantages.
*/
export const AdventurePointsSpent: FC = () => {
const translate = useTranslate()
@@ -24,6 +24,9 @@ import {
} from "../../../../slices/settingsSlice.ts"
import { AdventurePointsSpent } from "./AdventurePointsSpent.tsx"
/**
* Returns a page for managing disadvantages.
*/
export const Disadvantages: FC = () => {
const translate = useTranslate()
const dispatch = useAppDispatch()
@@ -10,6 +10,9 @@ import { selectAttributeAdjustmentId } from "../../../../slices/characterSlice.t
import { changeAttributeAdjustmentId } from "../../../../slices/raceSlice.ts"
import "./AttributeAdjustment.scss"
/**
* Returns a widget for changing the attribute adjustment set for the race.
*/
export const AttributesAdjustment: FC = () => {
const dispatch = useAppDispatch()
const translate = useTranslate()
@@ -11,6 +11,9 @@ type Props = {
value: number | string
}
/**
* Returns an attribute value block.
*/
export const AttributeBorder: FC<Props> = props => {
const { children, className, label, tooltip, tooltipMargin, value } = props
@@ -15,6 +15,9 @@ import "./Attributes.scss"
import { AttributeList } from "./AttributesList.tsx"
import { DerivedCharacteristicsList } from "./DerivedCharacteristicsList.tsx"
/**
* Returns a page for managing attributes.
*/
export const Attributes: FC = () => {
const translate = useTranslate()
const totalPoints = useAppSelector(selectTotalPoints)
@@ -9,6 +9,9 @@ type Props = {
isRemovingEnabled: boolean
}
/**
* Returns a list of attribute values.
*/
export const AttributeList: FC<Props> = props => {
const attributes = useAppSelector(selectVisibleAttributes)
@@ -14,6 +14,9 @@ type Props = {
isRemovingEnabled: boolean
}
/**
* Returns a single attribute value item with buttons to adjust the value.
*/
export const AttributeListItem: FC<Props> = props => {
const { attribute, isInCharacterCreation, isRemovingEnabled } = props
@@ -9,6 +9,9 @@ type Props = {
isRemovingEnabled: boolean
}
/**
* Returns a list of derived characteristic values.
*/
export const DerivedCharacteristicsList: FC<Props> = props => {
const { isInCharacterCreation, isRemovingEnabled } = props
@@ -27,6 +27,10 @@ type Props = {
isRemovingEnabled: boolean
}
/**
* Returns a single derived characterstics value item with optional buttons to
* manage the value.
*/
export const DerivedCharacteristicsListItem: FC<Props> = props => {
const { attribute, isInCharacterCreation, isRemovingEnabled } = props
@@ -30,6 +30,10 @@ type Props = {
isRemovingEnabled: boolean
}
/**
* Returns a value item and buttons to adjust the value for a supplementary
* derived characteristic value.
*/
export const DerivedCharacteristicsListItemPermanent: FC<Props> = props => {
const { attribute, isRemovingEnabled } = props
@@ -10,6 +10,9 @@ type Props = {
remove(value: number): void
}
/**
* Returns a sheet for managing permanent energy loss.
*/
export const PermanentLossSheet: FC<Props> = props => {
const { remove, isOpen, close } = props
@@ -19,6 +19,9 @@ type Props = {
close(): void
}
/**
* Returns a sheet for managing lost and redeemed energy points.
*/
export const PermanentPointsSheet: FC<Props> = props => {
const {
id,
@@ -115,6 +115,9 @@ const CloseCombatTechniquesListItem: FC<Props> = props => {
)
}
/**
* Displays a close combat technique.
*/
const MemoCloseCombatTechniquesListItem = memo(CloseCombatTechniquesListItem)
export { MemoCloseCombatTechniquesListItem as CloseCombatTechniquesListItem }
@@ -47,6 +47,9 @@ const isTopMarginNeeded = (
mprev: DisplayedCombatTechnique | undefined,
) => sortOrder === "group" && mprev !== undefined && curr.kind !== mprev.kind
/**
* Returns a page for managing combat techniques.
*/
export const CombatTechniques: FC = () => {
const translate = useTranslate()
const translateMap = useTranslateMap()
@@ -113,6 +113,9 @@ const RangedCombatTechniquesListItem: FC<Props> = props => {
)
}
/**
* Displays a ranged combat technique.
*/
const MemoRangedCombatTechniquesListItem = memo(RangedCombatTechniquesListItem)
export { MemoRangedCombatTechniquesListItem as RangedCombatTechniquesListItem }
@@ -12,29 +12,28 @@ type Props = {
close(): void
}
/**
* Returns a sheet for adding adventure points to the character.
*/
export const OverviewAddAP: FC<Props> = props => {
const { addAdventurePoints, isRemovingEnabled, isOpen, close } = props
const translate = useTranslate()
const [ value, setValue ] = useState("")
const [ prevIsOpen, setPrevIsOpen ] = useState(false)
const [value, setValue] = useState("")
const [prevIsOpen, setPrevIsOpen] = useState(false)
if (prevIsOpen !== isOpen) {
setValue("")
setPrevIsOpen(isOpen)
}
const addAP =
useCallback(
() => {
const mvalue = parseInt(value)
const addAP = useCallback(() => {
const mvalue = parseInt(value)
if (mvalue !== undefined) {
addAdventurePoints(mvalue)
}
},
[ addAdventurePoints, value ]
)
if (mvalue !== undefined) {
addAdventurePoints(mvalue)
}
}, [addAdventurePoints, value])
return (
<Dialog
@@ -44,7 +43,7 @@ export const OverviewAddAP: FC<Props> = props => {
{
disabled: isRemovingEnabled
? !isInteger(value)
: (!isNaturalNumber(value) || value === "0"),
: !isNaturalNumber(value) || value === "0",
label: translate("Add"),
onClick: addAP,
},
@@ -54,14 +53,14 @@ export const OverviewAddAP: FC<Props> = props => {
]}
close={close}
isOpen={isOpen}
>
>
<TextField
hint={translate("How many Adventure Points do you want to add?")}
value={value}
onChange={setValue}
fullWidth
valid={isRemovingEnabled ? isInteger(value) : isNaturalNumber(value) && value !== "0"}
/>
/>
</Dialog>
)
}
@@ -9,9 +9,47 @@ import { minus, plusMinus, signStr } from "../../../../../shared/utils/math.ts"
import { isEmptyOr, isFloat, isNaturalNumber } from "../../../../../shared/utils/regex.ts"
import { assertExhaustive } from "../../../../../shared/utils/typeSafety.ts"
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { selectAvailableEyeColors, selectAvailableEyeColorsIdDice, selectAvailableHairColors, selectAvailableHairColorsIdDice, selectAvailableSocialStatuses, selectRandomHeightCalculation, selectRandomWeightCalculation } from "../../../../selectors/personalDataSelectors.ts"
import { selectAge, selectCharacteristics, selectDateOfBirth, selectFamily, selectOtherInfo, selectPlaceOfBirth, selectPredefinedEyeColor, selectPredefinedHairColor, selectSize, selectSocialStatusId, selectTitle, selectWeight } from "../../../../slices/characterSlice.ts"
import { rerollEyeColor, rerollHairColor, rerollSize, rerollWeight, setAge, setCharacteristics, setDateOfBirth, setFamily, setOtherInfo, setPlaceOfBirth, setPredefinedEyeColor, setPredefinedHairColor, setSize, setSocialStatus, setTitle, setWeight } from "../../../../slices/personalDataSlice.ts"
import {
selectAvailableEyeColors,
selectAvailableEyeColorsIdDice,
selectAvailableHairColors,
selectAvailableHairColorsIdDice,
selectAvailableSocialStatuses,
selectRandomHeightCalculation,
selectRandomWeightCalculation,
} from "../../../../selectors/personalDataSelectors.ts"
import {
selectAge,
selectCharacteristics,
selectDateOfBirth,
selectFamily,
selectOtherInfo,
selectPlaceOfBirth,
selectPredefinedEyeColor,
selectPredefinedHairColor,
selectSize,
selectSocialStatusId,
selectTitle,
selectWeight,
} from "../../../../slices/characterSlice.ts"
import {
rerollEyeColor,
rerollHairColor,
rerollSize,
rerollWeight,
setAge,
setCharacteristics,
setDateOfBirth,
setFamily,
setOtherInfo,
setPlaceOfBirth,
setPredefinedEyeColor,
setPredefinedHairColor,
setSize,
setSocialStatus,
setTitle,
setWeight,
} from "../../../../slices/personalDataSlice.ts"
import { PersonalDataDropdown } from "./PersonalDataDropdown.tsx"
import { PersonalDataDropdownWithReroll } from "./PersonalDataDropdownWithReroll.tsx"
import { PersonalDataTextField } from "./PersonalDataTextField.tsx"
@@ -23,6 +61,9 @@ const validateEmptyOrNaturalNumber = (value: string | undefined) =>
const validateEmptyOrFloat = (value: string | undefined) =>
value === undefined || isEmptyOr(isFloat, value)
/**
* Returns a page for managing personal data of the character.
*/
export const PersonalData: FC = () => {
const translate = useTranslate()
const translateMap = useTranslateMap()
@@ -34,23 +75,27 @@ export const PersonalData: FC = () => {
const hairColorOptions = useMemo(
() =>
availableHairColors
.map((hairColor): DropdownOption<number> => ({
id: hairColor.id,
name: translateMap(hairColor.translations)?.name ?? hairColor.id.toFixed(),
}))
.map(
(hairColor): DropdownOption<number> => ({
id: hairColor.id,
name: translateMap(hairColor.translations)?.name ?? hairColor.id.toFixed(),
}),
)
.sort(reduceCompare(compareAt(x => x.name, localeCompare))),
[ availableHairColors, localeCompare, translateMap ],
[availableHairColors, localeCompare, translateMap],
)
const eyeColorOptions = useMemo(
() =>
availableEyeColors
.map((eyeColor): DropdownOption<number> => ({
id: eyeColor.id,
name: translateMap(eyeColor.translations)?.name ?? eyeColor.id.toFixed(),
}))
.map(
(eyeColor): DropdownOption<number> => ({
id: eyeColor.id,
name: translateMap(eyeColor.translations)?.name ?? eyeColor.id.toFixed(),
}),
)
.sort(reduceCompare(compareAt(x => x.name, localeCompare))),
[ availableEyeColors, localeCompare, translateMap ],
[availableEyeColors, localeCompare, translateMap],
)
const randomHeightCalculation = useAppSelector(selectRandomHeightCalculation)
@@ -58,13 +103,12 @@ export const PersonalData: FC = () => {
const heightCalculationString = useMemo(() => {
const dice = translate("D")
const randomPart =
randomHeightCalculation.random
.map(({ number, sides }) => ` ${signStr(sides)} ${number}${dice}${sides}`)
.join("")
const randomPart = randomHeightCalculation.random
.map(({ number, sides }) => ` ${signStr(sides)} ${number}${dice}${sides}`)
.join("")
return ` (${randomHeightCalculation.base}${randomPart})`
}, [ randomHeightCalculation.base, randomHeightCalculation.random, translate ])
}, [randomHeightCalculation.base, randomHeightCalculation.random, translate])
const randomWeightCalculation = useAppSelector(selectRandomWeightCalculation)
@@ -74,36 +118,40 @@ export const PersonalData: FC = () => {
const signStrWeight = (offsetStrategy: WeightDiceOffsetStrategy) => {
switch (offsetStrategy) {
case "Add": return "+"
case "Subtract": return minus
case "AddEvenSubtractOdd": return plusMinus
default: return assertExhaustive(offsetStrategy)
case "Add":
return "+"
case "Subtract":
return minus
case "AddEvenSubtractOdd":
return plusMinus
default:
return assertExhaustive(offsetStrategy)
}
}
const randomPart =
randomWeightCalculation.random
.map(({ number, sides, offset_strategy }) => ` ${signStrWeight(offset_strategy)} ${number}${dice}${sides}`)
.join("")
const randomPart = randomWeightCalculation.random
.map(
({ number, sides, offset_strategy }) =>
` ${signStrWeight(offset_strategy)} ${number}${dice}${sides}`,
)
.join("")
return ` (${size} ${minus} ${randomWeightCalculation.base}${randomPart})`
}, [
randomWeightCalculation.base,
randomWeightCalculation.random,
translate,
])
}, [randomWeightCalculation.base, randomWeightCalculation.random, translate])
const availableSocialStatuses = useAppSelector(selectAvailableSocialStatuses)
const socialStatusOptions = useMemo(
() =>
availableSocialStatuses
.map((socialStatus): DropdownOption<number> => ({
id: socialStatus.id,
name: translateMap(socialStatus.translations)?.name ?? socialStatus.id.toFixed(),
}))
.map(
(socialStatus): DropdownOption<number> => ({
id: socialStatus.id,
name: translateMap(socialStatus.translations)?.name ?? socialStatus.id.toFixed(),
}),
)
.sort(reduceCompare(compareAt(x => x.name, localeCompare))),
[ availableSocialStatuses, localeCompare, translateMap ],
[availableSocialStatuses, localeCompare, translateMap],
)
const dispatch = useAppDispatch()
@@ -112,22 +160,22 @@ export const PersonalData: FC = () => {
const handleRerollHairColor = useCallback(
() => dispatch(rerollHairColor(availableHairColorsIdDice)),
[ dispatch, availableHairColorsIdDice ]
[dispatch, availableHairColorsIdDice],
)
const handleRerollEyeColor = useCallback(
() => dispatch(rerollEyeColor(availableEyeColorsIdDice)),
[ dispatch, availableEyeColorsIdDice ]
[dispatch, availableEyeColorsIdDice],
)
const handleRerollSize = useCallback(
() => dispatch(rerollSize(randomHeightCalculation)),
[ dispatch, randomHeightCalculation ]
[dispatch, randomHeightCalculation],
)
const handleRerollWeight = useCallback(
() => dispatch(rerollWeight(randomWeightCalculation, randomHeightCalculation)),
[ dispatch, randomWeightCalculation, randomHeightCalculation ]
[dispatch, randomWeightCalculation, randomHeightCalculation],
)
return (
@@ -136,23 +184,23 @@ export const PersonalData: FC = () => {
label={translate("Family")}
selector={selectFamily}
action={setFamily}
/>
/>
<PersonalDataTextField
label={translate("Place of Birth")}
selector={selectPlaceOfBirth}
action={setPlaceOfBirth}
/>
/>
<PersonalDataTextField
label={translate("Date of Birth")}
selector={selectDateOfBirth}
action={setDateOfBirth}
/>
/>
<PersonalDataTextField
label={translate("Age")}
selector={selectAge}
action={setAge}
validator={validateEmptyOrNaturalNumber}
/>
/>
<PersonalDataDropdownWithReroll
label={translate("Hair Color")}
rerollLabel={translate("Reroll Hair Color")}
@@ -160,7 +208,7 @@ export const PersonalData: FC = () => {
selector={selectPredefinedHairColor}
action={setPredefinedHairColor}
onReroll={handleRerollHairColor}
/>
/>
<PersonalDataDropdownWithReroll
label={translate("Eye Color")}
rerollLabel={translate("Reroll Eye Color")}
@@ -168,7 +216,7 @@ export const PersonalData: FC = () => {
selector={selectPredefinedEyeColor}
action={setPredefinedEyeColor}
onReroll={handleRerollEyeColor}
/>
/>
<PersonalDataTextFieldWithReroll
label={`${translate("Size")}${heightCalculationString}`}
rerollLabel={translate("Reroll Size")}
@@ -176,7 +224,7 @@ export const PersonalData: FC = () => {
action={setSize}
onReroll={handleRerollSize}
validator={validateEmptyOrFloat}
/>
/>
<PersonalDataTextFieldWithReroll
label={`${translate("Weight")}${weightCalculationString}`}
rerollLabel={translate("Reroll Weight")}
@@ -184,28 +232,24 @@ export const PersonalData: FC = () => {
action={setWeight}
onReroll={handleRerollWeight}
validator={validateEmptyOrNaturalNumber}
/>
<PersonalDataTextField
label={translate("Title")}
selector={selectTitle}
action={setTitle}
/>
/>
<PersonalDataTextField label={translate("Title")} selector={selectTitle} action={setTitle} />
<PersonalDataDropdown
label={translate("Social Status")}
selector={selectSocialStatusId}
action={setSocialStatus}
options={socialStatusOptions}
/>
/>
<PersonalDataTextField
label={translate("Characteristics")}
selector={selectCharacteristics}
action={setCharacteristics}
/>
/>
<PersonalDataTextField
label={translate("Other Information")}
selector={selectOtherInfo}
action={setOtherInfo}
/>
/>
</div>
)
}
@@ -5,20 +5,18 @@ import { DropdownOption } from "../../../../../shared/components/dropdown/Dropdo
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { RootState } from "../../../../store.ts"
export type Props = {
type Props = {
label: string
selector: (state: RootState) => number | undefined
options: DropdownOption<number>[]
action: ActionCreatorWithPayload<number>
}
/**
* Returns a dropdown for use with personal data.
*/
export const PersonalDataDropdown: FC<Props> = props => {
const {
label,
selector,
options,
action,
} = props
const { label, selector, options, action } = props
const dispatch = useAppDispatch()
const value = useAppSelector(selector)
@@ -27,7 +25,7 @@ export const PersonalDataDropdown: FC<Props> = props => {
(newValue: number) => {
dispatch(action(newValue))
},
[ dispatch, action ]
[dispatch, action],
)
return (
@@ -38,7 +36,7 @@ export const PersonalDataDropdown: FC<Props> = props => {
onChange={handleSetValue}
options={options}
disabled={options.length === 1 && value === options[0]!.id}
/>
/>
</div>
)
}
@@ -7,7 +7,7 @@ import { InputButtonGroup } from "../../../../../shared/components/inputButtonGr
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { RootState } from "../../../../store.ts"
export type Props = {
type Props = {
label: string
rerollLabel: string
selector: (state: RootState) => number | undefined
@@ -16,15 +16,11 @@ export type Props = {
onReroll: () => void
}
/**
* Returns a dropdown with a reroll option for use with personal data.
*/
export const PersonalDataDropdownWithReroll: FC<Props> = props => {
const {
label,
rerollLabel,
selector,
options,
action,
onReroll,
} = props
const { label, rerollLabel, selector, options, action, onReroll } = props
const dispatch = useAppDispatch()
const value = useAppSelector(selector)
@@ -33,7 +29,7 @@ export const PersonalDataDropdownWithReroll: FC<Props> = props => {
(newValue: number) => {
dispatch(action(newValue))
},
[ dispatch, action ]
[dispatch, action],
)
return (
@@ -44,13 +40,13 @@ export const PersonalDataDropdownWithReroll: FC<Props> = props => {
onChange={handleSetValue}
options={options}
disabled={options.length === 1 && value === options[0]!.id}
/>
/>
<IconButton
icon="&#xE913;"
label={rerollLabel}
onClick={onReroll}
disabled={options.length === 1}
/>
/>
</InputButtonGroup>
)
}
@@ -4,20 +4,18 @@ import { TextFieldLazy } from "../../../../../shared/components/textField/TextFi
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { RootState } from "../../../../store.ts"
export type Props = {
type Props = {
label: string
selector: (state: RootState) => string | undefined
action: ActionCreatorWithPayload<string>
validator?: (value: string | undefined) => boolean
}
/**
* Returns a textfield for use with personal data.
*/
export const PersonalDataTextField: FC<Props> = props => {
const {
label,
selector,
action,
validator,
} = props
const { label, selector, action, validator } = props
const dispatch = useAppDispatch()
const value = useAppSelector(selector)
@@ -26,7 +24,7 @@ export const PersonalDataTextField: FC<Props> = props => {
(newValue: string) => {
dispatch(action(newValue))
},
[ dispatch, action ]
[dispatch, action],
)
return (
@@ -36,7 +34,7 @@ export const PersonalDataTextField: FC<Props> = props => {
value={value}
onChange={handleSetValue}
valid={validator ? validator(value) : true}
/>
/>
</div>
)
}
@@ -6,7 +6,7 @@ import { TextFieldLazy } from "../../../../../shared/components/textField/TextFi
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { RootState } from "../../../../store.ts"
export type Props = {
type Props = {
label: string
rerollLabel: string
selector: (state: RootState) => string | undefined
@@ -15,15 +15,11 @@ export type Props = {
validator?: (value: string | undefined) => boolean
}
/**
* Returns a textfield with a reroll option for use with personal data.
*/
export const PersonalDataTextFieldWithReroll: FC<Props> = props => {
const {
label,
rerollLabel,
selector,
action,
onReroll,
validator,
} = props
const { label, rerollLabel, selector, action, onReroll, validator } = props
const dispatch = useAppDispatch()
const value = useAppSelector(selector)
@@ -32,7 +28,7 @@ export const PersonalDataTextFieldWithReroll: FC<Props> = props => {
(newValue: string) => {
dispatch(action(newValue))
},
[ dispatch, action ]
[dispatch, action],
)
return (
@@ -42,12 +38,8 @@ export const PersonalDataTextFieldWithReroll: FC<Props> = props => {
value={value}
onChange={handleSetValue}
valid={validator ? validator(value) : true}
/>
<IconButton
icon="&#xE913;"
label={rerollLabel}
onClick={onReroll}
/>
/>
<IconButton icon="&#xE913;" label={rerollLabel} onClick={onReroll} />
</InputButtonGroup>
)
}
@@ -18,20 +18,39 @@ import { useProfessionName } from "../../../../hooks/professionName.ts"
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { selectCanAddAdventurePoints } from "../../../../selectors/characterSelectors.ts"
import { selectCurrentCulture } from "../../../../selectors/cultureSelectors.ts"
import { selectCurrentExperienceLevel, selectStartExperienceLevel } from "../../../../selectors/experienceLevelSelectors.ts"
import {
selectCurrentExperienceLevel,
selectStartExperienceLevel,
} from "../../../../selectors/experienceLevelSelectors.ts"
import { selectCurrentProfession } from "../../../../selectors/professionSelectors.ts"
import { selectCanDefineCustomProfessionName, selectCanFinishCharacterCreation, selectShowFinishCharacterCreation } from "../../../../selectors/profileSelectors.ts"
import {
selectCanDefineCustomProfessionName,
selectCanFinishCharacterCreation,
selectShowFinishCharacterCreation,
} from "../../../../selectors/profileSelectors.ts"
import { selectCurrentRace, selectCurrentRaceVariant } from "../../../../selectors/raceSelectors.ts"
import { deleteAvatar, finishCharacterCreation, selectAvatar, selectName, selectSex, selectTotalAdventurePoints, setName } from "../../../../slices/characterSlice.ts"
import {
deleteAvatar,
finishCharacterCreation,
selectAvatar,
selectName,
selectSex,
selectTotalAdventurePoints,
setName,
} from "../../../../slices/characterSlice.ts"
import { setCustomProfessionName } from "../../../../slices/professionSlice.ts"
import { PersonalData } from "./PersonalData.tsx"
import "./ProfileOverview.scss"
/**
* Returns a page for managing the characters profile, including the name,
* adventure points and personal data.
*/
export const ProfileOverview: FC = () => {
const dispatch = useAppDispatch()
const name = useAppSelector(selectName)
const [ isEditingName, setIsEditingName ] = useState(false)
const [ isEditingProfessionName, setIsEditingProfessionName ] = useState(false)
const [isEditingName, setIsEditingName] = useState(false)
const [isEditingProfessionName, setIsEditingProfessionName] = useState(false)
const translate = useTranslate()
const translateMap = useTranslateMap()
@@ -55,7 +74,7 @@ export const ProfileOverview: FC = () => {
dispatch(setName(newName))
setIsEditingName(false)
},
[ dispatch ]
[dispatch],
)
const handleEditProfessionName = useCallback(
@@ -63,79 +82,62 @@ export const ProfileOverview: FC = () => {
dispatch(setCustomProfessionName(newName))
setIsEditingProfessionName(false)
},
[ dispatch ]
[dispatch],
)
const handleStartEditName = useCallback(
() => setIsEditingName(true),
[ setIsEditingName ]
)
const handleStartEditName = useCallback(() => setIsEditingName(true), [setIsEditingName])
const handleCancelEditName = useCallback(
() => setIsEditingName(false),
[ setIsEditingName ]
)
const handleCancelEditName = useCallback(() => setIsEditingName(false), [setIsEditingName])
const handleStartEditProfessionName = useCallback(
() => setIsEditingProfessionName(true),
[ setIsEditingProfessionName ]
[setIsEditingProfessionName],
)
const handleCancelEditProfessionName = useCallback(
() => setIsEditingProfessionName(false),
[ setIsEditingProfessionName ]
[setIsEditingProfessionName],
)
const handleDeleteAvatar = useCallback(
() => dispatch(deleteAvatar()),
[ dispatch ]
)
const handleDeleteAvatar = useCallback(() => dispatch(deleteAvatar()), [dispatch])
const handlFinishCharacterCreation = useCallback(
() => dispatch(finishCharacterCreation()),
[ dispatch ]
[dispatch],
)
const nameElement = isEditingName
? (
<EditText
className="change-name"
cancel={handleCancelEditName}
submit={handleEditName}
text={name}
autoFocus
submitLabel={translate("Edit Name")}
cancelLabel={translate("Cancel")}
/>
)
: (
<h1 className="confirm-edit">
{name}
<IconButton label={translate("Edit Name")} icon="&#xE90c;" onClick={handleStartEditName} />
</h1>
)
const nameElement = isEditingName ? (
<EditText
className="change-name"
cancel={handleCancelEditName}
submit={handleEditName}
text={name}
autoFocus
submitLabel={translate("Edit Name")}
cancelLabel={translate("Cancel")}
/>
) : (
<h1 className="confirm-edit">
{name}
<IconButton label={translate("Edit Name")} icon="&#xE90c;" onClick={handleStartEditName} />
</h1>
)
const professionNameElement =
canDefineCustomProfessionName
? (isEditingProfessionName
? (
<EditText
submit={handleEditProfessionName}
submitLabel={translate("Edit Profession Name")}
cancel={handleCancelEditProfessionName}
cancelLabel={translate("Cancel")}
text={professionName}
/>
)
: (
<Button
className="edit-profession-name-btn"
onClick={handleStartEditProfessionName}
>
{translate("Edit Profession Name")}
</Button>
))
: null
const professionNameElement = canDefineCustomProfessionName ? (
isEditingProfessionName ? (
<EditText
submit={handleEditProfessionName}
submitLabel={translate("Edit Profession Name")}
cancel={handleCancelEditProfessionName}
cancelLabel={translate("Cancel")}
text={professionName}
/>
) : (
<Button className="edit-profession-name-btn" onClick={handleStartEditProfessionName}>
{translate("Edit Profession Name")}
</Button>
)
) : null
return (
<Page id="personal-data">
@@ -146,103 +148,89 @@ export const ProfileOverview: FC = () => {
<AvatarWrapper src={avatar ?? ""} /* TODO: onClick={openEditCharacterAvatar} */ />
<div className="text-wrapper">
{nameElement}
{
currentProfession === undefined
? null
: (
<VerticalList className="rcp">
<span>
{((): string => {
switch (sex.type) {
case "Male": return translate("Male")
case "Female": return translate("Female")
case "BalThani": return translate("BalThani")
case "Tsajana": return translate("Tsajana")
case "Custom": return sex.name
default: return assertExhaustive(sex)
}
})()}
</span>
<span className="race">
{currentRace === undefined
? ""
: getFullRaceName(translateMap, currentRace, currentRaceVariant)}
</span>
<span className="culture">
{currentCulture === undefined
? ""
: getFullCultureName(translateMap, currentCulture)}
</span>
<span className="profession">
{fullProfessionName}
</span>
</VerticalList>
)
}
{currentProfession === undefined ? null : (
<VerticalList className="rcp">
<span>
{((): string => {
switch (sex.type) {
case "Male":
return translate("Male")
case "Female":
return translate("Female")
case "BalThani":
return translate("BalThani")
case "Tsajana":
return translate("Tsajana")
case "Custom":
return sex.name
default:
return assertExhaustive(sex)
}
})()}
</span>
<span className="race">
{currentRace === undefined
? ""
: getFullRaceName(translateMap, currentRace, currentRaceVariant)}
</span>
<span className="culture">
{currentCulture === undefined
? ""
: getFullCultureName(translateMap, currentCulture)}
</span>
<span className="profession">{fullProfessionName}</span>
</VerticalList>
)}
<VerticalList className="el">
<span>
{
startExpLevel === undefined || currentExpLevel === undefined
{startExpLevel === undefined || currentExpLevel === undefined
? "—"
: currentExpLevel.id === startExpLevel.id
? (translateMap(startExpLevel.translations)?.name ?? "—")
: `${(translateMap(currentExpLevel.translations)?.name ?? "—")} (${(translateMap(startExpLevel.translations)?.name ?? "—")})`
}
</span>
<span>
{translate("{0} AP", totalAdventurePoints ?? 0)}
? translateMap(startExpLevel.translations)?.name ?? "—"
: `${translateMap(currentExpLevel.translations)?.name ?? "—"} (${
translateMap(startExpLevel.translations)?.name ?? "—"
})`}
</span>
<span>{translate("{0} AP", totalAdventurePoints ?? 0)}</span>
</VerticalList>
</div>
</GridItem>
<GridItem className="main-profile-actions">
{
canAddAdventurePoints
? (
<Button
className="add-ap"
disabled // TODO: onClick={openAddAdventurePoints}
>
{translate("Add AP")}
</Button>
)
: null
}
{canAddAdventurePoints ? (
<Button
className="add-ap"
disabled // TODO: onClick={openAddAdventurePoints}
>
{translate("Add AP")}
</Button>
) : null}
<Button
className="delete-avatar"
onClick={handleDeleteAvatar}
disabled={avatar === undefined}
>
>
{translate("Delete Avatar")}
</Button>
{professionNameElement}
</GridItem>
{
currentProfession === undefined
? null
: (
<GridItem>
<h3>{translate("Personal Data")}</h3>
<PersonalData />
</GridItem>
)
}
{
showFinishCharacterCreation
? (
<GridItem>
<Button
className="end-char-creation"
onClick={handlFinishCharacterCreation}
primary
disabled={!canFinishCharacterCreation}
>
{translate("Finish Character Creation")}
</Button>
</GridItem>
)
: null
}
{currentProfession === undefined ? null : (
<GridItem>
<h3>{translate("Personal Data")}</h3>
<PersonalData />
</GridItem>
)}
{showFinishCharacterCreation ? (
<GridItem>
<Button
className="end-char-creation"
onClick={handlFinishCharacterCreation}
primary
disabled={!canFinishCharacterCreation}
>
{translate("Finish Character Creation")}
</Button>
</GridItem>
) : null}
{
// TODO: Maybe.elem(3)(phase)
// ? (
@@ -11,6 +11,9 @@ import { SelectGetById } from "../../../../selectors/basicCapabilitySelectors.ts
import { selectStaticFocusRules } from "../../../../slices/databaseSlice.ts"
import { FocusRulesItem } from "./FocusRulesItem.tsx"
/**
* Returns a page section for managing focus rules.
*/
export const FocusRules: FC = () => {
const translate = useTranslate()
const translateMap = useTranslateMap()
@@ -17,7 +17,7 @@ type Props = {
}
/**
*
* Returns a single focus rule item.
*/
export const FocusRulesItem: FC<Props> = props => {
const {
@@ -14,6 +14,9 @@ import { SelectGetById } from "../../../../selectors/basicCapabilitySelectors.ts
import { selectStaticOptionalRules } from "../../../../slices/databaseSlice.ts"
import { OptionalRulesItem } from "./OptionalRulesItem.tsx"
/**
* Returns a page section for managing optional rules.
*/
export const OptionalRules: FC = () => {
const translate = useTranslate()
const translateMap = useTranslateMap()
@@ -22,7 +22,7 @@ type Props = {
}
/**
*
* Returns a single optional rule item.
*/
export const OptionalRulesItem: FC<Props> = props => {
const {
@@ -14,6 +14,9 @@ import { selectStaticPublications } from "../../../../slices/databaseSlice.ts"
import { switchIncludeAllPublications } from "../../../../slices/rulesSlice.ts"
import { RuleSourcesListItem } from "./RuleSourcesListItem.tsx"
/**
* Returns a page section for managing the publications to use for a character.
*/
export const RuleSources: FC = () => {
const translate = useTranslate()
const translateMap = useTranslateMap()
@@ -1,8 +1,14 @@
import { Publication, PublicationTranslation } from "optolith-database-schema/types/source/Publication"
import {
Publication,
PublicationTranslation,
} from "optolith-database-schema/types/source/Publication"
import { FC, useCallback } from "react"
import { Checkbox } from "../../../../../shared/components/checkbox/Checkbox.tsx"
import { useAppDispatch, useAppSelector } from "../../../../hooks/redux.ts"
import { selectIncludeAllPublications, selectIncludePublications } from "../../../../slices/characterSlice.ts"
import {
selectIncludeAllPublications,
selectIncludePublications,
} from "../../../../slices/characterSlice.ts"
import { switchIncludePublication } from "../../../../slices/rulesSlice.ts"
type Props = {
@@ -12,8 +18,13 @@ type Props = {
}
}
/**
* Returns a single publication item.
*/
export const RuleSourcesListItem: FC<Props> = props => {
const { publication: { publication, publicationTranslation } } = props
const {
publication: { publication, publicationTranslation },
} = props
const dispatch = useAppDispatch()
const includeAllPublications = useAppSelector(selectIncludeAllPublications) === true
@@ -21,23 +32,23 @@ export const RuleSourcesListItem: FC<Props> = props => {
const handleSwitch = useCallback(
() => dispatch(switchIncludePublication(publication.id)),
[ dispatch, publication.id ],
[dispatch, publication.id],
)
return (
<Checkbox
className={publication.contains_adult_content ? "adult-content" : undefined}
checked={
publication.category === "CoreRules"
|| (includePublications?.includes(publication.id) ?? false)
|| (!publication.contains_adult_content && includeAllPublications)
publication.category === "CoreRules" ||
(includePublications?.includes(publication.id) ?? false) ||
(!publication.contains_adult_content && includeAllPublications)
}
onClick={handleSwitch}
label={publicationTranslation.name}
disabled={
(includeAllPublications && !publication.contains_adult_content)
|| publication.category === "CoreRules"
(includeAllPublications && !publication.contains_adult_content) ||
publication.category === "CoreRules"
}
/>
/>
)
}
@@ -8,6 +8,9 @@ import { OptionalRules } from "./OptionalRules.tsx"
import { RuleSources } from "./RuleSources.tsx"
import "./Rules.scss"
/**
* Returns a page for managing a characters ruleset.
*/
export const Rules: FC = () => (
<Page id="rules">
<Main>
@@ -1,5 +1,9 @@
import { FC } from "react"
/**
* An additional value for a skill row is defined by a value and a class name
* that can be used to style the value.
*/
export type AdditionalValue = {
className: string
value?: string | number
@@ -9,6 +13,9 @@ type Props = {
addValues?: AdditionalValue[]
}
/**
* Returns a list of additional values for the skill row.
*/
export const SkillAdditionalValues: FC<Props> = props => {
const { addValues } = props
@@ -18,6 +18,9 @@ type Props = {
selectForInfo(id: number): void
}
/**
* Returns a list of buttons for a skill row.
*/
export const SkillButtons: React.FC<Props> = props => {
const {
activateDisabled,
@@ -7,6 +7,9 @@ type Props = {
getGroupName?: (id: number) => string
}
/**
* Returns a row section that display a skill group.
*/
export const SkillGroup: FC<Props> = props => {
const { addText, group, getGroupName } = props
@@ -8,6 +8,9 @@ type Props = {
ic?: ImprovementCost
}
/**
* Returns a row section that displays an improvement cost.
*/
export const SkillImprovementCost: FC<Props> = props => {
const { ic } = props
@@ -7,6 +7,9 @@ type Props = {
addPoint?(id: number): void
}
/**
* Returns a row section that displays a skill rating.
*/
export const SkillRating: FC<Props> = props => {
const { isNotActive, noIncrease, sr, addPoint } = props
@@ -46,6 +46,9 @@ const isTopMarginNeeded = (
mprev !== undefined &&
curr.static.group.id.skill_group !== mprev.static.group.id.skill_group
/**
* Returns a page for managing skills.
*/
export const Skills: FC = () => {
const translate = useTranslate()
const translateMap = useTranslateMap()
@@ -19,6 +19,9 @@ import {
selectSpecialAbilitiesSortOrder,
} from "../../../../slices/settingsSlice.ts"
/**
* Returns a page for managing special abilities.
*/
export const SpecialAbilities: FC = () => {
const translate = useTranslate()
const dispatch = useAppDispatch()