Add user preference for black vs white display order

- Gear icon in header opens settings modal
- Preference stored in cookie for server-side Velocity rendering
- ViewServlet reads blackFirst cookie into Velocity context
- Velocity conditionals in pairing, results, and result-sheets templates
This commit is contained in:
Claude Brisson
2025-11-30 10:54:52 +01:00
parent 17697845fd
commit 9a379052e5
8 changed files with 128 additions and 5 deletions

View File

@@ -158,3 +158,22 @@ Translations in `view-webapp/.../WEB-INF/translations/`
- French (fr)
- German (de)
- Korean (ko)
## Current Work
### User Preferences (feature/user-preferences branch)
Implemented "black vs white" display order option:
- Gear icon in header opens settings modal
- Preference stored in cookie (`blackFirst`) for server-side Velocity rendering
- localStorage backup via store2 (`prefs.blackFirst`)
- Velocity conditionals in tour-pairing.inc.html, tour-results.inc.html, result-sheets.html
- ViewServlet reads cookie and sets `$blackFirst` in Velocity context
Files modified:
- `view-webapp/.../layouts/standard.html` - gear icon + settings modal
- `view-webapp/.../sass/main.scss` - settings modal styles
- `view-webapp/.../js/main.js` - prefs object + modal handlers + cookie set
- `view-webapp/.../kotlin/.../ViewServlet.kt` - read blackFirst cookie
- `view-webapp/.../tour-pairing.inc.html` - `#if($blackFirst)` conditionals
- `view-webapp/.../tour-results.inc.html` - `#if($blackFirst)` conditionals + inverted result display
- `view-webapp/.../result-sheets.html` - `#if($blackFirst)` conditionals

View File

