Added translations switch button

This commit is contained in:
Alexis
2025-03-23 16:53:09 +01:00
parent 4c34f8e644
commit fe1a657c04
14 changed files with 278 additions and 1473 deletions

View File

@@ -0,0 +1,17 @@
// Animations on load
@media screen and (prefers-reduced-motion: no-preference) {
.appear-from-top {
animation: fadeIn .2s cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}

View File

@@ -78,4 +78,48 @@
&.btn-shadow {
box-shadow: rgba(0, 0, 0, 0.15) 0 .2rem .3rem;
}
}
.btn-round {
display: grid;
place-items: center;
height: 45px;
aspect-ratio: 1;
background-color: var(--white);
border: 1px solid var(--slate-400);
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
border-radius: 100vmax;
pointer-events: all;
cursor: pointer;
outline: .2rem solid transparent;
transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s;
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95);
svg {
width: 1.15em;
transition-property: fill;
transition-duration: .15s;
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
&:hover,
&:focus-visible {
outline-color: color-mix(in srgb, var(--blue-500) 20%, transparent);
border-color: var(--blue-500);
svg {
fill: var(--blue-500);
}
}
&.active {
color: var(--white);
background-color: var(--blue-500);
border-color: var(--blue-700);
svg {
fill: var(--white);
}
}
}

View File

@@ -3,9 +3,16 @@
padding-inline: .75rem;
border-radius: 8px;
background-color: var(--white);
border: 1px solid var(--slate-100);
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
min-width: 15rem;
.card-arrow {
fill: var(--white);
}
&.p-sm {
min-width: fit-content;
padding: .5rem .25rem;
}
}

View File

@@ -0,0 +1,3 @@
// :root {
// --sidebar-size: 3rem;
// }

View File

@@ -4,8 +4,10 @@
@use 'leaflet';
@use 'forms';
@use 'animations';
@use 'buttons';
@use 'card';
@use 'map';
:root {
--white: #fff;

View File

@@ -4,6 +4,7 @@ import type { MapProps } from '@/types/Map';
import MarkerCreator from './overlay/MarkerCreator.vue';
import { t } from '@/i18n/store';
import { getLangFromUrl } from '@/i18n/utils';
import MapActions from './MapActions.astro';
interface Props extends MapProps {}
@@ -26,10 +27,12 @@ const byText = t('common.by', lang)
const seeMapText = t('maps.go-to-map', lang)
---
<div class="world-wrapper">
<div id="world"></div>
<MarkerCreator client:only="vue" mapKey={mapKey} />
<div class="map-wrapper">
<div class="world-wrapper">
<div id="world"></div>
<MarkerCreator client:only="vue" mapKey={mapKey} />
</div>
</div>
<script
@@ -667,25 +670,24 @@ map.addEventListener('click', (event) => {
<style lang="scss" define:vars={{ backgroundColor }}>
html,
body {
height: 100vh;
margin: 0;
position: relative;
}
.map-wrapper {
position: relative;
// display: grid;
// grid-template-columns: var(--sidebar-size) 1fr;
}
#world {
height: 100vh;
height: 100dvh;
}
.world-wrapper {
position: fixed;
inset: 0;
position: relative;
z-index: 0;
isolation: isolate;
#world {
height: 100%;
width: 100%;
}
}
.leaflet-container {
aspect-ratio: 1551 / 1605;
background-color: var(--backgroundColor);
}
</style>

View File

@@ -3,6 +3,7 @@ import type { MapMarker, PlayerMarker } from "@/types/Leaflet";
import type { MapOverlayProps } from "@/types/Map";
import SearchMarkers from "./overlay/SearchMarkers.vue";
import LangSwitcher from "./overlay/LangSwitcher.vue";
import MapOverlayBreadcrumbs from "./MapOverlayBreadcrumbs.astro";
interface Props extends MapOverlayProps {}
@@ -18,27 +19,38 @@ const {
},
breadcrumbs = [],
} = Astro.props
const currentUrl = Astro.url
---
<div class="world-overlay">
<SearchMarkers client:only="vue" mapKey={mapKey} markers={markers} players={players} searchConfig={searchConfig} >
<div slot="fallback" style="height: 45px;"></div>
</SearchMarkers>
<div class="top-bar">
<SearchMarkers client:only="vue" mapKey={mapKey} markers={markers} players={players} searchConfig={searchConfig} >
<div slot="fallback" style="height: 45px;"></div>
</SearchMarkers>
<LangSwitcher currentUrl={currentUrl} client:only="vue" />
</div>
<MapOverlayBreadcrumbs breadcrumbs={breadcrumbs} />
</div>
<style lang="scss">
.world-overlay {
position: fixed;
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 10;
padding: 10px;
padding: 1rem;
pointer-events: none;
display: grid;
row-gap: .25rem;
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
}
</style>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 570 KiB

