* Complete add-player modal

* New player listing
* New player action layout
This commit is contained in:
MaxJa4
2024-01-15 21:26:46 +01:00
parent 04a2a2815d
commit 80d0489b41
16 changed files with 308 additions and 107 deletions

View File

@@ -4,9 +4,10 @@
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues"> <option name="myValues">
<value> <value>
<list size="2"> <list size="3">
<item index="0" class="java.lang.String" itemvalue="hx-get" /> <item index="0" class="java.lang.String" itemvalue="hx-get" />
<item index="1" class="java.lang.String" itemvalue="hx-target" /> <item index="1" class="java.lang.String" itemvalue="hx-target" />
<item index="2" class="java.lang.String" itemvalue="hx-vals" />
</list> </list>
</value> </value>
</option> </option>

View File

@@ -31,8 +31,8 @@ func GetAllClans(c *gin.Context) {
c.JSON(http.StatusOK, clans) c.JSON(http.StatusOK, clans)
} }
// GetAllClanOptions GET /clan_options // GetAllClansHTML GET /clans_html
func GetAllClanOptions(c *gin.Context) { func GetAllClansHTML(c *gin.Context) {
var clans []models.Clan var clans []models.Clan
models.DB.Find(&clans) models.DB.Find(&clans)

View File

@@ -2,10 +2,26 @@ package controllers
import ( import (
"InfrantrySkillCalculator/models" "InfrantrySkillCalculator/models"
"InfrantrySkillCalculator/utils"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"log"
"net/http" "net/http"
"os"
"strconv"
) )
type AddPlayerInput struct {
Name string `json:"name" binding:"required"`
ClanID uint `json:"clan_id" binding:"required"`
}
type UpdatePlayerInput struct {
Name string `json:"name"`
ClanID uint `json:"clan_id"`
}
// GetAllPlayers GET /player // GetAllPlayers GET /player
func GetAllPlayers(c *gin.Context) { func GetAllPlayers(c *gin.Context) {
var players []models.Player var players []models.Player
@@ -13,3 +29,154 @@ func GetAllPlayers(c *gin.Context) {
c.JSON(http.StatusOK, players) c.JSON(http.StatusOK, players)
} }
// GetPlayersByClanHTML GET /players_html
func GetPlayersByClanHTML(c *gin.Context) {
var players []models.Player
clanId := c.Request.URL.Query().Get("id")
if err := models.DB.Where("clan_id = ?", utils.StringToUint(clanId)).Find(&players).Error; err != nil {
return
}
file, err := os.ReadFile("./templates/player_list_item.html")
if err != nil {
return
}
playerItem := string(file)
//player_item = strings.Replace(player_item, "PLAYERNAME", player.Name, 1)
var htmlOptions string
for _, player := range players {
htmlOptions += fmt.Sprintf(playerItem, player.Name)
}
c.Header("Content-Type", "text/html")
c.String(http.StatusOK, htmlOptions)
}
// AddPlayer POST /player
func AddPlayer(c *gin.Context) {
var input AddPlayerInput
if err := c.BindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var player models.Player
if err := models.DB.Where("name = ?", input.Name).First(&player).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Player with this name already exists!"})
return
}
player = models.Player{Name: input.Name, ClanID: input.ClanID}
models.DB.Create(&player)
//UpdatePlayerTimestamp()
c.JSON(http.StatusOK, player)
_, err := fmt.Fprintf(utils.GinWriter, "Added player '"+player.Name+"'\n")
if err != nil {
log.Fatal(err)
}
}
// GetPlayerByID GET /player/:id
func GetPlayerByID(c *gin.Context) {
player := FindPlayerByID(utils.StringToUint(c.Param("id")))
if player == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
c.JSON(http.StatusOK, player)
}
// GetPlayerIDByName GET /playerid/:name
func GetPlayerIDByName(c *gin.Context) {
var player models.Player
if err := FindPlayerByName(&player, c).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
c.JSON(http.StatusOK, player.ID)
}
// UpdatePlayerByID PATCH /player/:id
func UpdatePlayerByID(c *gin.Context) {
player := FindPlayerByID(utils.StringToUint(c.Param("id")))
if player == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
var input UpdatePlayerInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if player.Name != input.Name {
if err := FindPlayerByName(&player, c).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Player with this name already exists!"})
return
}
}
msg := "Updating player '" + player.Name + "'#" + strconv.FormatUint(uint64(player.ID), 10)
if player.Name != input.Name {
msg += " (new: '" + input.Name + "')"
}
msg += " with clan #" + utils.UintToString(player.ClanID)
if player.ClanID != input.ClanID {
msg += " (new: ä" + utils.UintToString(input.ClanID) + ")"
}
_, err := fmt.Fprintf(utils.GinWriter, msg+"\n")
if err != nil {
log.Fatal(err)
}
models.DB.Model(&player).Updates(map[string]interface{}{
"Name": input.Name,
"ClanID": input.ClanID,
})
//UpdatePlayerTimestamp()
c.JSON(http.StatusOK, player)
}
// DeletePlayerByID DELETE /player/:id
func DeletePlayerByID(c *gin.Context) {
player := FindPlayerByID(utils.StringToUint(c.Param("id")))
if player == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
return
}
models.DB.Delete(&player)
//UpdatePlayerTimestamp()
c.JSON(http.StatusOK, true)
_, err := fmt.Fprintf(utils.GinWriter, "Deleted player '"+player.Name+"'\n")
if err != nil {
log.Fatal(err)
}
}
func FindPlayerByID(id uint) *models.Player {
var res models.Player
if err := models.DB.Where("id = ?", id).First(&res).Error; err != nil {
return nil
} else {
return &res
}
}
func FindPlayerByName(out interface{}, c *gin.Context) *gorm.DB {
return models.DB.Where("name = ?", c.Param("name")).First(out)
}