@@ -43,6 +43,11 @@ class ViewServlet : VelocityViewServlet() {
}
}
val lang = request.getAttribute("lang") as String
// User preferences - read from cookie
val blackFirst = request.cookies?.find { it.name == "blackFirst" }?.value == "true"
context.put("blackFirst", blackFirst)
/*
val menu = menuEntries!![uri]
var title: String? = null

View File

@@ -523,10 +523,22 @@
}
}
#logout {
#logout, #settings {
cursor: pointer;
}
#settings-modal {
.setting {
margin: 0.5em 0;
label {
display: flex;
align-items: center;
gap: 0.5em;
cursor: pointer;
}
}
}
@media screen {
#players-list {
font-size: smaller;

View File

@@ -44,6 +44,9 @@
#translate('tour-menu.inc.html')
#end
<div id="header-right">
<div id="settings" title="Settings">
<i class="fa fa-cog"></i>
</div>
<div id="lang">
<i class="$translate.flags[$request.lang] flag"></i>
</div>
@@ -84,6 +87,23 @@
#end
#end
</div>
<div id="settings-modal" class="popup">
<div class="popup-body">
<div class="popup-header">Settings</div>
<div class="popup-content">
<div class="setting">
<label>
<input type="checkbox" id="pref-black-first" #if($blackFirst)checked#end />
Display games as "Black vs White" (instead of "White vs Black")
</label>
</div>
</div>
<div class="popup-footer">
<button class="ui button" id="settings-save">Save</button>
<button class="ui button gray close">Cancel</button>
</div>
</div>
</div>
<script type="text/javascript" src="/lib/store2-2.14.2.min.js"></script>
<script type="text/javascript" src="/lib/tablesort-5.4.0/tablesort.min.js"></script>
<script type="text/javascript" src="/lib/tablesort-5.4.0/sorts/tablesort.number.min.js"></script>

View File

@@ -1,6 +1,21 @@
// Utilities
const characters ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
// User preferences
const prefs = {
get: function(key) {
return store('prefs.' + key);
},
set: function(key, value) {
store('prefs.' + key, value);
},
getAll: function() {
return {
blackFirst: this.get('blackFirst') || false
};
}
};
function randomString(length) {
let result = '';
const charactersLength = characters.length;
@@ -349,6 +364,21 @@ onLoad(() => {
if (!dialog) close_modal();
});
// Settings modal handlers
$('#settings').on('click', e => {
modal('settings-modal');
});
$('#settings-save').on('click', e => {
let blackFirst = $('#pref-black-first')[0].checked;
prefs.set('blackFirst', blackFirst);
// Set cookie for server-side rendering (expires in 1 year)
document.cookie = `blackFirst=${blackFirst}; path=/; max-age=31536000; SameSite=Lax`;
close_modal();
// Reload page to apply new preference
window.location.reload();
});
if (isTouchDevice()) {
$("[title]").on('click', e => {
let item = e.target.closest('[title]');

View File

@@ -59,17 +59,27 @@
Surround winner's name or ½-½
</div>
<div class="players">
#if($blackFirst)
<div class="black player">
<div class="color">Black</div>
<div class="name">$black.name $!black.firstname #rank($black.rank)<br/>#if($black.country)($black.country.toUpperCase()#if($black.club), $black.club#end)#end</div>
</div>
<div class="equal">½-½</div>
<div class="white player">
<div class="color">White</div>
<div class="name">$white.name $!white.firstname #rank($white.rank)<br/>#if($white.country)($white.country.toUpperCase()#if($white.club), $white.club#end)#end</div>
</div>
#else
<div class="white player">
<div class="color">White</div>
<div class="name">$white.name $!white.firstname #rank($white.rank)<br/>#if($white.country)($white.country.toUpperCase()#if($white.club), $white.club#end)#end</div>
## <div class="pin">$white.egf</div>
</div>
<div class="equal">½-½</div>
<div class="black player">
<div class="color">Black</div>
<div class="name">$black.name $!black.firstname #rank($black.rank)<br/>#if($black.country)($black.country.toUpperCase()#if($black.club), $black.club#end)#end</div>
## <div class="pin">$black.egf</div>
</div>
#end
</div>
<div class="signatures">
<div class="signature">Signature:</div>

View File

@@ -69,15 +69,21 @@
</button>
</div>
<div>
<div id="paired" class="multi-select" title="white vs. black">##
<div id="paired" class="multi-select" title="#if($blackFirst)black vs. white#{else}white vs. black#end">##
#foreach($game in $games)
#set($white = $pmap[$game.w])
#set($black = $pmap[$game.b])
<div class="listitem game" data-id="$game.id">
<div class="table" data-value="$game.t">${game.t}.</div>
#if($blackFirst)
<div class="black" data-id="$game.b">#if($black)$black.name#if($black.firstname) $black.firstname#end#{else}BIP#end</div>
<div class="levels">#if($black)#rank($black.rank)#end&nbsp;/&nbsp;#if($white)#rank($white.rank)#end</div>
<div class="white" data-id="$game.w">#if($white)$white.name#if($white.firstname) $white.firstname#end#{else}BIP#end</div>
#else
<div class="white" data-id="$game.w">#if($white)$white.name#if($white.firstname) $white.firstname#end#{else}BIP#end</div>
<div class="levels">#if($white)#rank($white.rank)#end&nbsp;/&nbsp;#if($black)#rank($black.rank)#end</div>
<div class="black" data-id="$game.b">#if($black)$black.name#if($black.firstname) $black.firstname#end#{else}BIP#end</div>
#end
<div class="handicap" data-value="$game.h">#if($game.h)h$game.h#{else}&nbsp;#end</div>
</div>
#end##
@@ -98,8 +104,13 @@
<thead>
<tr>
<th>Tbl</th>
#if($blackFirst)
<th>Black</th>
<th>White</th>
#else
<th>White</th>
<th>Black</th>
#end
<th>Hd</th>
</tr>
</thead>
@@ -109,8 +120,13 @@
#set($black = $pmap[$game.b])
<tr>
<td class="t" data-table="${game.t}">${game.t}</td>
#if($blackFirst)
<td class="left">#if($black)${black.name} $!{black.firstname} (#rank($black.rank) $!black.country $!black.club)#{else}BIP#end</td>
<td class="left">#if($white)${white.name} $!{white.firstname} (#rank($white.rank) $!white.country $!white.club)#{else}BIP#end</td>
#else
<td class="left">#if($white)${white.name} $!{white.firstname} (#rank($white.rank) $!white.country $!white.club)#{else}BIP#end</td>
<td class="left">#if($black)${black.name} $!{black.firstname} (#rank($black.rank) $!black.country $!black.club)#{else}BIP#end</td>
#end
<td>${game.h}</td>
</tr>
#end

View File

@@ -18,23 +18,34 @@
<table id="results-table" class="ui celled striped table">
<thead class="centered">
<th data-sort-method="number">table</th>
#if($blackFirst)
<th>black</th>
<th>white</th>
#else
<th>white</th>
<th>black</th>
#end
<th>hd</th>
<th>result</th>
</thead>
<tbody>
#set($dispRst = {'?':'?', 'w':'1-0', 'b':'0-1', '=':'½-½', 'X':'X', '#':'1-1', '0':'0-0'})
#set($dispRstInv = {'?':'?', 'w':'0-1', 'b':'1-0', '=':'½-½', 'X':'X', '#':'1-1', '0':'0-0'})
#foreach($game in $individualGames)
#set($white = $plmap[$game.w])
#set($black = $plmap[$game.b])
#if($black && $white)
<tr id="result-$game.id" data-id="$game.id">
<td data-sort="$game.t">${game.t}.</td>
#if($blackFirst)
<td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name#if($black.firstname) $black.firstname#end"><span>#if($black)$black.name#if($black.firstname) $black.firstname#end #rank($black.rank)#{else}BIP#end</span></td>
<td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name#if($white.firstname) $white.firstname#end"><span>#if($white)$white.name#if($white.firstname) $white.firstname#end #rank($white.rank)#{else}BIP#end</span></td>
#else
<td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name#if($white.firstname) $white.firstname#end"><span>#if($white)$white.name#if($white.firstname) $white.firstname#end #rank($white.rank)#{else}BIP#end</span></td>
<td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name#if($black.firstname) $black.firstname#end"><span>#if($black)$black.name#if($black.firstname) $black.firstname#end #rank($black.rank)#{else}BIP#end</span></td>
#end
<td class="handicap centered">$!game.h</td>
<td class="result centered" data-sort="$game.r" data-result="$game.r">$dispRst[$game.r]</td>
<td class="result centered" data-sort="$game.r" data-result="$game.r">#if($blackFirst)$dispRstInv[$game.r]#{else}$dispRst[$game.r]#end</td>
</tr>
#end
#end