bug fixes and css variables

This commit is contained in:
NilsUeter
2023-04-10 22:12:19 +02:00
parent 4007862149
commit 76152e603d
4 changed files with 289 additions and 65 deletions
+6 -5
View File
@@ -4,6 +4,7 @@ import { useState } from "react";
import { Roster } from "./Roster"; import { Roster } from "./Roster";
function App() { function App() {
const [error, setError] = useState();
const [roster, setRoster] = useState(); const [roster, setRoster] = useState();
function handleFileSelect(event) { function handleFileSelect(event) {
const files = event?.target?.files; const files = event?.target?.files;
@@ -12,7 +13,7 @@ function App() {
const reader = new FileReader(); const reader = new FileReader();
reader.onerror = () => { reader.onerror = () => {
reader.abort(); reader.abort();
console.log("Failed to read roster file."); setError("Failed to read roster file.");
}; };
reader.onloadend = async () => { reader.onloadend = async () => {
const content = reader.result; const content = reader.result;
@@ -60,7 +61,7 @@ function App() {
renderer.render(rosterTitle, rosterList, forceUnits); */ renderer.render(rosterTitle, rosterList, forceUnits); */
} }
} else { } else {
console.log("No support for game type '" + gameType + "'."); setError("No support for game type '" + gameType + "'.");
} }
} }
@@ -70,9 +71,9 @@ function App() {
<input type="file" accept=".ros, .rosz" onChange={handleFileSelect} /> <input type="file" accept=".ros, .rosz" onChange={handleFileSelect} />
Select roster file (.ros, .rosz) Select roster file (.ros, .rosz)
</label> </label>
<div style={{ display: "flex", flexDirection: "column", gap: 32 }}> <div style={{ color: "red" }}>{error}</div>
<Roster roster={roster} />
</div> <Roster roster={roster} />
</div> </div>
); );
} }
+264 -56
View File
@@ -8,12 +8,18 @@ export const Roster = ({ roster }) => {
if (!roster) { if (!roster) {
return null; return null;
} }
const { _name, _forces } = roster; const { _name, _cost, _forces } = roster;
return ( return (
<> <div
style={{
display: "flex",
flexDirection: "column",
gap: 32,
}}
>
<div <div
style={{ style={{
backgroundColor: "#536766", backgroundColor: "var(--primary-color)",
color: "#fff", color: "#fff",
padding: "4px 16px", padding: "4px 16px",
fontSize: " 1.7em", fontSize: " 1.7em",
@@ -21,43 +27,115 @@ export const Roster = ({ roster }) => {
textTransform: "uppercase", textTransform: "uppercase",
}} }}
> >
{_name} {_name} [{_cost._commandPoints} CP]
</div> </div>
{_forces.map((force, index) => ( {_forces.map((force, index) => (
<Force key={index} force={force} /> <Force key={index} force={force} />
))} ))}
</> </div>
); );
}; };
const getPrimaryColor = (catalog) => {
switch (catalog) {
case "Adeptus Astartes":
return "#536766";
case "Adeptus Custodes":
return "#536766";
case "Adeptus Mechanicus":
return "#536766";
case "Astra Militarum":
return "#536766";
case "Chaos Daemons":
return "#536766";
case "Chaos Space Marines":
return "#536766";
case "Death Guard":
return "#536766";
case "Drukhari":
return "#536766";
case "Genestealer Cults":
return "#536766";
case "Grey Knights":
return "#536766";
case "Harlequins":
return "#536766";
case "Imperial Knights":
return "#536766";
case "Necrons":
return "#536766";
case "Orks":
return "#536766";
case "Sisters of Battle":
return "#536766";
case "Space Marines":
return "#536766";
case "Tau Empire":
return "#536766";
case "Thousand Sons":
return "#536766";
case "Tyranids":
return "#44264C";
case "Ynnari":
return "#536766";
default:
return "#536766";
}
};
const Force = ({ force }) => { const Force = ({ force }) => {
const { _units, rules } = force; const { _units, rules, _catalog } = force;
const primaryColor = getPrimaryColor(_catalog);
return ( return (
<> <div
style={{
display: "contents",
"--primary-color": primaryColor,
"--primary-color-transparent": "#53676660",
}}
>
{_units.map((unit, index) => ( {_units.map((unit, index) => (
<Unit key={index} unit={unit} /> <Unit key={index} unit={unit} />
))} ))}
<ForceRules rules={rules} /> <ForceRules rules={rules} />
</> </div>
); );
}; };
const Unit = ({ unit }) => { const Unit = ({ unit }) => {
const { _name, weapons, abilities, keywords, factions, rules, modelStats } = let {
unit; _name,
weapons,
abilities,
keywords,
factions,
rules,
modelStats,
_modelList,
_psykers,
} = unit;
const hasDifferentProfiles = weapons.some(
(weapon) =>
weapon._selectionName !== weapon._name &&
!weapon._name.includes("(Shooting)") &&
!weapon._name.includes("(Melee)")
);
const meleeWeapons = weapons const meleeWeapons = weapons
.filter((weapon) => weapon._range === "Melee") .filter((weapon) => weapon._range === "Melee" && weapon._range !== "-")
.sort((a, b) => a._selectionName.localeCompare(b._selectionName)); .sort((a, b) => a._selectionName.localeCompare(b._selectionName));
const rangedWeapons = weapons const rangedWeapons = weapons
.filter((weapon) => weapon._range !== "Melee") .filter((weapon) => weapon._range !== "Melee" && weapon._range !== "-")
.sort((a, b) => a._selectionName.localeCompare(b._selectionName)); .sort((a, b) => a._selectionName.localeCompare(b._selectionName));
modelStats = modelStats.sort((a, b) => b._name.length - a._name.length); // Sort by length of name, so that for example "Primaris Intercessor Sergeant" is before "Primaris Intercessor"
return ( return (
<div <div
className="avoid-page-break"
style={{ style={{
fontSize: 13, fontSize: 13,
fontWeight: 500, fontWeight: 500,
border: "2px solid #536766", border: "2px solid var(--primary-color)",
backgroundColor: "#DFE0E2", backgroundColor: "#DFE0E2",
}} }}
> >
@@ -65,40 +143,56 @@ const Unit = ({ unit }) => {
style={{ style={{
padding: "24px 16px", padding: "24px 16px",
paddingRight: 0, paddingRight: 0,
paddingBottom: 24 + 50 * (modelStats.length - 1), paddingBottom: 24,
backgroundColor: "#2F2F31", background:
"linear-gradient(90deg, rgba(20,21,25,1) 0%, rgba(48,57,62,1) 45%, rgba(73,74,79,1) 100%)",
color: "#fff", color: "#fff",
}} }}
> >
<div <div
style={{ style={{
padding: "4px 16px", padding: "4px 16px",
backgroundColor: "#53676680",
color: "#fff", color: "#fff",
minHeight: 85, minHeight: 85,
position: "relative",
}} }}
> >
<div
style={{
position: "absolute",
left: 0,
top: 0,
backgroundColor: "var(--primary-color-transparent)",
minHeight: 85,
width: "100%",
}}
></div>
<div <div
style={{ style={{
fontSize: "1.7em", fontSize: "1.7em",
fontWeight: 800, fontWeight: 800,
textTransform: "uppercase", textTransform: "uppercase",
zIndex: 1,
position: "relative",
}} }}
> >
{_name} {_name}
</div> </div>
<div <div
style={{ style={{
position: "absolute", width: "calc(100% - 32px)",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 6, gap: 6,
zIndex: 1,
position: "relative",
}} }}
> >
{modelStats.map((model, index) => ( {modelStats.map((model, index) => (
<ModelStats <ModelStats
key={index} key={index}
modelStats={model} modelStats={model}
modelList={_modelList}
index={index} index={index}
showName={modelStats.length > 1} showName={modelStats.length > 1}
/> />
@@ -110,9 +204,11 @@ const Unit = ({ unit }) => {
<div <div
style={{ style={{
flex: "1", flex: "1",
borderRight: "2px solid #536766", borderRight: "2px solid var(--primary-color)",
position: "relative", position: "relative",
paddingBottom: 40, paddingBottom: 40,
display: "flex",
flexDirection: "column",
}} }}
> >
<table <table
@@ -129,9 +225,10 @@ const Unit = ({ unit }) => {
weapons={meleeWeapons} weapons={meleeWeapons}
modelStats={modelStats} modelStats={modelStats}
/> />
<Psykers title="PSYKER" psykers={_psykers} />
</table> </table>
<div style={{ flex: "1" }}></div> <div style={{ flex: "1" }}></div>
{ {hasDifferentProfiles && (
<table className="weapons-table" style={{ width: "100%" }}> <table className="weapons-table" style={{ width: "100%" }}>
<tr className="emptyRow"> <tr className="emptyRow">
<td style={{ width: 37 }}>{Arrow}</td> <td style={{ width: 37 }}>{Arrow}</td>
@@ -148,15 +245,15 @@ const Unit = ({ unit }) => {
</td> </td>
</tr> </tr>
</table> </table>
} )}
<Keywords keywords={keywords} /> <Keywords keywords={keywords} />
</div> </div>
<div <div
style={{ style={{
padding: 20, padding: 20,
paddingBottom: 80, paddingBottom: 50,
flex: "1", flex: "1",
maxWidth: 375, maxWidth: 400,
position: "relative", position: "relative",
}} }}
> >
@@ -164,7 +261,7 @@ const Unit = ({ unit }) => {
style={{ style={{
fontSize: "1.1em", fontSize: "1.1em",
padding: "1px 8px", padding: "1px 8px",
backgroundColor: "#536766", backgroundColor: "var(--primary-color)",
color: "#fff", color: "#fff",
fontWeight: 600, fontWeight: 600,
minHeight: 27, minHeight: 27,
@@ -184,11 +281,14 @@ const Unit = ({ unit }) => {
); );
}; };
const ModelStats = ({ modelStats, index, showName }) => { const ModelStats = ({ modelStats, index, showName, modelList }) => {
let { move, toughness, save, wounds, leadership, _name } = modelStats; let { move, toughness, save, wounds, leadership, _name } = modelStats;
if (!wounds) { if (!wounds) {
wounds = "/"; wounds = "/";
} }
const modelListMatches = modelList
.filter((model) => model.includes(_name))
.map((model) => "(" + model.split("(")[1]);
return ( return (
<div style={{ display: "flex", gap: 16, alignItems: "center" }}> <div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<Characteristic title="M" characteristic={move} index={index} /> <Characteristic title="M" characteristic={move} index={index} />
@@ -197,9 +297,28 @@ const ModelStats = ({ modelStats, index, showName }) => {
<Characteristic title="W" characteristic={wounds} index={index} /> <Characteristic title="W" characteristic={wounds} index={index} />
<Characteristic title="LD" characteristic={leadership} index={index} /> <Characteristic title="LD" characteristic={leadership} index={index} />
{showName && ( {showName && (
<div style={{ marginTop: index === 0 ? 16 : 0, marginLeft: -10 }}> <>
{_name} <div
</div> style={{
marginTop: index === 0 ? 16 : 0,
marginLeft: -10,
whiteSpace: "nowrap",
}}
>
{_name}
</div>
<div
style={{
marginTop: index === 0 ? 16 : 0,
marginLeft: -10,
fontSize: "0.7em",
}}
>
{modelListMatches.map((model, index) => (
<div>{model}</div>
))}
</div>
</>
)} )}
</div> </div>
); );
@@ -215,7 +334,7 @@ const FactionIcon = () => {
width: 54, width: 54,
height: 54, height: 54,
transform: "rotate(-45deg)", transform: "rotate(-45deg)",
border: "2px solid #536766", border: "2px solid var(--primary-color)",
overflow: "hidden", overflow: "hidden",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@@ -236,6 +355,9 @@ const FactionIcon = () => {
}; };
const Keywords = ({ keywords }) => { const Keywords = ({ keywords }) => {
keywords = Array.from(keywords).filter(
(keyword) => keyword !== "Configuration"
);
return ( return (
<div <div
style={{ style={{
@@ -247,12 +369,11 @@ const Keywords = ({ keywords }) => {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
backgroundColor: "#BCBCBE", backgroundColor: "#BCBCBE",
border: "2px solid #536766", border: "2px solid var(--primary-color)",
width: "calc(100% - 18px)", width: "calc(100% - 18px)",
backgroundSize: "contain", backgroundSize: "contain",
minHeight: 54, minHeight: 54,
gap: 3, gap: 3,
color: "#101010",
}} }}
> >
<span style={{ fontSize: "1.1em" }}>KEYWORDS:</span> <span style={{ fontSize: "1.1em" }}>KEYWORDS:</span>
@@ -277,7 +398,7 @@ const Factions = ({ factions }) => {
flexDirection: "column", flexDirection: "column",
background: `url(${factionBackground})`, background: `url(${factionBackground})`,
color: "#fff", color: "#fff",
border: "2px solid #536766", border: "2px solid var(--primary-color)",
borderLeft: "none", borderLeft: "none",
width: "calc(100% - 18px)", width: "calc(100% - 18px)",
backgroundSize: "contain", backgroundSize: "contain",
@@ -295,14 +416,14 @@ const Factions = ({ factions }) => {
}; };
const Rules = ({ rules }) => { const Rules = ({ rules }) => {
if (!rules || rules.size === 0) return null;
return ( return (
<div <div
style={{ style={{
padding: "5px 2px", padding: "5px 2px",
display: "flex",
alignItems: "center",
gap: 3, gap: 3,
borderBottom: "1px dotted #9e9fa1", borderBottom: "1px dotted #9e9fa1",
lineHeight: 1.3,
}} }}
> >
<span style={{ fontSize: ".8em" }}>RULES: </span> <span style={{ fontSize: ".8em" }}>RULES: </span>
@@ -314,29 +435,30 @@ const Rules = ({ rules }) => {
}; };
const Abilities = ({ abilities }) => { const Abilities = ({ abilities }) => {
if (!abilities) return null;
return ( return (
<div <div
style={{ style={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 8, gap: 8,
color: "#213547",
padding: "4px 2px", padding: "4px 2px",
borderBottom: "1px dotted rgb(158, 159, 161)", borderBottom: "1px dotted rgb(158, 159, 161)",
}} }}
> >
{[...abilities.keys()].map((ability) => ( {abilities &&
<div [...abilities.keys()].map((ability) => (
key={ability} <div
style={{ key={ability}
fontSize: ".8em", style={{
lineHeight: 1.4, fontSize: ".8em",
}} lineHeight: 1.4,
> }}
<span style={{ fontWeight: 700 }}>{ability}:</span>{" "} >
{abilities.get(ability)} <span style={{ fontWeight: 700 }}>{ability}:</span>{" "}
</div> {abilities.get(ability)}
))} </div>
))}
</div> </div>
); );
}; };
@@ -358,9 +480,10 @@ const FancyBox = ({ children }) => {
return ( return (
<div <div
style={{ style={{
color: "#536766", color: "var(--primary-color)",
padding: 2, padding: 2,
background: "linear-gradient(-45deg, transparent 4px, #536766 0)", background:
"linear-gradient(-45deg, transparent 4px, var(--primary-color) 0)",
}} }}
> >
<div <div
@@ -387,7 +510,7 @@ const Weapons = ({ title, weapons, modelStats }) => {
<thead> <thead>
<tr <tr
style={{ style={{
backgroundColor: "#536766", backgroundColor: "var(--primary-color)",
color: "#fff", color: "#fff",
}} }}
> >
@@ -396,7 +519,7 @@ const Weapons = ({ title, weapons, modelStats }) => {
<img src={isMelee ? meleeIcon : rangedIcon} /> <img src={isMelee ? meleeIcon : rangedIcon} />
</div> </div>
</th> </th>
<th style={{ textAlign: "left" }}>{title}</th> <th style={{ textAlign: "left", width: "44%" }}>{title}</th>
<th>RANGE</th> <th>RANGE</th>
<th>A</th> <th>A</th>
<th>{isMelee ? "WS" : "BS"}</th> <th>{isMelee ? "WS" : "BS"}</th>
@@ -428,17 +551,31 @@ const Weapons = ({ title, weapons, modelStats }) => {
const Weapon = ({ weapon, modelStats, isMelee, index }) => { const Weapon = ({ weapon, modelStats, isMelee, index }) => {
let { _name, _selectionName, _range, _type, str, _ap, _damage } = weapon; let { _name, _selectionName, _range, _type, str, _ap, _damage } = weapon;
const [type, attacks] = _type.split(" "); var lastWhiteSpace = _type.lastIndexOf(" ");
const type = _type.substring(0, lastWhiteSpace);
const attacks = _type.substring(lastWhiteSpace + 1);
const bs = modelStats[0]._bs; const bs = modelStats[0]._bs;
const ws = modelStats[0]._ws; const ws = modelStats[0]._ws;
const strModel = modelStats[0].str; const strModel = modelStats[0].str;
const meleeAttacks = modelStats[0]._attacks; const meleeAttacks = modelStats[0]._attacks;
const differentProfiles = _selectionName !== _name; if (_name === "Krak grenades") {
const interestingType = type !== "Melee"; _name = "Krak grenade";
}
const differentProfiles =
_selectionName !== _name &&
!_name.includes("(Shooting)") &&
!_name.includes("(Melee)");
const interestingType = type && type !== "Melee";
if (differentProfiles && _name.endsWith(" grenades")) { if (differentProfiles && _name.endsWith(" grenades")) {
_name = _name.replace(" grenades", ""); _name = _name.replace(" grenades", "");
} }
if (differentProfiles && _name.endsWith(" grenade")) {
_name = _name.replace(" grenade", "");
}
if (differentProfiles && _name.includes(" - ")) {
_name = _name.split(" - ")[1];
}
return ( return (
<tr <tr
className={ className={
@@ -463,7 +600,11 @@ const Weapon = ({ weapon, modelStats, isMelee, index }) => {
{_name} {_name}
{interestingType && ( {interestingType && (
<span <span
style={{ fontSize: ".8em", fontWeight: 700, color: "#536766" }} style={{
fontSize: ".8em",
fontWeight: 700,
color: "var(--primary-color)",
}}
> >
[{type}] [{type}]
</span> </span>
@@ -481,22 +622,89 @@ const Weapon = ({ weapon, modelStats, isMelee, index }) => {
}; };
const calculateWeaponStrength = (strModel, strWeapon) => { const calculateWeaponStrength = (strModel, strWeapon) => {
if (strWeapon.startsWith("User")) return strModel;
if (strWeapon.startsWith("x")) if (strWeapon.startsWith("x"))
return strModel * parseInt(strWeapon.replace("x", "")); return strModel * parseInt(strWeapon.replace("x", ""));
return strModel + parseInt(strWeapon, 10); return strModel + parseInt(strWeapon, 10);
}; };
const Psykers = ({ title, psykers }) => {
return (
<>
{psykers.length > 0 && (
<thead>
<tr
style={{
backgroundColor: "var(--primary-color)",
color: "#fff",
}}
>
<th style={{ width: 37 }}>
<div style={{ display: "flex" }}>
<img src={rangedIcon} />
</div>
</th>
<th style={{ textAlign: "left" }}>{title}</th>
<th>Cast</th>
<th>Deny</th>
<th style={{ textAlign: "left" }} colSpan="4">
Powers Known
</th>
</tr>
</thead>
)}
<tbody>
{psykers.map((psyker, index) => (
<Psyker key={psyker._name} psyker={psyker} index={index} />
))}
{psykers.length > 0 && (
<tr className="emptyRow">
<td colSpan={8}></td>
</tr>
)}
</tbody>
</>
);
};
const Psyker = ({ psyker, index }) => {
let { _name, _cast, _deny, _powers } = psyker;
return (
<tr className={index % 2 ? "rowOtherColor" : ""}>
<td style={{ borderBottom: "none", backgroundColor: "#dfe0e2" }}></td>
<td style={{ textAlign: "left" }}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "wrap",
gap: "0 4px",
}}
>
{_name}
</div>
</td>
<td>{_cast}</td>
<td>{_deny}</td>
<td style={{ textAlign: "left" }} colSpan="4">
{_powers}
</td>
</tr>
);
};
const ForceRules = ({ rules }) => { const ForceRules = ({ rules }) => {
return ( return (
<div <div
className="avoid-page-break"
style={{ style={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 8, gap: 8,
color: "#213547",
padding: "20px 20px", padding: "20px 20px",
backgroundColor: "#dfe0e2", backgroundColor: "#dfe0e2",
border: "2px solid #536766", border: "2px solid var(--primary-color)",
}} }}
> >
{[...rules.keys()] {[...rules.keys()]
+19 -1
View File
@@ -17,6 +17,8 @@
* { * {
box-sizing: border-box; box-sizing: border-box;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
} }
a { a {
@@ -74,9 +76,10 @@ button:focus-visible {
} }
#root { #root {
max-width: 1000px; max-width: 1100px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
color: #111113;
} }
.App { .App {
@@ -103,6 +106,7 @@ button:focus-visible {
padding: 1px 8px; padding: 1px 8px;
border-bottom: 1px dotted #9e9fa1; border-bottom: 1px dotted #9e9fa1;
text-align: center; text-align: center;
vertical-align: baseline;
} }
.emptyRow { .emptyRow {
@@ -120,3 +124,17 @@ button:focus-visible {
.differentProfiles + .differentProfiles { .differentProfiles + .differentProfiles {
border-top: 1px solid #dfe0e2; border-top: 1px solid #dfe0e2;
} }
@media print {
.print-display-none {
display: none;
}
.avoid-page-break {
break-inside: avoid;
}
#root {
min-width: 1000px;
}
}
-3
View File
@@ -1,10 +1,8 @@
export class BaseNotes { export class BaseNotes {
_name = ""; _name = "";
_customName = "";
_customNotes = ""; _customNotes = "";
name() { name() {
if (this._customName) return this._customName;
return this._name; return this._name;
} }
@@ -604,7 +602,6 @@ function ExpandBaseNotes(root, obj) {
element = root.parentElement.parentElement; element = root.parentElement.parentElement;
} }
obj._customName = element.getAttributeNode("customName")?.nodeValue;
let child = element.firstElementChild; let child = element.firstElementChild;
if (child && child.tagName === "customNotes") { if (child && child.tagName === "customNotes") {
obj._customNotes = child.textContent; obj._customNotes = child.textContent;