Binary file not shown.

10
main.go
View File

@@ -32,8 +32,6 @@ func main() {
"./templates/index.html", "./templates/index.html",
"./templates/components/home_clan_bar.html", "./templates/components/home_clan_bar.html",
"./templates/components/opp_clan_bar.html", "./templates/components/opp_clan_bar.html",
"./templates/components/home_player_bar.html",
"./templates/components/opp_player_bar.html",
"./templates/components/home_player_list.html", "./templates/components/home_player_list.html",
"./templates/components/opp_player_list.html", "./templates/components/opp_player_list.html",
"./templates/components/bottom_controls.html", "./templates/components/bottom_controls.html",
@@ -62,13 +60,19 @@ func main() {
}) })
router.GET("/clans", controllers.GetAllClans) router.GET("/clans", controllers.GetAllClans)
router.GET("/clan_options", controllers.GetAllClanOptions) router.GET("/clans_html", controllers.GetAllClansHTML)
router.GET("/clan/:id", controllers.GetClanByID) router.GET("/clan/:id", controllers.GetClanByID)
router.POST("/clan", controllers.AddClan) router.POST("/clan", controllers.AddClan)
router.PATCH("/clan/:id", controllers.UpdateClanByID) router.PATCH("/clan/:id", controllers.UpdateClanByID)
router.DELETE("/clan/:id", controllers.DeleteClanByID) router.DELETE("/clan/:id", controllers.DeleteClanByID)
router.GET("/players", controllers.GetAllPlayers) router.GET("/players", controllers.GetAllPlayers)
router.GET("/players_html", controllers.GetPlayersByClanHTML)
router.GET("/player/:id", controllers.GetPlayerByID)
router.GET("/playerid/:name", controllers.GetPlayerIDByName)
router.POST("/player", controllers.AddPlayer)
router.PATCH("/player/:id", controllers.UpdatePlayerByID)
router.DELETE("/player/:id", controllers.DeletePlayerByID)
log.Println("Running on 8000...") log.Println("Running on 8000...")
log.Fatal(router.Run(":8000")) log.Fatal(router.Run(":8000"))

View File

