feat: add user popout to navbar

This commit is contained in:
Ash Monty 2022-07-06 11:12:31 +02:00
parent 0fe9aabaae
commit f1c18efe35
No known key found for this signature in database
GPG Key ID: 740B7C88251D49B6
16 changed files with 498 additions and 334 deletions

View File

@ -79,27 +79,6 @@
color: var(--text-secondary);
}
@media screen and (max-width: 1064px) {
.selected-locale .locale-names {
display: none;
}
.selected-locale {
width: 80px;
margin-left: auto;
margin-right: 12px;
}
.locale-dropdown {
width: fit-content;
}
.select-box .options-container {
width: 150px;
right: 12px;
}
}
@media screen and (max-width: 946px) {
header nav a:not(.keep-on-mobile) {
display: none;

View File

@ -1,6 +1,5 @@
.select-box {
display: flex;
width: 188px;
flex-direction: column;
position: relative;
user-select: none;
@ -12,7 +11,7 @@
.select-box .options-container {
max-height: 0;
width: 100%;
width: fit-content;
opacity: 0;
transition: all 0.4s;
overflow: hidden;
@ -20,34 +19,8 @@
background-color: var(--btn-secondary);
order: 1;
position: absolute;
top: 50px;
}
.selected-locale {
margin-bottom: 8px;
position: relative;
width: 188px;
height: 45px;
border-radius: 5px;
display: flex;
align-items: center;
background-color: var(--btn-secondary);
color: white;
order: 0;
}
.selected-locale::after {
content: "";
width: 1.2rem;
height: 1.2rem;
background: url("/assets/images/down-arrow.svg");
position: absolute;
right: 15px;
top: 50%;
transition: transform 150ms;
transform: translateY(-50%);
background-size: contain;
background-position: center;
top: 48px;
right: 0;
}
.select-box .option .item {
@ -67,25 +40,21 @@
overflow-y: auto;
}
.select-box .options-container.active + .selected-locale::after {
.select-box .options-container.active + .locale-dropdown-toggle::after {
transform: translateY(-50%) rotateX(180deg);
}
.select-box .options-container::-webkit-scrollbar {
width: 8px;
background: #0d141f;
background: #81878f;
background: #f1f2f3;
background: var(--btn-secondary);
border-radius: 0 5px 5px 0;
}
.select-box .options-container::-webkit-scrollbar-thumb {
background: #525861;
background: #81878f;
background: var(--text-secondary);
border-radius: 0 5px 5px 0;
}
.select-box .option,
.selected-locale {
.select-box .option {
padding: 12px 15px;
cursor: pointer;
}

View File

@ -94,10 +94,108 @@ header nav a:hover {
transition: color 50ms ease-in-out;
}
.locale-dropdown {
header .right-section {
display: grid;
grid-auto-flow: column;
grid-gap: 24px;
margin-left: auto;
z-index: 2;
height: 45px;
color: var(--text-secondary);
}
header .locale-dropdown-toggle {
width: fit-content;
height: 24px;
padding: 0;
margin: auto;
transition: color 150ms;
cursor: pointer;
}
header .locale-dropdown-toggle:hover,
header .locale-dropdown-toggle.active {
color: var(--text);
}
header .user-widget-wrapper {
height: 24px;
}
header .user-widget-wrapper a.login-link {
color: var(--text-secondary);
text-decoration: none;
transition: color 150ms;
}
header .user-widget-wrapper a.login-link:hover {
color: var(--text);
}
header .user-widget-wrapper.logged-in {
position: relative;
width: 32px;
height: 32px;
}
header .user-widget-wrapper.logged-in .user-widget-toggle {
width: 32px;
height: 32px;
background: var(--text-secondary-2);
border-radius: 50%;
overflow: hidden;
cursor: pointer;
}
header .user-widget-wrapper .user-widget-toggle img,
header .user-widget .user-avatar img {
width: 100%;
height: 100%;
}
header .user-widget {
max-height: 0;
overflow: hidden;
box-sizing: border-box;
transition: max-height 300ms, padding 200ms, opacity 150ms;
position: absolute;
right: 0;
top: 48px;
padding: 0;
background: #2a2f50;
border-radius: 8px;
text-align: center;
opacity: 0;
box-shadow: 0 0 10px -2px #111531;
}
header .user-widget.active {
max-height: 100vh;
padding: 36px;
opacity: 1;
}
header .user-widget .user-avatar {
width: 128px;
height: 128px;
margin: auto;
background: var(--text-secondary-2);
border-radius: 50%;
overflow: hidden;
}
header .user-widget .user-info {
margin-top: 12px;
}
header .user-widget .user-info .mii-name {
color: var(--text);
}
header .user-widget .buttons {
margin-top: 12px;
}
header .user-widget .button {
margin-top: 12px;
width: 100%;
padding: 8px 60px;
cursor: pointer;
}
header .user-widget .button.logout {
background: #383f6b;
color: var(--text);
}
/* Misc */
@ -1118,25 +1216,6 @@ footer div.discord-server-card svg {
margin: auto !important;
}
.selected-locale .locale-names {
display: none;
}
.selected-locale {
width: 80px;
margin-left: auto;
margin-right: 12px;
}
.locale-dropdown {
width: fit-content;
}
.select-box .options-container {
width: 150px;
right: 12px;
}
footer {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, fit-content(100%));
@ -1168,13 +1247,3 @@ footer div.discord-server-card svg {
margin: 0 10px;
}
}
@media screen and (max-width: 330px) {
.locale-dropdown .selected-locale {
width: 50px;
}
.locale-dropdown .selected-locale::after {
display: none;
}
}

View File

@ -0,0 +1,85 @@
/* eslint-disable no-undef, no-unused-vars */
// Account widget handler
const userWidgetToggle = document.querySelector('.user-widget-toggle') ;
const userWidget = document.querySelector('.user-widget');
// Open widget on click, close locale dropdown
userWidgetToggle?.addEventListener('click', () => {
userWidget.classList.toggle('active');
localeOptionsContainer.classList.toggle('active');
localeDropdownToggle.classList.toggle('active');
});
// Locale dropdown handler
function localeDropdownHandler(selectedLocale) {
document.cookie = `preferredLocale=${selectedLocale};max-age=31536000`;
window.location.reload();
}
const localeDropdown = document.querySelector(
'.locale-dropdown[data-dropdown]'
);
const localeDropdownOptions = document.querySelectorAll(
'.locale-dropdown[data-dropdown] .options-container'
);
const localeDropdownToggle = document.querySelector('.locale-dropdown-toggle');
const localeOptionsContainer = localeDropdown.querySelector('.options-container');
const localeOptionsList = localeDropdown.querySelectorAll('.option');
// click dropdown element will open dropdown
localeDropdownToggle.addEventListener('click', () => {
localeOptionsContainer.classList.toggle('active');
localeDropdownToggle.classList.toggle('active');
});
// clicking on any option will close dropdown and change value
localeOptionsList.forEach((option) => {
option.addEventListener('click', () => {
localeDropdownToggle.classList.remove('active');
localeOptionsContainer.classList.remove('active');
const selectedLocale = option.querySelector('label').getAttribute('for');
localeDropdownHandler(selectedLocale);
});
});
// close all dropdowns on scroll
document.addEventListener('scroll', () => {
localeDropdownOptions.forEach((el) => el.classList.remove('active'));
localeDropdownToggle.classList.remove('active');
userWidget.classList.remove('active');
});
// click outside of dropdown will close all dropdowns
document.addEventListener('click', (e) => {
const targetElement = e.target;
let found = false;
if (
localeDropdown == targetElement ||
localeDropdown.contains(targetElement)
) {
found = true;
userWidget.classList.remove('active');
}
if (
userWidget == targetElement ||
userWidget.contains(targetElement) ||
userWidgetToggle == targetElement ||
userWidgetToggle.contains(targetElement)
) {
found = true;
localeDropdownToggle.classList.remove('active');
localeOptionsContainer.classList.remove('active');
}
if (found) return;
// click outside of dropdowns
userWidget.classList.remove('active');
localeDropdownToggle.classList.remove('active');
localeOptionsContainer.classList.remove('active');
});

View File

@ -1,64 +0,0 @@
/* eslint-disable no-undef, no-unused-vars */
function setDefaultDropdownLocale(localeString) {
try {
const selected = document.querySelector('.selected-locale');
let item = document.querySelector(`label[for=${localeString}`);
if (!item) { // if locale can't be found, default to en-US
localeString = 'en-US';
item = document.querySelector(`label[for=${localeString}`);
}
selected.innerHTML = item.innerHTML;
} catch (e) {} // If it errors it's probably because there isn't a navbar in the view
}
function localeDropdownHandler(selectedLocale) {
document.cookie = `preferredLocale=${selectedLocale};max-age=31536000`;
window.location.reload();
}
const dropdowns = document.querySelectorAll('*[data-dropdown]');
const dropdownOptions = document.querySelectorAll(
'*[data-dropdown] .options-container'
);
dropdowns.forEach((el) => {
const selected = el.querySelector('.selected-locale');
const optionsContainer = el.querySelector('.options-container');
const optionsList = el.querySelectorAll('.option');
// click dropdown element will open dropdown
selected.addEventListener('click', () => {
optionsContainer.classList.toggle('active');
});
// clicking on any option will close dropdown and change value
optionsList.forEach((option) => {
option.addEventListener('click', () => {
selected.innerHTML = option.querySelector('label').innerHTML;
optionsContainer.classList.remove('active');
const selectedLocale = option.querySelector('label').getAttribute('for');
localeDropdownHandler(selectedLocale);
});
});
});
// close all dropdowns on scroll
document.addEventListener('scroll', () => {
dropdownOptions.forEach((el) => el.classList.remove('active'));
});
// click outside of dropdown will close all dropdowns
document.addEventListener('click', (e) => {
const targetElement = e.target;
// check if target is from a dropdown
let found = false;
dropdowns.forEach((v) => {
if (v == targetElement || v.contains(targetElement)) found = true;
});
if (found) return;
// click outside of dropdowns
dropdownOptions.forEach((el) => el.classList.remove('active'));
});

View File

@ -193,6 +193,8 @@ router.get('/', async (request, response) => {
renderData.discordAuthURL = discordAuthURL;
}
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
response.render('account/account', renderData);
});
@ -297,6 +299,15 @@ router.post('/register', async (request, response) => {
response.redirect('/account');
});
router.get('/logout', async(_request, response) => {
response.clearCookie('refresh_token', { domain: '.pretendo.network' });
response.clearCookie('access_token', { domain: '.pretendo.network' });
response.clearCookie('token_type', { domain: '.pretendo.network' });
response.clearCookie('ph', { domain: '.pretendo.network' });
response.redirect('/');
});
router.get('/connect/discord', async (request, response) => {
let tokens;
try {

View File

@ -4,14 +4,20 @@ const router = new Router();
router.get('/', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
response.render('aprilfools', {
const renderData = {
layout: 'main',
locale,
localeString: reqLocale.toString(),
});
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
};
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
if (renderData.isLoggedIn) {
const account = await util.getAccount(request, response);
renderData.account = account;
}
response.render('aprilfools', renderData);
});
module.exports = router;

View File

@ -36,18 +36,21 @@ const postList = () => {
};
router.get('/', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const localeString = reqLocale.toString();
response.render('blog/blog', {
const renderData = {
layout: 'main',
locale,
localeString,
postList
});
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
postList,
};
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
if (renderData.isLoggedIn) {
const account = await util.getAccount(request, response);
renderData.account = account;
}
response.render('blog/blog', renderData);
});
// RSS feed
@ -69,10 +72,19 @@ router.get('/feed.xml', async (request, response) => {
router.get('/:slug', async (request, response, next) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const renderData = {
layout: 'blog-opengraph',
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
postList,
};
const localeString = reqLocale.toString();
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
if (renderData.isLoggedIn) {
const account = await util.getAccount(request, response);
renderData.account = account;
}
// Get the name of the post from the URL
const postName = request.params.slug;
@ -89,6 +101,7 @@ router.get('/:slug', async (request, response, next) => {
// Convert the post info into JSON and separate it and the content
// eslint-disable-next-line prefer-const
let { data: postInfo, content } = matter(rawPost);
renderData.postInfo = postInfo;
// Replace [yt-iframe](videoID) with the full <iframe />
content = content
@ -97,14 +110,9 @@ router.get('/:slug', async (request, response, next) => {
// Convert the content into HTML
const htmlPost = marked.parse(content);
renderData.htmlPost = htmlPost;
response.render('blog/blogpost', {
layout: 'blog-opengraph',
locale,
localeString,
postInfo,
htmlPost,
});
response.render('blog/blogpost', renderData);
});
module.exports = router;

View File

@ -11,32 +11,45 @@ router.get('/', async (request, response) => {
});
router.get('/search', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const localeString = reqLocale.toString();
response.render('docs/search', {
const renderData = {
layout: 'main',
locale,
localeString,
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
currentPage: request.params.slug,
});
};
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
if (renderData.isLoggedIn) {
const account = await util.getAccount(request, response);
renderData.account = account;
}
response.render('docs/search', renderData);
});
router.get('/:slug', async (request, response, next) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const renderData = {
layout: 'main',
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
currentPage: request.params.slug,
};
const localeString = reqLocale.toString();
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
if (renderData.isLoggedIn) {
const account = await util.getAccount(request, response);
renderData.account = account;
}
// Get the name of the page from the URL
const pageName = request.params.slug;
let markdownLocale = localeString;
let markdownLocale = renderData.localeString;
let missingInLocale = false;
// Check if the MD file exists in the user's locale, if not try en-US and show notice, or finally log error and show 404.
if (fs.existsSync(path.join('docs', localeString, `${pageName}.md`))) {
if (fs.existsSync(path.join('docs', renderData.localeString, `${pageName}.md`))) {
null;
} else if (fs.existsSync(path.join('docs', 'en-US', `${pageName}.md`))) {
markdownLocale = 'en-US';
@ -45,6 +58,7 @@ router.get('/:slug', async (request, response, next) => {
next();
return;
}
renderData.missingInLocale = missingInLocale;
let content;
// Get the markdown file corresponding to the page.
@ -57,22 +71,16 @@ router.get('/:slug', async (request, response, next) => {
// Convert the content into HTML
content = marked.parse(content);
renderData.content = content;
// A boolean to show the quick links grid or not.
let showQuickLinks = false;
if (pageName === 'welcome') {
showQuickLinks = true;
}
renderData.showQuickLinks = showQuickLinks;
response.render('docs/docs', {
layout: 'main',
locale,
localeString,
content,
currentPage: request.params.slug,
missingInLocale,
showQuickLinks
});
response.render('docs/docs', renderData);
});
module.exports = router;

View File

@ -7,15 +7,26 @@ const { getTrelloCache } = require('../trello');
router.get('/', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const renderData = {
layout: 'main',
boards,
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
};
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
if (renderData.isLoggedIn) {
const account = await util.getAccount(request, response);
renderData.account = account;
}
const cache = await getTrelloCache();
// Builds the arrays of people for the special thanks section
// Shuffles the special thanks people
let specialThanksPeople = locale.specialThanks.people.slice();
const specialThanksPeople = renderData.locale.specialThanks.people.slice();
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
@ -29,7 +40,7 @@ router.get('/', async (request, response) => {
const specialThanksSecondRow = specialThanksPeople.slice(3, 7);
// Builds the final array to be sent to the view, and triples each row.
specialThanksPeople = {
renderData.specialThanksPeople = {
first: specialThanksFirstRow.concat(specialThanksFirstRow).concat(specialThanksFirstRow),
second: specialThanksSecondRow.concat(specialThanksSecondRow).concat(specialThanksSecondRow)
};
@ -74,14 +85,9 @@ router.get('/', async (request, response) => {
// Calculates global completion percentage
totalProgress.percent = Math.round(totalProgress._calc.percentageSum / cache.sections.length * 100);
response.render('home', {
layout: 'main',
featuredFeatureList: totalProgress,
boards,
locale,
localeString: reqLocale.toString(),
specialThanksPeople
});
renderData.featuredFeatureList = totalProgress;
response.render('home', renderData);
});
module.exports = router;

View File

@ -3,15 +3,20 @@ const util = require('../util');
const router = new Router();
router.get('/', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
response.render('localization', {
const renderData = {
layout: 'main',
locale,
localeString: reqLocale.toString(),
});
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
};
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
if (renderData.isLoggedIn) {
const account = await util.getAccount(request, response);
renderData.account = account;
}
response.render('localization', renderData);
});
module.exports = router;

View File

@ -6,19 +6,24 @@ const router = new Router();
const { getTrelloCache } = require('../trello');
router.get('/', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const cache = await getTrelloCache();
response.render('progress', {
const renderData = {
layout: 'main',
boards,
locale,
localeString: reqLocale.toString(),
progressLists: cache
});
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
};
renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph;
if (renderData.isLoggedIn) {
const account = await util.getAccount(request, response);
renderData.account = account;
}
const cache = await getTrelloCache();
renderData.cache = cache;
response.render('progress', renderData);
});
module.exports = router;

View File

@ -174,6 +174,46 @@ async function handleStripeEvent(event) {
}
}
async function getAccount(request, response) {
// Attempt to get user data
let apiResponse = await apiGetRequest('/v1/user', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
});
if (apiResponse.statusCode !== 200) {
// Assume expired, refresh and retry request
apiResponse = await apiPostGetRequest('/v1/login', {}, {
refresh_token: request.cookies.refresh_token,
grant_type: 'refresh_token'
});
if (apiResponse.statusCode !== 200) {
// TODO: Error message
return response.status(apiResponse.statusCode).json({
error: 'Bad'
});
}
const tokens = apiResponse.body;
apiResponse = await apiGetRequest('/v1/user', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
});
}
// If still failed, something went horribly wrong
if (apiResponse.statusCode !== 200) {
// TODO: Error message
return response.status(apiResponse.statusCode).json({
error: 'Bad'
});
}
// Return user account info
const account = apiResponse.body;
return account;
}
module.exports = {
fullUrl,
getLocale,
@ -181,5 +221,6 @@ module.exports = {
apiPostGetRequest,
apiDeleteGetRequest,
nintendoPasswordHash,
handleStripeEvent
handleStripeEvent,
getAccount
};

View File

@ -70,7 +70,6 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
<script src="/assets/js/progress-charts.js"></script>
<script src="/assets/js/locale-dropdown-handler.js"></script>
<script>setDefaultDropdownLocale("{{localeString}}")</script>
</body>
</html>

View File

@ -67,8 +67,6 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
<script src="/assets/js/progress-charts.js"></script>
<script src="/assets/js/locale-dropdown-handler.js"></script>
<script>setDefaultDropdownLocale("{{localeString}}")</script>
</body>
</html>

View File

@ -33,101 +33,140 @@
<a href="/blog" class="keep-on-mobile">{{ localeHelper locale "nav" "blog" }}</a>
</nav>
<!-- Ordered the locales in the same way Google orders them -->
<div class="select-box locale-dropdown" data-dropdown>
<div class="options-container">
<div class="option">
<input type="radio" class="radio" id="en-US" name="category" />
<label for="en-US">
<div class="item"><span class="lang">🇺🇸</span><span class="locale-names">English</span></div>
</label>
<div class="right-section">
<!-- Ordered the locales in the same way Google orders them -->
<div class="select-box locale-dropdown" data-dropdown>
<div class="options-container">
<div class="option">
<input type="radio" class="radio" id="en-US" name="category" />
<label for="en-US">
<div class="item"><span class="locale-names">English</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="de-DE" name="category" />
<label for="de-DE">
<div class="item"><span class="locale-names">Deutsch</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="es-ES" name="category" />
<label for="es-ES">
<div class="item"><span class="locale-names">Español</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="fr-FR" name="category" />
<label for="fr-FR">
<div class="item"><span class="locale-names">Français</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="it-IT" name="category" />
<label for="it-IT">
<div class="item"><span class="locale-names">Italiano</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="nl-NL" name="category" />
<label for="nl-NL">
<div class="item"><span class="locale-names">Nederlands</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="nb-NO" name="category" />
<label for="nb-NO">
<div class="item"><span class="locale-names">Norsk</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="pl-PL" name="category" />
<label for="pl-PL">
<div class="item"><span class="locale-names">Polski</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="pt-BR" name="category" />
<label for="pt-BR">
<div class="item"><span class="locale-names">Português</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ro-RO" name="category" />
<label for="ro-RO">
<div class="item"><span class="locale-names">Română</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="tr-TR" name="category" />
<label for="tr-TR">
<div class="item"><span class="locale-names">Türkçe</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ru-RU" name="category" />
<label for="ru-RU">
<div class="item"><span class="locale-names">Pусский</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ar-AR" name="category" />
<label for="ar-AR">
<div class="item"><span class="locale-names">اَلْعَرَبِيَّةُ</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ja-JP" name="category" />
<label for="ja-JP">
<div class="item"><span class="locale-names">日本語</span></div>
</div>
<div class="option">
<input type="radio" class="radio" id="ko-KR" name="category" />
<label for="ko-KR">
<div class="item"><span class="locale-names">한국어</span></div>
</label>
</div>
</div>
<div class="option">
<input type="radio" class="radio" id="de-DE" name="category" />
<label for="de-DE">
<div class="item"><span class="lang">🇩🇪</span><span class="locale-names">Deutsch</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="es-ES" name="category" />
<label for="es-ES">
<div class="item"><span class="lang">🇪🇸</span><span class="locale-names">Español</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="fr-FR" name="category" />
<label for="fr-FR">
<div class="item"><span class="lang">🇫🇷</span><span class="locale-names">Français</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="it-IT" name="category" />
<label for="it-IT">
<div class="item"><span class="lang">🇮🇹</span><span class="locale-names">Italiano</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="nl-NL" name="category" />
<label for="nl-NL">
<div class="item"><span class="lang">🇳🇱</span><span class="locale-names">Nederlands</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="nb-NO" name="category" />
<label for="nb-NO">
<div class="item"><span class="lang">🇳🇴</span><span class="locale-names">Norsk</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="pl-PL" name="category" />
<label for="pl-PL">
<div class="item"><span class="lang">🇵🇱</span><span class="locale-names">Polski</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="pt-BR" name="category" />
<label for="pt-BR">
<div class="item"><span class="lang">🇧🇷</span><span class="locale-names">Português</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ro-RO" name="category" />
<label for="ro-RO">
<div class="item"><span class="lang">🇷🇴</span><span class="locale-names">Română</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="tr-TR" name="category" />
<label for="tr-TR">
<div class="item"><span class="lang">🇹🇷</span><span class="locale-names">Türkçe</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ru-RU" name="category" />
<label for="ru-RU">
<div class="item"><span class="lang">🇷🇺</span><span class="locale-names">Pусский</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ar-AR" name="category" />
<label for="ar-AR">
<div class="item"><span class="lang">🇸🇦</span><span class="locale-names">اَلْعَرَبِيَّةُ</span></div>
</label>
</div>
<div class="option">
<input type="radio" class="radio" id="ja-JP" name="category" />
<label for="ja-JP">
<div class="item"><span class="lang">🇯🇵</span><span class="locale-names">日本語</span></div>
</div>
<div class="option">
<input type="radio" class="radio" id="ko-KR" name="category" />
<label for="ko-KR">
<div class="item"><span class="lang">🇰🇷</span><span class="locale-names">한국어</span></div>
</label>
<div class="locale-dropdown-toggle">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
</div>
</div>
<div class="selected-locale">
<div class="item"><span class="lang"></span></div>
</div>
{{#if isLoggedIn}}
<div class="user-widget-wrapper logged-in">
<div class="user-widget-toggle">
<img src="{{ account.mii.image_url }}" alt="{{ account.mii.name }}" />
</div>
<div class="user-widget">
<div class="user-avatar">
<img src="{{ account.mii.image_url }}" alt="{{ account.mii.name }}" />
</div>
<div class="user-info">
<div class="mii-name">{{ account.mii.name }}</div>
<div class="pnid">{{ account.username }}</div>
</div>
<div class="buttons">
<a href="/account">
<button class="button primary">
Settings
</button>
</a>
<a href="/account/logout">
<button class="button logout">
Logout
</button>
</a>
</div>
</div>
</div>
{{else}}
<div class="user-widget-wrapper">
<a class="login-link" href="/account/login">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
</a>
</div>
{{/if}}
</div>
</header>
<script src="/assets/js/header-handler.js"></script>