refactor language selector into separate component
Extract language switching functionality from UserHeader into a new LanguageSelector component. In mobile view, the selector appears in the top bar next to the hamburger menu. In desktop view, it appears in the navigation bar to the left of the UserHeader. - Create LanguageSelector component with local element bindings - Update Header component with language_selector_mobile and language_selector_desktop slots - Remove language selector code from UserHeader - Update recipe and main layouts to use new component - Hide desktop language selector inside mobile hamburger menu
This commit is contained in:
@@ -79,6 +79,16 @@ nav[hidden]{
|
||||
display: none;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
.right-buttons{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.header-right{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
:global(svg.symbol){
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
@@ -174,18 +184,32 @@ footer{
|
||||
:global(.site_header li:focus-within){
|
||||
transform: unset;
|
||||
}
|
||||
.nav_site .header-right{
|
||||
flex-direction: column;
|
||||
}
|
||||
.language-selector-desktop{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class=wrapper lang=de>
|
||||
<div>
|
||||
<div class=button_wrapper>
|
||||
<a href="/"><Symbol></Symbol></a>
|
||||
<button class=nav_button on:click={() => {toggle_sidebar()}}><svg xmlns="http://www.w3.org/2000/svg" height="0.5em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></button>
|
||||
<div class="right-buttons">
|
||||
<slot name=language_selector_mobile></slot>
|
||||
<button class=nav_button on:click={() => {toggle_sidebar()}}><svg xmlns="http://www.w3.org/2000/svg" height="0.5em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<nav hidden class=nav_site>
|
||||
<a class=entry href="/"><Symbol></Symbol></a>
|
||||
<slot name=links></slot>
|
||||
<div class="header-right">
|
||||
<div class="language-selector-desktop">
|
||||
<slot name=language_selector_desktop></slot>
|
||||
</div>
|
||||
<slot name=right_side></slot>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
163
src/lib/components/LanguageSelector.svelte
Normal file
163
src/lib/components/LanguageSelector.svelte
Normal file
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { recipeTranslationStore } from '$lib/stores/recipeTranslation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let currentLang = $state('de');
|
||||
let currentPath = $state('');
|
||||
let langButton: HTMLButtonElement;
|
||||
let langOptions: HTMLDivElement;
|
||||
|
||||
$effect(() => {
|
||||
// Update current language and path when page changes
|
||||
if (typeof window !== 'undefined') {
|
||||
const path = window.location.pathname;
|
||||
currentPath = path;
|
||||
if (path.startsWith('/recipes')) {
|
||||
currentLang = 'en';
|
||||
} else if (path.startsWith('/rezepte')) {
|
||||
currentLang = 'de';
|
||||
} else if (path === '/') {
|
||||
// On main page, read from localStorage
|
||||
const preferredLanguage = localStorage.getItem('preferredLanguage');
|
||||
currentLang = preferredLanguage === 'en' ? 'en' : 'de';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function toggle_language_options(){
|
||||
if (langOptions) {
|
||||
langOptions.hidden = !langOptions.hidden;
|
||||
}
|
||||
}
|
||||
|
||||
async function switchLanguage(lang: 'de' | 'en') {
|
||||
// Update the current language state immediately
|
||||
currentLang = lang;
|
||||
|
||||
// Store preference
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('preferredLanguage', lang);
|
||||
}
|
||||
|
||||
// Get the current path directly from window
|
||||
const path = typeof window !== 'undefined' ? window.location.pathname : currentPath;
|
||||
|
||||
// If on main page, dispatch event instead of reloading
|
||||
if (path === '/') {
|
||||
window.dispatchEvent(new CustomEvent('languagechange', { detail: { lang } }));
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have recipe translation data from store, use the correct short names
|
||||
const recipeData = $recipeTranslationStore;
|
||||
if (recipeData) {
|
||||
if (lang === 'en' && recipeData.englishShortName) {
|
||||
await goto(`/recipes/${recipeData.englishShortName}`);
|
||||
return;
|
||||
} else if (lang === 'de' && recipeData.germanShortName) {
|
||||
await goto(`/rezepte/${recipeData.germanShortName}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert current path to target language (for non-recipe pages)
|
||||
let newPath = path;
|
||||
if (lang === 'en' && path.startsWith('/rezepte')) {
|
||||
newPath = path.replace('/rezepte', '/recipes');
|
||||
} else if (lang === 'de' && path.startsWith('/recipes')) {
|
||||
newPath = path.replace('/recipes', '/rezepte');
|
||||
} else if (!path.startsWith('/rezepte') && !path.startsWith('/recipes')) {
|
||||
// On other pages (glaube, cospend, etc), go to recipe home
|
||||
newPath = lang === 'en' ? '/recipes' : '/rezepte';
|
||||
}
|
||||
|
||||
await goto(newPath);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if(langButton && !langButton.contains(e.target as Node)){
|
||||
if (langOptions) langOptions.hidden = true;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClick);
|
||||
};
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.language-selector{
|
||||
position: relative;
|
||||
}
|
||||
.language-button{
|
||||
width: auto;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
background-color: var(--nord3);
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 100ms;
|
||||
border: none;
|
||||
}
|
||||
.language-button:hover{
|
||||
background-color: var(--nord2);
|
||||
}
|
||||
.language-options{
|
||||
--bg_color: var(--nord3);
|
||||
box-sizing: border-box;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 10px);
|
||||
background-color: var(--bg_color);
|
||||
width: 10ch;
|
||||
padding: 0.5rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
.language-options button{
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
transition: background-color 100ms;
|
||||
}
|
||||
.language-options button:hover{
|
||||
background-color: var(--nord2);
|
||||
}
|
||||
.language-options button.active{
|
||||
background-color: var(--nord14);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="language-selector">
|
||||
<button bind:this={langButton} onclick={toggle_language_options} class="language-button">
|
||||
{currentLang.toUpperCase()}
|
||||
</button>
|
||||
<div bind:this={langOptions} class="language-options" hidden>
|
||||
<button
|
||||
class:active={currentLang === 'de'}
|
||||
onclick={() => switchLanguage('de')}
|
||||
>
|
||||
DE
|
||||
</button>
|
||||
<button
|
||||
class:active={currentLang === 'en'}
|
||||
onclick={() => switchLanguage('en')}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,99 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { recipeTranslationStore } from '$lib/stores/recipeTranslation';
|
||||
|
||||
let { user, showLanguageSelector = false } = $props();
|
||||
|
||||
let currentLang = $state('de');
|
||||
let currentPath = $state('');
|
||||
|
||||
$effect(() => {
|
||||
// Update current language and path when page changes
|
||||
if (typeof window !== 'undefined') {
|
||||
const path = window.location.pathname;
|
||||
currentPath = path;
|
||||
if (path.startsWith('/recipes')) {
|
||||
currentLang = 'en';
|
||||
} else if (path.startsWith('/rezepte')) {
|
||||
currentLang = 'de';
|
||||
} else if (path === '/') {
|
||||
// On main page, read from localStorage
|
||||
const preferredLanguage = localStorage.getItem('preferredLanguage');
|
||||
currentLang = preferredLanguage === 'en' ? 'en' : 'de';
|
||||
}
|
||||
}
|
||||
});
|
||||
let { user } = $props();
|
||||
|
||||
function toggle_options(){
|
||||
const el = document.querySelector("#options")
|
||||
el.hidden = !el.hidden
|
||||
}
|
||||
|
||||
function toggle_language_options(){
|
||||
const el = document.querySelector("#language-options")
|
||||
el.hidden = !el.hidden
|
||||
}
|
||||
|
||||
function switchLanguage(lang: 'de' | 'en') {
|
||||
// Update the current language state immediately
|
||||
currentLang = lang;
|
||||
|
||||
// Store preference
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('preferredLanguage', lang);
|
||||
}
|
||||
|
||||
// Get the current path directly from window
|
||||
const path = typeof window !== 'undefined' ? window.location.pathname : currentPath;
|
||||
|
||||
// If on main page, dispatch event instead of reloading
|
||||
if (path === '/') {
|
||||
window.dispatchEvent(new CustomEvent('languagechange', { detail: { lang } }));
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have recipe translation data from store, use the correct short names
|
||||
const recipeData = $recipeTranslationStore;
|
||||
if (recipeData) {
|
||||
if (lang === 'en' && recipeData.englishShortName) {
|
||||
goto(`/recipes/${recipeData.englishShortName}`);
|
||||
return;
|
||||
} else if (lang === 'de' && recipeData.germanShortName) {
|
||||
goto(`/rezepte/${recipeData.germanShortName}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert current path to target language (for non-recipe pages)
|
||||
let newPath = path;
|
||||
if (lang === 'en' && path.startsWith('/rezepte')) {
|
||||
newPath = path.replace('/rezepte', '/recipes');
|
||||
} else if (lang === 'de' && path.startsWith('/recipes')) {
|
||||
newPath = path.replace('/recipes', '/rezepte');
|
||||
} else if (!path.startsWith('/rezepte') && !path.startsWith('/recipes')) {
|
||||
// On other pages (glaube, cospend, etc), go to recipe home
|
||||
newPath = lang === 'en' ? '/recipes' : '/rezepte';
|
||||
}
|
||||
|
||||
goto(newPath);
|
||||
}
|
||||
|
||||
onMount( () => {
|
||||
document.addEventListener("click", (e) => {
|
||||
const userButton = document.querySelector("#button")
|
||||
const langButton = document.querySelector("#language-button")
|
||||
|
||||
if(userButton && !userButton.contains(e.target)){
|
||||
const options = document.querySelector("#options");
|
||||
if (options) options.hidden = true;
|
||||
}
|
||||
|
||||
if(langButton && !langButton.contains(e.target)){
|
||||
const langOptions = document.querySelector("#language-options");
|
||||
if (langOptions) langOptions.hidden = true;
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -149,59 +71,6 @@
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
.language-selector{
|
||||
position: relative;
|
||||
}
|
||||
#language-button{
|
||||
width: auto;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
background-color: var(--nord3);
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 100ms;
|
||||
}
|
||||
#language-button:hover{
|
||||
background-color: var(--nord2);
|
||||
}
|
||||
#language-options{
|
||||
--bg_color: var(--nord3);
|
||||
box-sizing: border-box;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 10px);
|
||||
background-color: var(--bg_color);
|
||||
width: 10ch;
|
||||
padding: 0.5rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
#language-options button{
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
transition: background-color 100ms;
|
||||
}
|
||||
#language-options button:hover{
|
||||
background-color: var(--nord2);
|
||||
}
|
||||
#language-options button.active{
|
||||
background-color: var(--nord14);
|
||||
}
|
||||
.header-right{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
#options{
|
||||
--bg_color: var(--nord3);
|
||||
box-sizing: border-box;
|
||||
@@ -266,30 +135,7 @@ h2 + p{
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="header-right">
|
||||
{#if showLanguageSelector}
|
||||
<div class="language-selector">
|
||||
<button on:click={toggle_language_options} id="language-button">
|
||||
{currentLang.toUpperCase()}
|
||||
</button>
|
||||
<div id="language-options" hidden>
|
||||
<button
|
||||
class:active={currentLang === 'de'}
|
||||
on:click={() => switchLanguage('de')}
|
||||
>
|
||||
DE
|
||||
</button>
|
||||
<button
|
||||
class:active={currentLang === 'en'}
|
||||
on:click={() => switchLanguage('en')}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if user}
|
||||
{#if user}
|
||||
<button on:click={toggle_options} style="background-image: url(https://bocken.org/static/user/thumb/{user.nickname}.webp)" id=button>
|
||||
<div id=options class="speech top" hidden>
|
||||
<h2>{user.name}</h2>
|
||||
@@ -300,7 +146,6 @@ h2 + p{
|
||||
</ul>
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
{:else}
|
||||
<a class=entry href=/login>Log In</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import Header from '$lib/components/Header.svelte'
|
||||
import UserHeader from '$lib/components/UserHeader.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
let { data } = $props();
|
||||
|
||||
let user = $derived(data.session?.user);
|
||||
@@ -9,6 +10,8 @@ let user = $derived(data.session?.user);
|
||||
<Header>
|
||||
<ul class=site_header slot=links>
|
||||
</ul>
|
||||
<UserHeader {user} slot=right_side showLanguageSelector={true}></UserHeader>
|
||||
<LanguageSelector slot=language_selector_mobile />
|
||||
<LanguageSelector slot=language_selector_desktop />
|
||||
<UserHeader {user} slot=right_side></UserHeader>
|
||||
<slot></slot>
|
||||
</Header>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import Header from '$lib/components/Header.svelte'
|
||||
import UserHeader from '$lib/components/UserHeader.svelte';
|
||||
import UserHeader from '$lib/components/UserHeader.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
let { data } = $props();
|
||||
|
||||
let user = $derived(data.session?.user);
|
||||
@@ -29,6 +30,8 @@ const labels = $derived({
|
||||
<li><a href="/{data.recipeLang}/tag">{labels.keywords}</a></li>
|
||||
<li><a href="/rezepte/tips-and-tricks">{labels.tips}</a></li>
|
||||
</ul>
|
||||
<UserHeader slot=right_side {user} showLanguageSelector={true}></UserHeader>
|
||||
<LanguageSelector slot=language_selector_mobile />
|
||||
<LanguageSelector slot=language_selector_desktop />
|
||||
<UserHeader slot=right_side {user}></UserHeader>
|
||||
<slot></slot>
|
||||
</Header>
|
||||
|
||||
Reference in New Issue
Block a user