@@ -9,21 +9,19 @@ function setupClanButtons(dropdownId, delBtnId, editBtnId) {
}); });
} }
function setupPlayerButtons(dropdownId, listId, delBtnId, editBtnId, addBtnId) { function getSelectedClanId(dropdownId) {
const dropdown = document.getElementById(dropdownId);
const selectedClanId = dropdown.options[dropdown.selectedIndex].value;
return parseInt(selectedClanId);
}
function setupPlayerButtons(dropdownId, listId, addBtnId) {
const dropdown = document.getElementById(dropdownId); const dropdown = document.getElementById(dropdownId);
const deleteButton = document.getElementById(delBtnId);
const editButton = document.getElementById(editBtnId);
const addButton = document.getElementById(addBtnId); const addButton = document.getElementById(addBtnId);
dropdown.addEventListener('change', function () { dropdown.addEventListener('change', function () {
deleteButton.disabled = !this.value;
editButton.disabled = !this.value && (dropdown.selectedIndex !== -1);
addButton.disabled = !this.value && (dropdown.selectedIndex !== -1); addButton.disabled = !this.value && (dropdown.selectedIndex !== -1);
}); addButton.classList.toggle("bg-secondary-subtle");
dropdown.addEventListener('change', function () {
deleteButton.disabled = (this.selectedIndex !== -1);
editButton.disabled = (this.selectedIndex !== -1);
}); });
} }

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="input-group input-group-lg mb-3"> <div class="input-group input-group-lg mb-3">
<select class="form-select form-control border-secondary" id="home-clan" hx-get="/players" hx-target="#home-player-list"> <select class="form-select form-control border-secondary" id="home-clan" hx-get="/players_html" hx-target="#home-player-list" hx-vals='js:{"id": getSelectedClanId("home-clan")}'>
<option disabled selected value>Auswählen...</option> <option disabled selected value>Auswählen...</option>
<!-- Options will be loaded dynamically --> <!-- Options will be loaded dynamically -->
</select> </select>
@@ -26,7 +26,7 @@
<script lang="javascript"> <script lang="javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
setupClanButtons('home-clan', 'home-delete', 'home-edit'); setupClanButtons('home-clan', 'home-delete', 'home-edit');
htmx.ajax('GET', '/clan_options', {target: '#home-clan'}); htmx.ajax('GET', '/clans_html', {target: '#home-clan'});
}); });
</script> </script>

View File

@@ -1,34 +0,0 @@
{{ define "home_player_bar" }}
<div class="row mt-3 justify-content-between align-items-center">
<div class="col-auto">
<div class="btn-group btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-secondary-emphasis py-1 px-3"><i class="bi bi-check-square fs-4"></i></button>
<button type="button" class="btn btn-outline-secondary text-secondary-emphasis py-1 px-3"><i class="bi bi-square fs-4"></i></button>
</div>
</div>
<div class="col px-0">
<span class="badge fs-5 w-100 text-secondary-emphasis">0 von 0 ausgewählt</span>
</div>
<div class="col-auto">
<div class="btn-group btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-danger py-1 px-3" id="home-player-delete" disabled>
<i class="bi bi-person-dash fs-4"></i>
</button>
<button type="button" class="btn btn-outline-secondary text-primary py-1 px-3" id="home-player-edit" disabled>
<i class="bi bi-person-gear fs-4"></i>
</button>
<button type="button" class="btn btn-outline-secondary text-success py-1 px-3" id="home-player-add" data-bs-toggle="modal" data-bs-list="#home-player-list" data-bs-target="#addPlayerModal" disabled>
<i class="bi bi-person-add fs-4"></i>
</button>
</div>
</div>
</div>
<script lang="javascript">
document.addEventListener('DOMContentLoaded', function() {
setupPlayerButtons('home-clan', 'home-player-list', 'home-player-delete', 'home-player-edit', 'home-player-add');
});
</script>
{{ end }}

View File

@@ -1,8 +1,32 @@
{{ define "home_player_list" }} {{ define "home_player_list" }}
<label for="home-player-list" class="col-form-label col-form-label-lg pt-0">Spieler:</label> <div class="row">
<select multiple class="form-control form-control-lg overflow-auto border-secondary" id="home-player-list" size="10"> <div class="col">
<!-- Player list items go here --> <div id="home-player-list" class="border rounded p-1 overflow-auto" style="height: 47vh;">
</select>
</div>
</div>
<div class="col-auto ps-0">
<div class="btn-group-vertical btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-primary-emphasis"><i class="bi bi-check-square fs-4"></i></button>
<button type="button" class="btn btn-outline-secondary text-primary-emphasis"><i class="bi bi-square fs-4"></i></button>
</div>
<br>
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="home-player-add" data-bs-toggle="modal" data-bs-list="#home-player-list" data-bs-target="#addPlayerModal" disabled>
<i class="bi bi-person-add fs-4"></i>
</button>
<br>
<div class="vstack text-center border border-secondary rounded mt-2">
<i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i>
<span class="badge fs-5 mb-2">0</span>
</div>
</div>
</div>
<script lang="javascript">
document.addEventListener('DOMContentLoaded', function() {
setupPlayerButtons('home-clan', 'home-player-list', 'home-player-add');
});
</script>
{{ end }} {{ end }}

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="input-group input-group-lg mb-3"> <div class="input-group input-group-lg mb-3">
<select class="form-select form-control border-secondary" id="opponent-clan" hx-get="/players" hx-target="#opponent-player-list"> <select class="form-select form-control border-secondary" id="opponent-clan" hx-get="/players_html" hx-target="#opponent-player-list" hx-vals='js:{"id": getSelectedClanId("opponent-clan")}'>
<option disabled selected value>Auswählen...</option> <option disabled selected value>Auswählen...</option>
</select> </select>
<button class="btn btn-lg btn-outline-secondary text-danger" type="button" id="opponent-delete" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#deleteClanModal" disabled> <button class="btn btn-lg btn-outline-secondary text-danger" type="button" id="opponent-delete" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#deleteClanModal" disabled>
@@ -25,7 +25,7 @@
<script lang="javascript"> <script lang="javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
setupClanButtons('opponent-clan', 'opponent-delete', 'opponent-edit'); setupClanButtons('opponent-clan', 'opponent-delete', 'opponent-edit');
htmx.ajax('GET', '/clan_options', {target: '#opponent-clan'}); htmx.ajax('GET', '/clans_html', {target: '#opponent-clan'});
}); });
</script> </script>

