Skip to content

Commit

Permalink
SRD spell search
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul-Ladyman committed May 18, 2024
1 parent df76134 commit 229e6a2
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 60 deletions.
11 changes: 11 additions & 0 deletions src/client/dnd5eapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ export async function getMonster(monster) {
return {};
}
}

export async function getSpells() {
try {
const response = await fetch(`${BASE_API_URL}/api/spells`, { 'Content-Type': 'application/json' });
const json = await response.json();
const { results } = json;
return results || [];
} catch {
return [];
}
}
110 changes: 63 additions & 47 deletions src/components/app/DungeonMasterApp.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import React, { useEffect, useRef, useMemo } from 'react';
import React, {
useEffect,
useRef,
useMemo,
useState,
} from 'react';
import isHotkey from 'is-hotkey';
import '../App.css';
import CreateCreatureForm from '../page/create-creature-form/CreateCreatureForm';
Expand Down Expand Up @@ -59,10 +64,13 @@ import {
dismissErrors,
updateErrors,
} from '../../state/ErrorManager';
import { getSpells } from '../../client/dnd5eapi';
import SrdContext from './SrdContext';

function DungeonMasterApp({
state, setState, shareBattle, onlineError,
}) {
const [srdSpells, setSrdSpells] = useState([]);
const creaturesRef = useRef(null);

const updateBattle = (update, doShare = true) => (...args) => {
Expand Down Expand Up @@ -103,6 +111,10 @@ function DungeonMasterApp({
focusedCreature,
} = state;

useEffect(() => {
getSpells().then(setSrdSpells);
}, []);

useEffect(() => {
window.onbeforeunload = () => true;

Expand Down Expand Up @@ -146,6 +158,8 @@ function DungeonMasterApp({
loadBattle,
}), [state]);

const srd = useMemo(() => ({ srdSpells }));

const onScrollActiveInitiative = () => {
creaturesRef.current.scrollToCreature(activeCreatureId);
};
Expand All @@ -161,54 +175,56 @@ function DungeonMasterApp({
}, [errorCreatureId]);

return (
<BattleManagerContext.Provider value={battleManagement}>
<BattleToolbar
initiative={activeCreatureName}
round={round}
secondsElapsed={secondsElapsed}
creatureCount={creatureCount}
nextInitiative={updateBattle(nextInitiative)}
shareEnabled={shareEnabled}
isSaveLoadSupported={isSaveLoadSupported}
rulesSearchOpen={rulesSearchOpened}
onScrollActiveInitiative={onScrollActiveInitiative}
/>
{ errors && (
<Errors
errors={state.errors}
dismissErrors={updateBattle(dismissErrors, false)}
/>
)}
<AriaAnnouncements announcements={ariaAnnouncements} />
<div className="main-footer-wrapper">
<RulesSearchBar
rulesSearchOpened={rulesSearchOpened}
onSearch={updateBattle(toggleRulesSearch, false)}
<SrdContext.Provider value={srd}>
<BattleManagerContext.Provider value={battleManagement}>
<BattleToolbar
initiative={activeCreatureName}
round={round}
secondsElapsed={secondsElapsed}
creatureCount={creatureCount}
nextInitiative={updateBattle(nextInitiative)}
shareEnabled={shareEnabled}
isSaveLoadSupported={isSaveLoadSupported}
rulesSearchOpen={rulesSearchOpened}
onScrollActiveInitiative={onScrollActiveInitiative}
/>
<main className="main">
<Title
shareEnabled={shareEnabled}
battleId={battleId}
/>
<CreateCreatureForm
createCreature={updateBattle(addCreature)}
handleCreateCreatureErrors={updateBattle(handleCreateCreatureErrors)}
createCreatureErrors={state.createCreatureErrors}
/>
<Creatures
ref={creaturesRef}
creatures={creatures}
activeCreatureId={activeCreatureId}
errorCreatureId={errorCreatureId}
focusedCreature={focusedCreature}
round={round}
secondsElapsed={secondsElapsed}
creatureManagement={creatureManagement}
{ errors && (
<Errors
errors={state.errors}
dismissErrors={updateBattle(dismissErrors, false)}
/>
)}
<AriaAnnouncements announcements={ariaAnnouncements} />
<div className="main-footer-wrapper">
<RulesSearchBar
rulesSearchOpened={rulesSearchOpened}
onSearch={updateBattle(toggleRulesSearch, false)}
/>
</main>
<Footer />
</div>
</BattleManagerContext.Provider>
<main className="main">
<Title
shareEnabled={shareEnabled}
battleId={battleId}
/>
<CreateCreatureForm
createCreature={updateBattle(addCreature)}
handleCreateCreatureErrors={updateBattle(handleCreateCreatureErrors)}
createCreatureErrors={state.createCreatureErrors}
/>
<Creatures
ref={creaturesRef}
creatures={creatures}
activeCreatureId={activeCreatureId}
errorCreatureId={errorCreatureId}
focusedCreature={focusedCreature}
round={round}
secondsElapsed={secondsElapsed}
creatureManagement={creatureManagement}
/>
</main>
<Footer />
</div>
</BattleManagerContext.Provider>
</SrdContext.Provider>
);
}

Expand Down
4 changes: 4 additions & 0 deletions src/components/app/SrdContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContext } from 'react';

const SrdContext = createContext(null);
export default SrdContext;
45 changes: 35 additions & 10 deletions src/components/creature/toolbar/tools/spellcasting/SpellList.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useState } from 'react';
import CreatureToolbarInput from '../CreatureToolbarInput';
import React, { useState, useContext } from 'react';
import CrossIcon from '../../../../icons/CrossIcon';
import Input from '../../../../form/Input';
import { maxSpellsPerDay } from '../../../../../domain/spellcasting';
import ComboboxList from '../../../../form/ComboboxList';
import SrdContext from '../../../../app/SrdContext';

function Spell({
spellProperty,
Expand Down Expand Up @@ -84,6 +85,17 @@ function Spells({
);
}

function getFilteredSpells(name, spellData) {
if (name.length < 2) return [];
return spellData
.filter((spell) => spell.name && spell.name.toLowerCase().includes(name.toLowerCase()))
.map((spell) => ({
...spell,
text: spell.name,
id: spell.index,
}));
}

export default function SpellList({
spells,
spellProperty,
Expand All @@ -96,18 +108,31 @@ export default function SpellList({
useSpellMax,
displayMaxExceeded,
}) {
const [name, setName] = useState('');
const srd = useContext(SrdContext);
const { srdSpells } = srd;
const spellRightControls = {
rightTitle: 'Add spell',
RightSubmitIcon: <CrossIcon />,
};
return (
<div>
<div className="spellcasting-spell-input">
<CreatureToolbarInput
ariaLabel={`Add spells for ${creatureName}`}
<ComboboxList
value={name}
setValue={setName}
list={getFilteredSpells(name, srdSpells)}
id={`${creatureId}-${id}-spells`}
dropdownId={`${creatureId}-${id}-spells-dropdown`}
dropdownLabel="Select spell"
label="Spells"
rightSubmit={(spell) => addSpell(creatureId, spell)}
rightControls={{
rightTitle: 'Add spell',
RightSubmitIcon: <CrossIcon />,
}}
inputId={`${creatureId}-${id}-spells`}
listAriaLabel="Spell search results"
inputAriaLabel={`Add spells for ${creatureName}`}
inputAriaLabelItemSelected={`Add spells for ${creatureName}`}
rightControls={spellRightControls}
rightControlsItemSelected={spellRightControls}
handleSubmit={() => addSpell(creatureId, name)}
spellCheck={false}
/>
</div>
<Spells
Expand Down
2 changes: 1 addition & 1 deletion test-integration/create-creature.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { http, HttpResponse } from 'msw';
import msw from './mocks/server';
import DmApp from './page-object-models/dmApp';
import CreateCreatureForm from './page-object-models/createCreatureForm';
import msw from './mocks/server';
import random from '../src/util/random';

jest.mock('../src/util/random');
Expand Down
63 changes: 63 additions & 0 deletions test-integration/creature-toolbar/spellcasting-tool.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { http, HttpResponse } from 'msw';
import msw from '../mocks/server';
import DmApp from '../page-object-models/dmApp';
import { maxSpellSlots } from '../../src/domain/spellcasting';

Expand Down Expand Up @@ -405,3 +407,64 @@ describe('Total spells', () => {
await dmApp.spellcastingTool.assertTotalSpellValue('Couatl', 'Dream', 1);
});
});

describe('SRD spell search', () => {
it('adds an SRD spell to the list', async () => {
const dmApp = new DmApp();
await dmApp.createCreatureForm.addCreature('goblin');
await dmApp.creatureToolbar.selectTool('goblin', 'Spellcasting');
await dmApp.spellcastingTool.openUsedSpells('goblin');
await dmApp.spellcastingTool.addUsedSrdSpell('goblin', 'Acid Arrow');
});

it('filters the SRD spell list by the search term', async () => {
const dmApp = new DmApp();
await dmApp.createCreatureForm.addCreature('goblin');
await dmApp.creatureToolbar.selectTool('goblin', 'Spellcasting');
await dmApp.spellcastingTool.openUsedSpells('goblin');
await dmApp.spellcastingTool.searchUsedSrdSpell('goblin', 'Acid');
await dmApp.spellcastingTool.assertUsedSrdSpell('goblin', 'Acid Arrow');
await dmApp.spellcastingTool.assertUsedSrdSpell('goblin', 'Acid Splash');
await dmApp.spellcastingTool.assertNotUsedSrdSpell('goblin', 'Aid');
});

it('displays no spells if they do not include a name', async () => {
msw.use(
http.get('https://www.dnd5eapi.co/api/spells', () => HttpResponse.json({
results: [{}],
})),
);
const dmApp = new DmApp();
await dmApp.createCreatureForm.addCreature('goblin');
await dmApp.creatureToolbar.selectTool('goblin', 'Spellcasting');
await dmApp.spellcastingTool.openUsedSpells('goblin');
await dmApp.spellcastingTool.searchUsedSrdSpell('goblin', 'Acid Arrow');
await dmApp.spellcastingTool.assertNoUsedSrdSpells('goblin');
});

it('displays no spells if the response is malformed', async () => {
msw.use(
http.get('https://www.dnd5eapi.co/api/spells', () => new HttpResponse('malformed')),
);
const dmApp = new DmApp();
await dmApp.createCreatureForm.addCreature('goblin');
await dmApp.creatureToolbar.selectTool('goblin', 'Spellcasting');
await dmApp.spellcastingTool.openUsedSpells('goblin');
await dmApp.spellcastingTool.searchUsedSrdSpell('goblin', 'Acid Arrow');
await dmApp.spellcastingTool.assertNoUsedSrdSpells('goblin');
});

it('displays no spells if there was an error fetching them', async () => {
msw.use(
http.get('https://www.dnd5eapi.co/api/spells', () => new HttpResponse(null, {
status: 500,
})),
);
const dmApp = new DmApp();
await dmApp.createCreatureForm.addCreature('goblin');
await dmApp.creatureToolbar.selectTool('goblin', 'Spellcasting');
await dmApp.spellcastingTool.openUsedSpells('goblin');
await dmApp.spellcastingTool.searchUsedSrdSpell('goblin', 'Acid Arrow');
await dmApp.spellcastingTool.assertNoUsedSrdSpells('goblin');
});
});
23 changes: 23 additions & 0 deletions test-integration/mocks/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,29 @@ export default [
},
)),

http.get('https://www.dnd5eapi.co/api/spells', () => HttpResponse.json({
results: [
{
index: 'acid-arrow',
name: 'Acid Arrow',
level: 2,
url: '/api/spells/acid-arrow',
},
{
index: 'acid-splash',
name: 'Acid Splash',
level: 0,
url: '/api/spells/acid-splash',
},
{
index: 'aid',
name: 'Aid',
level: 2,
url: '/api/spells/aid',
},
],
})),

graphql.mutation('CREATE_BATTLE', () => HttpResponse.json({
data: {
createDndbattletracker: {
Expand Down
Loading

0 comments on commit 229e6a2

Please sign in to comment.