10 Commits
1.4.5 ... 1.4.6

Author SHA1 Message Date
Alexis
809edc167d Merge branch 'dev' 2025-03-26 16:54:26 +01:00
Alexis
ada061862f Fixed overflowing templates 2025-03-26 15:57:37 +01:00
Alexis
7d60b81763 Fixed oopsie 2025-03-26 15:50:55 +01:00
Alexis
635c58736c Fixed lang button alignment 2025-03-26 15:50:32 +01:00
Alexis
15f18a8f95 Added copy link component 2025-03-26 15:43:20 +01:00
Alexis
3460689d3d Changed marker builder
They're in the DOM now, so much easier to manage
2025-03-23 21:19:13 +01:00
Alexis
78c1991f6a Normalize string function 2025-03-23 18:41:51 +01:00
Alexis
98e65fe6e1 Added point parameter for initial setup 2025-03-23 18:10:38 +01:00
Alexis
769391fad0 Removed unused import 2025-03-23 17:16:58 +01:00
Alexis
8da145039c Version bump 2025-03-23 17:12:28 +01:00
7 changed files with 194 additions and 66 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "leim-maps",
"type": "module",
"version": "1.4.5",
"version": "1.4.6",
"scripts": {
"dev": "astro dev",
"start": "astro dev",

View File

@@ -103,14 +103,14 @@
text-underline-offset: 2px;
text-decoration: underline;
&:hover {
&:hover,
&:focus-visible {
text-decoration: none;
color: var(--green-500);
}
}
.map-link {
margin-top: .5rem;
display: inline-flex;
align-items: center;
gap: .5ch;
@@ -121,9 +121,15 @@
}
}
> * + * {
.tooltip-content > * + * {
margin: 0;
margin-top: .4em;
margin-top: .66em;
}
.tooltip-footer {
display: grid;
gap: .15em;
font-size: .95em;
}
}
}

View File

@@ -0,0 +1,81 @@
---
import { t } from '@/i18n/store';
import { getLangFromUrl } from '@/i18n/utils';
interface Props {
text: string,
}
const { text } = Astro.props
const lang = getLangFromUrl(Astro.url);
const placeholder = t('common.copyLink', lang)
const message = t('common.copiedLink', lang)
---
<button data-target={text} data-copy={placeholder} data-copy-message={message}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M240,88.23a54.43,54.43,0,0,1-16,37L189.25,160a54.27,54.27,0,0,1-38.63,16h-.05A54.63,54.63,0,0,1,96,119.84a8,8,0,0,1,16,.45A38.62,38.62,0,0,0,150.58,160h0a38.39,38.39,0,0,0,27.31-11.31l34.75-34.75a38.63,38.63,0,0,0-54.63-54.63l-11,11A8,8,0,0,1,135.7,59l11-11A54.65,54.65,0,0,1,224,48,54.86,54.86,0,0,1,240,88.23ZM109,185.66l-11,11A38.41,38.41,0,0,1,70.6,208h0a38.63,38.63,0,0,1-27.29-65.94L78,107.31A38.63,38.63,0,0,1,144,135.71a8,8,0,0,0,16,.45A54.86,54.86,0,0,0,144,96a54.65,54.65,0,0,0-77.27,0L32,130.75A54.62,54.62,0,0,0,70.56,224h0a54.28,54.28,0,0,0,38.64-16l11-11A8,8,0,0,0,109,185.66Z"></path></svg>
<span data-copy-text>
{placeholder}
</span>
</button>
<script>
const copyColor = 'initial'
const copyColorSuccess = 'var(--green-500)'
const copyTimeout = 2000
const dataCopyButtons = document.querySelectorAll<HTMLButtonElement>('button[data-copy]')
dataCopyButtons.forEach(button => {
button.addEventListener('click', () => {
const copyText = button.getAttribute('data-target')
navigator.clipboard.writeText(copyText!)
changeCopyText(button)
})
})
function changeCopyText(copyButton: HTMLButtonElement) {
const copyPlaceholder = copyButton.getAttribute('data-copy')
const copyPlaceholderSuccess = copyButton.getAttribute('data-copy-message')
const dataText = copyButton.querySelector<HTMLSpanElement>('[data-copy-text]')
const svg = copyButton.querySelector<SVGElement>('svg')
if (!dataText || !svg) return
dataText.textContent = copyPlaceholderSuccess
dataText.style.color = copyColorSuccess
svg.style.fill = copyColorSuccess
setTimeout(() => {
dataText.textContent = copyPlaceholder
dataText.style.color = copyColor
svg.style.fill = copyColor
}, copyTimeout)
}
</script>
<style lang="scss">
button {
display: inline-flex;
align-items: center;
gap: .5ch;
color: var(--slate-700);
cursor: pointer;
text-underline-offset: 2px;
text-decoration: none;
&:hover,
&:focus-visible {
text-decoration: underline;
}
svg {
width: 1rem;
height: 1rem;
fill: var(--slate-700);
}
}
</style>