View File

@@ -1,34 +0,0 @@
{{ define "opp_player_bar" }}
<div class="row mt-3 justify-content-between align-items-center">
<div class="col-auto">
<div class="btn-group btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-secondary-emphasis py-1 px-3"><i class="bi bi-check-square fs-4"></i></button>
<button type="button" class="btn btn-outline-secondary text-secondary-emphasis py-1 px-3"><i class="bi bi-square fs-4"></i></button>
</div>
</div>
<div class="col px-0">
<span class="badge fs-5 w-100 text-secondary-emphasis">0 von 0 ausgewählt</span>
</div>
<div class="col-auto">
<div class="btn-group btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-danger py-1 px-3" id="opponent-player-delete" disabled>
<i class="bi bi-person-dash fs-4"></i>
</button>
<button type="button" class="btn btn-outline-secondary text-primary py-1 px-3" id="opponent-player-edit" disabled>
<i class="bi bi-person-gear fs-4"></i>
</button>
<button type="button" class="btn btn-outline-secondary text-success py-1 px-3" id="opponent-player-add" data-bs-toggle="modal" data-bs-list="#opponent-player-list" data-bs-target="#addPlayerModal" disabled>
<i class="bi bi-person-add fs-4"></i>
</button>
</div>
</div>
</div>
<script lang="javascript">
document.addEventListener('DOMContentLoaded', function() {
setupPlayerButtons('opponent-clan', 'opponent-player-list', 'opponent-player-delete', 'opponent-player-edit', 'opponent-player-add');
});
</script>
{{ end }}

View File

@@ -1,8 +1,32 @@
{{ define "opp_player_list" }} {{ define "opp_player_list" }}
<label for="opponent-player-list" class="col-form-label col-form-label-lg pt-0">Spieler:</label> <div class="row">
<select multiple class="form-control form-control-lg overflow-auto border-secondary" id="opponent-player-list" size="10"> <div class="col">
<!-- Player list items go here --> <div id="opponent-player-list" class="border rounded p-1 overflow-auto" style="height: 47vh;">
</select>
</div>
</div>
<div class="col-auto ps-0">
<div class="btn-group-vertical btn-group-lg" role="group">
<button type="button" class="btn btn-outline-secondary text-primary-emphasis"><i class="bi bi-check-square fs-4"></i></button>
<button type="button" class="btn btn-outline-secondary text-primary-emphasis"><i class="bi bi-square fs-4"></i></button>
</div>
<br>
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="opponent-player-add" data-bs-toggle="modal" data-bs-list="#opponent-player-list" data-bs-target="#addPlayerModal" disabled>
<i class="bi bi-person-add fs-4"></i>
</button>
<br>
<div class="vstack text-center border border-secondary rounded mt-2">
<i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i>
<span class="badge fs-5 mb-2">0</span>
</div>
</div>
</div>
<script lang="javascript">
document.addEventListener('DOMContentLoaded', function() {
setupPlayerButtons('opponent-clan', 'opponent-player-list', 'opponent-player-add');
});
</script>
{{ end }} {{ end }}

View File

