Skip to content

Commit

Permalink
9 implement character display component (#10)
Browse files Browse the repository at this point in the history
* Start character display

* Send file hook

* basic character display

* make initiative editable

* process input after enter key

* Remove dynamic data

* Format

* Add delete functionality
  • Loading branch information
bduhbya authored Sep 9, 2024
1 parent eb2578b commit a8d9adc
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 38 deletions.
16 changes: 16 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Run Main App",
"type": "shell",
"command": "npm run dev",
"runOptions": {
"runOn": "default"
},
"options": {
"cwd": "${workspaceFolder}"
}
}
]
}
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions src/app/components/CharacterDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useState } from "react";
// import strings from "@/strings";

export interface CharacterDisplayProps {
sourceFile: File | null;
}

export const CharacterDisplay: React.FC<CharacterDisplayProps> = ({
sourceFile,
}) => {
const [characterData, setCharacterData] = useState<JSON | null>(null);

if (sourceFile !== null) {
const reader = new FileReader();
reader.onload = () => {
try {
const jsonData = JSON.parse(reader.result as string);
setCharacterData(jsonData);
} catch (error) {
console.error("Error parsing JSON file:", error);
}
};
reader.readAsText(sourceFile);
}

if (characterData === null) {
return (
<div className="flex flex-col items-center justify-center">
<p className="text-black dark:text-white">No file selected</p>
</div>
);
} else {
const tableRows = Object.keys(characterData).map((key) => (
<tr key={key}>
<td className="font-bold"> {key}: </td>
<td>{characterData[key]}</td>
</tr>
));

return (
<div className="flex flex-col items-center justify-center">
{characterData !== null && <div>{tableRows}</div>}
</div>
);
}
};
96 changes: 69 additions & 27 deletions src/app/components/CombatTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { BasicDialog, DialogData, DialogType } from "./BasicDialog";

export const activeCharacterTestId = "active-combat-character-row-";