View File

@@ -4,7 +4,8 @@ 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';
import { normalizeString } from '@/utils/Strings';
import CopyText from '../global/CopyText.astro';
interface Props extends MapProps {}
@@ -33,6 +34,45 @@ const seeMapText = t('maps.go-to-map', lang)
<MarkerCreator client:only="vue" mapKey={mapKey} />
</div>
<div style="height: 0; width: 0; overflow: hidden; display: none;">
{markers.map((m, index) => (
<div id="popup-template" data-id={`marker-${index}`} class="tooltip-content">
{m.cover && m.coverAuthor && m.coverLink && (
<figure class={m.coverPortrait ? 'portrait' : 'landscape'}>
<img src={`/images/cover/${m.cover}`} alt={m.title} width={m.coverPortrait ? 210 : 420} />
<figcaption>
<cite>{byText} <a href={m.coverLink} target="_blank">{m.coverAuthor}</a></cite>
</figcaption>
</figure>
)}
{m.link ? (
<a href={m.link} target="_blank" class="title">{m.title}</a>
) : (
<strong class="title">{m.title}</strong>
)}
{m.description && <p>{m.description}</p>}
<div class="tooltip-footer">
{m.mapId && (
<div>
<a href={m.mapId} class="map-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="80" y1="112" x2="144" y2="112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="112" cy="112" r="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="168.57" y1="168.57" x2="224" y2="224" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="80" x2="112" y2="144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
<span>
{ seeMapText }
</span>
</a>
</div>
)}
<div>
<CopyText text={`${Astro.url}?p=${normalizeString(m.title)}`} />
</div>
</div>
</div>
))}
</div>
</div>
<script
@@ -51,6 +91,16 @@ const seeMapText = t('maps.go-to-map', lang)
}}
defer
>
/** UTILITIES */
/**
* Normalize a string to be used as a key
* @param str
*/
function normalizeString(str) {
if (!str) return ''
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()
}
/**
* LEAFLET MAP SETUP
*/
@@ -131,6 +181,12 @@ const layerGroups = {
quests: L.layerGroup(),
}
// Prepare initial fly to coords in case of ?p= query param
let initialFlyToMarker = false;
// Get ?p= query param
const urlParams = new URLSearchParams(window.location.search)
const initialPoint = normalizeString(urlParams.get('p'))
/**
* Build all markers and their related info
*/
@@ -204,54 +260,9 @@ for (let i = 0; i < markers.length; i++) {
const marker = L.marker(coords, { icon: markerIconOverride }).addTo(map)
// Build popup content
let popupContent = ""
if (m.cover && m.coverAuthor && m.coverLink) {
const figureClass = m.coverPortrait ? 'portrait' : 'landscape';
const figureWidth = m.coverPortrait ? 210 : 420;
popupContent += `
<figure class="${figureClass}">
<img src="/images/cover/${m.cover}" alt="${m.title}" width="${figureWidth}" />
<figcaption>
<cite>${byText} <a href="${m.coverLink}" target="_blank">${m.coverAuthor}</a></cite>
</figcaption>
</figure>
`
}
if (m.link) {
popupContent += `
<a href="${m.link}" target="_blank" class="title">
${m.title}
</a>
`
} else {
popupContent += `
<strong class="title">
${m.title}
</strong>
`
}
if (m.description) {
popupContent += `<p>${m.description}</p>`
}
if (m.mapId) {
popupContent += `
<a href="${m.mapId}" class="map-link">
<span>
${ seeMapText }
</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="80" y1="112" x2="144" y2="112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="112" cy="112" r="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="168.57" y1="168.57" x2="224" y2="224" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="80" x2="112" y2="144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
</a>
`
}
marker.bindPopup(popupContent).openPopup()
// Get popup content
const popupTemplate = document.querySelector(`#popup-template[data-id="marker-${i}"]`)
marker.bindPopup(popupTemplate)
const displayTooltip = m.group === 'capitals'
@@ -270,6 +281,18 @@ for (let i = 0; i < markers.length; i++) {
marker.openPopup()
}, flyToDuration * 1000)
})
/**
* In case the initial point is set, prepare event name for fly to
*/
// First we parse the marker title to :
// - Change special characters (not accentuated characters) and spaces to hyphens
// - Lowercase the string
const parsedMarkerTitle = normalizeString(m.title)
if (initialPoint && parsedMarkerTitle === initialPoint) {
initialFlyToMarker = m.title
}
}
/**
@@ -572,7 +595,7 @@ function getCoordsSetupFromURL() {
function hasInitialSetup() {
const params = new URL(document.location).searchParams
return params.has('lat') && params.has('lon')
return params.has('lat') && params.has('lon') || params.has('p')
}
/**
@@ -593,16 +616,20 @@ function setupToURL() {
/**
* Uses the URL params to setup the leaflet map
*/
function setupFromURL() {
function setupFromURL(p) {
const setup = getCoordsSetupFromURL()
map.setView({ lat: setup.get('lat'), lng: setup.get('lon') }, setup.get('zoom') | minZoom, { animate: false })
if (setup.has('p')) {
document.dispatchEvent(new CustomEvent(`fly-to-${p}`))
} else {
map.setView({ lat: setup.get('lat'), lng: setup.get('lon') }, setup.get('zoom') | minZoom, { animate: false })
}
}
// Check if the map has initial params in the URL
// If not, just setup the default URL
if (hasInitialSetup()) {
setupFromURL()
setupFromURL(initialFlyToMarker)
} else {
setupToURL()
}

View File

@@ -42,24 +42,26 @@ a {
color: inherit;
line-height: 1;
min-width: 14ch;
font-size: .9em;
&:hover,
&:focus-visible {
background-color: var(--slate-200);
}
small {
font-size: .9em;
&[aria-current="page"] {
pointer-events: none;
cursor: default;
}
.flag {
display: grid;
place-items: center;
}
.valid {
fill: var(--green-500);
color: var(--green-500);
}
&[aria-current="page"] {
pointer-events: none;
cursor: default;
}
}
</style>

View File

@@ -1,4 +1,4 @@
export const availableLangs = ['en', 'fr'] as const;
export const availableLangs = ['fr', 'en'] as const;
export type Language = typeof availableLangs[number];
export type LanguageDict = Record<Language, any>;
@@ -11,6 +11,8 @@ export const translations: LanguageDict = {
'common.title': 'Titre',
'common.create': 'Créer',
'common.by': 'par',
'common.copyLink': 'Copier le lien',
'common.copiedLink': 'Lien copié !',
'continents': 'Continents',
'cities': 'Villes',
'others': 'Autres',
@@ -36,6 +38,8 @@ export const translations: LanguageDict = {
'common.title': 'Title',
'common.create': 'Create',
'common.by': 'by',
'common.copyLink': 'Copy link',
'common.copiedLink': 'Link copied!',
'continents': 'Continents',
'cities': 'Cities',
'others': 'Others',

8
src/utils/Strings.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* Normalize a string to be used as a key
* @param str
*/
export function normalizeString(str: string) {
if (!str) return ''
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()
}