Skip to content

Commit

Permalink
Rework quest earning creation
Browse files Browse the repository at this point in the history
  • Loading branch information
Palbolsky committed Jan 10, 2025
1 parent e3868ac commit 8a7993b
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { DarkButton, PrimaryButton } from '@components/buttons';
import { useRefreshUI } from '@components/editor';
import { Editor } from '@components/editor/Editor';
import { InputContainer, InputWithTopLabelContainer, Label } from '@components/inputs';
import { SelectCustomSimple } from '@components/SelectCustom';
import { QUEST_EARNINGS, StudioQuest, StudioQuestEarningType } from '@modelEntities/quest';
import { QUEST_EARNINGS, StudioQuestEarningType } from '@modelEntities/quest';
import { createQuestEarning } from '@utils/entityCreation';
import { useProjectQuests } from '@hooks/useProjectData';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
import styled from 'styled-components';
import { QuestEarningItem, QuestEarningMoney, QuestEarningPokemon } from './earnings';
import { EditorHandlingClose, useEditorHandlingClose } from '@components/editor/useHandleCloseEditor';
import { useQuestPage } from '@src/hooks/usePage';
import { useUpdateQuest } from './useUpdateQuest';
import { Select } from '@ds/Select';
import { useEarningQuest } from './useEarningQuest';
import { cloneEntity } from '@utils/cloneEntity';
import { cleanNaNValue } from '@utils/cleanNaNValue';
import styled from 'styled-components';
import React, { forwardRef, useMemo } from 'react';
import { assertUnreachable } from '@utils/assertUnreachable';

const earningCategoryEntries = (t: TFunction<'database_quests'>) => QUEST_EARNINGS.map((earning) => ({ value: earning, label: t(earning) }));

Expand All @@ -22,50 +27,76 @@ const ButtonContainer = styled.div`
`;

type QuestNewEarningEditorProps = {
quest: StudioQuest;
onClose: () => void;
closeDialog: () => void;
};

