Compare commits
85 Commits
1.4.5
...
53cd12ac4d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53cd12ac4d | ||
|
|
dae2bb7524 | ||
|
|
b06ef38fd7 | ||
|
|
2e0884a6b7 | ||
|
|
d43b7761f0 | ||
|
|
7164ec65b9 | ||
|
|
e7a793a25e | ||
|
|
5198dcf656 | ||
|
|
39a4f91e2e | ||
|
|
ae3bd70e3c | ||
|
|
3b70b878a4 | ||
|
|
232ef4d914 | ||
|
|
df81c74d76 | ||
|
|
d85708a571 | ||
|
|
3911e55a6c | ||
|
|
c09df15821 | ||
|
|
defd3968fd | ||
|
|
0386cc3fe0 | ||
|
|
832c37ef9d | ||
|
|
f2f42c0400 | ||
|
|
d49d5e79f2 | ||
|
|
4209771189 | ||
|
|
e43baa13da | ||
|
|
b243c3129d | ||
|
|
3f60672bb0 | ||
|
|
5792d7d15f | ||
|
|
91ea78c632 | ||
|
|
961fd0d198 | ||
|
|
1cba7b398b | ||
|
|
05d3bca10b | ||
|
|
3acfc6db47 | ||
|
|
6c350bf2d5 | ||
|
|
712d408736 | ||
|
|
bfdd433a5f | ||
|
|
ac6558392c | ||
|
|
484fe3b031 | ||
|
|
c88133a9a3 | ||
|
|
1cb77dd6f0 | ||
|
|
a2fe47210f | ||
|
|
38dfd3c952 | ||
|
|
19100d0881 | ||
|
|
e8ac66b2bd | ||
|
|
464cf334cf | ||
|
|
dd8a506ef3 | ||
|
|
a2253a165f | ||
|
|
dbfce2a737 | ||
|
|
b660c61b81 | ||
|
|
78a16ba907 | ||
|
|
99e66f6feb | ||
|
|
cfe3fe2dcc | ||
|
|
42b3cfd725 | ||
|
|
d3c0a87770 | ||
|
|
3e1d7c5200 | ||
|
|
e893e133cc | ||
|
|
4cf2f66d42 | ||
|
|
f003df7d58 | ||
|
|
355e9ae009 | ||
|
|
ca689c23af | ||
|
|
c282c7ec7a | ||
|
|
0bc65e96c8 | ||
|
|
c6bf77462e | ||
|
|
fb26813932 | ||
|
|
6152b707d1 | ||
|
|
a40bf13d98 | ||
|
|
c68b1baf8c | ||
|
|
199bb98412 | ||
|
|
82aa2f4d35 | ||
|
|
86dbe7ba2b | ||
|
|
8b976e909a | ||
|
|
47f5df936e | ||
|
|
6d281e14ea | ||
|
|
cd99ed2444 | ||
|
|
3e1382c404 | ||
|
|
c84745b35a | ||
|
|
f5a76f9c6e | ||
|
|
809edc167d | ||
|
|
ada061862f | ||
|
|
7d60b81763 | ||
|
|
635c58736c | ||
|
|
15f18a8f95 | ||
|
|
3460689d3d | ||
|
|
78c1991f6a | ||
|
|
98e65fe6e1 | ||
|
|
769391fad0 | ||
|
|
8da145039c |
26
README.md
@@ -1,5 +1,25 @@
|
||||
# Leim Maps
|
||||
Repository for interactive maps using Leim's Wiki data
|
||||
🗺️ A simple Astro static site to display custom maps with Leaflet. Includes sample images with maps from my own world.
|
||||
|
||||
## Demo
|
||||
Live demo available at https://maps.alexcreates.fr
|
||||
## Features
|
||||
* Add your own markers for cities, NPCs, or quests
|
||||
* Search through markers
|
||||
* Create multiple maps for different continents or countries
|
||||
* Measure distances between two points
|
||||
* Custom client-side markers
|
||||
|
||||
## Documentation
|
||||
### Making usable map assets
|
||||
Since we're not using earth maps, we need to build one compatible with Leaflet. I used Zoomify to generate usable tile data from my SVG maps. I don't know if this will fit your use case ; if you use something else, you can contact me !
|
||||
|
||||
> [!IMPORTANT]
|
||||
> As of writing this, it appears Zoomify Unlimited Converter (which is what I used) is not available on Linux or for download anymore
|
||||
|
||||
### Enabling features
|
||||
Some features are able to be toggled on and off. If you don't use multiple languages, or if you don't want the custom markers, you can disable them in the .env file. By default, all of them are true.
|
||||
|
||||
## Contributing
|
||||
I'm not currently accepting pull-requests for this project, but if you have a suggestion, feel free to contact me !
|
||||
|
||||
## Notes
|
||||
As someone with some degree of [aphantasia](https://en.wikipedia.org/wiki/Aphantasia), I struggle with visualization and immersion, and DMing in general. If you find yourself in the same situation, I hope my little thing made your life easier !
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
import { availableLangs, defaultLang } from './src/i18n/ui';
|
||||
import { defineConfig, envField } from 'astro/config';
|
||||
|
||||
import vue from "@astrojs/vue";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [vue(), sitemap()],
|
||||
output: 'static',
|
||||
site: 'https://maps.alexcreates.fr',
|
||||
// i18n: {
|
||||
// locales: availableLangs,
|
||||
// defaultLocale: defaultLang,
|
||||
// routing: {
|
||||
// prefixDefaultLocale: true,
|
||||
// }
|
||||
// }
|
||||
integrations: [vue(), sitemap()],
|
||||
output: 'static',
|
||||
site: 'https://maps.alexcreates.fr',
|
||||
prefetch: true,
|
||||
env: {
|
||||
schema: {
|
||||
ENABLE_TOOLBAR: envField.boolean({ context: "server", access: "secret", default: true }),
|
||||
ENABLE_TOOLBAR_TAGS: envField.boolean({ context: "server", access: "secret", default: true }),
|
||||
ENABLE_CUSTOM_MARKERS: envField.boolean({ context: "server", access: "secret", default: true }),
|
||||
ENABLE_BREADCRUMB: envField.boolean({ context: "server", access: "secret", default: true }),
|
||||
ENABLE_I18N: envField.boolean({ context: "server", access: "secret", default: true }),
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
server: {
|
||||
watch: {
|
||||
ignored: [
|
||||
'**/dist/**', // dist shouldn't be watched
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
22
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "leim-maps",
|
||||
"type": "module",
|
||||
"version": "1.4.5",
|
||||
"version": "1.4.14",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
@@ -10,18 +10,18 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "^3.3.0",
|
||||
"@astrojs/vue": "^5.0.7",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@nanostores/vue": "^0.11.0",
|
||||
"@types/leaflet": "^1.9.17",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"astro": "^5.5.4",
|
||||
"nanostores": "^0.11.4",
|
||||
"@astrojs/sitemap": "^3.7.3",
|
||||
"@astrojs/vue": "^7.0.1",
|
||||
"@nanostores/persistent": "1.3.4",
|
||||
"@nanostores/vue": "^1.1.0",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"astro": "^7.0.7",
|
||||
"nanostores": "^1.4.0",
|
||||
"radix-vue": "^1.9.17",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.39"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.86.0"
|
||||
"sass": "^1.101.0"
|
||||
}
|
||||
}
|
||||
|
||||
5164
pnpm-lock.yaml
generated
10
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
esbuild: true
|
||||
sharp: true
|
||||
vue-demi: true
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- esbuild
|
||||
- sharp
|
||||
- vue-demi
|
||||
1
public/icon/question-mark-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="#FFF"><path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 440 B |
1
public/icon/question-mark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 428 B |
1
public/icon/ruler-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#FFF" viewBox="0 0 256 256"><path d="M235.32,73.37,182.63,20.69a16,16,0,0,0-22.63,0L20.68,160a16,16,0,0,0,0,22.63l52.69,52.68a16,16,0,0,0,22.63,0L235.32,96A16,16,0,0,0,235.32,73.37ZM84.68,224,32,171.31l32-32,26.34,26.35a8,8,0,0,0,11.32-11.32L75.31,128,96,107.31l26.34,26.35a8,8,0,0,0,11.32-11.32L107.31,96,128,75.31l26.34,26.35a8,8,0,0,0,11.32-11.32L139.31,64l32-32L224,84.69Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 461 B |
1
public/icon/ruler.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#020617" viewBox="0 0 256 256"><path d="M235.32,73.37,182.63,20.69a16,16,0,0,0-22.63,0L20.68,160a16,16,0,0,0,0,22.63l52.69,52.68a16,16,0,0,0,22.63,0L235.32,96A16,16,0,0,0,235.32,73.37ZM84.68,224,32,171.31l32-32,26.34,26.35a8,8,0,0,0,11.32-11.32L75.31,128,96,107.31l26.34,26.35a8,8,0,0,0,11.32-11.32L107.31,96,128,75.31l26.34,26.35a8,8,0,0,0,11.32-11.32L139.31,64l32-32L224,84.69Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 464 B |
1
public/icon/square-arrow-out-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M224,104a8,8,0,0,1-13.66,5.66L184,83.31l-42.34,42.35a8,8,0,0,1-11.32-11.32L172.69,72,146.34,45.66A8,8,0,0,1,152,32h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z" fill="#FFF" /></svg>
|
||||
|
After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 29 KiB |
BIN
public/images/cover/Académie_Centrale_Kaldélienne.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 25 KiB |
BIN
public/images/cover/Ambrose.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 34 KiB |
BIN
public/images/cover/Cantane.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 44 KiB |
BIN
public/images/cover/Cordouac.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 82 KiB |
BIN
public/images/cover/Faille_de_Kiam.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 23 KiB |
BIN
public/images/cover/Faranne.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 20 KiB |
BIN
public/images/cover/Fasàch.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 26 KiB |
BIN
public/images/cover/Garràn.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 65 KiB |
BIN
public/images/cover/Horfer.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 80 KiB |
BIN
public/images/cover/Les Mines Blanches.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB |
BIN
public/images/cover/Miverne.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 641 KiB |
BIN
public/images/cover/Plaines_de_poussière.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB |
BIN
public/images/cover/Port-brisé.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB |
BIN
public/images/cover/Port_de_Borélis.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 35 KiB |
BIN
public/images/cover/Rocheverre.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 36 KiB |
BIN
public/images/cover/Soubreciel.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 154 KiB |
BIN
public/images/cover/Tourgrise.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 52 KiB |
BIN
public/images/cover/Tours_des_Cages.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 36 KiB |
BIN
public/images/cover/Willeau.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
134
public/js/leaflet-zoomify.min.js
vendored
@@ -1,130 +1,4 @@
|
||||
/*
|
||||
* L.TileLayer.Zoomify display Zoomify tiles with Leaflet
|
||||
*
|
||||
* Based on the Leaflet.Zoomify (https://github.com/turban/Leaflet.Zoomify)
|
||||
* from turban (https://github.com/turban)
|
||||
*
|
||||
*/
|
||||
|
||||
L.TileLayer.Zoomify = L.TileLayer.extend({
|
||||
options: {
|
||||
width: -1, // Must be set by user, max zoom image width
|
||||
height: -1, // Must be set by user, max zoom image height
|
||||
tileGroupPrefix: 'TileGroup',
|
||||
tilesPerTileGroup: 256
|
||||
},
|
||||
|
||||
initialize: function (url, options) {
|
||||
L.TileLayer.prototype.initialize.call(this, url, options);
|
||||
|
||||
// Replace with automatic loading from ImageProperties.xml
|
||||
if (this.options.width < 0 || this.options.height < 0) {
|
||||
throw new Error('The user must set the Width and Height of the Zoomify image');
|
||||
}
|
||||
},
|
||||
|
||||
beforeAdd: function (map) {
|
||||
var imageSize = L.point(this.options.width, this.options.height);
|
||||
|
||||
// Build the zoom sizes of the pyramid and cache them in an array
|
||||
this._imageSize = [imageSize];
|
||||
this._gridSize = [this._getGridSize(imageSize)];
|
||||
|
||||
// Register the image size in pixels and the grid size in # of tiles for each zoom level
|
||||
while (imageSize.x > this.options.tileSize || imageSize.y > this.options.tileSize) {
|
||||
imageSize = imageSize.divideBy(2).ceil();
|
||||
this._imageSize.push(imageSize);
|
||||
this._gridSize.push(this._getGridSize(imageSize));
|
||||
}
|
||||
|
||||
// We built the cache from bottom to top, but leaflet uses a top to bottom index for the zoomlevel,
|
||||
// so reverse it for easy indexing by current zoomlevel
|
||||
this._imageSize.reverse();
|
||||
this._gridSize.reverse();
|
||||
|
||||
// Register our max supported zoom level
|
||||
var maxNativeZoom = this._gridSize.length - 1;
|
||||
this.options.maxNativeZoom = maxNativeZoom;
|
||||
|
||||
// Register our bounds for this zoomify layer based on the maximum zoom
|
||||
var maxZoomGrid = this._gridSize[maxNativeZoom],
|
||||
maxX = maxZoomGrid.x * this.options.tileSize,
|
||||
maxY = maxZoomGrid.y * this.options.tileSize,
|
||||
southEast = map.unproject([maxX, maxY], maxNativeZoom);
|
||||
this.options.bounds = new L.LatLngBounds([[0, 0], southEast]);
|
||||
|
||||
L.TileLayer.prototype.beforeAdd.call(this, map);
|
||||
},
|
||||
|
||||
// Calculate the grid size for a given image size (based on tile size)
|
||||
_getGridSize: function (imageSize) {
|
||||
var tileSize = this.options.tileSize;
|
||||
return L.point(Math.ceil(imageSize.x / tileSize), Math.ceil(imageSize.y / tileSize));
|
||||
},
|
||||
|
||||
// Extend the add tile function to update our arbitrary sized border tiles
|
||||
_addTile: function (coords, container) {
|
||||
// Load the tile via the original leaflet code
|
||||
L.TileLayer.prototype._addTile.call(this, coords, container);
|
||||
|
||||
// Get out imagesize in pixels for this zoom level and our grid size
|
||||
var imageSize = this._imageSize[this._getZoomForUrl()],
|
||||
gridSize = this._gridSize[this._getZoomForUrl()];
|
||||
|
||||
// The real tile size (default:256) and the display tile size (if zoom > maxNativeZoom)
|
||||
var realTileSize = L.GridLayer.prototype.getTileSize.call(this),
|
||||
displayTileSize = L.TileLayer.prototype.getTileSize.call(this);
|
||||
|
||||
// Get the current tile to adjust
|
||||
var key = this._tileCoordsToKey(coords),
|
||||
tile = this._tiles[key].el;
|
||||
|
||||
// Calculate the required size of the border tiles
|
||||
var scaleFactor = L.point( (imageSize.x % realTileSize.x),
|
||||
(imageSize.y % realTileSize.y)).unscaleBy(realTileSize);
|
||||
|
||||
// Update tile dimensions if we are on a border
|
||||
if ((imageSize.x % realTileSize.x) > 0 && coords.x === gridSize.x - 1) {
|
||||
tile.style.width = displayTileSize.scaleBy(scaleFactor).x + 'px';
|
||||
}
|
||||
|
||||
if ((imageSize.y % realTileSize.y) > 0 && coords.y === gridSize.y - 1) {
|
||||
tile.style.height = displayTileSize.scaleBy(scaleFactor).y + 'px';
|
||||
}
|
||||
},
|
||||
|
||||
// Construct the tile url, by inserting our tilegroup before we template the url
|
||||
getTileUrl: function (coords) {
|
||||
// Set our TileGroup variable, and then let the original tile templater do the work
|
||||
this.options.g = this.options.tileGroupPrefix + this._getTileGroup(coords);
|
||||
|
||||
// Call the original templater
|
||||
return L.TileLayer.prototype.getTileUrl.call(this, coords);
|
||||
},
|
||||
|
||||
// Calculates the TileGroup number, each group contains 256 tiles. The tiles are stored from topleft to bottomright
|
||||
_getTileGroup: function (coords) {
|
||||
var zoom = this._getZoomForUrl(),
|
||||
num = 0,
|
||||
gridSize;
|
||||
|
||||
// Get the total number of tiles from the lowest zoom level to our zoomlevel
|
||||
for (var z = 0; z < zoom; z++) {
|
||||
gridSize = this._gridSize[z];
|
||||
num += gridSize.x * gridSize.y;
|
||||
}
|
||||
|
||||
// Add the remaining tiles from this zoom layer to the running total of tiles
|
||||
num += coords.y * this._gridSize[zoom].x + coords.x;
|
||||
return Math.floor(num / this.options.tilesPerTileGroup);
|
||||
},
|
||||
|
||||
getBounds: function () {
|
||||
return this.options.bounds;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
L.tileLayer.zoomify = function (url, options) {
|
||||
return new L.TileLayer.Zoomify(url, options);
|
||||
};
|
||||
L.TileLayer.Zoomify=L.TileLayer.extend({options:{width:-1,height:-1,tileGroupPrefix:'TileGroup',tilesPerTileGroup:256},initialize:function(url,options){L.TileLayer.prototype.initialize.call(this,url,options);if(this.options.width<0||this.options.height<0){throw new Error('The user must set the Width and Height of the Zoomify image')}},beforeAdd:function(map){var imageSize=L.point(this.options.width,this.options.height);this._imageSize=[imageSize];this._gridSize=[this._getGridSize(imageSize)];while(imageSize.x>this.options.tileSize||imageSize.y>this.options.tileSize){imageSize=imageSize.divideBy(2).ceil();this._imageSize.push(imageSize);this._gridSize.push(this._getGridSize(imageSize))}
|
||||
this._imageSize.reverse();this._gridSize.reverse();var maxNativeZoom=this._gridSize.length-1;this.options.maxNativeZoom=maxNativeZoom;var maxZoomGrid=this._gridSize[maxNativeZoom],maxX=maxZoomGrid.x*this.options.tileSize,maxY=maxZoomGrid.y*this.options.tileSize,southEast=map.unproject([maxX,maxY],maxNativeZoom);this.options.bounds=new L.LatLngBounds([[0,0],southEast]);L.TileLayer.prototype.beforeAdd.call(this,map)},_getGridSize:function(imageSize){var tileSize=this.options.tileSize;return L.point(Math.ceil(imageSize.x/tileSize),Math.ceil(imageSize.y/tileSize))},_addTile:function(coords,container){L.TileLayer.prototype._addTile.call(this,coords,container);var imageSize=this._imageSize[this._getZoomForUrl()],gridSize=this._gridSize[this._getZoomForUrl()];var realTileSize=L.GridLayer.prototype.getTileSize.call(this),displayTileSize=L.TileLayer.prototype.getTileSize.call(this);var key=this._tileCoordsToKey(coords),tile=this._tiles[key].el;var scaleFactor=L.point((imageSize.x%realTileSize.x),(imageSize.y%realTileSize.y)).unscaleBy(realTileSize);if((imageSize.x%realTileSize.x)>0&&coords.x===gridSize.x-1){tile.style.width=displayTileSize.scaleBy(scaleFactor).x+'px'}
|
||||
if((imageSize.y%realTileSize.y)>0&&coords.y===gridSize.y-1){tile.style.height=displayTileSize.scaleBy(scaleFactor).y+'px'}},getTileUrl:function(coords){this.options.g=this.options.tileGroupPrefix+this._getTileGroup(coords);return L.TileLayer.prototype.getTileUrl.call(this,coords)},_getTileGroup:function(coords){var zoom=this._getZoomForUrl(),num=0,gridSize;for(var z=0;z<zoom;z++){gridSize=this._gridSize[z];num+=gridSize.x*gridSize.y}
|
||||
num+=coords.y*this._gridSize[zoom].x+coords.x;return Math.floor(num/this.options.tilesPerTileGroup)},getBounds:function(){return this.options.bounds}});L.tileLayer.zoomify=function(url,options){return new L.TileLayer.Zoomify(url,options)}
|
||||
BIN
public/leaflet/images/marker-icon-2x.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/leaflet/images/marker-icon.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/leaflet/images/marker-shadow.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
49
public/robots.txt
Normal file
@@ -0,0 +1,49 @@
|
||||
User-agent: GPTBot
|
||||
Disallow: /
|
||||
User-agent: ChatGPT-User
|
||||
Disallow: /
|
||||
User-agent: Google-Extended
|
||||
Disallow: /
|
||||
User-agent: PerplexityBot
|
||||
Disallow: /
|
||||
User-agent: Amazonbot
|
||||
Disallow: /
|
||||
User-agent: ClaudeBot
|
||||
Disallow: /
|
||||
User-agent: Omgilibot
|
||||
Disallow: /
|
||||
User-Agent: FacebookBot
|
||||
Disallow: /
|
||||
User-Agent: Applebot
|
||||
Disallow: /
|
||||
User-agent: anthropic-ai
|
||||
Disallow: /
|
||||
User-agent: Bytespider
|
||||
Disallow: /
|
||||
User-agent: Claude-Web
|
||||
Disallow: /
|
||||
User-agent: Diffbot
|
||||
Disallow: /
|
||||
User-agent: ImagesiftBot
|
||||
Disallow: /
|
||||
User-agent: Omgilibot
|
||||
Disallow: /
|
||||
User-agent: Omgili
|
||||
Disallow: /
|
||||
User-agent: YouBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: AhrefsBot
|
||||
Disallow: /
|
||||
User-agent: AhrefsSiteAudit
|
||||
Disallow: /
|
||||
User-agent: SemrushBot
|
||||
Disallow: /
|
||||
User-agent: YandexMarket
|
||||
Disallow: /
|
||||
User-agent: YandexBot
|
||||
Disallow: /
|
||||
User-agent: DataForSeoBot
|
||||
Disallow: /
|
||||
|
||||
Sitemap: https://maps.alexcreates.fr/sitemap-index.xml
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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);
|
||||
animation: fadeIn .2s var(--timing-function);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
gap: .5rem;
|
||||
font-size: .9em;
|
||||
font-weight: 500;
|
||||
color: var(--foreground);
|
||||
border: 1px solid transparent;
|
||||
border-radius: .2rem;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition-property: color, background-color, border-color;
|
||||
transition-duration: .2s;
|
||||
transition-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
@@ -18,40 +19,40 @@
|
||||
|
||||
&.btn-secondary {
|
||||
&:not(:disabled) {
|
||||
background-color: var(--slate-200);
|
||||
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--white);
|
||||
background-color: var(--blue-500);
|
||||
color: var(--background);
|
||||
background-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--slate-500);
|
||||
background-color: var(--slate-100);
|
||||
color: color-mix(in srgb, var(--foreground) 50%, var(--background));
|
||||
background-color: color-mix(in srgb, var(--foreground) 10%, var(--background));
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-info {
|
||||
&:not(:disabled) {
|
||||
background-color: var(--slate-200);
|
||||
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--white);
|
||||
background-color: var(--blue-500);
|
||||
color: var(--background);
|
||||
background-color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
&:not(:disabled) {
|
||||
background-color: var(--slate-200);
|
||||
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--white);
|
||||
color: var(--background);
|
||||
background-color: var(--red-500);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +77,7 @@
|
||||
}
|
||||
|
||||
&.btn-shadow {
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0 .2rem .3rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,41 +86,42 @@
|
||||
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;
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
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);
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
svg {
|
||||
fill: var(--foreground);
|
||||
width: 1.15em;
|
||||
transition-property: fill;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95);
|
||||
transition-timing-function: var(--timing-function);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
outline-color: color-mix(in srgb, var(--blue-500) 20%, transparent);
|
||||
border-color: var(--blue-500);
|
||||
outline-color: color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
border-color: var(--accent);
|
||||
|
||||
svg {
|
||||
fill: var(--blue-500);
|
||||
fill: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--white);
|
||||
background-color: var(--blue-500);
|
||||
border-color: var(--blue-700);
|
||||
color: var(--background);
|
||||
background-color: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent) 75%, var(--background));
|
||||
|
||||
svg {
|
||||
fill: var(--white);
|
||||
fill: var(--background);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
padding-block: 0.75rem;
|
||||
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;
|
||||
color: var(--foreground);
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
min-width: 15rem;
|
||||
|
||||
.card-arrow {
|
||||
fill: var(--white);
|
||||
fill: var(--background);
|
||||
}
|
||||
|
||||
&.p-sm {
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: .4rem .5rem;
|
||||
border: 1px solid var(--slate-300);
|
||||
border: 1px solid color-mix(in srgb, var(--foreground) 30%, var(--background));
|
||||
font-size: .8em;
|
||||
border-radius: .2rem;
|
||||
border-radius: var(--border-radius);
|
||||
transition-property: box-shadow;
|
||||
transition-duration: .2s;
|
||||
transition-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--slate-300) 20%, transparent);
|
||||
box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,142 +10,40 @@
|
||||
.leaflet-map-pane {
|
||||
.leaflet-popup-pane {
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: .25rem;
|
||||
color: var(--foreground);
|
||||
background-color: var(--background);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: clip;
|
||||
|
||||
.leaflet-popup-content {
|
||||
min-width: 20rem;
|
||||
margin: 1rem;
|
||||
|
||||
figure {
|
||||
position: relative;
|
||||
margin-inline: -1.1rem;
|
||||
margin-top: -1.1rem;
|
||||
margin-bottom: .5rem;
|
||||
|
||||
&.landscape {
|
||||
aspect-ratio: 16 / 9;
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
&.portrait {
|
||||
aspect-ratio: 15 / 16;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
// Make a linear gradient from top to bottom, from transparent to black
|
||||
background: linear-gradient(to bottom, transparent 70%, var(--black) 100%);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
}
|
||||
|
||||
figcaption cite {
|
||||
position: absolute;
|
||||
font-size: 85%;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
bottom: .5rem;
|
||||
right: 1rem;
|
||||
color: var(--white);
|
||||
z-index: 20;
|
||||
opacity: .6;
|
||||
transition-property: opacity;
|
||||
transition-duration: .2s;
|
||||
transition-timing-function: cubic-bezier(0.23, 1, 0.320, 1);
|
||||
|
||||
a {
|
||||
padding-right: 0;
|
||||
color: var(--white);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: .15rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--blue-500);
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
figcaption cite {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
strong.title {
|
||||
color: var(--green-500);
|
||||
}
|
||||
|
||||
a.title,
|
||||
a.map-link {
|
||||
text-underline-offset: 2px;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--green-500);
|
||||
}
|
||||
}
|
||||
|
||||
.map-link {
|
||||
margin-top: .5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .5ch;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin: 0;
|
||||
margin-top: .4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
width: .8rem;
|
||||
height: .8rem;
|
||||
background-color: var(--background);
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-tooltip-pane {
|
||||
.leaflet-tooltip {
|
||||
&.capital-name {
|
||||
padding: .2rem .4rem;
|
||||
color: #EEE;
|
||||
color: var(--foreground);
|
||||
font-weight: bold;
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
transition-property: color, text-shadow;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
@include text.stroke(2, #18181b);
|
||||
@include text.stroke(2, var(--background));
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
@@ -155,3 +53,56 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border-color: var(--border);
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
color: var(--foreground);
|
||||
background-color: var(--background);
|
||||
border-color: var(--border);
|
||||
transition-property: color, background-color, border-color, outline-color;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--background);
|
||||
background-color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-measure {
|
||||
color: var(--foreground);
|
||||
background-color: var(--background);
|
||||
transition-property: color, background-color, border-color, outline-color;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
.leaflet-control-measure-toggle {
|
||||
color: var(--foreground);
|
||||
background-color: var(--background);
|
||||
border-color: var(--border);
|
||||
background-image: url('/icon/ruler.svg');
|
||||
background-size: 66%;
|
||||
transition-property: color, background-color, border-color, outline-color;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--background);
|
||||
background-color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.leaflet-touch .leaflet-control-measure {
|
||||
.leaflet-control-measure-toggle {
|
||||
background-image: url('/icon/ruler-dark.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/assets/scss/radix/_dialog.scss
Normal file
@@ -0,0 +1,88 @@
|
||||
.dialog-overlay {
|
||||
background-color: color-mix(in srgb, var(--black) 50%, transparent);
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
animation: show-overlay 150ms var(--timing-function);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 200;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 50rem;
|
||||
max-width: calc(100% - 4rem);
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 25px;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
color: var(--foreground);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
animation: show-content 150ms var(--timing-function);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin-bottom: .5em;
|
||||
font-size: 1.5em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dialog-description {
|
||||
overflow-y: auto;
|
||||
padding-right: 1rem;
|
||||
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
font-size: 1em;
|
||||
color: color-mix(in srgb, var(--foreground) 80%, var(--background));
|
||||
line-height: 1.5;
|
||||
|
||||
> * + * {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: none;
|
||||
color: var(--green-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-overlay {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-content {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -48%) scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
115
src/assets/scss/radix/_toast.scss
Normal file
@@ -0,0 +1,115 @@
|
||||
.toast-viewport {
|
||||
--viewport-padding: 25px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--viewport-padding);
|
||||
gap: 10px;
|
||||
width: 480px;
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
z-index: 2147483647;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.toast-root {
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
display: grid;
|
||||
grid-template-areas: 'title action' 'description action';
|
||||
grid-template-columns: auto max-content;
|
||||
column-gap: 15px;
|
||||
align-items: center;
|
||||
color: var(--foreground);
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--background);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
||||
|
||||
&.toast-success {
|
||||
color: var(--green-700);
|
||||
border: 1px solid var(--green-700);
|
||||
background-color: color-mix(in srgb, var(--green-500) 17.5%, var(--background));
|
||||
|
||||
.toast-icon svg {
|
||||
fill: var(--green-700);
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.toast-icon) {
|
||||
padding-left: 2.2rem;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: .66rem;
|
||||
}
|
||||
}
|
||||
.toast-root[data-state='open'] {
|
||||
animation: slideIn 150ms var(--timing-function);
|
||||
}
|
||||
.toast-root[data-state='closed'] {
|
||||
animation: hide 100ms var(--timing-function);
|
||||
}
|
||||
.toast-root[data-swipe='move'] {
|
||||
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||
}
|
||||
.toast-root[data-swipe='cancel'] {
|
||||
transform: translateX(0);
|
||||
transition: transform 200ms var(--timing-function);
|
||||
}
|
||||
.toast-root[data-swipe='end'] {
|
||||
animation: swipeOut 100ms var(--timing-function);
|
||||
}
|
||||
|
||||
@keyframes hide {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swipeOut {
|
||||
from {
|
||||
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||
}
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
grid-area: title;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: var(--slate-12);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.toast-description {
|
||||
grid-area: description;
|
||||
margin: 0;
|
||||
color: var(--slate-11);
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.toast-action {
|
||||
grid-area: action;
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
@use 'card';
|
||||
@use 'map';
|
||||
|
||||
@use 'radix/toast';
|
||||
@use 'radix/dialog';
|
||||
|
||||
:root {
|
||||
--white: #fff;
|
||||
--black: #111;
|
||||
@@ -34,19 +37,34 @@
|
||||
--orange-700: #c2410c;
|
||||
--purple-500: #8b5cf6;
|
||||
--purple-700: #6d28d9;
|
||||
|
||||
--foreground: var(--black);
|
||||
--background: var(--white);
|
||||
--border: var(--slate-400);
|
||||
--accent: var(--blue-500);
|
||||
|
||||
--border-radius: .25rem;
|
||||
--shadow: color-mix(in srgb, var(--foreground) 10%, transparent) 0px 4px 12px;
|
||||
--timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--foreground: var(--slate-300);
|
||||
--background: var(--slate-950);
|
||||
--border: var(--slate-600);
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
color: var(--slate-900);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
a[href^='http'] {
|
||||
padding-right: 1.25em;
|
||||
white-space: nowrap;
|
||||
orphans: 2;
|
||||
}
|
||||
|
||||
a[href^='http']::after {
|
||||
position: absolute;
|
||||
content: url('/icon/square-arrow-out.svg');
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
@@ -54,6 +72,16 @@ a[href^='http']::after {
|
||||
margin-left: .25em;
|
||||
}
|
||||
|
||||
.dark {
|
||||
a[href^='http']::after {
|
||||
content: url('/icon/square-arrow-out-dark.svg');
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
}
|
||||
81
src/components/global/CopyText.astro
Normal 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: color-mix(in srgb, var(--foreground) 70%, var(--background));
|
||||
cursor: pointer;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
fill: color-mix(in srgb, var(--foreground) 70%, var(--background));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
src/components/global/ThemeSwitch.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
|
||||
import { currentLang, t } from '@/i18n/store';
|
||||
|
||||
const isDark = useDark({ storageKey: "theme", disableTransition: false })
|
||||
const toggleDark = useToggle(isDark)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-theme-toggle id="theme-switcher" class="wrapper appear-from-top">
|
||||
<button @click="toggleDark()" class="btn-round" :title="t('nav.theme.aria-label')">
|
||||
<svg v-if="isDark" xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M120,40V32a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-8-8A8,8,0,0,0,50.34,61.66Zm0,116.68-8,8a8,8,0,0,0,11.32,11.32l8-8a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l8-8a8,8,0,0,0-11.32-11.32l-8,8A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l8,8a8,8,0,0,0,11.32-11.32ZM40,120H32a8,8,0,0,0,0,16h8a8,8,0,0,0,0-16Zm88,88a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,128,208Zm96-88h-8a8,8,0,0,0,0,16h8a8,8,0,0,0,0-16Z"></path></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M233.54,142.23a8,8,0,0,0-8-2,88.08,88.08,0,0,1-109.8-109.8,8,8,0,0,0-10-10,104.84,104.84,0,0,0-52.91,37A104,104,0,0,0,136,224a103.09,103.09,0,0,0,62.52-20.88,104.84,104.84,0,0,0,37-52.91A8,8,0,0,0,233.54,142.23ZM188.9,190.34A88,88,0,0,1,65.66,67.11a89,89,0,0,1,31.4-26A106,106,0,0,0,96,56,104.11,104.11,0,0,0,200,160a106,106,0,0,0,14.92-1.06A89,89,0,0,1,188.9,190.34Z"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.btn-round {
|
||||
position: relative;
|
||||
|
||||
> svg {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import { ENABLE_CUSTOM_MARKERS } from "astro:env/server";
|
||||
|
||||
import type { MapMarker, PlayerMarker } from '@/types/Leaflet';
|
||||
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 MarkerContent from './MarkerContent.astro';
|
||||
|
||||
interface Props extends MapProps {}
|
||||
|
||||
@@ -20,18 +20,23 @@ const {
|
||||
rulerHideWalkDistance = false,
|
||||
backgroundColor = 'transparent',
|
||||
} = Astro.props
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
|
||||
const byText = t('common.by', lang)
|
||||
const seeMapText = t('maps.go-to-map', lang)
|
||||
---
|
||||
|
||||
<div class="map-wrapper">
|
||||
<div class="world-wrapper">
|
||||
<main class="world-wrapper">
|
||||
<div id="world"></div>
|
||||
|
||||
<MarkerCreator client:only="vue" mapKey={mapKey} />
|
||||
|
||||
{ENABLE_CUSTOM_MARKERS &&
|
||||
<MarkerCreator client:only="vue" mapKey={mapKey} />
|
||||
}
|
||||
</main>
|
||||
|
||||
<div style="height: 0; width: 0; overflow: hidden; display: none;">
|
||||
{markers.map((m, index) => (
|
||||
<div id="popup-template" data-id={`marker-${index}`}>
|
||||
<MarkerContent marker={m} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,12 +50,20 @@ const seeMapText = t('maps.go-to-map', lang)
|
||||
mapWidth,
|
||||
rulerDistanceRatio,
|
||||
rulerMainUnit,
|
||||
rulerHideWalkDistance,
|
||||
byText,
|
||||
seeMapText
|
||||
rulerHideWalkDistance
|
||||
}}
|
||||
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 +144,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 +223,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 +244,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 +558,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 +579,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()
|
||||
}
|
||||
@@ -612,6 +602,19 @@ map.on('moveend', () => {
|
||||
setupToURL()
|
||||
})
|
||||
|
||||
/**
|
||||
* Listener for querying a copy of the current coordinates
|
||||
*/
|
||||
window.addEventListener('on-copy-coords', () => {
|
||||
const lon = convertScaleToX(Number(localStorage.getItem('lastHeldXPosition')))
|
||||
const lat = convertScaleToY(Number(localStorage.getItem('lastHeldYPosition')))
|
||||
const coordsText = JSON.stringify({ x: lon, y: lat })
|
||||
|
||||
navigator.clipboard.writeText(coordsText)
|
||||
|
||||
window.dispatchEvent(new CustomEvent('on-toast-push', { bubbles: true, detail: { preset: 'copied-coords' }}))
|
||||
})
|
||||
|
||||
/**
|
||||
* UTILITIES
|
||||
*/
|
||||
@@ -619,7 +622,7 @@ map.on('moveend', () => {
|
||||
* Convert x coords to scale with ratio, zoom and offset
|
||||
* @param {number} x
|
||||
*/
|
||||
function convertXToScale(x) {
|
||||
function convertXToScale(x) {
|
||||
return (x * xRatio * zoomRatio) + xOffset
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
---
|
||||
import { ENABLE_BREADCRUMB, ENABLE_I18N, ENABLE_TOOLBAR, ENABLE_TOOLBAR_TAGS } from "astro:env/server";
|
||||
|
||||
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";
|
||||
import ToastService from "./overlay/ToastService.vue";
|
||||
import ThemeSwitch from "../global/ThemeSwitch.vue";
|
||||
|
||||
interface Props extends MapOverlayProps {}
|
||||
|
||||
@@ -25,16 +29,36 @@ const currentUrl = Astro.url
|
||||
|
||||
<div class="world-overlay">
|
||||
<div class="top-bar">
|
||||
<SearchMarkers client:only="vue" mapKey={mapKey} markers={markers} players={players} searchConfig={searchConfig} >
|
||||
<div slot="fallback" style="height: 45px;"></div>
|
||||
</SearchMarkers>
|
||||
{ENABLE_TOOLBAR &&
|
||||
<SearchMarkers
|
||||
client:only="vue"
|
||||
mapKey={mapKey}
|
||||
markers={markers}
|
||||
players={players}
|
||||
searchConfig={searchConfig}
|
||||
disableTags={!ENABLE_TOOLBAR_TAGS}
|
||||
>
|
||||
<div slot="fallback" style="height: 45px;"></div>
|
||||
</SearchMarkers>
|
||||
}
|
||||
|
||||
<LangSwitcher currentUrl={currentUrl} client:only="vue" />
|
||||
|
||||
<div class="right-menu">
|
||||
{ENABLE_I18N &&
|
||||
<LangSwitcher currentUrl={currentUrl} client:only="vue" />
|
||||
}
|
||||
|
||||
<ThemeSwitch client:only="vue" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MapOverlayBreadcrumbs breadcrumbs={breadcrumbs} />
|
||||
{ENABLE_BREADCRUMB &&
|
||||
<MapOverlayBreadcrumbs breadcrumbs={breadcrumbs} />
|
||||
}
|
||||
</div>
|
||||
|
||||
<ToastService client:only="vue" />
|
||||
|
||||
<style lang="scss">
|
||||
.world-overlay {
|
||||
position: absolute;
|
||||
@@ -53,5 +77,15 @@ const currentUrl = Astro.url
|
||||
align-items: top;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
display: grid;
|
||||
gap: .5rem;
|
||||
align-content: flex-start;
|
||||
|
||||
@media screen and (width >= 900px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,24 +14,24 @@ const { breadcrumbs = [] } = Astro.props
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<li>
|
||||
{index === breadcrumbs.length - 1 &&
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0f172a" viewBox="0 0 256 256" class="folder"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64,123.2,86.4A8,8,0,0,0,128,88h72v16H69.77a16,16,0,0,0-15.18,10.94L40,158.7V64Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0f172a" viewBox="0 0 256 256" class="icon folder"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64,123.2,86.4A8,8,0,0,0,128,88h72v16H69.77a16,16,0,0,0-15.18,10.94L40,158.7V64Z"></path></svg>
|
||||
|
||||
<span class="breadcrumb" aria-current="page">
|
||||
<span class="label">
|
||||
<h1 class="label">
|
||||
{breadcrumb.name}
|
||||
</span>
|
||||
</h1>
|
||||
</span>
|
||||
}
|
||||
{!(index === breadcrumbs.length - 1) &&
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0f172a" viewBox="0 0 256 256" class="folder"><path d="M216,72H131.31L104,44.69A15.86,15.86,0,0,0,92.69,40H40A16,16,0,0,0,24,56V200.62A15.4,15.4,0,0,0,39.38,216H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72ZM40,56H92.69l16,16H40ZM216,200H40V88H216Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0f172a" viewBox="0 0 256 256" class="icon folder"><path d="M216,72H131.31L104,44.69A15.86,15.86,0,0,0,92.69,40H40A16,16,0,0,0,24,56V200.62A15.4,15.4,0,0,0,39.38,216H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72ZM40,56H92.69l16,16H40ZM216,200H40V88H216Z"></path></svg>
|
||||
|
||||
<a href={breadcrumb.url} class="breadcrumb" class:list={{ active: index === breadcrumbs.length - 1 }}>
|
||||
<a href={breadcrumb.url} class="breadcrumb" class:list={{ active: index === breadcrumbs.length - 1 }} data-astro-prefetch="hover">
|
||||
<span class="label">
|
||||
{breadcrumb.name}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="#0f172a" viewBox="0 0 256 256"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="#0f172a" viewBox="0 0 256 256" class="icon"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path></svg>
|
||||
}
|
||||
</li>
|
||||
))}
|
||||
@@ -48,17 +48,25 @@ menu {
|
||||
margin-top: .5rem;
|
||||
padding-block: .25rem;
|
||||
padding-inline: .8rem;
|
||||
border: 1px solid var(--slate-400);
|
||||
background: var(--white);
|
||||
color: var(--foreground);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--background);
|
||||
border-radius: 100vmax;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
|
||||
pointer-events: all;
|
||||
transition-property: color, background-color, border-color, outline-color;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .25ch;
|
||||
|
||||
svg {
|
||||
fill: var(--foreground);
|
||||
}
|
||||
|
||||
&:has(a:hover, a:focus-visible) {
|
||||
svg.folder {
|
||||
fill: var(--blue-500);
|
||||
@@ -83,7 +91,7 @@ menu {
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.1em;
|
||||
fill: var(--foreground);
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
176
src/components/maps/MarkerContent.astro
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
import type { MapMarker } from '@/types/Leaflet';
|
||||
import { normalizeString } from '@/utils/Strings';
|
||||
import { getLangFromUrl } from '@/i18n/utils';
|
||||
import { t } from '@/i18n/store';
|
||||
|
||||
import CopyText from '@/components/global/CopyText.astro';
|
||||
|
||||
interface Props {
|
||||
marker: MapMarker
|
||||
}
|
||||
|
||||
const { marker: m } = Astro.props as Props;
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
|
||||
const byText = t('common.by', lang)
|
||||
const seeMapText = t('maps.go-to-map', lang)
|
||||
---
|
||||
|
||||
<div 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>
|
||||
<span class="author">
|
||||
<span>{byText}</span>
|
||||
<cite>
|
||||
<a href={m.coverLink} target="_blank">{m.coverAuthor}</a>
|
||||
</cite>
|
||||
</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
)}
|
||||
{m.link ? (
|
||||
<a href={m.link} target="_blank" class="title">{m.title}</a>
|
||||
) : (
|
||||
<strong class="title">{m.title}</strong>
|
||||
)}
|
||||
{m.description && <p set:html={m.description} />}
|
||||
|
||||
<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>
|
||||
|
||||
<style lang="scss">
|
||||
figure {
|
||||
position: relative;
|
||||
margin-inline: -1.1rem;
|
||||
margin-top: -1.1rem;
|
||||
margin-bottom: .5rem;
|
||||
|
||||
&.landscape {
|
||||
aspect-ratio: 16 / 9;
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
&.portrait {
|
||||
aspect-ratio: 15 / 16;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
// Make a linear gradient from top to bottom, from transparent to black
|
||||
background: linear-gradient(to bottom, transparent 70%, var(--black) 100%);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
}
|
||||
|
||||
figcaption .author {
|
||||
position: absolute;
|
||||
font-size: 85%;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
bottom: .5rem;
|
||||
right: 1rem;
|
||||
color: var(--white);
|
||||
z-index: 20;
|
||||
opacity: .6;
|
||||
transition-property: opacity;
|
||||
transition-duration: .2s;
|
||||
transition-timing-function: cubic-bezier(0.23, 1, 0.320, 1);
|
||||
|
||||
a {
|
||||
padding-right: 0;
|
||||
color: var(--white);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: .15rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--blue-500);
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
figcaption .author {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
strong.title {
|
||||
color: var(--green-500);
|
||||
}
|
||||
|
||||
a.title,
|
||||
a.map-link {
|
||||
text-underline-offset: 2px;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: none;
|
||||
color: var(--green-500);
|
||||
}
|
||||
}
|
||||
|
||||
.map-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .5ch;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-content > * + * {
|
||||
margin: 0;
|
||||
margin-top: .66em;
|
||||
}
|
||||
|
||||
.tooltip-footer {
|
||||
display: grid;
|
||||
gap: .15em;
|
||||
font-size: .95em;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
"x": 83.6,
|
||||
"y": -185.2
|
||||
},
|
||||
"cover": "Ambrose.jpg",
|
||||
"cover": "Ambrose.webp",
|
||||
"coverAuthor": "Claudio Pilia",
|
||||
"coverLink": "https://pilia-art.com/portfolio-item/game-of-thrones-the-city-of-tyrosh/"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
"x": 133.46875,
|
||||
"y": -146.0625
|
||||
},
|
||||
"cover": "Cordouac.jpg",
|
||||
"cover": "Cordouac.webp",
|
||||
"coverAuthor": "Vincent Joyal",
|
||||
"coverLink": "https://www.artstation.com/artwork/6B0b0"
|
||||
},
|
||||
@@ -45,7 +45,7 @@
|
||||
"x": 132.26953,
|
||||
"y": -98.19141
|
||||
},
|
||||
"cover": "Tourgrise.png",
|
||||
"cover": "Tourgrise.webp",
|
||||
"coverAuthor": "saya kuroha",
|
||||
"coverLink": "https://www.pixiv.net/en/artworks/83793821",
|
||||
"coverPortrait": true
|
||||
@@ -79,7 +79,7 @@
|
||||
"x": 168.39491,
|
||||
"y": -109.13376
|
||||
},
|
||||
"cover": "Port-brisé.jpg",
|
||||
"cover": "Port-brisé.webp",
|
||||
"coverAuthor": "JC Jongwon Park",
|
||||
"coverLink": "https://www.artstation.com/artwork/okmQL"
|
||||
},
|
||||
@@ -122,7 +122,7 @@
|
||||
"x": 204.42515,
|
||||
"y": -226.85883
|
||||
},
|
||||
"cover": "Soubreciel.jpg",
|
||||
"cover": "Soubreciel.webp",
|
||||
"coverAuthor": "Tony Andreas Rudolph",
|
||||
"coverLink": "https://www.artstation.com/artwork/EB84"
|
||||
},
|
||||
@@ -166,7 +166,7 @@
|
||||
"y": -51.54687
|
||||
},
|
||||
"mapId": "/en/aldys/borelis",
|
||||
"cover": "Port_de_Borélis.jpeg",
|
||||
"cover": "Port_de_Borélis.webp",
|
||||
"coverAuthor": "Anton Bezrukov",
|
||||
"coverLink": "https://www.artstation.com/artwork/oODL5B"
|
||||
},
|
||||
@@ -210,7 +210,7 @@
|
||||
"y": -210.87224307207197
|
||||
},
|
||||
"mapId": "/en/aldys/cantane",
|
||||
"cover": "Cantane.jpg",
|
||||
"cover": "Cantane.webp",
|
||||
"coverAuthor": "Andreas Roller",
|
||||
"coverLink": "https://en.wikipedia.org/wiki/Andreas_Roller"
|
||||
},
|
||||
@@ -223,7 +223,7 @@
|
||||
"y": -210.9293951755887
|
||||
},
|
||||
"mapId": "/en/aldys/cantane/mines-blanches",
|
||||
"cover": "Les Mines Blanches.png",
|
||||
"cover": "Les Mines Blanches.webp",
|
||||
"coverAuthor": "MeckanicalMind",
|
||||
"coverLink": "https://www.deviantart.com/meckanicalmind/art/R3-Mines-258469134"
|
||||
},
|
||||
@@ -236,7 +236,7 @@
|
||||
"x": 185.57431,
|
||||
"y": -229.08588
|
||||
},
|
||||
"cover": "Horfer.jpg",
|
||||
"cover": "Horfer.webp",
|
||||
"coverAuthor": "Nele-Diel",
|
||||
"coverLink": "https://www.deviantart.com/nele-diel/art/City-in-the-Lowlands-693529688"
|
||||
},
|
||||
@@ -519,7 +519,7 @@
|
||||
"x": 121.47116769539711,
|
||||
"y": -94.66997667550434
|
||||
},
|
||||
"cover": "Faille_de_Kiam.jpg",
|
||||
"cover": "Faille_de_Kiam.webp",
|
||||
"coverAuthor": "Laura Sava",
|
||||
"coverLink": "https://www.behance.net/gallery/16130223/Zariels-Doom",
|
||||
"coverPortrait": true
|
||||
@@ -573,7 +573,7 @@
|
||||
"x": 107.50352,
|
||||
"y": -67.36631
|
||||
},
|
||||
"cover": "Willeau.jpg",
|
||||
"cover": "Willeau.webp",
|
||||
"coverAuthor": "Inconnu",
|
||||
"coverLink": "https://www.hebus.com/image-339033.html"
|
||||
},
|
||||
@@ -635,7 +635,7 @@
|
||||
"x": 168.31641,
|
||||
"y": -60.54297
|
||||
},
|
||||
"cover": "Tours_des_Cages.jpg",
|
||||
"cover": "Tours_des_Cages.webp",
|
||||
"coverAuthor": "25kartinok",
|
||||
"coverLink": "https://www.deviantart.com/25kartinok/art/the-dark-tower-700231527",
|
||||
"coverPortrait": true
|
||||
@@ -677,7 +677,7 @@
|
||||
"x": 159.57688930332125,
|
||||
"y": -157.4739168466964
|
||||
},
|
||||
"cover": "Plaines_de_poussière.png",
|
||||
"cover": "Plaines_de_poussière.webp",
|
||||
"coverAuthor": "Inconnu",
|
||||
"coverLink": "https://www.pinterest.com/pin/pin-de-soup-em-inspiration--279223245638830461"
|
||||
},
|
||||
@@ -710,7 +710,7 @@
|
||||
"x": 285.0990986858211,
|
||||
"y": -146.08986069534564
|
||||
},
|
||||
"cover": "Garràn.jpg",
|
||||
"cover": "Garràn.webp",
|
||||
"coverAuthor": "Dmitry Dubinsky",
|
||||
"coverLink": "https://elhour.artstation.com/projects/RYynYm?album_id=47640"
|
||||
},
|
||||
@@ -773,7 +773,7 @@
|
||||
"x": 326.7497076703842,
|
||||
"y": -206.67114645854178
|
||||
},
|
||||
"cover": "Fasàch.jpg",
|
||||
"cover": "Fasàch.webp",
|
||||
"coverAuthor": "Liang Mark",
|
||||
"coverLink": "https://www.artstation.com/artwork/Jll2Jd"
|
||||
},
|
||||
@@ -886,7 +886,7 @@
|
||||
"x": 83.80737652386473,
|
||||
"y": -184.52963682119173
|
||||
},
|
||||
"cover": "Académie_Centrale_Kaldélienne.jpg",
|
||||
"cover": "Académie_Centrale_Kaldélienne.webp",
|
||||
"coverAuthor": "Pengzhen Zhang",
|
||||
"coverLink": "https://www.artstation.com/artwork/zAOWNd",
|
||||
"icon": "graduation-cap"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"x": 119.6667886200924,
|
||||
"y": -17.945469721492717
|
||||
},
|
||||
"cover": "Faranne.jpg",
|
||||
"cover": "Faranne.webp",
|
||||
"coverAuthor": "Pablo Mendoza Peces",
|
||||
"coverLink": "https://www.artstation.com/artwork/dAJr1"
|
||||
},
|
||||
@@ -32,7 +32,7 @@
|
||||
"x": 113.52851847725508,
|
||||
"y": -19.882939646724424
|
||||
},
|
||||
"cover": "Miverne.jpg",
|
||||
"cover": "Miverne.webp",
|
||||
"coverAuthor": "Andrei-Pervukhin",
|
||||
"coverLink": "https://www.deviantart.com/andrei-pervukhin/art/Medieval-Village-383779337"
|
||||
},
|
||||
@@ -75,7 +75,7 @@
|
||||
"x": 122.87040892704968,
|
||||
"y": -58.7910005354329
|
||||
},
|
||||
"cover": "Rocheverre.jpg",
|
||||
"cover": "Rocheverre.webp",
|
||||
"coverAuthor": "Creative Intelligence Arts",
|
||||
"coverLink": "https://www.kickstarter.com/projects/1300298569/project-phoenix-japans-indie-rpg-feat-aaa-talent"
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"x": 83.6,
|
||||
"y": -185.2
|
||||
},
|
||||
"cover": "Ambrose.jpg",
|
||||
"cover": "Ambrose.webp",
|
||||
"coverAuthor": "Claudio Pilia",
|
||||
"coverLink": "https://pilia-art.com/portfolio-item/game-of-thrones-the-city-of-tyrosh/"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
"x": 133.46875,
|
||||
"y": -146.0625
|
||||
},
|
||||
"cover": "Cordouac.jpg",
|
||||
"cover": "Cordouac.webp",
|
||||
"coverAuthor": "Vincent Joyal",
|
||||
"coverLink": "https://www.artstation.com/artwork/6B0b0"
|
||||
},
|
||||
@@ -45,7 +45,7 @@
|
||||
"x": 132.26953,
|
||||
"y": -98.19141
|
||||
},
|
||||
"cover": "Tourgrise.png",
|
||||
"cover": "Tourgrise.webp",
|
||||
"coverAuthor": "saya kuroha",
|
||||
"coverLink": "https://www.pixiv.net/en/artworks/83793821",
|
||||
"coverPortrait": true
|
||||
@@ -79,7 +79,7 @@
|
||||
"x": 168.39491,
|
||||
"y": -109.13376
|
||||
},
|
||||
"cover": "Port-brisé.jpg",
|
||||
"cover": "Port-brisé.webp",
|
||||
"coverAuthor": "JC Jongwon Park",
|
||||
"coverLink": "https://www.artstation.com/artwork/okmQL"
|
||||
},
|
||||
@@ -122,7 +122,7 @@
|
||||
"x": 204.42515,
|
||||
"y": -226.85883
|
||||
},
|
||||
"cover": "Soubreciel.jpg",
|
||||
"cover": "Soubreciel.webp",
|
||||
"coverAuthor": "Tony Andreas Rudolph",
|
||||
"coverLink": "https://www.artstation.com/artwork/EB84"
|
||||
},
|
||||
@@ -166,7 +166,7 @@
|
||||
"y": -51.54687
|
||||
},
|
||||
"mapId": "/fr/aldys/borelis",
|
||||
"cover": "Port_de_Borélis.jpeg",
|
||||
"cover": "Port_de_Borélis.webp",
|
||||
"coverAuthor": "Anton Bezrukov",
|
||||
"coverLink": "https://www.artstation.com/artwork/oODL5B"
|
||||
},
|
||||
@@ -210,7 +210,7 @@
|
||||
"y": -210.87224307207197
|
||||
},
|
||||
"mapId": "/fr/aldys/cantane",
|
||||
"cover": "Cantane.jpg",
|
||||
"cover": "Cantane.webp",
|
||||
"coverAuthor": "Andreas Roller",
|
||||
"coverLink": "https://en.wikipedia.org/wiki/Andreas_Roller"
|
||||
},
|
||||
@@ -223,7 +223,7 @@
|
||||
"y": -210.9293951755887
|
||||
},
|
||||
"mapId": "/fr/aldys/cantane/mines-blanches",
|
||||
"cover": "Les Mines Blanches.png",
|
||||
"cover": "Les Mines Blanches.webp",
|
||||
"coverAuthor": "MeckanicalMind",
|
||||
"coverLink": "https://www.deviantart.com/meckanicalmind/art/R3-Mines-258469134"
|
||||
},
|
||||
@@ -236,7 +236,7 @@
|
||||
"x": 185.57431,
|
||||
"y": -229.08588
|
||||
},
|
||||
"cover": "Horfer.jpg",
|
||||
"cover": "Horfer.webp",
|
||||
"coverAuthor": "Nele-Diel",
|
||||
"coverLink": "https://www.deviantart.com/nele-diel/art/City-in-the-Lowlands-693529688"
|
||||
},
|
||||
@@ -519,7 +519,7 @@
|
||||
"x": 121.47116769539711,
|
||||
"y": -94.66997667550434
|
||||
},
|
||||
"cover": "Faille_de_Kiam.jpg",
|
||||
"cover": "Faille_de_Kiam.webp",
|
||||
"coverAuthor": "Laura Sava",
|
||||
"coverLink": "https://www.behance.net/gallery/16130223/Zariels-Doom",
|
||||
"coverPortrait": true
|
||||
@@ -573,7 +573,7 @@
|
||||
"x": 107.50352,
|
||||
"y": -67.36631
|
||||
},
|
||||
"cover": "Willeau.jpg",
|
||||
"cover": "Willeau.webp",
|
||||
"coverAuthor": "Inconnu",
|
||||
"coverLink": "https://www.hebus.com/image-339033.html"
|
||||
},
|
||||
@@ -635,7 +635,7 @@
|
||||
"x": 168.31641,
|
||||
"y": -60.54297
|
||||
},
|
||||
"cover": "Tours_des_Cages.jpg",
|
||||
"cover": "Tours_des_Cages.webp",
|
||||
"coverAuthor": "25kartinok",
|
||||
"coverLink": "https://www.deviantart.com/25kartinok/art/the-dark-tower-700231527",
|
||||
"coverPortrait": true
|
||||
@@ -677,7 +677,7 @@
|
||||
"x": 159.57688930332125,
|
||||
"y": -157.4739168466964
|
||||
},
|
||||
"cover": "Plaines_de_poussière.png",
|
||||
"cover": "Plaines_de_poussière.webp",
|
||||
"coverAuthor": "Inconnu",
|
||||
"coverLink": "https://www.pinterest.com/pin/pin-de-soup-em-inspiration--279223245638830461"
|
||||
},
|
||||
@@ -710,7 +710,7 @@
|
||||
"x": 285.0990986858211,
|
||||
"y": -146.08986069534564
|
||||
},
|
||||
"cover": "Garràn.jpg",
|
||||
"cover": "Garràn.webp",
|
||||
"coverAuthor": "Dmitry Dubinsky",
|
||||
"coverLink": "https://elhour.artstation.com/projects/RYynYm?album_id=47640"
|
||||
},
|
||||
@@ -773,7 +773,7 @@
|
||||
"x": 326.7497076703842,
|
||||
"y": -206.67114645854178
|
||||
},
|
||||
"cover": "Fasàch.jpg",
|
||||
"cover": "Fasàch.webp",
|
||||
"coverAuthor": "Liang Mark",
|
||||
"coverLink": "https://www.artstation.com/artwork/Jll2Jd"
|
||||
},
|
||||
@@ -886,7 +886,7 @@
|
||||
"x": 83.80737652386473,
|
||||
"y": -184.52963682119173
|
||||
},
|
||||
"cover": "Académie_Centrale_Kaldélienne.jpg",
|
||||
"cover": "Académie_Centrale_Kaldélienne.webp",
|
||||
"coverAuthor": "Pengzhen Zhang",
|
||||
"coverLink": "https://www.artstation.com/artwork/zAOWNd",
|
||||
"icon": "graduation-cap"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"x": 119.6667886200924,
|
||||
"y": -17.945469721492717
|
||||
},
|
||||
"cover": "Faranne.jpg",
|
||||
"cover": "Faranne.webp",
|
||||
"coverAuthor": "Pablo Mendoza Peces",
|
||||
"coverLink": "https://www.artstation.com/artwork/dAJr1"
|
||||
},
|
||||
@@ -32,7 +32,7 @@
|
||||
"x": 113.52851847725508,
|
||||
"y": -19.882939646724424
|
||||
},
|
||||
"cover": "Miverne.jpg",
|
||||
"cover": "Miverne.webp",
|
||||
"coverAuthor": "Andrei-Pervukhin",
|
||||
"coverLink": "https://www.deviantart.com/andrei-pervukhin/art/Medieval-Village-383779337"
|
||||
},
|
||||
@@ -75,7 +75,7 @@
|
||||
"x": 122.87040892704968,
|
||||
"y": -58.7910005354329
|
||||
},
|
||||
"cover": "Rocheverre.jpg",
|
||||
"cover": "Rocheverre.webp",
|
||||
"coverAuthor": "Creative Intelligence Arts",
|
||||
"coverLink": "https://www.kickstarter.com/projects/1300298569/project-phoenix-japans-indie-rpg-feat-aaa-talent"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PopoverArrow, PopoverContent, PopoverRoot, PopoverTrigger } from 'radix
|
||||
import LangSwitcherButton from './LangSwitcherButton.vue';
|
||||
|
||||
import { useStore } from '@nanostores/vue';
|
||||
import { currentLang } from '@/i18n/store';
|
||||
import { currentLang, t } from '@/i18n/store';
|
||||
import { availableLangs } from '@/i18n/ui';
|
||||
|
||||
const $currentLang = useStore(currentLang);
|
||||
@@ -22,7 +22,7 @@ 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 wrapperHovered = useElementHover(wrapper, { delayEnter: 100, delayLeave: 500 })
|
||||
const { focused: buttonFocused } = useFocus(buttonRef)
|
||||
|
||||
const isMenuOpen = computed(() => menuModel.value || wrapperHovered.value)
|
||||
@@ -39,7 +39,7 @@ watch([wrapperHovered, buttonFocused], (value) => {
|
||||
<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 }">
|
||||
<button ref="buttonRef" class="btn-round" :title="t('nav.lang.aria-label')" :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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Language } from '@/i18n/ui';
|
||||
import { useUrlSearchParams } from '@vueuse/core';
|
||||
|
||||
import type { Language } from '@/i18n/ui';
|
||||
import { t } from '@/i18n/store';
|
||||
import { switchLang } from '@/i18n/utils';
|
||||
|
||||
@@ -12,10 +13,12 @@ defineProps<{
|
||||
|
||||
const flagWidth = 18;
|
||||
const flagHeight = 18;
|
||||
|
||||
const { lat, lon, zoom } = useUrlSearchParams<{ lat: number; lon: number; zoom: number }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :href="switchLang(currentUrl, lang).pathname" :aria-disabled="disabled" :aria-current="disabled ? 'page' : 'false'" :tabindex="disabled ? -1 : 0">
|
||||
<a :href="`${switchLang(currentUrl, lang).pathname}?lat=${lat}&lon=${lon}&zoom=${zoom}`" :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>
|
||||
|
||||
@@ -42,24 +45,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;
|
||||
}
|
||||
|
||||
.valid {
|
||||
fill: var(--green-500);
|
||||
color: var(--green-500);
|
||||
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
}
|
||||
|
||||
&[aria-current="page"] {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.flag {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.valid {
|
||||
fill: var(--green-500);
|
||||
color: var(--green-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -36,6 +36,7 @@ function switchMenuMode(newMode: MenuMode) {
|
||||
|
||||
onMounted(() => {
|
||||
const mapRef = document.getElementById('world')
|
||||
|
||||
if (mapRef) {
|
||||
mapRef.addEventListener('contextmenu', handleContextMenu)
|
||||
mapRef.addEventListener('mousedown', hideMenu)
|
||||
@@ -188,6 +189,11 @@ function hideMarkerModal() {
|
||||
function setTitleError(error: Error | null) {
|
||||
markerTitleInputError.value = error
|
||||
}
|
||||
|
||||
function handleCoordsCopy() {
|
||||
markerMenu.value?.dispatchEvent(new CustomEvent('on-copy-coords', { bubbles: true }))
|
||||
hideMenu()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -196,7 +202,16 @@ function setTitleError(error: Error | null) {
|
||||
<menu v-show="shouldBeShown" ref="markerMenu">
|
||||
<li>
|
||||
<button @click="switchMenuMode('editing-marker')">
|
||||
{{ t('maps.markers.new') }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256"><path d="M128,16a88.1,88.1,0,0,0-88,88c0,31.4,14.51,64.68,42,96.25a254.19,254.19,0,0,0,41.45,38.3,8,8,0,0,0,9.18,0A254.19,254.19,0,0,0,174,200.25c27.45-31.57,42-64.85,42-96.25A88.1,88.1,0,0,0,128,16Zm32,96H136v24a8,8,0,0,1-16,0V112H96a8,8,0,0,1,0-16h24V72a8,8,0,0,1,16,0V96h24a8,8,0,0,1,0,16Z"></path></svg>
|
||||
|
||||
<span>{{ t('maps.markers.new') }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="handleCoordsCopy">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32Zm-8,128H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
|
||||
|
||||
<span>{{ t('maps.copyCoords') }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</menu>
|
||||
@@ -260,10 +275,11 @@ menu {
|
||||
padding-block: .25rem;
|
||||
top: calc(var(--mouse-y) - .5rem);
|
||||
left: calc(var(--mouse-x) + .75rem);
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--slate-400);
|
||||
color: var(--foreground);
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: .3rem;
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 12px;
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 1000;
|
||||
transition-property: top, left;
|
||||
transition-duration: .16s;
|
||||
@@ -271,14 +287,27 @@ menu {
|
||||
|
||||
li {
|
||||
a, button {
|
||||
display: block;
|
||||
display: flex;
|
||||
gap: .75ch;
|
||||
align-items: center;
|
||||
padding: .33rem 1rem;
|
||||
padding-left: .66rem;
|
||||
font-size: .77em;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
svg {
|
||||
fill: var(--foreground);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: var(--slate-100);
|
||||
color: color-mix(in srgb, var(--accent) 90%, var(--background));
|
||||
background-color: color-mix(in srgb, var(--accent) 10%, var(--background));
|
||||
|
||||
svg {
|
||||
fill: color-mix(in srgb, var(--accent) 75%, var(--background));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,10 +318,11 @@ dialog {
|
||||
max-width: calc(100% - 6rem);
|
||||
padding: 1.5rem 2rem;
|
||||
z-index: 9999;
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--slate-400);
|
||||
color: var(--foreground);
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: .75rem;
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 12px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: visible;
|
||||
transform: translateY(1rem);
|
||||
transition: visibility 0s ease-out .3s, opacity .3s ease-out, transform .3s ease-out;
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUpdated, ref, watch } from 'vue';
|
||||
import { computed, onUpdated, ref, watch } from 'vue'
|
||||
import { useElementHover, useFocus } from '@vueuse/core'
|
||||
import { PopoverArrow, PopoverContent, PopoverRoot, PopoverTrigger } from 'radix-vue'
|
||||
import { useStore } from '@nanostores/vue';
|
||||
import {
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverRoot,
|
||||
PopoverTrigger,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from 'radix-vue'
|
||||
|
||||
import { currentLang } from '@/i18n/store';
|
||||
import { t } from '@/i18n/store';
|
||||
import { useStore } from '@nanostores/vue'
|
||||
|
||||
const $currentLang = useStore(currentLang);
|
||||
import { currentLang } from '@/i18n/store'
|
||||
import { t } from '@/i18n/store'
|
||||
|
||||
const navModel = ref(false);
|
||||
const $currentLang = useStore(currentLang)
|
||||
|
||||
const navModel = ref(false)
|
||||
const legalModelOpened = ref(false)
|
||||
|
||||
const wrapper = ref<HTMLDivElement | null>(null)
|
||||
const buttonRef = ref<HTMLButtonElement | null>(null)
|
||||
const wrapperHovered = useElementHover(wrapper, { delayEnter: 500, delayLeave: 500 })
|
||||
const wrapperHovered = useElementHover(wrapper, { delayEnter: 100, delayLeave: 500 })
|
||||
const { focused: buttonFocused } = useFocus(buttonRef)
|
||||
|
||||
const isNavOpened = computed(() => navModel.value || wrapperHovered.value)
|
||||
const isNavOpened = computed(() => navModel.value || wrapperHovered.value || legalModelOpened.value )
|
||||
|
||||
watch([wrapperHovered, buttonFocused], (value) => {
|
||||
// Check if all values from array are false
|
||||
@@ -25,7 +39,7 @@ watch([wrapperHovered, buttonFocused], (value) => {
|
||||
}
|
||||
})
|
||||
|
||||
let url: URL | null = null;
|
||||
let url: URL | null = null
|
||||
|
||||
onUpdated(() => {
|
||||
url = new URL(window.location.href)
|
||||
@@ -80,8 +94,7 @@ const menus: Menu[] = [
|
||||
{
|
||||
name: 'Mines Blanches',
|
||||
img: '/images/aldys-cantane-mines-blanches-cover.png',
|
||||
url: `/${$currentLang.value}/aldys/cantane/mines-blanches`,
|
||||
newItem: true
|
||||
url: `/${$currentLang.value}/aldys/cantane/mines-blanches`
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -92,7 +105,7 @@ const menus: Menu[] = [
|
||||
<div class="wrapper" ref="wrapper">
|
||||
<PopoverRoot :open="isNavOpened" @update:open="() => navModel = true">
|
||||
<PopoverTrigger as-child>
|
||||
<button ref="buttonRef" class="btn-round" :class="{ 'active': isNavOpened }">
|
||||
<button ref="buttonRef" class="btn-round" :title="t('nav.map.aria-label')" :class="{ 'active': isNavOpened }">
|
||||
<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>
|
||||
@@ -130,6 +143,26 @@ const menus: Menu[] = [
|
||||
</li>
|
||||
</menu>
|
||||
</template>
|
||||
|
||||
<div class="legal">
|
||||
<DialogRoot v-model:open="legalModelOpened">
|
||||
<DialogTrigger as-child>
|
||||
<button :href="`/${$currentLang}/legal`">
|
||||
{{ t('legal.cta') }}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="dialog-overlay" />
|
||||
|
||||
<DialogContent class="dialog-content">
|
||||
<DialogTitle class="dialog-title">
|
||||
{{ t('legal.cta') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription class="dialog-description" v-html="t('legal.text')" />
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
</div>
|
||||
@@ -151,6 +184,38 @@ const menus: Menu[] = [
|
||||
margin-left: .25rem;
|
||||
}
|
||||
|
||||
.legal {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
font-size: .8em;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration: underline;
|
||||
gap: .5ch;
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: none;
|
||||
color: var(--green-500);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: url('/icon/question-mark.svg');
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .legal::before {
|
||||
content: url('/icon/question-mark-dark.svg');
|
||||
}
|
||||
|
||||
.map-menu {
|
||||
margin-top: .25rem;
|
||||
margin-bottom: .5rem;
|
||||
@@ -189,7 +254,7 @@ const menus: Menu[] = [
|
||||
img {
|
||||
display: block;
|
||||
border-radius: .3rem;
|
||||
border: .1rem solid var(--white);
|
||||
border: .1rem solid var(--background);
|
||||
outline: .15rem solid transparent;
|
||||
}
|
||||
|
||||
@@ -202,15 +267,15 @@ const menus: Menu[] = [
|
||||
pointer-events: none;
|
||||
|
||||
.fill { fill: var(--orange-500); }
|
||||
.stroke { fill: var(--white); }
|
||||
.stroke { fill: var(--background); }
|
||||
}
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: .75em;
|
||||
text-align: center;
|
||||
color: var(--slate-500);
|
||||
color: color-mix(in srgb, var(--foreground) 80%, var(--background));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@ const props = defineProps<{
|
||||
players: PlayerMarker,
|
||||
mapKey?: string,
|
||||
searchConfig?: SearchConfig
|
||||
disableTags?: boolean
|
||||
}>()
|
||||
|
||||
const customMarkersKey = props.mapKey ? `custom-markers-${props.mapKey}` : 'custom-markers'
|
||||
@@ -209,7 +210,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav ref="searchBarWrapper" class="toolbar appear-from-top" :key="navKey">
|
||||
<nav ref="searchBarWrapper" :aria-label="t('nav.aria-label')" class="toolbar appear-from-top" :key="navKey">
|
||||
<SearchMapSwitch />
|
||||
|
||||
<div ref="searchBar" class="search-w" :data-focused="shouldBeActive">
|
||||
@@ -286,6 +287,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
</div>
|
||||
|
||||
<SearchMarkersTags
|
||||
v-if="!props.disableTags"
|
||||
:current-search-mode="currentSearchMode"
|
||||
:custom-markers="customMarkersData"
|
||||
:search-config="props.searchConfig"
|
||||
@@ -306,11 +308,15 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
margin-left: calc(45px + .5rem);
|
||||
padding: .5rem 1.2rem;
|
||||
width: calc(100% - 3rem);
|
||||
background: var(--white);
|
||||
border: 1px solid var(--slate-400);
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 25px;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
|
||||
box-shadow: var(--shadow);
|
||||
pointer-events: all;
|
||||
transition-property: color, background-color, border-color, outline-color;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: var(--timing-function);
|
||||
|
||||
@media screen and (width >= 900px) {
|
||||
width: clamp(25rem, 35dvw, 30rem);
|
||||
@@ -336,7 +342,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
flex-grow: 1;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--slate-400);
|
||||
color: color-mix(in srgb, var(--foreground) 75%, var(--background));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +352,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
transform: translateY(-50%);
|
||||
left: 0;
|
||||
width: $search-icon-size;
|
||||
fill: var(--slate-400);
|
||||
fill: color-mix(in srgb, var(--foreground) 75%, var(--background));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -373,16 +379,20 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: var(--slate-300);
|
||||
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&::before {
|
||||
background-color: var(--slate-200);
|
||||
outline: 1px dotted var(--slate-500);
|
||||
outline: 4px auto var(--slate-900);
|
||||
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
|
||||
outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,22 +407,22 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
background-color: var(--slate-300);
|
||||
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&::before {
|
||||
background-color: var(--slate-200);
|
||||
outline: 1px dotted var(--slate-500);
|
||||
outline: 4px auto var(--slate-900);
|
||||
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
|
||||
outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
font-size: 1.1em;
|
||||
color: var(--slate-500);
|
||||
color: color-mix(in srgb, var(--foreground) 75%, var(--background));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +444,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--slate-300);
|
||||
background-color: color-mix(in srgb, var(--foreground) 30%, var(--background));
|
||||
}
|
||||
|
||||
> * + * {
|
||||
@@ -451,7 +461,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
}
|
||||
|
||||
&:hover:not(:has(a:hover)) {
|
||||
background-color: var(--slate-100);
|
||||
background-color: color-mix(in srgb, var(--foreground) 10%, var(--background));
|
||||
|
||||
.title {
|
||||
text-decoration: underline;
|
||||
@@ -488,9 +498,14 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
.title {
|
||||
font-weight: 500;
|
||||
text-underline-offset: 2px;
|
||||
|
||||
& + .desc {
|
||||
margin-top: .25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--slate-500);
|
||||
color: color-mix(in srgb, var(--foreground) 50%, var(--background));
|
||||
|
||||
.map-link {
|
||||
margin-top: .5rem;
|
||||
@@ -526,12 +541,12 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: .75rem;
|
||||
fill: var(--slate-400);
|
||||
fill: color-mix(in srgb, var(--foreground) 70%, var(--background));
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px dotted var(--slate-500);
|
||||
outline: 4px auto var(--slate-900);
|
||||
outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
|
||||
outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
|
||||
|
||||
.title {
|
||||
text-decoration: underline;
|
||||
@@ -598,8 +613,9 @@ function resetAllFields(actionAfter?: "focusAfter") {
|
||||
|
||||
&[data-focused="true"] {
|
||||
margin-bottom: 1em;
|
||||
|
||||
.input-w {
|
||||
border-bottom: 1px solid var(--slate-200);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--foreground) 20%, var(--background));
|
||||
margin-bottom: .5rem;
|
||||
padding-bottom: .5rem;
|
||||
|
||||
|
||||
@@ -125,10 +125,11 @@ function emitCategorySwitch(newCategory: MapMarkerGroup) {
|
||||
padding-inline: .8rem;
|
||||
font-weight: 600;
|
||||
font-size: .85em;
|
||||
background: var(--white);
|
||||
border: 1px solid var(--slate-400);
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 100vmax;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
|
||||
box-shadow: var(--shadow);
|
||||
white-space: nowrap;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
@@ -145,6 +146,10 @@ function emitCategorySwitch(newCategory: MapMarkerGroup) {
|
||||
width: 1.1em;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--foreground);
|
||||
}
|
||||
|
||||
.label {
|
||||
text-underline-offset: .15rem;
|
||||
}
|
||||
@@ -154,12 +159,12 @@ function emitCategorySwitch(newCategory: MapMarkerGroup) {
|
||||
@each $c in $colors {
|
||||
&.#{$c} {
|
||||
&.active {
|
||||
color: var(--white);
|
||||
color: var(--background);
|
||||
background-color: var(--#{$c}-500);
|
||||
border-color: var(--#{$c}-700);
|
||||
|
||||
.icon {
|
||||
fill: var(--white);
|
||||
fill: var(--background);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
72
src/components/maps/overlay/ToastService.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { isVNode, onMounted } from "vue"
|
||||
import { ToastLifetime, useToast, type Toast } from './useToast.ts'
|
||||
import { ToastDescription, ToastProvider, ToastRoot, ToastTitle, ToastViewport } from 'radix-vue'
|
||||
|
||||
const { toast, toasts } = useToast()
|
||||
import { t } from '@/i18n/store';
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('on-toast-push', (e) => handleInserted(e as CustomEvent))
|
||||
})
|
||||
|
||||
function handleInserted(e: CustomEvent) {
|
||||
if ( e.detail.preset) {
|
||||
switch (e.detail.preset) {
|
||||
case "copied-coords":
|
||||
default:
|
||||
toast({
|
||||
description: t('toast.copyCoords.description'),
|
||||
duration: ToastLifetime.SHORT,
|
||||
variant: 'success'
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const toastData = e.detail.toast as Toast
|
||||
|
||||
toast(toastData)
|
||||
}
|
||||
|
||||
function handleUpdate(id: string) {
|
||||
toasts.value.forEach((t, i) => {
|
||||
if (t.id === id) {
|
||||
t.open = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ToastProvider>
|
||||
<ToastRoot
|
||||
class="toast-root"
|
||||
:class="toast.variant ? `toast-${toast.variant}` : ''"
|
||||
v-for="toast in toasts"
|
||||
:key="toast.id"
|
||||
v-bind="toast"
|
||||
@update:open="handleUpdate(toast.id)"
|
||||
>
|
||||
<div v-if="toast.variant === 'success'" class="toast-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256"><path d="M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z"></path></svg>
|
||||
</div>
|
||||
|
||||
<ToastTitle class="toast-title" v-if="toast.title">
|
||||
{{ toast.title }}
|
||||
</ToastTitle>
|
||||
<template v-if="toast.description">
|
||||
<ToastDescription class="toast-description" v-if="isVNode(toast.description)">
|
||||
<component :is="toast.description" />
|
||||
</ToastDescription>
|
||||
<ToastDescription v-else class="toast-description">
|
||||
{{ toast.description }}
|
||||
</ToastDescription>
|
||||
</template>
|
||||
</ToastRoot>
|
||||
|
||||
<ToastViewport class="toast-viewport"/>
|
||||
</ToastProvider>
|
||||
</template>
|
||||
176
src/components/maps/overlay/useToast.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { computed, ref } from "vue"
|
||||
import type { Component, VNode } from "vue"
|
||||
import type { ToastRootProps } from "radix-vue"
|
||||
|
||||
const TOAST_LIMIT = 3
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
export interface ToastProps extends ToastRootProps {
|
||||
onOpenChange?: ((value: boolean) => void) | undefined
|
||||
}
|
||||
|
||||
export enum ToastLifetime {
|
||||
SHORT = 2000,
|
||||
MEDIUM = 3500,
|
||||
LONG = 6000,
|
||||
}
|
||||
|
||||
export type StringOrVNode =
|
||||
| string
|
||||
| VNode
|
||||
| (() => VNode)
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: string
|
||||
description?: StringOrVNode
|
||||
action?: Component,
|
||||
variant?: "default" | "success" | "destructive"
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
function addToRemoveQueue(toastId: string) {
|
||||
if (toastTimeouts.has(toastId))
|
||||
return
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: actionTypes.REMOVE_TOAST,
|
||||
toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
const state = ref<State>({
|
||||
toasts: [],
|
||||
})
|
||||
|
||||
function dispatch(action: Action) {
|
||||
switch (action.type) {
|
||||
case actionTypes.ADD_TOAST:
|
||||
state.value.toasts = [action.toast, ...state.value.toasts].slice(0, TOAST_LIMIT)
|
||||
break
|
||||
|
||||
case actionTypes.UPDATE_TOAST:
|
||||
state.value.toasts = state.value.toasts.map(t =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
)
|
||||
break
|
||||
|
||||
case actionTypes.DISMISS_TOAST: {
|
||||
const { toastId } = action
|
||||
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
}
|
||||
else {
|
||||
state.value.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
state.value.toasts = state.value.toasts.map(t =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case actionTypes.REMOVE_TOAST:
|
||||
if (action.toastId === undefined)
|
||||
state.value.toasts = []
|
||||
else
|
||||
state.value.toasts = state.value.toasts.filter(t => t.id !== action.toastId)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
return {
|
||||
toasts: computed(() => state.value.toasts),
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast(props: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: actionTypes.UPDATE_TOAST,
|
||||
toast: { ...props, id },
|
||||
})
|
||||
|
||||
const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: actionTypes.ADD_TOAST,
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open: boolean) => {
|
||||
if (!open)
|
||||
dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
export { toast, useToast }
|
||||
12
src/env.d.ts
vendored
@@ -1,2 +1,14 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly ENABLE_TOOLBAR: boolean
|
||||
readonly ENABLE_TOOLBAR_TAGS: boolean
|
||||
readonly ENABLE_CUSTOM_MARKERS: boolean
|
||||
readonly ENABLE_BREADCRUMB: boolean
|
||||
readonly ENABLE_I18N: boolean
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -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',
|
||||
@@ -19,6 +21,10 @@ export const translations: LanguageDict = {
|
||||
'capitals': 'Capitales',
|
||||
'points-of-interest': 'Points d\'intérêt',
|
||||
'personals': 'Personnels',
|
||||
'nav.aria-label': 'Menu principal',
|
||||
'nav.map.aria-label': 'Changer de carte',
|
||||
'nav.lang.aria-label': 'Changer de langue',
|
||||
'nav.theme.aria-label': 'Changer de thème',
|
||||
'maps.go-to-map': 'Voir la carte détaillée',
|
||||
'maps.go-to-player': 'Aller à la position actuelle des joueurs',
|
||||
'maps.seeQuest': 'Voir la quête',
|
||||
@@ -26,9 +32,23 @@ export const translations: LanguageDict = {
|
||||
'maps.search': 'Rechercher la carte',
|
||||
'maps.searchPlaceholder': "Ville, point d'intérêt…",
|
||||
'maps.closeSearch': 'Enlever le filtre',
|
||||
'maps.copyCoords': 'Copier les coordonnées',
|
||||
'maps.markers.new': 'Nouveau marqueur',
|
||||
'maps.markers.addPersonal': 'Ajouter un marqueur personnel',
|
||||
'maps.markers.newNotice': "Le marqueur sera sauvegardé mais n'apparaîtra que sur votre carte !"
|
||||
'maps.markers.newNotice': "Le marqueur sera sauvegardé mais n'apparaîtra que sur votre carte !",
|
||||
'toast.copyCoords.description': 'Les coordonnées ont été copiées dans le presse-papiers !',
|
||||
'legal.cta': 'Mentions légales',
|
||||
'legal.text': `
|
||||
<p>Cette application est construite avec <a href="https://astro.build/" target="_blank">Astro</a>, <a href="https://leafletjs.com/" target="_blank">Leaflet</a> et <a href="https://vuejs.org/" target="_blank">Vue</a>. J'utilise également des icônes de <a href="https://phosphoricons.com/" target="_blank">Phosphor</a> et elle est actuellement hébergée sur <a href="https://www.ovhcloud.com" target="_blank">OVH</a>.</p>
|
||||
<p>En ce qui concerne les œuvres d'art que j'utilise sur les marqueurs, j'essaie de les sourcer du mieux que je peux. Si vous connaissez la source originale d'une œuvre d'art que je n'indique pas, veuillez me contacter par mail.</p>
|
||||
<p>Je n'inclus pas d'images d'IA, car <strong>je suis farouchement opposé à l'IA générative</strong> pour la création de contenu. Si vous voyez une image qui semble être de l'IA sur ce site, faites-le-moi savoir immédiatement.</p>
|
||||
<h3>Données personnelles</h3>
|
||||
<p>Je ne collecte aucune donnée sur votre activité dans cette application. Les marqueurs personnalisés que vous pouvez placer n'existent que sur votre navigateur en tant que données de <a href="https://developer.mozilla.org/fr/docs/Web/API/Window/localStorage" target="_blank">local storage</a>.</p>
|
||||
<p>Mon hébergeur collecte des données de trafic pour des raisons de sécurité ; comme les adresses IP et leur emplacement approximatif, les types de navigateurs, les agents utilisateurs (si vous êtes un robot ou un humain) et l'état de la demande. </p>
|
||||
<p>J'utilise une instance Matomo pour mesurer l'audience du site, mais les données sont anonymisées et je ne construis aucun profil à partir de ces dernières.</p>
|
||||
<p><strong>Aucune de vos données n'est vendue ou transmise à un tiers quel qu'il soit</strong>.</p>
|
||||
<p>Si vous avez des questions, vous pouvez me contacter à <a href="mailto:contact@alexcreates.fr">contact@alexcreates.fr</a>.</p>
|
||||
`
|
||||
},
|
||||
'en': {
|
||||
'lang.fr': 'Français',
|
||||
@@ -36,6 +56,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',
|
||||
@@ -44,6 +66,10 @@ export const translations: LanguageDict = {
|
||||
'capitals': 'Capitals',
|
||||
'points-of-interest': 'Points of interest',
|
||||
'personals': 'Personals',
|
||||
'nav.aria-label': 'Main menu',
|
||||
'nav.map.aria-label': 'Switch map',
|
||||
'nav.lang.aria-label': 'Switch language',
|
||||
'nav.theme.aria-label': 'Switch color theme',
|
||||
'maps.go-to-map': 'See detailed map',
|
||||
'maps.go-to-player': 'Go to current players position',
|
||||
'maps.seeQuest': 'See the quest',
|
||||
@@ -51,8 +77,24 @@ export const translations: LanguageDict = {
|
||||
'maps.search': 'Search the map',
|
||||
'maps.searchPlaceholder': "City, point of interest…",
|
||||
'maps.closeSearch': 'Remove filter',
|
||||
'maps.copyCoords': 'Copy coordinates',
|
||||
'maps.markers.new': 'New marker',
|
||||
'maps.markers.addPersonal': 'Add a personal marker',
|
||||
'maps.markers.newNotice': "The marker will be saved but will only appear on your map!"
|
||||
'maps.markers.newNotice': "The marker will be saved but will only appear on your map!",
|
||||
'toast.copyCoords.description': 'Coordinates have been copied to your clipboard !',
|
||||
'legal.cta': 'Legal notice',
|
||||
'legal.text': `
|
||||
<p>This app is built with <a href="https://astro.build/" target="_blank">astro</a>, <a href="https://leafletjs.com/" target="_blank">leaflet</a> and <a href="https://vuejs.org/" target="_blank">vue</a>. I also use icons from <a href="https://phosphoricons.com/" target="_blank">phosphor</a> and it's currently hosted on <a href="https://www.ovhcloud.com" target="_blank">OVH</a>.</p>
|
||||
<p>For the artworks I use on the markers, I try to source them to the best of my knowledge, from what I can scrap off the internet. If you know the original source of an artwork, please contact me by email.</p>
|
||||
<p>I don't include AI images as <strong>I'm vehemently opposed to generative AI</strong> for content creation. If you see an image that looks like it might be AI on this site, let me know immediately.</p>
|
||||
<h3>Privacy</h3>
|
||||
<p>I don't collect any data of your activity with this app. The custom markers that you may place only exist on your browser as <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" target="_blank">local storage</a> data.</p>
|
||||
<p>My hosting provider collects traffic data for security reasons ; like IP addresses and their approximate location, browsers types, user agents (if you're a robot or a human) and the request status.</p>
|
||||
<p>This website doesn't use any other third-party app that might collect stuff from you (like Youtube or anything else)</p>
|
||||
<p>I do use a self-hosted Matomo Instance to measure website audience, but data is anonymised and I don't build profiles with it.</p>
|
||||
<p><strong>None of your data is sold or passed to any third-party whatsoever</strong>.</p>
|
||||
<p>If you have any questions, you can contact me at <a href="mailto:contact@alexcreates.fr">contact@alexcreates.fr</a>.</p>
|
||||
`
|
||||
}
|
||||
} as const;
|
||||
|
||||
@@ -16,6 +16,13 @@ const lang = getLangFromUrl(Astro.url) || defaultLang;
|
||||
<!doctype html>
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{title} — Leim Wiki</title>
|
||||
|
||||
<script is:inline src="/leaflet/leaflet.js" />
|
||||
<script is:inline type="text/javascript" src="/js/leaflet-measure.min.js"></script>
|
||||
<script is:inline type="text/javascript" src="/js/leaflet-zoomify.min.js"></script>
|
||||
<script>
|
||||
import { setLang } from "@/i18n/store";
|
||||
import { defaultLang, type Language } from "@/i18n/ui";
|
||||
@@ -25,12 +32,21 @@ const lang = getLangFromUrl(Astro.url) || defaultLang;
|
||||
setLang(currentLang || defaultLang);
|
||||
</script>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="/leaflet/leaflet.css" rel="preload" />
|
||||
<link rel="stylesheet" href="/leaflet-measure/leaflet-measure.css" rel="preload" />
|
||||
|
||||
<meta name="author" content="Alexis Pelé" />
|
||||
{head?.description && <meta name="description" content={head.description} /> }
|
||||
<meta name="viewport" content="width=device-width; initial-scale=1" />
|
||||
<meta property="og:author" content="Alexis Pelé" />
|
||||
<meta property="og:locale" content={lang}>
|
||||
<meta name="application-name" content="leim-maps">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="msapplication-config" content="none">
|
||||
|
||||
{head?.description && (
|
||||
<meta name="description" content={head.description} />
|
||||
<meta property="og:description" content={head.description} />
|
||||
)}
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title} — Leim Wiki</title>
|
||||
|
||||
<link rel="canonical" href={Astro.url}>
|
||||
|
||||
@@ -41,24 +57,83 @@ const lang = getLangFromUrl(Astro.url) || defaultLang;
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#6595b4">
|
||||
<meta name="msapplication-TileColor" content="#00aba9">
|
||||
<meta name="theme-color" content="#a9d1eb">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
{head?.image && <meta property="og:image" content={head.image} />}
|
||||
|
||||
{head?.description && <meta name="twitter:description" content={head.description} /> }
|
||||
{head?.image && <meta property="twitter:image" content={head.image} />}
|
||||
|
||||
<link rel="stylesheet" href="/leaflet/leaflet.css" rel="preload" />
|
||||
<script is:inline src="/leaflet/leaflet.js" />
|
||||
|
||||
<script is:inline type="text/javascript" src="/js/leaflet-zoomify.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/leaflet-measure/leaflet-measure.css" rel="preload" />
|
||||
<script is:inline type="text/javascript" src="/js/leaflet-measure.min.js"></script>
|
||||
{head?.image && (
|
||||
<meta property="og:image" content={`${Astro.site?.origin}${head.image}`} />
|
||||
<meta name="twitter:image" content={`${Astro.site?.origin}${head.image}`} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
)}
|
||||
<!-- Matomo -->
|
||||
<script is:inline>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="//stats.alexcreates.fr/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '3']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Matomo Code -->
|
||||
</head>
|
||||
<body>
|
||||
<script is:inline>
|
||||
let initialTheme = localStorage.getItem("theme");
|
||||
const darkModeMq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
// Initial paint from localstorage
|
||||
switch (initialTheme) {
|
||||
case "light":
|
||||
enableLightTheme();
|
||||
break;
|
||||
case "system":
|
||||
resetTheme();
|
||||
break;
|
||||
case "dark":
|
||||
default:
|
||||
enableDarkTheme();
|
||||
break;
|
||||
}
|
||||
|
||||
// Enables dark theme and updates preferences
|
||||
function enableDarkTheme() {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.remove('light');
|
||||
localStorage.setItem("theme", "dark");
|
||||
}
|
||||
|
||||
// Enables light theme and updates preferences
|
||||
function enableLightTheme() {
|
||||
document.documentElement.classList.add('light');
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
|
||||
// Reset theme to OS values
|
||||
function resetTheme() {
|
||||
const prefersDark = darkModeMq.matches;
|
||||
|
||||
if (prefersDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.remove('light');
|
||||
} else {
|
||||
document.documentElement.classList.add('light');
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
localStorage.setItem("theme", "system");
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,7 +4,12 @@ import Map from '@/components/maps/Map.astro'
|
||||
import MapOverlay from '@/components/maps/MapOverlay.astro'
|
||||
---
|
||||
|
||||
<Layout title="Borélis ~ Maps">
|
||||
<Layout
|
||||
title="Borélis ~ Maps"
|
||||
head={{
|
||||
description: "Map of Borélis, a harbour city on the northern shores of the Kaldelium Accord."
|
||||
}}
|
||||
>
|
||||
<MapOverlay
|
||||
mapKey='aldys-borelis'
|
||||
searchConfig={{
|
||||
@@ -17,6 +22,7 @@ import MapOverlay from '@/components/maps/MapOverlay.astro'
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Borélis'
|
||||
mapKey='aldys-borelis'
|
||||
zoomifyKey='fr/borelis'
|
||||
mapHeight={3710}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import type { MapMarker, PlayerMarker } from '@/types/Leaflet';
|
||||
import type { MapMarker } from '@/types/Leaflet';
|
||||
|
||||
import Layout from '@/layouts/Layout.astro';
|
||||
import Map from '@/components/maps/Map.astro'
|
||||
@@ -10,7 +10,12 @@ import markersData from '@/components/maps/data/en/aldys/cantane/markers.json'
|
||||
const markers = markersData.data as MapMarker[]
|
||||
---
|
||||
|
||||
<Layout title="Cantane ~ Maps">
|
||||
<Layout
|
||||
title="Cantane ~ Maps"
|
||||
head={{
|
||||
description: "Map of the old city of Cantane, south of the Kaldelium Accord."
|
||||
}}
|
||||
>
|
||||
<MapOverlay
|
||||
mapKey='aldys-cantane'
|
||||
markers={markers}
|
||||
@@ -24,6 +29,7 @@ const markers = markersData.data as MapMarker[]
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Cantane'
|
||||
mapKey='aldys-cantane'
|
||||
zoomifyKey='fr/cantane'
|
||||
mapHeight={4124}
|
||||
|
||||
@@ -10,7 +10,12 @@ import markersData from '@/components/maps/data/en/aldys/cantane/les-mines-blanc
|
||||
const markers = markersData.data as MapMarker[]
|
||||
---
|
||||
|
||||
<Layout title="White Hollow ~ Maps">
|
||||
<Layout
|
||||
title="White Hollow ~ Maps"
|
||||
head={{
|
||||
description: "Map of the White Hollow mining complex, left abandonned east of Cantane."
|
||||
}}
|
||||
>
|
||||
<MapOverlay
|
||||
mapKey='aldys-cantane-mines'
|
||||
markers={markers}
|
||||
@@ -25,6 +30,7 @@ const markers = markersData.data as MapMarker[]
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='White Hollow'
|
||||
mapKey='aldys-cantane-mines'
|
||||
zoomifyKey='fr/mines-blanches'
|
||||
mapHeight={6020}
|
||||
|
||||
@@ -12,7 +12,12 @@ const markers = markersData.data as MapMarker[]
|
||||
const players = playersData as PlayerMarker
|
||||
---
|
||||
|
||||
<Layout title="Bamast ~ Maps">
|
||||
<Layout
|
||||
title="Bamast ~ Maps"
|
||||
head={{
|
||||
description: "Map of the archipelago of Bamast and its many islands in the Balamian Ocean."
|
||||
}}
|
||||
>
|
||||
<MapOverlay
|
||||
mapKey='bamast'
|
||||
markers={markers}
|
||||
@@ -22,6 +27,7 @@ const players = playersData as PlayerMarker
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Bamast'
|
||||
mapKey='bamast'
|
||||
zoomifyKey='fr/bamast'
|
||||
mapHeight={18342}
|
||||
|
||||
@@ -16,8 +16,8 @@ const players = playersData as PlayerMarker
|
||||
title="Aldys ~ Maps"
|
||||
head={
|
||||
{
|
||||
description: "Carte de l'Alliance Kaldélienne sur la planète de Léim.",
|
||||
image: '/images/bailey-zindel-NRQV-hBF10M-unsplash.jpg'
|
||||
description: "Map of the Aldys continent on the planet of Léim, displaying the Kaldelium accord and the Common Lands of Syaltha.",
|
||||
image: '/bailey-zindel-NRQV-hBF10M-unsplash.jpg'
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -30,6 +30,7 @@ const players = playersData as PlayerMarker
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Aldys'
|
||||
mapKey='aldys'
|
||||
zoomifyKey='en/aldys-nord'
|
||||
mapHeight={21896}
|
||||
|
||||
@@ -4,7 +4,12 @@ import Map from '@/components/maps/Map.astro'
|
||||
import MapOverlay from '@/components/maps/MapOverlay.astro'
|
||||
---
|
||||
|
||||
<Layout title="Borélis ~ Cartographie">
|
||||
<Layout
|
||||
title="Borélis ~ Cartographie"
|
||||
head={{
|
||||
description: "Carte de la ville côtière de Borélis, sur les côtes gelées au nord de l'Alliance Kaldélienne."
|
||||
}}
|
||||
>
|
||||
<MapOverlay
|
||||
mapKey='aldys-borelis'
|
||||
searchConfig={{
|
||||
@@ -17,6 +22,7 @@ import MapOverlay from '@/components/maps/MapOverlay.astro'
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Borélis'
|
||||
mapKey='aldys-borelis'
|
||||
zoomifyKey='fr/borelis'
|
||||
mapHeight={3710}
|
||||
|
||||
@@ -10,7 +10,12 @@ import markersData from '@/components/maps/data/fr/aldys/cantane/markers.json'
|
||||
const markers = markersData.data as MapMarker[]
|
||||
---
|
||||
|
||||
<Layout title="Cantane ~ Cartographie">
|
||||
<Layout
|
||||
title="Cantane ~ Cartographie"
|
||||
head={{
|
||||
description: "Carte de la ville de Cantane, au sud de l'Alliance Kaldélienne."
|
||||
}}
|
||||
>
|
||||
<MapOverlay
|
||||
mapKey='aldys-cantane'
|
||||
markers={markers}
|
||||
@@ -24,6 +29,7 @@ const markers = markersData.data as MapMarker[]
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Cantane'
|
||||
mapKey='aldys-cantane'
|
||||
zoomifyKey='fr/cantane'
|
||||
mapHeight={4124}
|
||||
|
||||
@@ -10,7 +10,12 @@ import markersData from '@/components/maps/data/fr/aldys/cantane/les-mines-blanc
|
||||
const markers = markersData.data as MapMarker[]
|
||||
---
|
||||
|
||||
<Layout title="Les Mines Blanches ~ Cartographie">
|
||||
<Layout
|
||||
title="Les Mines Blanches ~ Cartographie"
|
||||
head={{
|
||||
description: "Cartographie du complexe des Mines Blanches, laissé à l'abandon à l'est de Cantane."
|
||||
}}
|
||||
>
|
||||
<MapOverlay
|
||||
mapKey='aldys-cantane-mines'
|
||||
markers={markers}
|
||||
@@ -25,6 +30,7 @@ const markers = markersData.data as MapMarker[]
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Les Mines Blanches'
|
||||
mapKey='aldys-cantane-mines'
|
||||
zoomifyKey='fr/mines-blanches'
|
||||
mapHeight={6020}
|
||||
|
||||
@@ -12,7 +12,12 @@ const markers = markersData.data as MapMarker[]
|
||||
const players = playersData as PlayerMarker
|
||||
---
|
||||
|
||||
<Layout title="Bamast ~ Cartographie">
|
||||
<Layout
|
||||
title="Bamast ~ Cartographie"
|
||||
head={{
|
||||
description: "Carte de l'Archipel de Bamast et ses nombreuses îles de l'Océan Balamien."
|
||||
}}
|
||||
>
|
||||
<MapOverlay
|
||||
mapKey='bamast'
|
||||
markers={markers}
|
||||
@@ -22,6 +27,7 @@ const players = playersData as PlayerMarker
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Bamast'
|
||||
mapKey='bamast'
|
||||
zoomifyKey='fr/bamast'
|
||||
mapHeight={18342}
|
||||
|
||||
@@ -16,8 +16,8 @@ const players = playersData as PlayerMarker
|
||||
title="Aldys ~ Cartographie"
|
||||
head={
|
||||
{
|
||||
description: "Carte de l'Alliance Kaldélienne sur la planète de Léim.",
|
||||
image: '/images/bailey-zindel-NRQV-hBF10M-unsplash.jpg'
|
||||
description: "Carte du continent Aldys sur la planète de Léim, comprenant l'Alliance Kaldélienne et les Terres de Syaltha.",
|
||||
image: '/bailey-zindel-NRQV-hBF10M-unsplash.jpg'
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -30,6 +30,7 @@ const players = playersData as PlayerMarker
|
||||
]}
|
||||
/>
|
||||
<Map
|
||||
title='Aldys'
|
||||
mapKey='aldys'
|
||||
zoomifyKey='fr/aldys-nord'
|
||||
mapHeight={21896}
|
||||
|
||||
@@ -7,26 +7,90 @@ export type MapCoords = {
|
||||
}
|
||||
|
||||
export type MapMarker = {
|
||||
/**
|
||||
* The title of the marker (must be unique).
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* The coordinates of the marker.
|
||||
* @example { x: 123, y: 456 }
|
||||
*/
|
||||
markerCoords: MapCoords,
|
||||
/**
|
||||
* The description of the marker.
|
||||
*/
|
||||
description?: string,
|
||||
/**
|
||||
* The link that can appear on the marker title.
|
||||
* @example https://example.com
|
||||
*/
|
||||
link?: string,
|
||||
/**
|
||||
* The ID of the map the marker is supposed to point to.
|
||||
*
|
||||
* This should be a valid path of the `maps/data` folder.
|
||||
* @example "fr/aldys/borelis"
|
||||
*/
|
||||
mapId?: string,
|
||||
/**
|
||||
* The group that the marker belongs to.
|
||||
* @example "capitals"
|
||||
*/
|
||||
group?: MapMarkerGroup,
|
||||
/**
|
||||
* The icon that the marker should use.
|
||||
* @example "castle"
|
||||
*/
|
||||
icon?: MapMarkerIcon,
|
||||
/**
|
||||
* Cover image for the marker.
|
||||
* This should be a valid image in the public folder.
|
||||
*
|
||||
* @example "Ambrose.webp"
|
||||
*/
|
||||
cover?: string,
|
||||
/**
|
||||
* The link to the cover image's source.
|
||||
*/
|
||||
coverLink?: string,
|
||||
/**
|
||||
* The author of the cover image.
|
||||
*/
|
||||
coverAuthor?: string,
|
||||
/**
|
||||
* Whether the cover image should be displayed in portrait mode.
|
||||
*/
|
||||
coverPortrait?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A marker that is used to display players' location on the map.
|
||||
*/
|
||||
export type PlayerMarker = {
|
||||
/**
|
||||
* The title of the marker (must be unique).
|
||||
*/
|
||||
title: string,
|
||||
/**
|
||||
* The coordinates of the marker.
|
||||
* @example { x: 123, y: 456 }
|
||||
*/
|
||||
markerCoords: MapCoords,
|
||||
/**
|
||||
* The description of the marker.
|
||||
*/
|
||||
description?: string,
|
||||
/**
|
||||
* The link that can appear on the marker title.
|
||||
* @example https://example.com
|
||||
*/
|
||||
link?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* A marker that is used to display a custom location on the map.
|
||||
* These are only used on the client side and are not saved anywhere else.
|
||||
*/
|
||||
export type CustomMarker = {
|
||||
lat: number,
|
||||
lon: number,
|
||||
|
||||
@@ -1,33 +1,106 @@
|
||||
import type { MapMarker, PlayerMarker } from "./Leaflet"
|
||||
|
||||
/**
|
||||
* A breadcrumb item.
|
||||
*/
|
||||
export interface BreadcrumbItem {
|
||||
name: string;
|
||||
url: string;
|
||||
/**
|
||||
* The label of the breadcrumb.
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* The URL the breadcrumb points to.
|
||||
*/
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Search configuration options for the map overlay.
|
||||
* This is used to filter the markers on the map.
|
||||
*/
|
||||
export interface SearchConfig {
|
||||
/** Whether to disable the quests in the search bar. */
|
||||
disableQuests?: boolean
|
||||
/** Whether to disable the capitals in the search bar. */
|
||||
disableCapitals?: boolean
|
||||
/** Whether to disable the landmarks in the search bar. */
|
||||
disableLandmarks?: boolean
|
||||
}
|
||||
|
||||
export interface MapProps {
|
||||
title?: string
|
||||
/**
|
||||
* The ID of the map.
|
||||
* This is used to identify the map and store custom markers in the local storage.
|
||||
* @example "fr/aldys/borelis"
|
||||
*/
|
||||
mapKey: string
|
||||
/**
|
||||
* The ID of the map in the zoomify folder.
|
||||
*/
|
||||
zoomifyKey: string
|
||||
/**
|
||||
* Height of the map in pixels.
|
||||
* This information is generated by the zoomify tool.
|
||||
* @example 30000
|
||||
*/
|
||||
mapHeight: number
|
||||
/**
|
||||
* Width of the map in pixels.
|
||||
* This information is generated by the zoomify tool.
|
||||
* @example 30000
|
||||
*/
|
||||
mapWidth: number
|
||||
/**
|
||||
* List of markers to display on the map.
|
||||
*/
|
||||
markers?: MapMarker[]
|
||||
/**
|
||||
* Current player position on the map.
|
||||
*/
|
||||
players?: PlayerMarker
|
||||
/**
|
||||
* Main unit of the map.
|
||||
*/
|
||||
rulerMainUnit?: string
|
||||
/**
|
||||
* Ratio used to convert the map units to their relevant unit.
|
||||
*/
|
||||
rulerDistanceRatio?: number
|
||||
/**
|
||||
* Whether to show the walking distance in the ruler.
|
||||
*/
|
||||
rulerHideWalkDistance?: boolean
|
||||
/**
|
||||
* Default background color of the map.
|
||||
* @exemple "#FFF"
|
||||
*/
|
||||
backgroundColor?: string
|
||||
}
|
||||
|
||||
export interface MapOverlayProps {
|
||||
/**
|
||||
* The ID of the map.
|
||||
* This is used to identify the map and store custom markers in the local storage.
|
||||
* @example "fr/aldys/borelis"
|
||||
*/
|
||||
mapKey: string
|
||||
/**
|
||||
* List of markers to display on the map.
|
||||
*/
|
||||
markers?: MapMarker[]
|
||||
/**
|
||||
* Current player position on the map.
|
||||
*/
|
||||
players?: PlayerMarker
|
||||
/**
|
||||
* Configuration for the search bar.
|
||||
* This is used to filter the markers on the map.
|
||||
* @example { disableQuests: true, disableCapitals: false, disableLandmarks: false }
|
||||
*/
|
||||
searchConfig?: SearchConfig
|
||||
/**
|
||||
* List of breadcrumbs to display.
|
||||
*/
|
||||
breadcrumbs?: BreadcrumbItem[]
|
||||
}
|
||||
|
||||
8
src/utils/Strings.ts
Normal 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()
|
||||
}
|
||||