@@ -3,7 +3,7 @@
<head> <head>
{{ template "header" . }} {{ template "header" . }}
</head> </head>
<body data-bs-theme="dark"> <body data-bs-theme="dark" class="h-auto">
<div class="container-xxl bg-dark mt-5 p-4 rounded-3 text-light"> <div class="container-xxl bg-dark mt-5 p-4 rounded-3 text-light">
<div class="row"> <div class="row">
<!-- Home-Clan Column --> <!-- Home-Clan Column -->
@@ -14,9 +14,6 @@
<!-- Player List --> <!-- Player List -->
{{ template "home_player_list" . }} {{ template "home_player_list" . }}
<!-- List Controls -->
{{ template "home_player_bar" . }}
</div> </div>
<!-- Opponent-Clan Column --> <!-- Opponent-Clan Column -->
@@ -27,9 +24,6 @@
<!-- Player List --> <!-- Player List -->
{{ template "opp_player_list" . }} {{ template "opp_player_list" . }}
<!-- List Controls -->
{{ template "opp_player_bar" . }}
</div> </div>
</div> </div>

View File

@@ -9,8 +9,8 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="text" class="form-control form-control-lg" id="clanName" placeholder="Clan-Name" disabled> <input type="text" class="form-control form-control-lg" id="playerClanName" placeholder="Clan-Name" disabled>
<label for="clanName">Clan-Name</label> <label for="playerClanName">Clan-Name</label>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input type="text" class="form-control form-control-lg" id="playerName" placeholder="Spieler-Name"> <input type="text" class="form-control form-control-lg" id="playerName" placeholder="Spieler-Name">
@@ -33,18 +33,45 @@
addPlayerModal.addEventListener('show.bs.modal', event => { addPlayerModal.addEventListener('show.bs.modal', event => {
const [playerList, otherPlayerList] = getPlayerLists(event); const [playerList, otherPlayerList] = getPlayerLists(event);
const selectedClan = getSelectedClan(event); const selectedClan = getSelectedClan(event);
const clanTag = addPlayerModal.querySelector('#playerName'); const playerName = addPlayerModal.querySelector('#playerName');
const clanName = addPlayerModal.querySelector('#clanName'); const clanName = addPlayerModal.querySelector('#playerClanName');
clanName.value = selectedClan.innerText; clanName.value = selectedClan.innerText;
const clanId = parseInt(selectedClan.value);
const submitButton = addPlayerModal.querySelector('button[name="submit"]'); const submitButton = addPlayerModal.querySelector('button[name="submit"]');
submitButton.addEventListener('click', function (e) { submitButton.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
submitButton.onclick = function () {} submitButton.onclick = function () {}
fetch("/player", {
method: "POST",
body: JSON.stringify({
name: playerName.value,
clan_id: clanId
}),
headers: {
"Content-type": "application/json; charset=UTF-8"
}
})
.then((response) => response.json())
.then((json) => {
const opt = document.createElement('option');
opt.innerText = playerName.value;
opt.value = json['ID'];
playerList.appendChild(opt.cloneNode(true));
playerList.selectedIndex = playerList.children.length - 1;
playerList.dispatchEvent(new Event('change'));
if (document.getElementById('home-clan').selectedIndex === document.getElementById('opponent-clan').selectedIndex)
otherPlayerList.appendChild(opt);
addPlayerModalBS.hide();
playerName.value = "";
clanName.value = "";
}).catch((error) => {
throw new Error(error)
});
}) })
}); });
} }

View File

@@ -0,0 +1,11 @@
<div class="input-group input-group-lg mb-1">
<div class="input-group-text py-1 px-2">
<input class="form-check-input fs-4 border-secondary mt-0" type="checkbox" value="">
</div>
<span class="form-control py-1 px-2">%s</span>
<button class="btn btn-outline-secondary text-secondary-emphasis dropdown-toggle py-1" type="button" data-bs-toggle="dropdown"></button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item text-primary fs-5" href="#"><i class="bi bi-person-gear fs-4 me-2"></i>Bearbeiten</a></li>
<li><a class="dropdown-item text-danger fs-5" href="#"><i class="bi bi-person-dash fs-4 me-2"></i>Löschen</a></li>
</ul>
</div>

19
utils/conv.go Normal file
View File

@@ -0,0 +1,19 @@
package utils
import (
"fmt"
"strconv"
)
func UintToString(val uint) string {
return strconv.FormatUint(uint64(val), 10)
}
func StringToUint(val string) uint {
res, _ := strconv.ParseUint(val, 10, 16)
return uint(res)
}
func FloatToString(val float32) string {
return fmt.Sprintf("%f", val)
}