[Client] Improve reaction images management on NPC admin page

- Fix HTML and improve style of the feature;
- Improve admin NPC's JavaScript and fix addition of a reaction image where
there is no one already present;
- Allow upload of images with image/jpg MIME type;
- Fix attribute name of a hidden input;
- Apply the hover effects of links and buttons on their focus, for all admin
pages.
This commit is contained in:
AudricV 2023-04-02 15:23:32 +02:00
parent 2b20185ff5
commit 0707dce218
No known key found for this signature in database
GPG Key ID: DA92EC7905614198
4 changed files with 184 additions and 71 deletions

View File

@ -9,6 +9,7 @@
} }
:root { :root {
--admin-black-color: #000000;
--admin-grey-color: #5A5656; --admin-grey-color: #5A5656;
--admin-red-color: #FF0000; --admin-red-color: #FF0000;
--admin-white-color: #FFFFFF; --admin-white-color: #FFFFFF;
@ -41,7 +42,7 @@ button {
justify-content: center; justify-content: center;
} }
button, input { button, input, select {
background-color: transparent; background-color: transparent;
border-color: var(--admin-white-color); border-color: var(--admin-white-color);
border-style: solid; border-style: solid;
@ -71,12 +72,12 @@ header a {
text-decoration: none; text-decoration: none;
} }
a:hover { a:focus, a:hover {
background-color: var(--admin-white-color); background-color: var(--admin-white-color);
color: var(--admin-grey-color); color: var(--admin-grey-color);
} }
a:hover > .action_icon, .action_button:hover > .action_icon { a:focus > .action_icon, .action_button:focus > .action_icon, a:hover > .action_icon, .action_button:hover > .action_icon {
fill: var(--admin-grey-color); fill: var(--admin-grey-color);
} }
@ -89,7 +90,7 @@ svg {
fill: var(--admin-white-color); fill: var(--admin-white-color);
} }
.action_button:hover { .action_button:focus, .action_button:hover {
background-color: var(--admin-white-color); background-color: var(--admin-white-color);
color: var(--admin-grey-color); color: var(--admin-grey-color);
} }

View File

@ -1,16 +1,36 @@
img[alt] {
font-size: 1em;
}
img[alt], .info_item, .section_title {
text-align: center;
}
input[type="text"] { input[type="text"] {
width: 20em; width: 20em;
} }
.action_buttons, .answer_groups { option {
align-content: center; background-color: var(--admin-white-color);
display: flex; color: var(--admin-black-color);
}
.action_buttons, .answer_groups, .reaction, #add_reactions {
flex-wrap: wrap; flex-wrap: wrap;
}
.action_buttons, .answer_group, .answer_groups, .reaction, #add_reactions {
display: flex;
justify-content: center; justify-content: center;
} }
.character_image { .answer_group, .reaction, #add_reactions {
width: 15em; align-items: center;
flex-direction: column;
}
.character_image, .reaction_image {
width: 20em;
} }
.character_image, #npc_name { .character_image, #npc_name {
@ -26,8 +46,8 @@ input[type="text"] {
font-size: 1.25em; font-size: 1.25em;
} }
.info_item, .section_title { .reaction, #add_reactions {
text-align: center; align-content: center;
} }
.section_title { .section_title {

View File

@ -1,5 +1,5 @@
const reactionsDelta = {} const reactionsDelta = {};
async function createOrUpdateNpc() { async function createOrUpdateNpc() {
const data = {}; const data = {};
@ -9,7 +9,7 @@ async function createOrUpdateNpc() {
const allAnswersJson = []; const allAnswersJson = [];
data["allAnswers"] = allAnswersJson; data["allAnswers"] = allAnswersJson;
for (let answerTypeNode of document.querySelector(".answer_groups").children) { for (const answerTypeNode of document.querySelector(".answer_groups").children) {
const answersJson = []; const answersJson = [];
const answerTypeJson = {"answers": answersJson}; const answerTypeJson = {"answers": answersJson};
allAnswersJson.push(answerTypeJson); allAnswersJson.push(answerTypeJson);
@ -20,35 +20,33 @@ async function createOrUpdateNpc() {
} }
await makeAPIRequest("admin/setNpc", {"npc": data, "lang": "FR"}, {"content": "json"}); await makeAPIRequest("admin/setNpc", {"npc": data, "lang": "FR"}, {"content": "json"});
await uploadReactionsDelta(); await uploadReactionsDelta();
alert("Opération effectuée avec succès"); alert("Opération effectuée avec succès");
} }
async function uploadReactionsDelta() { async function uploadReactionsDelta() {
let requests = []; const requests = [];
const npcId = document.querySelector("#npc_id").value;
for (const [traitId, reactionNode] of Object.entries(reactionsDelta)) {
for(const [traitId, reactionNode] of Object.entries(reactionsDelta)){
const formData = new FormData(); const formData = new FormData();
formData.append("npc_id", npc_id.value); formData.append("npc_id", npcId);
formData.append("trait_id", traitId); formData.append("trait_id", traitId);
if(reactionNode === null) formData.append("file", "null"); if (reactionNode === null) {
else{ formData.append("file", "null");
const file = reactionNode.querySelector(".img_input").files[0] } else {
const file = reactionNode.querySelector(".img_input").files[0];
formData.append("file", file ? file : ""); formData.append("file", file ? file : "");
} }
requests.push(makeAPIRequest("admin/setReaction", formData, {"content": "form"})); requests.push(makeAPIRequest("admin/setReaction", formData, {"content": "form"}));
} }
for(request of requests){ for (const request of requests) {
await request; await request;
} }
} }
async function deleteNpc() { async function deleteNpc() {
@ -62,51 +60,141 @@ async function deleteNpc() {
document.location = "/admin"; document.location = "/admin";
} }
function changeReaction(inputNode){ function changeImageReaction(imageInputElement) {
const parentNode = inputNode.parentNode; const parentNode = imageInputElement.parentNode;
const imgNode = parentNode.querySelector('img'); const imgNode = parentNode.querySelector('img');
const traitId = parentNode.querySelector('.trait_id').value; const traitId = parentNode.querySelector('.trait_id').value;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e)=>{ reader.addEventListener("load", event => {
imgNode.src = e.target.result imgNode.src = event.target.result
} });
reader.readAsDataURL(inputNode.files[0]); reader.readAsDataURL(imageInputElement.files[0]);
reactionsDelta[traitId] = parentNode; reactionsDelta[traitId] = parentNode;
} }
function deleteReaction(node){ function deleteImageReaction(reactionDeletionButton) {
const reactionNode = node.parentNode; if (!confirm("Voulez-vous vraiment supprimer l'image de cette réaction ?")) {
return;
}
const reactionNode = reactionDeletionButton.parentNode;
const traitId = reactionNode.querySelector(".trait_id").value; const traitId = reactionNode.querySelector(".trait_id").value;
const reactionName = reactionNode.querySelector("p").innerText; const reactionName = reactionNode.querySelector(".reaction_name").innerText;
reactionNode.parentNode.removeChild(reactionNode); reactionNode.parentNode.removeChild(reactionNode);
const option = document.createElement("option"); const option = document.createElement("option");
option.value = traitId option.value = traitId;
option.innerText = reactionName option.innerText = reactionName;
reactions_to_add.appendChild(option); const addReactionsSelectorElement = document.getElementById("add_reactions_selector");
if (addReactionsSelectorElement === null) {
// No add_reactions_selector element, this should never happen
// Do nothing in this case
return;
}
addReactionsSelectorElement.appendChild(option);
reactionsDelta[traitId] = null; reactionsDelta[traitId] = null;
} }
function addReaction(selectNode){ function addReaction(addReactionsSelectorElement) {
const selectedOptionNode = selectNode.selectedOptions[0]; const selectedOptionNode = addReactionsSelectorElement.selectedOptions[0];
const traitId = selectedOptionNode.value; const traitId = selectedOptionNode.value;
const reactionName = selectedOptionNode.innerText; const reactionName = selectedOptionNode.innerText;
selectNode.removeChild(selectedOptionNode); addReactionsSelectorElement.removeChild(selectedOptionNode);
const newReaction = reactions.querySelector("div").cloneNode(true); const newReactionElement = document.createElement("section");
newReaction.querySelector("img").src = ""; newReactionElement.classList.add("reaction");
newReaction.querySelector(".img_input").value = null;
newReaction.querySelector(".trait_id").value = traitId
newReaction.querySelector("p").innerText = reactionName
reactions.appendChild(newReaction); const reactionNameElement = document.createElement("h3");
reactionNameElement.classList.add("reaction_name");
reactionNameElement.textContent = reactionName;
reactionsDelta[traitId] = newReaction; newReactionElement.appendChild(reactionNameElement);
const imageElement = document.createElement("img");
imageElement.classList.add("reaction_image");
imageElement.setAttribute("alt", "Image d'une réaction d'un personnage");
imageElement.src = "/static/images/no_photography_white.svg";
newReactionElement.appendChild(imageElement);
const imageInputElement = document.createElement("input");
imageInputElement.classList.add("img_input");
imageInputElement.setAttribute("type", "file");
imageInputElement.setAttribute("accept", "image/png, image/jpg, image/jpeg");
imageInputElement.addEventListener("change", () => changeImageReaction(imageInputElement));
newReactionElement.appendChild(imageInputElement);
const traitIdInputElement = document.createElement("input");
traitIdInputElement.classList.add("trait_id");
traitIdInputElement.setAttribute("type", "hidden");
traitIdInputElement.setAttribute("value", traitId);
newReactionElement.appendChild(traitIdInputElement);
const buttonElement = document.createElement("button");
buttonElement.classList.add("delete_question_btn", "action_button", "short_color_transition");
buttonElement.setAttribute("title", "Cliquez ici pour supprimer l'image de cette réaction");
buttonElement.addEventListener("click", () => deleteImageReaction(buttonElement));
const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgElement.classList.add("action_icon", "short_color_transition");
svgElement.setAttribute("viewBox", "0 0 48 48");
const pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path");
pathElement.setAttribute("d",
"M12.45 38.7 9.3 35.55 20.85 24 9.3 12.5l3.15-3.2L24 20.8 35.55 9.3l3.15 3.2L27.2 24l11.5 11.55-3.15 3.15L24 27.2Z");
svgElement.appendChild(pathElement);
buttonElement.appendChild(svgElement);
buttonElement.appendChild(document.createTextNode("Supprimer l'image de la réaction"));
newReactionElement.appendChild(buttonElement);
const reactionsElement = document.getElementById("reactions");
if (reactionsElement === null) {
// No add_reactions_selector element, this should never happen
// Do nothing in this case
return;
}
reactionsElement.appendChild(newReactionElement);
reactionsDelta[traitId] = newReactionElement;
} }
function setListenersToImageInputs() {
for (const imageInput of document.getElementsByClassName("img_input")) {
imageInput.addEventListener("change", () => changeImageReaction(imageInput));
}
}
function setListenersToImageReactionsRemovalButtons() {
for (const imageReactionRemovalButton of document.getElementsByClassName("delete_image_reaction_btn")) {
imageReactionRemovalButton.addEventListener("click", () => deleteImageReaction(imageReactionRemovalButton));
}
}
function setListenersToAddReactionsSelector() {
const addReactionsSelectorElement = document.getElementById("add_reactions_selector");
if (addReactionsSelectorElement === null) {
// No add_reactions_selector element, this should never happen
// Do nothing in this case
return;
}
addReactionsSelectorElement.addEventListener("change", () => addReaction(addReactionsSelectorElement));
}
setListenersToImageReactionsRemovalButtons();
setListenersToImageInputs();
setListenersToAddReactionsSelector();

View File

@ -26,37 +26,41 @@
<h1 class="page_title">Truth Inquiry - Interface d'administration</h1> <h1 class="page_title">Truth Inquiry - Interface d'administration</h1>
<h2 class="page_category">{{'Gestion' if npc.get('id') else 'Ajout'}} d'un personnage</h2> <h2 class="page_category">{{'Gestion' if npc.get('id') else 'Ajout'}} d'un personnage</h2>
<p class="page_description">Cliquez sur les champs pour éditer les informations. Dans les réponses aux questions lors de l'interrogation, utilisez «&nbsp;{NPC}&nbsp;» pour faire référence au nom d'un personnage et «&nbsp;{SALLE}&nbsp;» pour faire référence au nom d'une salle.</p> <p class="page_description">Cliquez sur les champs pour éditer les informations. Dans les réponses aux questions lors de l'interrogation, utilisez «&nbsp;{NPC}&nbsp;» pour faire référence au nom d'un personnage et «&nbsp;{SALLE}&nbsp;» pour faire référence au nom d'une salle.</p>
<section> <section id="character_info">
<h2 class="section_title">Informations sur le personnage</h2> <h2 class="section_title">Informations sur le personnage</h2>
<input id="npc_id" value="{{ npc.get('id') or ''}}" hidden="hidden"> <input id="npc_id" value="{{ npc.get('id') or ''}}" type="hidden">
<p class="info_item">Nom du personnage</p> <p class="info_item">Nom du personnage</p>
<input type="text" id="npc_name" value="{{ npc.get('name') or ''}}" title="Saisissez le nom du personnage" aria-label="Nom du personnage"> <input type="text" id="npc_name" value="{{ npc.get('name') or ''}}" title="Saisissez le nom du personnage" aria-label="Nom du personnage">
<p class="info_item">Image du personnage</p> <p class="info_item">Image du personnage</p>
<img class="character_image" alt="{{'Image du personnage' + (' ' + npc.get('name') if npc.get('name') else '')}}" src="{{'/static/images/no_photography_white.svg' if npc.get('img') == None else '/api/v1/getNpcImage?npcid=' + npc.get('img')|string}}"> <img class="character_image" alt="{{'Image du personnage' + (' ' + npc.get('name') if npc.get('name') else '')}}" src="{{'/static/images/no_photography_white.svg' if npc.get('img') == None else '/api/v1/getNpcImage?npcid=' + npc.get('img')|string}}">
</section> </section>
<section id="reactions"> <section id="reactions">
<h2 class="section_title">Réactions</h2> <h2 class="section_title">Images des réactions</h2>
{%for reaction in npc.get("reactions") or []%} {%for reaction in npc.get("reactions") or []%}
<div> <section class="reaction">
<p> {{reaction.get('name')}} </p> <h3 class="reaction_name">{{reaction.get('name')}}</h3>
<img src="{{reaction.get('url')}}" style="max-width: 100; max-height: 100px"> <img class="reaction_image" alt="Image d'une réaction d'un personnage" src="{{reaction.get('url')}}">
<input class="img_input", type="file" accept="image/png, image/jpeg" onchange="changeReaction(this)"> <input class="img_input" type="file" accept="image/png, image/jpg, image/jpeg">
<input class="trait_id", type="hidden" value="{{reaction.get('trait_id')}}"> <input class="trait_id" type="hidden" value="{{reaction.get('trait_id')}}">
<button onclick="deleteReaction(this)">Delete reaction</button> <button class="delete_image_reaction_btn action_button short_color_transition" title="Cliquez ici pour supprimer l'image de cette réaction">
</div> <svg class="action_icon short_color_transition" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 48 48">
<path d="M12.45 38.7 9.3 35.55 20.85 24 9.3 12.5l3.15-3.2L24 20.8 35.55 9.3l3.15 3.2L27.2 24l11.5 11.55-3.15 3.15L24 27.2Z"/>
</svg>
Supprimer l'image de la réaction
</button>
</section>
{%endfor%} {%endfor%}
</section> </section>
<section id="add_reactions">
<select id="reactions_to_add" onchange="addReaction(this)"> <h2 class="section_title">Réactions à ajouter</h2>
<option value="" default></option> <select id="add_reactions_selector">
<option value="" selected="selected">Sélectionnez une réaction à ajouter</option>
{%for reaction_to_add in npc.get("reactions_to_add") or []%} {%for reaction_to_add in npc.get("reactions_to_add") or []%}
<option value="{{reaction_to_add.get('trait_id')}}">{{reaction_to_add.get('name')}}</option> <option value="{{reaction_to_add.get('trait_id')}}">{{reaction_to_add.get('name')}}</option>
{%endfor%} {%endfor%}
</select> </select>
</section>
<section> <section id="interrogation_answers">
<h2 class="section_title">Réponses aux questions lors de l'interrogation</h2> <h2 class="section_title">Réponses aux questions lors de l'interrogation</h2>
<div class="answer_groups"> <div class="answer_groups">
{%for answer_type in npc.get("answers") or []%} {%for answer_type in npc.get("answers") or []%}