change language menu to settings menu and add timezone settings - fixes #402

Sat, 09 Nov 2024 11:47:20 +0100

author
Mike Becker <universe@uap-core.de>
date
Sat, 09 Nov 2024 11:47:20 +0100
changeset 335
1eed60b779da
parent 334
2696337baa3d
child 336
c69deb8f9416

change language menu to settings menu and add timezone settings - fixes #402

src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/Constants.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/LanguageServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/SettingsServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/LanguageView.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/viewmodel/SettingsView.kt file | annotate | diff | comparison | revisions
src/main/resources/localization/strings.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/strings_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog-de.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/error.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-view.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/language.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/settings.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/versions.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/date-with-tooltip.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-list.jspf file | annotate | diff | comparison | revisions
src/main/webapp/language.css file | annotate | diff | comparison | revisions
src/main/webapp/settings.css file | annotate | diff | comparison | revisions
--- a/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt	Sat Nov 09 11:47:20 2024 +0100
@@ -33,12 +33,15 @@
 import jakarta.servlet.http.HttpServletRequest
 import jakarta.servlet.http.HttpServletResponse
 import java.sql.SQLException
+import java.time.ZoneId
 import java.util.*
 
 abstract class AbstractServlet : HttpServlet() {
 
     companion object {
+        const val COOKIE_MAX_AGE = 2592000 // 30 days
         const val LANGUAGE_COOKIE_NAME = "lpit_language"
+        const val TIMEZONE_COOKIE_NAME = "lpit_timezone"
     }
     
     protected val logger = MyLogger()
@@ -138,6 +141,18 @@
             logger.trace("Continuing session {0} with language {1}", session.id, sessionLocale)
         }
 
+        // determine the timezone
+        if (session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) == null) {
+            // timezone selection stored in cookie
+            val cookieTimezone = cookieTimezone(http)
+
+            // if no cookie, fall back to server's timezone (the browser does not transmit one)
+            val timezone = cookieTimezone ?: ZoneId.systemDefault()
+
+            selectTimezone(http, timezone)
+            logger.debug("Timezone for session {0} set to {1}", session.id, timezone)
+        }
+
         // if this is an error path, bypass the normal flow
         if (fullPath.startsWith("/error/")) {
             http.styleSheets = listOf("error")
@@ -198,6 +213,23 @@
         http.request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME }
             ?.runCatching {Locale.forLanguageTag(this.value)}?.getOrNull()
 
+    protected fun sessionLanguage(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale
+
+    private fun cookieTimezone(http: HttpRequest): ZoneId? =
+        http.request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME }
+            ?.runCatching { ZoneId.of(this.value)}?.getOrNull()
+
+    protected fun sessionTimezone(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) as String
+
+    protected fun selectTimezone(http: HttpRequest, zoneId: ZoneId) {
+        http.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id)
+        val cookie = Cookie(TIMEZONE_COOKIE_NAME, zoneId.id)
+        cookie.isHttpOnly = true
+        cookie.path = http.request.contextPath
+        cookie.maxAge = COOKIE_MAX_AGE
+        http.response.addCookie(cookie)
+    }
+
     protected fun selectLanguage(http: HttpRequest, locale: Locale) {
         http.response.locale = locale
         http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale)
@@ -209,7 +241,7 @@
             cookie.maxAge = 0
         } else {
             cookie.value = locale.language
-            cookie.maxAge = 2592000 // 30 days
+            cookie.maxAge = COOKIE_MAX_AGE
         }
         http.response.addCookie(cookie)
     }
--- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt	Sat Nov 09 11:47:20 2024 +0100
@@ -106,4 +106,9 @@
      * Key for the current language selection within the session.
      */
     const val SESSION_ATTR_LANGUAGE = "language"
