refactor: unify recipe routes into [recipeLang] slug with full bilingual support
Consolidate /rezepte and /recipes routes into single [recipeLang] structure to eliminate code duplication. All pages now use conditional API routing and reactive labels based on language parameter. - Merge duplicate route structures into /[recipeLang] with 404 for invalid slugs - Add English API endpoints for search, favorites, tags, and categories - Implement language dropdown in header with localStorage persistence - Convert all pages to use Svelte 5 runes (, , ) - Add German-only redirects (301) for add/edit pages - Make all view pages (list, detail, filters, search, favorites) fully bilingual - Remove floating language switcher in favor of header dropdown
This commit is contained in:
@@ -1,17 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
export let user;
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
// Convert current path to target language
|
||||
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 === '/' || (!path.startsWith('/rezepte') && !path.startsWith('/recipes'))) {
|
||||
// On main page or other pages, go to recipe home
|
||||
newPath = lang === 'en' ? '/recipes' : '/rezepte';
|
||||
}
|
||||
|
||||
goto(newPath);
|
||||
}
|
||||
|
||||
onMount( () => {
|
||||
document.addEventListener("click", (e) => {
|
||||
const el = document.querySelector("#button")
|
||||
if(!el.contains(e.target)){
|
||||
document.querySelector("#options").hidden = true
|
||||
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;
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -68,6 +125,59 @@
|
||||
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;
|
||||
@@ -132,17 +242,41 @@ h2 + p{
|
||||
}
|
||||
</style>
|
||||
|
||||
{#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>
|
||||
<p>({user.nickname})</p>
|
||||
<ul>
|
||||
<li><a href="https://sso.bocken.org/if/user/#/settings" >Einstellungen</a></li>
|
||||
<li><a href="/logout" >Log Out</a></li>
|
||||
</ul>
|
||||
<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>
|
||||
</button>
|
||||
{:else}
|
||||
<a class=entry href=/login>Log In</a>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#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>
|
||||
<p>({user.nickname})</p>
|
||||
<ul>
|
||||
<li><a href="https://sso.bocken.org/if/user/#/settings" >Einstellungen</a></li>
|
||||
<li><a href="/logout" >Log Out</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<a class=entry href=/login>Log In</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user