View File

@@ -0,0 +1,72 @@
<script lang="ts" setup>
import { computed, onUpdated, ref, watch } from 'vue';
import { useElementHover, useFocus } from '@vueuse/core'
import { PopoverArrow, PopoverContent, PopoverRoot, PopoverTrigger } from 'radix-vue'
import LangSwitcherButton from './LangSwitcherButton.vue';
import { useStore } from '@nanostores/vue';
import { currentLang } from '@/i18n/store';
import { availableLangs } from '@/i18n/ui';
const $currentLang = useStore(currentLang);
defineProps({
currentUrl: {
type: URL,
required: true
}
});
const menuModel = ref(false);
const wrapper = ref<HTMLDivElement | null>(null)
const buttonRef = ref<HTMLButtonElement | null>(null)
const wrapperHovered = useElementHover(wrapper, { delayEnter: 500, delayLeave: 500 })
const { focused: buttonFocused } = useFocus(buttonRef)
const isMenuOpen = computed(() => menuModel.value || wrapperHovered.value)
watch([wrapperHovered, buttonFocused], (value) => {
// Check if all values from array are false
if (!value.some(Boolean)) {
menuModel.value = false
}
})
</script>
<template>
<div ref="wrapper" class="wrapper appear-from-top">
<PopoverRoot :open="isMenuOpen" @update:open="() => menuModel = true">
<PopoverTrigger as-child>
<button ref="buttonRef" class="btn-round" :class="{ 'active': isMenuOpen }">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M247.15,212.42l-56-112a8,8,0,0,0-14.31,0l-21.71,43.43A88,88,0,0,1,108,126.93,103.65,103.65,0,0,0,135.69,64H160a8,8,0,0,0,0-16H104V32a8,8,0,0,0-16,0V48H32a8,8,0,0,0,0,16h87.63A87.76,87.76,0,0,1,96,116.35a87.74,87.74,0,0,1-19-31,8,8,0,1,0-15.08,5.34A103.63,103.63,0,0,0,84,127a87.55,87.55,0,0,1-52,17,8,8,0,0,0,0,16,103.46,103.46,0,0,0,64-22.08,104.18,104.18,0,0,0,51.44,21.31l-26.6,53.19a8,8,0,0,0,14.31,7.16L148.94,192h70.11l13.79,27.58A8,8,0,0,0,240,224a8,8,0,0,0,7.15-11.58ZM156.94,176,184,121.89,211.05,176Z"></path></svg>
</button>
</PopoverTrigger>
<PopoverContent side="bottom" :side-offset="6" :collision-padding="16" class="card p-sm" style="z-index: 100;">
<PopoverArrow class="card-arrow" />
<menu>
<li v-for="lang in availableLangs" :key="lang">
<LangSwitcherButton :lang="lang" :currentUrl="currentUrl" :disabled="$currentLang === lang" />
</li>
</menu>
</PopoverContent>
</PopoverRoot>
</div>
</template>
<style lang="scss" scoped>
.wrapper {
pointer-events: all;
}
.btn-round {
position: relative;
> svg {
width: 1.25em;
height: 1.25em;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<script lang="ts" setup>
import type { Language } from '@/i18n/ui';
import { t } from '@/i18n/store';
import { switchLang } from '@/i18n/utils';
defineProps<{
lang: Language,
currentUrl: URL
disabled?: boolean
}>();
const flagWidth = 18;
const flagHeight = 18;
</script>
<template>
<a :href="switchLang(currentUrl, lang).pathname" :aria-disabled="disabled" :aria-current="disabled ? 'page' : 'false'" :tabindex="disabled ? -1 : 0">
<div class="flag">
<svg v-if="disabled" xmlns="http://www.w3.org/2000/svg" :height="flagHeight" :width="flagWidth" class="valid" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"></path></svg>
<svg v-else-if="lang === 'en'" xmlns="http://www.w3.org/2000/svg" :height="flagHeight" :width="flagWidth" viewBox="0 0 36 36"><path fill="#00247D" d="M0 9.059V13h5.628zM4.664 31H13v-5.837zM23 25.164V31h8.335zM0 23v3.941L5.63 23zM31.337 5H23v5.837zM36 26.942V23h-5.631zM36 13V9.059L30.371 13zM13 5H4.664L13 10.837z"/><path fill="#CF1B2B" d="M25.14 23l9.712 6.801c.471-.479.808-1.082.99-1.749L28.627 23H25.14zM13 23h-2.141l-9.711 6.8c.521.53 1.189.909 1.938 1.085L13 23.943V23zm10-10h2.141l9.711-6.8c-.521-.53-1.188-.909-1.937-1.085L23 12.057V13zm-12.141 0L1.148 6.2C.677 6.68.34 7.282.157 7.949L7.372 13h3.487z"/><path fill="#EEE" d="M36 21H21v10h2v-5.836L31.335 31H32c1.117 0 2.126-.461 2.852-1.199L25.14 23h3.487l7.215 5.052c.093-.337.158-.686.158-1.052v-.058L30.369 23H36v-2zM0 21v2h5.63L0 26.941V27c0 1.091.439 2.078 1.148 2.8l9.711-6.8H13v.943l-9.914 6.941c.294.07.598.116.914.116h.664L13 25.163V31h2V21H0zM36 9c0-1.091-.439-2.078-1.148-2.8L25.141 13H23v-.943l9.915-6.942C32.62 5.046 32.316 5 32 5h-.663L23 10.837V5h-2v10h15v-2h-5.629L36 9.059V9zM13 5v5.837L4.664 5H4c-1.118 0-2.126.461-2.852 1.2l9.711 6.8H7.372L.157 7.949C.065 8.286 0 8.634 0 9v.059L5.628 13H0v2h15V5h-2z"/><path fill="#CF1B2B" d="M21 15V5h-6v10H0v6h15v10h6V21h15v-6z"/></svg>
<svg v-else-if="lang === 'fr'" xmlns="http://www.w3.org/2000/svg" :height="flagHeight" :width="flagWidth" viewBox="0 0 36 36"><path fill="#ED2939" d="M36 27c0 2.209-1.791 4-4 4h-8V5h8c2.209 0 4 1.791 4 4v18z"/><path fill="#002495" d="M4 5C1.791 5 0 6.791 0 9v18c0 2.209 1.791 4 4 4h8V5H4z"/><path fill="#EEE" d="M12 5h12v26H12z"/></svg>
</div>
<small :class="{ 'valid': disabled }">
{{ t(`lang.${lang}`) }}
</small>
</a>
</template>
<style lang="scss" scoped>
a {
display: flex;
align-items: center;
gap: .75ch;
padding-block: .25rem;
padding-inline: .5rem;
border-radius: .25rem;
text-decoration: none;
color: inherit;
line-height: 1;
min-width: 14ch;
&:hover,
&:focus-visible {
background-color: var(--slate-200);
}
small {
font-size: .9em;
}
.valid {
fill: var(--green-500);
color: var(--green-500);
}
&[aria-current="page"] {
pointer-events: none;
cursor: default;
}
}
</style>

View File

@@ -96,7 +96,7 @@ const menus: Menu[] = [
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M228.92,49.69a8,8,0,0,0-6.86-1.45L160.93,63.52,99.58,32.84a8,8,0,0,0-5.52-.6l-64,16A8,8,0,0,0,24,56V200a8,8,0,0,0,9.94,7.76l61.13-15.28,61.35,30.68A8.15,8.15,0,0,0,160,224a8,8,0,0,0,1.94-.24l64-16A8,8,0,0,0,232,200V56A8,8,0,0,0,228.92,49.69ZM96,176a8,8,0,0,0-1.94.24L40,189.75V62.25L95.07,48.48l.93.46Zm120,17.75-55.07,13.77-.93-.46V80a8,8,0,0,0,1.94-.23L216,66.25Z"/></svg>
</button>
</PopoverTrigger>
<PopoverContent side="bottom" :side-offset="6" :collision-padding="10" class="card" style="z-index: 100;">
<PopoverContent side="bottom" :side-offset="6" :collision-padding="16" class="card" style="z-index: 100;">
<PopoverArrow class="card-arrow" />
<template v-for="menu in menus">
@@ -141,15 +141,9 @@ const menus: Menu[] = [
@media screen and (width < 900px) {
position: absolute;
top: 10px;
left: 10px;
}
}
button {
cursor: pointer;
}
.menu-title {
font-size: .9em;
font-weight: 600;
@@ -157,52 +151,6 @@ button {
margin-left: .25rem;
}
.btn-round {
display: grid;
place-items: center;
height: 45px;
aspect-ratio: 1;
background-color: var(--white);
border: 1px solid var(--slate-400);
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
border-radius: 100vmax;
pointer-events: all;
outline: .2rem solid transparent;
transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s;
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95);
svg {
transition-property: fill;
transition-duration: .15s;
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
&:hover,
&:focus-visible {
outline-color: color-mix(in srgb, var(--blue-500) 20%, transparent);
border-color: var(--blue-500);
svg {
fill: var(--blue-500);
}
}
&.active {
color: var(--white);
background-color: var(--blue-500);
border-color: var(--blue-700);
svg {
fill: var(--white);
}
}
}
svg {
width: 1.15em;
}
.map-menu {
margin-top: .25rem;
margin-bottom: .5rem;
@@ -249,6 +197,7 @@ svg {
position: absolute;
top: -0.3rem;
right: -0.3rem;
width: 1.15em;
z-index: 10;
pointer-events: none;

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { useStore } from '@nanostores/vue';
import { onClickOutside, useFocus, useFocusWithin, useLocalStorage, useMagicKeys, whenever } from '@vueuse/core';
import { computed, onMounted, onUpdated, ref, watch } from 'vue';
@@ -7,11 +6,15 @@ import type { MapMarker, MapMarkerGroup, PlayerMarker } from '@/types/Leaflet';
import type { SearchConfig } from '@/types/Map';
import type { SearchMode } from '@/types/Search';
import { t } from '@/i18n/store';
import { currentLang, t } from '@/i18n/store';
import { useStore } from '@nanostores/vue';
import SearchMapSwitch from './SearchMapSwitch.vue';
import SearchMarkersTags from './SearchMarkersTags.vue';
const $currentLang = useStore(currentLang)
const navKey = computed(() => `search-${$currentLang.value}`)
const props = defineProps<{
markers: MapMarker[],
players: PlayerMarker,
@@ -206,7 +209,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
</script>
<template>
<nav ref="searchBarWrapper" class="toolbar">
<nav ref="searchBarWrapper" class="toolbar appear-from-top" :key="navKey">
<SearchMapSwitch />
<div ref="searchBar" class="search-w" :data-focused="shouldBeActive">
@@ -607,22 +610,4 @@ function resetAllFields(actionAfter?: "focusAfter") {
}
}
}
// Animations on load
@media screen and (prefers-reduced-motion: no-preference) {
.toolbar {
animation: fadeIn .2s cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
</style>

View File

@@ -6,6 +6,8 @@ export const defaultLang: Language = 'fr';
export const translations: LanguageDict = {
'fr': {
'lang.fr': 'Français',
'lang.en': 'English',
'common.title': 'Titre',
'common.create': 'Créer',
'common.by': 'par',
@@ -29,6 +31,8 @@ export const translations: LanguageDict = {
'maps.markers.newNotice': "Le marqueur sera sauvegardé mais n'apparaîtra que sur votre carte !"
},
'en': {
'lang.fr': 'Français',
'lang.en': 'English',
'common.title': 'Title',
'common.create': 'Create',
'common.by': 'by',

View File

@@ -1,7 +1,27 @@
import { defaultLang, availableLangs, type Language, type LanguageDict } from './ui';
import { defaultLang, availableLangs, type Language } from './ui';
/**
* This function will get the language from the URL
*
* @param url The URL to get the language from
* @returns The language found in the URL, or the default language if not found
*/
export function getLangFromUrl(url: URL): Language {
const [, lang] = url.pathname.split('/');
if (availableLangs.includes(lang as Language)) return lang as Language;
return defaultLang
}
/**
* This function will generate a new URL with the given language
* It will replace the first part of the path with the new language
* For example, if the URL is /fr/quests and the new language is 'en', the new URL will be /en/quests
*
* @param url The active URL
* @param lang The target language to generate the new URL
*/
export function switchLang(url: URL, lang: Language): URL {
const parts = url.pathname.split('/');
parts[1] = lang;
return new URL(parts.join('/'), url.origin);
}