const CombatTracker: React.FC = () => {
export interface CombatTrackerProps {
SetCharacterFile: (file: File) => void;
}

const CombatTracker: React.FC<CombatTrackerProps> = ({ SetCharacterFile }) => {
const DIRECTION_UP = "up";
const DIRECTION_DOWN = "down";
type Direction = typeof DIRECTION_UP | typeof DIRECTION_DOWN;
Expand Down Expand Up @@ -44,8 +48,8 @@ const CombatTracker: React.FC = () => {
setPendingCharacter({
name: jsonData.name || "UNKNOWN",
fileReference: file,
dynamicData: jsonData,
initiative: 0,
initiativeDisplay: 0,
active: false,
});
} catch (error) {
Expand All @@ -65,9 +69,13 @@ const CombatTracker: React.FC = () => {
}
};

const handleConfirmAddCharacter = (newCharacter: Character) => {
const handleConfirmAddCharacter = (
newCharacter: Character,
SetCharacterFile: (file: File) => void,
) => {
if (combatCharacters.length === 0) {
newCharacter.active = true;
SetCharacterFile(newCharacter.fileReference);
}
// Add the character to combatCharacters
setCombatCharacters([...combatCharacters, newCharacter]);
Expand All @@ -81,28 +89,38 @@ const CombatTracker: React.FC = () => {
setPendingCharacter(null);
};

const handleCharacterClick = (character: Character) => {
// TODO: activate a callback to set the character display in parent
const dynamicDataKeys = Object.keys(character.dynamicData);

const tableRows = dynamicDataKeys.map((key) => (
<tr key={key}>
<td>{key}</td>
<td>{character.dynamicData[key]}</td>
</tr>
));

const modalContent = (
<div>
<h3>{character.name} - Dynamic Data</h3>
<table>
<tbody>{tableRows}</tbody>
</table>
</div>
);
const handleInitiativeInputChange = (initiative: string, index: number) => {
const newCombatCharacters = [...combatCharacters];
var newInitiative = 0;
if (initiative !== "") {
newInitiative = parseInt(initiative);
}
newCombatCharacters[index].initiativeDisplay = newInitiative;
setCombatCharacters(newCombatCharacters);
};

const handleInitiativeChange = (index: number) => {
const newCombatCharacters = [...combatCharacters];
const newInitiative = newCombatCharacters[index].initiativeDisplay;
newCombatCharacters[index].initiative = newInitiative;
setCombatCharacters(newCombatCharacters);
};

const handleCharacterDelete = (index: number) => {
const newCombatCharacters = [...combatCharacters];
if (newCombatCharacters[index].active) {
// If the character being deleted is the active character, set the next character as active
newCombatCharacters[(index + 1) % newCombatCharacters.length].active = true;
}
newCombatCharacters.splice(index, 1);
setCombatCharacters(newCombatCharacters);
};

// Using window.alert for simplicity, consider using a modal library for a better user experience
window.alert(JSON.stringify(character.dynamicData, null, 2));
const handleCharacterClick = (
character: Character,
SetCharacterFile: (file: File) => void,
) => {
SetCharacterFile(character.fileReference);
};

const handleMoveActiveCharacter = (direction: Direction) => {
Expand Down Expand Up @@ -165,6 +183,7 @@ const CombatTracker: React.FC = () => {
onConfirm={handleConfirmAddCharacter}
onCancel={handleCancelAddCharacter}
duplicateEntryOrEmpty={isDuplicateOrEmpty}
SetCharacterFile={SetCharacterFile}
/>
)}
<table className="border-collapse border">
Expand All @@ -175,15 +194,16 @@ const CombatTracker: React.FC = () => {
{strings.currentCharacterColumnLabel}
</th>
<th className="border p-2">{strings.characterNameColumnLabel}</th>
<th className="border p-2">{strings.actionsLabel}</th>
<th className="border p-2">{strings.initiativeColumnLabel}</th>
</tr>
</thead>
<tbody>
{combatCharacters.map((character, index) => (
<tr
key={index}
onClick={() => handleCharacterClick(character)}
className={character.active ? "bg-gray-200" : ""}
onClick={() => handleCharacterClick(character, SetCharacterFile)}
className={character.active ? "bg-gray-100 dark:bg-gray-800" : ""}
data-testid={
character.active ? `${activeCharacterTestId}${index}` : ""
}
Expand All @@ -192,7 +212,29 @@ const CombatTracker: React.FC = () => {
{character.active && <CheckmarkIconPositive />}
</td>
<td className="border p-2">{character.name}</td>
<td className="border p-2">{character.initiative}</td>
<td className="border p-2">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded-full mb-4 mr-4"
onClick={() => handleCharacterDelete(index)}
>
{strings.deleteString}
</button>
</td>
<td className="border p-2">
<input
type="number"
className="w-full p-1 border rounded bg-white dark:bg-gray-800 text-black dark:text-white"
value={character.initiativeDisplay}
onChange={(e) =>
handleInitiativeInputChange(e.target.value, index)
}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleInitiativeChange(index);
}
}}
/>
</td>
</tr>
))}
</tbody>
Expand Down
10 changes: 8 additions & 2 deletions src/app/components/InitiativeInputDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import strings from "@/strings";

export type InitiativeInputDialogProps = {
character: Character;
onConfirm: (newCharacter: Character) => void;
onConfirm: (
newCharacter: Character,
SetCharacterFile: (file: File) => void,
) => void;
onCancel: () => void;
duplicateEntryOrEmpty: boolean;
SetCharacterFile: (file: File) => void;
};
export const DEFAULT_INITIATIVE = 0;

Expand All @@ -16,6 +20,7 @@ const InitiativeInputDialog: React.FC<InitiativeInputDialogProps> = ({
onConfirm,
onCancel,
duplicateEntryOrEmpty,
SetCharacterFile,
}) => {
const [initiative, setInitiative] = useState<number>(DEFAULT_INITIATIVE);
const [name, setName] = useState<string>(character.name);
Expand All @@ -24,8 +29,9 @@ const InitiativeInputDialog: React.FC<InitiativeInputDialogProps> = ({
...character,
name: name,
initiative: initiative,
initiativeDisplay: initiative,
};
onConfirm(newCharacter);
onConfirm(newCharacter, SetCharacterFile);
};
const handleCancel = () => {
onCancel();
Expand Down
4 changes: 2 additions & 2 deletions src/app/lib/definitionMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,16 @@ export const mockSingleCharacterWarrior: Character = {
name: mockSingleCharacterWarriorDataParsed.name,
initiative: 0,
fileReference: mockSingleCharacterFile_warrior,
dynamicData: mockSingleCharacterWarriorDataParsed,
active: false,
initiativeDisplay: 0,
};

export const mockSingleCharacterMage: Character = {
name: mockSingleCharacterMageDataParsed.name,
initiative: 0,
fileReference: mockSingleCharacterFile_mage,
dynamicData: mockSingleCharacterMageDataParsed,
active: false,
initiativeDisplay: 0,
};

export const mockSingleCharacterWarriorFile = mockSingleCharacterFile_warrior;
Expand Down
3 changes: 1 addition & 2 deletions src/app/lib/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
export class Character {
name!: string;
initiative!: number;
initiativeDisplay!: number;
fileReference!: File;
active!: boolean;
// TODO: remove this field and read the dynamic data from the fileReference
dynamicData!: Record<string, string | number>; // Object to store dynamic data fields
}
15 changes: 14 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
"use client";
import Image from "next/image";
import CombatTracker from "./components/CombatTracker";
import {
CharacterDisplay,
CharacterDisplayProps,
} from "./components/CharacterDisplay";
import { useState } from "react";
// import {

export default function Home() {
// TODO: add round tracker and eslapsed real time
const [characterFile, setCharacterFile] = useState<File | null>(null);
return (
<main className="flex min-h-screen flex-col items-left justify-between p-24">
{/* <div
Expand Down Expand Up @@ -32,7 +39,13 @@ export default function Home() {
/>
</a>
</div> */}
<CombatTracker />

<div className="mai flex justify-start items-center gap-x-4">
<CombatTracker SetCharacterFile={setCharacterFile} />
<div className="flex-grow">
<CharacterDisplay sourceFile={characterFile} />
</div>
</div>
{/* </div> */}

{/* <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
Expand Down
2 changes: 2 additions & 0 deletions src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const strings = {
// Common strings
cancelString: "Cancel",
deleteString: "Delete",

// CombatTracker strings
addToCombatButton: "Add Character to Combat",
Expand All @@ -15,6 +16,7 @@ const strings = {
fileParsingError: "Unable to parse JSON file",
moveUpLabel: "Move Up",
moveDownLabel: "Move Down",
actionsLabel: "Actions",

// InitiativeInputDialog strings
initiativePrompt: "Enter Initiative",
Expand Down

0 comments on commit a8d9adc

Please sign in to comment.