diff --git a/example/src/main/resources/static/css/main.css b/example/src/main/resources/static/css/main.css index 3de3421c..658720a0 100644 --- a/example/src/main/resources/static/css/main.css +++ b/example/src/main/resources/static/css/main.css @@ -67,3 +67,84 @@ body { .eu-logo-fixed img { height: 86px; } + +.language-dropdown { + position: relative; + display: inline-block; +} + +.language-btn { + background-color: #f8f9fa; + color: #212529; + border: 1px solid #e9ecef; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + min-width: 60px; + justify-content: space-between; + transition: background-color 0.2s, border-color 0.2s; +} + +.language-btn:hover { + background-color: #e9ecef; +} + +.dropdown-arrow { + font-size: 10px; + transition: transform 0.2s; +} + +.language-btn.active .dropdown-arrow { + transform: rotate(180deg); +} + +.language-menu { + position: absolute; + top: 100%; + left: 0; + background-color: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 4px; + padding: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15); + z-index: 1000; + display: none; + min-width: 200px; + margin-top: 2px; +} + +.language-menu.show { + display: block; +} + +.language-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +} + +.language-option { + background: none; + border: none; + color: #212529; + padding: 8px 12px; + text-align: left; + cursor: pointer; + border-radius: 2px; + font-size: 14px; + white-space: nowrap; + transition: background-color 0.2s; +} + +.language-option:hover { + background-color: #e9ecef; +} + +.language-option.selected { + font-weight: bold; +} diff --git a/example/src/main/resources/static/js/index.js b/example/src/main/resources/static/js/index.js new file mode 100644 index 00000000..9d2881ae --- /dev/null +++ b/example/src/main/resources/static/js/index.js @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020-2025 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +"use strict"; + +const NO_LANGUAGE_SELECTED = { lang: "auto", display: "AUTO", name: "Auto" }; + +const SUPPORTED_LANGUAGES = [ + { lang: "et", display: "ET", name: "Eesti" }, + { lang: "en", display: "EN", name: "English" }, + { lang: "ru", display: "RU", name: "Русский" }, + { lang: "fi", display: "FI", name: "Suomi" }, + { lang: "hr", display: "HR", name: "Hrvatska" }, + { lang: "de", display: "DE", name: "Deutsch" }, + { lang: "fr", display: "FR", name: "Française" }, + { lang: "nl", display: "NL", name: "Nederlands" }, + { lang: "cs", display: "CS", name: "Čeština" }, + { lang: "sk", display: "SK", name: "Slovenština" }, + NO_LANGUAGE_SELECTED +]; + +const languageComponent = { + selectedLang: document.querySelector("#selected-lang"), + languageButton: document.querySelector("#language-button"), + languageMenu: document.querySelector("#language-menu"), + languageGrid: document.querySelector(".language-grid"), + languageOptions: () => document.querySelectorAll(".language-option") +}; + +/** + * Reads the `lang` query parameter from the current URL and validates it. + * + * - Returns the language code (e.g. "en", "et") if present and included in SUPPORTED_LANGUAGES. + * - Returns `null` if the parameter is missing, empty, or not in the supported list. + * + * This ensures that only recognized languages are passed to the app. If `null` + * is returned, the Web eID application will fall back to the OS default locale. + */ +export function getValidatedLangFromUrl() { + const lang = new URLSearchParams(window.location.search).get("lang"); + if (!lang) { + return null; + } + const normalizedLang = lang.trim().toLowerCase(); + const language = SUPPORTED_LANGUAGES.find(lang => lang.lang === normalizedLang); + return language ? language.lang : null; +} + +/** + * Creates a language selections in UI component + * + * @param lang Language to be selected in component, example en + * @param onLangChange Action to be executed when language is changed + */ +export function setupLanguageSelection(lang, onLangChange) { + const language = SUPPORTED_LANGUAGES.find(supportedLanguage => supportedLanguage.lang === lang) ?? NO_LANGUAGE_SELECTED; + + SUPPORTED_LANGUAGES.forEach(supportedLanguage => { + if (supportedLanguage === NO_LANGUAGE_SELECTED) { + return; + } + + const button = document.createElement("button"); + button.className = "language-option"; + button.textContent = supportedLanguage.name; + + if (supportedLanguage === language) { + button.classList.add("selected"); + setSelectedLanguage(supportedLanguage); + } + + button.addEventListener("click", () => { + languageComponent.languageOptions().forEach(o => o.classList.remove("selected")); + button.classList.add("selected"); + + setSelectedLanguage(supportedLanguage); + hideLanguageSelectionMenu(); + + onLangChange(supportedLanguage.lang); + }); + + languageComponent.languageGrid.appendChild(button); + }); + + setSelectedLanguage(language); + + languageComponent.languageButton.onclick = e => { + e.stopPropagation(); + showLanguageSelectionMenu(); + }; + + document.onclick = () => { + hideLanguageSelectionMenu(); + }; + + languageComponent.languageMenu.onclick = e => e.stopPropagation(); +} + +function setSelectedLanguage(supportedLanguage) { + languageComponent.selectedLang.textContent = supportedLanguage.display; +} + +function showLanguageSelectionMenu() { + languageComponent.languageMenu.classList.toggle("show"); + languageComponent.languageButton.classList.toggle("active"); +} + +function hideLanguageSelectionMenu() { + languageComponent.languageMenu.classList.remove("show"); + languageComponent.languageButton.classList.remove("active"); +} diff --git a/example/src/main/resources/templates/index.html b/example/src/main/resources/templates/index.html index 051ab855..13a37445 100644 --- a/example/src/main/resources/templates/index.html +++ b/example/src/main/resources/templates/index.html @@ -87,15 +87,31 @@
+ +
The privacy policy of the test service is available here.
@@ -250,6 +266,7 @@