export const QuestNewEarningEditor = ({ quest, onClose }: QuestNewEarningEditorProps) => {
export const QuestNewEarningEditor = forwardRef<EditorHandlingClose, QuestNewEarningEditorProps>(({ closeDialog }, ref) => {
const { t } = useTranslation('database_quests');
const refreshUI = useRefreshUI();
const { quest } = useQuestPage();
const updateQuest = useUpdateQuest(quest);
const earningOptions = useMemo(() => earningCategoryEntries(t), [t]);
const [newEarning, setNewEarning] = useState(createQuestEarning('earning_money'));
const { setProjectDataValues: setQuest } = useProjectQuests();
const { earning, refs, updateEarning, checkIsValid, isValid } = useEarningQuest(createQuestEarning('earning_money'));
const earningMethodName = earning.earningMethodName;

useEditorHandlingClose(ref);

const changeEarning = (value: StudioQuestEarningType) => {
if (value === newEarning.earningMethodName) return;
setNewEarning(createQuestEarning(value));
if (value === earning.earningMethodName) return;

updateEarning(createQuestEarning(value));
};

const onClickNew = () => {
quest.earnings.push(newEarning);
setQuest({ [quest.dbSymbol]: quest });
onClose();
if (!isValid) return;

const newEarning = cloneEntity(earning);
switch (earningMethodName) {
case 'earning_money': {
if (!refs.inputRef.current) return;

newEarning.earningArgs[0] = cleanNaNValue(refs.inputRef.current.valueAsNumber, 100);
break;
}
case 'earning_item': {
if (!refs.entityRef.current || !refs.inputRef.current) return;

newEarning.earningArgs[0] = refs.entityRef.current;
newEarning.earningArgs[1] = cleanNaNValue(refs.inputRef.current.valueAsNumber, 1);
break;
}
case 'earning_pokemon':
case 'earning_egg': {
if (!refs.entityRef.current) return;

newEarning.earningArgs[0] = refs.entityRef.current;
break;
}
default:
assertUnreachable(earningMethodName);
}
updateQuest({ earnings: [...quest.earnings, newEarning] });
closeDialog();
};

return (
<Editor type="creation" title={t('earning')}>
<InputContainer>
<InputWithTopLabelContainer>
<Label htmlFor="earning-type">{t('earning_type')}</Label>
<SelectCustomSimple
id={'earning-type-select'}
value={newEarning.earningMethodName}
options={earningOptions}
onChange={(value) => refreshUI(changeEarning(value as StudioQuestEarningType))}
noTooltip
/>
<Select id="earning-type" value={earning.earningMethodName} options={earningOptions} onChange={changeEarning} />
</InputWithTopLabelContainer>
{newEarning.earningMethodName === 'earning_money' && <QuestEarningMoney earning={newEarning} />}
{newEarning.earningMethodName === 'earning_item' && <QuestEarningItem earning={newEarning} />}
{newEarning.earningMethodName === 'earning_pokemon' && <QuestEarningPokemon earning={newEarning} />}
{newEarning.earningMethodName === 'earning_egg' && <QuestEarningPokemon earning={newEarning} />}
{earningMethodName === 'earning_money' && <QuestEarningMoney earning={earning} refs={refs} checkIsValid={checkIsValid} />}
{earningMethodName === 'earning_item' && <QuestEarningItem earning={earning} refs={refs} checkIsValid={checkIsValid} />}
{earningMethodName === 'earning_pokemon' && <QuestEarningPokemon earning={earning} refs={refs} />}
{earningMethodName === 'earning_egg' && <QuestEarningPokemon earning={earning} refs={refs} />}
<ButtonContainer>
<PrimaryButton onClick={onClickNew}>{t('add_earning')}</PrimaryButton>
<DarkButton onClick={onClose}>{t('cancel')}</DarkButton>
<PrimaryButton onClick={onClickNew} disabled={!isValid}>
{t('add_earning')}
</PrimaryButton>
<DarkButton onClick={closeDialog}>{t('cancel')}</DarkButton>
</ButtonContainer>
</InputContainer>
</Editor>
);
};
});
QuestNewEarningEditor.displayName = 'QuestNewEarningEditor';
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
import React from 'react';
import { useRefreshUI } from '@components/editor';
import { InputContainer, InputWithLeftLabelContainer, InputWithTopLabelContainer, Label } from '@components/inputs';
import { SelectItem } from '@components/selects';
import { useTranslation } from 'react-i18next';
import { InputNumber } from '../goals/InputNumber';
import { InputNumber2 } from '../goals/InputNumber';
import { QuestEarningProps } from './QuestEarningProps';
import { SelectItem2 } from '@components/selects/SelectItem';
import { DbSymbol } from '@modelEntities/dbSymbol';