+
+    /**
+     * Key for the current timezone selection within the session.
+     */
+    const val SESSION_ATTR_TIMEZONE = "timezone"
 }
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/LanguageServlet.kt	Sat Nov 09 10:54:57 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-/*
- * Copyright 2021 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package de.uapcore.lightpit.servlet
-
-import de.uapcore.lightpit.AbstractServlet
-import de.uapcore.lightpit.Constants
-import de.uapcore.lightpit.HttpRequest
-import de.uapcore.lightpit.dao.DataAccessObject
-import de.uapcore.lightpit.viewmodel.LanguageView
-import jakarta.servlet.annotation.WebServlet
-import java.util.*
-
-@WebServlet(urlPatterns = ["/language/*"])
-class LanguageServlet : AbstractServlet() {
-
-    init {
-        get("/", this::viewLanguages)
-        post("/", this::selectLanguage)
-    }
-
-    private fun viewLanguages(http: HttpRequest, dao: DataAccessObject) {
-        with(http) {
-            view = LanguageView(
-                availableLanguages(),
-                session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale,
-                request.locale
-            )
-            styleSheets = listOf("language")
-            render("language")
-        }
-    }
-
-    private fun selectLanguage(http: HttpRequest, dao: DataAccessObject) {
-        val lang = http.param("language")
-        if (lang != null) {
-            val locale = Locale.forLanguageTag(lang)
-            if (!locale.language.isNullOrBlank()) {
-                super.selectLanguage(http, locale)
-            }
-        }
-
-        viewLanguages(http, dao)
-    }
-
-}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/SettingsServlet.kt	Sat Nov 09 11:47:20 2024 +0100
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2021 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package de.uapcore.lightpit.servlet
+
+import de.uapcore.lightpit.AbstractServlet
+import de.uapcore.lightpit.HttpRequest
+import de.uapcore.lightpit.dao.DataAccessObject
+import de.uapcore.lightpit.viewmodel.SettingsView
+import jakarta.servlet.annotation.WebServlet
+import java.time.ZoneId
+import java.util.*
+
+@WebServlet(urlPatterns = ["/settings/*"])
+class SettingsServlet : AbstractServlet() {
+
+    init {
+        get("/", this::viewSettings)
+        post("/", this::selectSettings)
+    }
+
+    private fun viewSettings(http: HttpRequest, dao: DataAccessObject) {
+        with(http) {
+            view = SettingsView(
+                availableLanguages(),
+                sessionLanguage(http),
+                request.locale,
+                sessionTimezone(http)
+            )
+            styleSheets = listOf("settings")
+            render("settings")
+        }
+    }
+
+    private fun selectSettings(http: HttpRequest, dao: DataAccessObject) {
+        val lang = http.param("language")
+        if (lang != null) {
+            val locale = Locale.forLanguageTag(lang)
+            if (!locale.language.isNullOrBlank()) {
+                super.selectLanguage(http, locale)
+            }
+        }
+
+        val timezone = http.param("timezone")
+        timezone?.runCatching { ZoneId.of(this) }?.onSuccess { selectTimezone(http, it) }
+
+        viewSettings(http, dao)
+    }
+
+}
\ No newline at end of file
--- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/LanguageView.kt	Sat Nov 09 10:54:57 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-/*
- * Copyright 2021 Mike Becker. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package de.uapcore.lightpit.viewmodel
-
-import java.util.*
-
-class LanguageView(
-    val languages: List<Locale>,
-    val currentLanguage: Locale,
-    val browserLanguage: Locale
-) : View()
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/SettingsView.kt	Sat Nov 09 11:47:20 2024 +0100
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package de.uapcore.lightpit.viewmodel
+
+import java.time.ZoneId
+import java.util.*
+
+class SettingsView(
+    val languages: List<Locale>,
+    val currentLanguage: Locale,
+    val browserLanguage: Locale,
+    val currentTimezone: String
+) : View() {
+    val timezones: List<String> = ZoneId.getAvailableZoneIds().sorted()
+}
\ No newline at end of file
--- a/src/main/resources/localization/strings.properties	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/resources/localization/strings.properties	Sat Nov 09 11:47:20 2024 +0100
@@ -36,7 +36,6 @@
 button.issue.create.another=Create another Issue
 button.issue.create=New Issue
 button.issue.edit=Edit
-button.language.submit = Switch language
 button.okay=OK
 button.project.create=New Project
 button.project.details=Project Details
@@ -138,8 +137,8 @@
 language.browser.unavailable = Browser language not available.
 menu.about=About
 menu.issues=Issues
-menu.languages=Language
 menu.projects=Projects
+menu.settings=Settings
 menu.users=Developer
 navmenu.all=all
 navmenu.components=Components
@@ -165,6 +164,8 @@
 project.vcs=Version Control
 project.vcs.none=Do not analyze repository
 project=Project
+settings.language=Language
+settings.timezone=Timezone
 user.displayname=Developer
 user.givenname=Given Name
 user.lastname=Last Name
--- a/src/main/resources/localization/strings_de.properties	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/resources/localization/strings_de.properties	Sat Nov 09 11:47:20 2024 +0100
@@ -36,7 +36,6 @@
 button.issue.create.another=Weiteren Vorgang erstellen
 button.issue.create=Neuer Vorgang
 button.issue.edit=Bearbeiten
-button.language.submit = Sprache ausw\u00e4hlen
 button.okay=OK
 button.project.create=Neues Projekt
 button.project.details=Projektdetails
@@ -138,8 +137,8 @@
 language.browser.unavailable = Browsersprache nicht verf\u00fcgbar.
 menu.about=Info
 menu.issues=Vorg\u00e4nge
-menu.languages=Sprache
 menu.projects=Projekte
+menu.settings=Einstellungen
 menu.users=Entwickler
 navmenu.all=Alle
 navmenu.components=Komponenten
@@ -165,6 +164,8 @@
 project.vcs=Versionskontrolle
 project.vcs.none=Keine Analyse durchf\u00fchren
 project=Projekt
+settings.language=Sprache
+settings.timezone=Zeitzone
 user.displayname=Entwickler
 user.givenname=Vorname
 user.lastname=Nachname
--- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Sat Nov 09 11:47:20 2024 +0100
@@ -27,6 +27,7 @@
 <h3>Version 1.4.0 (Vorschau)</h3>
 
 <ul>
+    <li>Das Sprachmenü ist nun ein Einstellungsmenü, in dem auch die Zeitzone eingestellt werden kann.</li>
     <li>Abbrechen-Button zum Kommentar-Editor hinzugefügt.</li>
     <li>Die Vorgangsliste zeigt nun die Komponente, wenn kein Filter auf Komponenten aktiv ist.</li>
     <li>Vorgangsstatus zur Vorschlagsliste für das Erstellen einer Vorgangsbeziehung hinzugefügt.</li>
--- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Sat Nov 09 11:47:20 2024 +0100
@@ -27,6 +27,7 @@
 <h3>Version 1.4.0 (snapshot)</h3>
 
 <ul>
+    <li>Change language menu to settings menu and add timezone settings.</li>
     <li>Add cancel button to comment editor.</li>
     <li>Add component tag to issue list when no component filter is active.</li>
     <li>Add issue status to the suggestions for creating an issue relation.</li>
--- a/src/main/webapp/WEB-INF/jsp/error.jsp	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jsp/error.jsp	Sat Nov 09 11:47:20 2024 +0100
@@ -47,7 +47,7 @@
         </tr>
         <tr>
             <th><fmt:message key="error.timestamp"/>:</th>
-            <td><fmt:formatDate type="both" value="<%= new java.util.Date()%>"/></td>
+            <td><fmt:formatDate type="both" value="<%= new java.util.Date()%>" timeZone="${sessionScope[Constants.SESSION_ATTR_TIMEZONE]}"/></td>
         </tr>
         <%--@elvariable id="exception" type="java.lang.Exception"--%>
         <c:if test="${not empty exception}">
--- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sat Nov 09 11:47:20 2024 +0100
@@ -151,7 +151,7 @@
         </c:if>
         <tr>
             <th><label for="issue-eta"><fmt:message key="issue.eta"/></label></th>
-            <td><input id="issue-eta" name="eta" type="date" value="<fmt:formatDate value="${issue.eta}" pattern="YYYY-MM-dd" />" /> </td>
+            <td><input id="issue-eta" name="eta" type="date" value="<fmt:formatDate value="${issue.eta}" pattern="YYYY-MM-dd" timeZone="${timezone}" />" /> </td>
         </tr>
         <c:if test="${issue.id ge 0}">
         <tr>
--- a/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jsp/issue-view.jsp	Sat Nov 09 11:47:20 2024 +0100
@@ -143,7 +143,7 @@
     </tr>
     <tr>
         <th><fmt:message key="issue.eta"/></th>
-        <td><fmt:formatDate value="${issue.eta}" /></td>
+        <td><fmt:formatDate value="${issue.eta}" timeZone="${timezone}"/></td>
     </tr>
     </tbody>
 </table>
@@ -298,10 +298,10 @@
                 </c:if>
             </div>
             <div class="smalltext">
-                <fmt:formatDate type="BOTH" value="${comment.created}" />
+                <fmt:formatDate type="BOTH" value="${comment.created}" timeZone="${timezone}" />
                 <c:if test="${comment.updateCount gt 0}">
                 <span class="comment-edit-info">
-                    (<fmt:message key="issue.comments.lastupdate"/> <fmt:formatDate type="BOTH" value="${comment.updated}" />, ${comment.updateCount} <fmt:message key="issue.comments.updateCount"/>)
+                    (<fmt:message key="issue.comments.lastupdate"/> <fmt:formatDate type="BOTH" value="${comment.updated}" timeZone="${timezone}" />, ${comment.updateCount} <fmt:message key="issue.comments.updateCount"/>)
                 </span>
                 </c:if>
             </div>
--- a/src/main/webapp/WEB-INF/jsp/language.jsp	Sat Nov 09 10:54:57 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-<%-- 
-DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
-
-Copyright 2021 Mike Becker. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
-
-2. Redistributions in binary form must reproduce the above copyright
-notice, this list of conditions and the following disclaimer in the
-documentation and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
---%>
-<%@page pageEncoding="UTF-8" %>
-<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-
-<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.LanguageView" scope="request"/>
-
-<form method="POST" id="lang-selector">
-    <c:forEach items="${viewmodel.languages}" var="l">
-        <label>
-            <input type="radio" name="language" value="${l.language}"
-                   <c:if test="${l.language eq viewmodel.currentLanguage.language}">checked</c:if>/>
-                ${l.displayLanguage}
-            (${l.getDisplayLanguage(viewmodel.currentLanguage)}
-            <c:if test="${not empty viewmodel.browserLanguage and l.language eq viewmodel.browserLanguage.language}"><c:set
-                    var="browserLanguagePresent" value="true"/>&nbsp;-&nbsp;<fmt:message key="language.browser"/></c:if>)
-        </label>
-    </c:forEach>
-    <c:if test="${not browserLanguagePresent}">
-        <span class="blNA"><fmt:message key="language.browser.unavailable"/></span>
-    </c:if>
-    <button type="submit"><fmt:message key="button.language.submit" /></button>
-</form>
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat Nov 09 11:47:20 2024 +0100
@@ -69,7 +69,7 @@
         <h2>
             <fmt:message key="version" /> <c:out value="${versionInfo.version.name}" /> - <fmt:message key="version.status.${versionInfo.version.status}"/>
             <c:if test="${not empty versionInfo.version.releaseOrEolDate}">
-                (<fmt:formatDate type="date" value="${versionInfo.version.releaseOrEolDate}"/>)
+                (<fmt:formatDate type="date" value="${versionInfo.version.releaseOrEolDate}" timeZone="${timezone}"/>)
             </c:if>
         </h2>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/jsp/settings.jsp	Sat Nov 09 11:47:20 2024 +0100
@@ -0,0 +1,59 @@
+<%-- 
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 2021 Mike Becker. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+--%>
+<%@page pageEncoding="UTF-8" %>
+<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.SettingsView" scope="request"/>
+
+<form method="POST" id="settings-form">
+    <h2><fmt:message key="settings.language"/></h2>
+    <div class="flex-column">
+        <c:forEach items="${viewmodel.languages}" var="l">
+            <label>
+                <input type="radio" name="language" value="${l.language}"
+                       <c:if test="${l.language eq viewmodel.currentLanguage.language}">checked</c:if>/>
+                    ${l.displayLanguage}
+                (${l.getDisplayLanguage(viewmodel.currentLanguage)}
+                <c:if test="${not empty viewmodel.browserLanguage and l.language eq viewmodel.browserLanguage.language}"><c:set
+                        var="browserLanguagePresent" value="true"/>&nbsp;-&nbsp;<fmt:message key="language.browser"/></c:if>)
+            </label>
+        </c:forEach>
+        <c:if test="${not browserLanguagePresent}">
+            <span class="blNA"><fmt:message key="language.browser.unavailable"/></span>
+        </c:if>
+    </div>
+    <h2><fmt:message key="settings.timezone"/></h2>
+    <div>
+        <select name="timezone">
+        <c:forEach items="${viewmodel.timezones}" var="tz">
+            <option value="${tz}" <c:if test="${tz eq viewmodel.currentTimezone}">selected</c:if> >${tz}</option>
+        </c:forEach>
+        </select>
+    </div>
+    <button type="submit"><fmt:message key="button.apply" /></button>
+</form>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jsp/site.jsp	Sat Nov 09 11:47:20 2024 +0100
@@ -57,6 +57,9 @@
 <%-- Define an alias for the optional JS file --%>
 <c:set scope="page" var="javascriptFile" value="${requestScope[Constants.REQ_ATTR_JAVASCRIPT]}"/>
 
+<%-- Define an alias for timezone --%>
+<c:set scope="page" var="timezone" value="${sessionScope[Constants.SESSION_ATTR_TIMEZONE]}" />
+
 <%-- Load resource bundle --%>
 <fmt:setLocale scope="request" value="${pageContext.response.locale}"/>
 <fmt:setBundle scope="request" basename="localization.strings"/>
@@ -108,9 +111,9 @@
                 </a>
             </div>
             <div class="menuEntry"
-                 <c:if test="${fn:startsWith(requestPath, '/language/')}">data-active</c:if> >
-                <a href="language/">
-                    <fmt:message key="menu.languages"/>
+                 <c:if test="${fn:startsWith(requestPath, '/settings/')}">data-active</c:if> >
+                <a href="settings/">
+                    <fmt:message key="menu.settings"/>
                 </a>
             </div>
             <div class="menuEntry"
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat Nov 09 11:47:20 2024 +0100
@@ -75,13 +75,13 @@
         <tr>
             <th><label for="version-release"><fmt:message key="version.release"/></label></th>
             <td>
-                <input id="version-release" name="release" type="date" value="<fmt:formatDate value="${version.release}" pattern="YYYY-MM-dd" />"/>
+                <input id="version-release" name="release" type="date" value="<fmt:formatDate value="${version.release}" pattern="YYYY-MM-dd" timeZone="${timezone}" />"/>
             </td>
         </tr>
         <tr>
             <th><label for="version-eol"><fmt:message key="version.eol"/></label></th>
             <td>
-                <input id="version-eol" name="eol" type="date" value="<fmt:formatDate value="${version.eol}" pattern="YYYY-MM-dd" />"/>
+                <input id="version-eol" name="eol" type="date" value="<fmt:formatDate value="${version.eol}" pattern="YYYY-MM-dd" timeZone="${timezone}" />"/>
             </td>
         </tr>
         </tbody>
--- a/src/main/webapp/WEB-INF/jsp/versions.jsp	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jsp/versions.jsp	Sat Nov 09 11:47:20 2024 +0100
@@ -87,7 +87,7 @@
                 </a>
                 <div class="version-tag version-${versionInfo.version.status}"
                         <c:if test="${not empty versionInfo.version.releaseOrEolDate}">
-                            title="<fmt:formatDate type="date" value="${versionInfo.version.releaseOrEolDate}"/>"
+                            title="<fmt:formatDate type="date" value="${versionInfo.version.releaseOrEolDate}" timeZone="${timezone}"/>"
                         </c:if>
                 >
                     <fmt:message key="version.status.${versionInfo.version.status}"/>
--- a/src/main/webapp/WEB-INF/jspf/date-with-tooltip.jspf	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jspf/date-with-tooltip.jspf	Sat Nov 09 11:47:20 2024 +0100
@@ -28,6 +28,6 @@
 dateValue: DateTime
 --%>
 
-<span title="<fmt:formatDate value="${dateValue}" type="time"/>">
-    <fmt:formatDate value="${dateValue}" type="date"/>
+<span title="<fmt:formatDate value="${dateValue}" type="time" timeZone="${timezone}"/>">
+    <fmt:formatDate value="${dateValue}" type="date" timeZone="${timezone}"/>
 </span>
--- a/src/main/webapp/WEB-INF/jspf/issue-list.jspf	Sat Nov 09 10:54:57 2024 +0100
+++ b/src/main/webapp/WEB-INF/jspf/issue-list.jspf	Sat Nov 09 11:47:20 2024 +0100
@@ -61,12 +61,12 @@
             </td>
             <td>
                 <span class="nowrap <c:if test="${issue.overdue}">eta-overdue</c:if> ">
-                    <fmt:formatDate value="${issue.eta}" />
+                    <fmt:formatDate value="${issue.eta}" timeZone="${timezone}" />
                 </span>
             </td>
             <td>
                 <span class="nowrap">
-                    <fmt:formatDate value="${issue.updated}" />
+                    <fmt:formatDate value="${issue.updated}" timeZone="${timezone}" />
                 </span>
             </td>
             <td>
--- a/src/main/webapp/language.css	Sat Nov 09 10:54:57 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- * 
- * Copyright 2021 Mike Becker. All rights reserved.
- * 
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- *   1. Redistributions of source code must retain the above copyright
- *      notice, this list of conditions and the following disclaimer.
- *
- *   2. Redistributions in binary form must reproduce the above copyright
- *      notice, this list of conditions and the following disclaimer in the
- *      documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- * 
- */
-
-#lang-selector {
-    display: flex;
-    flex-basis: content;
-    flex-direction: column;
-    align-items: flex-start;
-}
-
-input {
-    margin: .5em;
-}
-
-button {
-    margin-top: 1.5em;
-}
-
-/* browser language not available */
-span.blNA {
-    margin: .5em;
-    color: red;
-    font-style: italic;
-    font-size: smaller;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/settings.css	Sat Nov 09 11:47:20 2024 +0100
@@ -0,0 +1,51 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ * 
+ * Copyright 2021 Mike Becker. All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * 
+ */
+
+.flex-column {
+    display: flex;
+    flex-basis: content;
+    flex-direction: column;
+    align-items: flex-start;
+}
+
+input {
+    margin: .5em;
+}
+
+button {
+    margin-top: 1.5em;
+}
+
+/* browser language not available */
+span.blNA {
+    margin: .5em;
+    color: red;
+    font-style: italic;
+    font-size: smaller;
+}

mercurial