export const QuestEarningItem = ({ earning }: QuestEarningProps) => {
export const QuestEarningItem = ({ earning, refs, checkIsValid }: QuestEarningProps) => {
const { t } = useTranslation(['database_items', 'database_quests']);
const refreshUI = useRefreshUI();
const defaultItem = earning.earningArgs[0] === '__undef__' ? undefined : (earning.earningArgs[0] as DbSymbol);

return (
<InputContainer>
<InputWithTopLabelContainer>
<Label htmlFor="select-item">{t('database_items:item')}</Label>
<SelectItem
dbSymbol={earning.earningArgs[0] as string}
onChange={(selected) => refreshUI((earning.earningArgs[0] = selected))}
noLabel
noneValue
/>
<SelectItem2 name="select-item" optionRef={refs.entityRef} defaultValue={defaultItem} />
</InputWithTopLabelContainer>
<InputWithLeftLabelContainer>
<Label htmlFor="amount-item">{t('database_quests:amount')}</Label>
<InputNumber
<InputNumber2
name="amount-item"
value={earning.earningArgs[1] as number}
setValue={(value: number) => refreshUI((earning.earningArgs[1] = value))}
ref={refs.inputRef}
defaultValue={earning.earningArgs[1] as number}
onChange={() => checkIsValid && checkIsValid()}
/>
</InputWithLeftLabelContainer>
</InputContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import React from 'react';
import { useRefreshUI } from '@components/editor';
import { InputContainer, InputWithTopLabelContainer, Label } from '@components/inputs';
import { useTranslation } from 'react-i18next';
import { QuestEarningProps } from './QuestEarningProps';
import { EmbeddedUnitInput } from '@components/inputs/EmbeddedUnitInput';
import { cleanNaNValue } from '@utils/cleanNaNValue';

export const QuestEarningMoney = ({ earning }: QuestEarningProps) => {
export const QuestEarningMoney = ({ earning, refs, checkIsValid }: QuestEarningProps) => {
const { t } = useTranslation('database_quests');
const refreshUI = useRefreshUI();

return (
<InputContainer>
<InputWithTopLabelContainer>
<Label htmlFor="amount-money">{t('amount')}</Label>
<EmbeddedUnitInput
ref={refs.inputRef}
unit="P$"
step="1"
name="amount-money"
type="number"
min="1"
max="999_999_999"
value={isNaN(earning.earningArgs[0] as number) ? '' : earning.earningArgs[0]}
onChange={(event) => {
const newValue = event.target.value == '' ? Number.NaN : parseInt(event.target.value);
if (newValue < 1 || newValue > 999_999_999) return event.preventDefault();
refreshUI((earning.earningArgs[0] = newValue));
}}
onBlur={() => refreshUI((earning.earningArgs[0] = cleanNaNValue(earning.earningArgs[0] as number, 100)))}
defaultValue={earning.earningArgs[0]}
onChange={() => checkIsValid && checkIsValid()}
/>
</InputWithTopLabelContainer>
</InputContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import React from 'react';
import { useRefreshUI } from '@components/editor';
import { InputContainer, InputWithTopLabelContainer, Label } from '@components/inputs';
import { useTranslation } from 'react-i18next';
import { QuestEarningProps } from './QuestEarningProps';
import { SelectPokemon } from '@components/selects/SelectPokemon';
import { SelectPokemon2 } from '@components/selects/SelectPokemon';
import { DbSymbol } from '@modelEntities/dbSymbol';
import React from 'react';

export const QuestEarningPokemon = ({ earning, refs }: QuestEarningProps) => {
const { t } = useTranslation('database_pokemon');
const defaultCreature = earning.earningArgs[0] === '__undef__' ? undefined : (earning.earningArgs[0] as DbSymbol);

export const QuestEarningPokemon = ({ earning }: QuestEarningProps) => {
const { t } = useTranslation(['database_pokemon', 'select']);
const refreshUI = useRefreshUI();
return (
<InputContainer>
<InputWithTopLabelContainer>
<Label htmlFor="select-pokemon">{t('database_pokemon:pokemon')}</Label>
<SelectPokemon
dbSymbol={earning.earningArgs[0] as string}
onChange={(value) => refreshUI((earning.earningArgs[0] = value))}
undefValueOption={t('select:none')}
noLabel
/>
<Label htmlFor="select-creature">{t('pokemon')}</Label>
<SelectPokemon2 name="select-creature" optionRef={refs.entityRef} defaultValue={defaultCreature} />
</InputWithTopLabelContainer>
</InputContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { DbSymbol } from '@modelEntities/dbSymbol';
import { StudioQuestEarning } from '@modelEntities/quest';
import { MutableRefObject, RefObject } from 'react';

export type QuestEarningProps = {
earning: StudioQuestEarning;
refs: {
entityRef: MutableRefObject<DbSymbol | undefined>;
inputRef: RefObject<HTMLInputElement>;
};
checkIsValid?: () => void;
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { forwardRef } from 'react';
import { Input } from '@components/inputs';
import { cleanNaNValue } from '@utils/cleanNaNValue';

Expand All @@ -25,3 +25,14 @@ export const InputNumber = ({ name, value, setValue }: InputNumberProps) => {
/>
);
};

type InputNumber2Props = {
name: string;
defaultValue: number;
onChange: () => void;
};

export const InputNumber2 = forwardRef<HTMLInputElement, InputNumber2Props>(({ name, defaultValue, onChange }, ref) => {
return <Input ref={ref} type="number" name={name} min="1" max="999" defaultValue={defaultValue} onChange={() => onChange()} />;
});
InputNumber2.displayName = 'InputNumber2';
42 changes: 42 additions & 0 deletions src/views/components/database/quest/editors/useEarningQuest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { DbSymbol } from '@modelEntities/dbSymbol';
import { StudioQuestEarning } from '@modelEntities/quest';
import { assertUnreachable } from '@utils/assertUnreachable';
import { useRef, useState } from 'react';

export const useEarningQuest = (initialEarning: StudioQuestEarning) => {
const [earning, setEarning] = useState(initialEarning);
const [isValid, setIsValid] = useState<boolean>(true);
const entityRef = useRef<DbSymbol | undefined>();
const inputRef = useRef<HTMLInputElement>(null);

const checkIsValid = () => {
switch (earning.earningMethodName) {
case 'earning_money':
return setIsValid(!!inputRef.current && inputRef.current.validity.valid);
case 'earning_item':
return setIsValid(!!entityRef.current && !!inputRef.current && inputRef.current.validity.valid);
case 'earning_pokemon':
case 'earning_egg':
return setIsValid(!!entityRef.current);
default:
assertUnreachable(earning.earningMethodName);
}
return setIsValid(true);
};

const updateEarning = (earning: StudioQuestEarning) => {
setEarning(earning);
setIsValid(true);
};

return {
earning,
refs: {
entityRef,
inputRef,
},
updateEarning,
checkIsValid,
isValid,
};
};
17 changes: 17 additions & 0 deletions src/views/components/selects/SelectItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { useTranslation } from 'react-i18next';
import { useSelectOptions } from '@hooks/useSelectOptions';
import { StudioDropDown } from '@components/StudioDropDown';
import { SelectContainerWithLabel } from './SelectContainerWithLabel';
import { DbSymbol } from '@modelEntities/dbSymbol';
import { SelectOption } from '@ds/Select/types';
import { Select } from '@ds/Select';

type SelectItemProps = {
dbSymbol: string;
Expand Down Expand Up @@ -34,3 +37,17 @@ export const SelectItem = ({ dbSymbol, onChange, noLabel, noneValue, undefValueO
</SelectContainerWithLabel>
);
};

type SelectItem2Props = {
name: string;
defaultValue?: DbSymbol;
optionRef?: React.MutableRefObject<'__undef__' | DbSymbol | undefined>;
onChange?: (v: DbSymbol) => void;
};

export const SelectItem2 = (props: SelectItem2Props) => {
const { t } = useTranslation('database_items');
const itemOptions = useSelectOptions('items') as SelectOption<DbSymbol>[];

return <Select options={itemOptions} notFoundLabel={t('item_deleted')} chooseValue={itemOptions[0]?.value || '__undef__'} {...props} />;
};
3 changes: 2 additions & 1 deletion src/views/components/selects/SelectPokemon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ export const SelectPokemon = ({ dbSymbol, onChange, breakpoint, noLabel, undefVa
type SelectPokemon2Props = {
name: string;
defaultValue?: DbSymbol;
optionRef?: React.MutableRefObject<'__undef__' | DbSymbol | undefined>;
onChange?: (v: DbSymbol) => void;
};

export const SelectPokemon2 = (props: SelectPokemon2Props) => {
const { t } = useTranslation('database_pokemon');
const creatureOptions = useSelectOptions('creatures') as SelectOption<DbSymbol>[];

return <Select options={creatureOptions} notFoundLabel={t('pokemon_deleted')} chooseValue="__undef__" {...props} />;
return <Select options={creatureOptions} notFoundLabel={t('pokemon_deleted')} chooseValue={creatureOptions[0]?.value || '__undef__'} {...props} />;
};

0 comments on commit 8a7993b

Please sign in to comment.