feat(map): display geolocated episodes on a map page
This commit is contained in:
parent
652fa3659e
commit
4357cc25cc
|
@ -781,6 +781,12 @@ $routes->group('@(:podcastName)', function ($routes): void {
|
||||||
$routes->get('/credits', 'CreditsController', [
|
$routes->get('/credits', 'CreditsController', [
|
||||||
'as' => 'credits',
|
'as' => 'credits',
|
||||||
]);
|
]);
|
||||||
|
$routes->get('/map', 'MapMarkerController', [
|
||||||
|
'as' => 'map',
|
||||||
|
]);
|
||||||
|
$routes->get('/episodes-markers', 'MapMarkerController::getEpisodesMarkers', [
|
||||||
|
'as' => 'episodes-markers',
|
||||||
|
]);
|
||||||
$routes->get('/pages/(:slug)', 'PageController/$1', [
|
$routes->get('/pages/(:slug)', 'PageController/$1', [
|
||||||
'as' => 'page',
|
'as' => 'page',
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -124,8 +124,8 @@ class PodcastImportController extends BaseController
|
||||||
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
|
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
|
||||||
$location = new Location(
|
$location = new Location(
|
||||||
(string) $nsPodcast->location,
|
(string) $nsPodcast->location,
|
||||||
(string) $nsPodcast->location->attributes()['geo'],
|
$nsPodcast->location->attributes()['geo'] === null ? null : (string) $nsPodcast->location->attributes()['geo'],
|
||||||
(string) $nsPodcast->location->attributes()['osm'],
|
$nsPodcast->location->attributes()['osm'] === null ? null : (string) $nsPodcast->location->attributes()['osm'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (property_exists($nsPodcast, 'guid') && $nsPodcast->guid !== null) {
|
if (property_exists($nsPodcast, 'guid') && $nsPodcast->guid !== null) {
|
||||||
|
@ -338,8 +338,8 @@ class PodcastImportController extends BaseController
|
||||||
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
|
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
|
||||||
$location = new Location(
|
$location = new Location(
|
||||||
(string) $nsPodcast->location,
|
(string) $nsPodcast->location,
|
||||||
(string) $nsPodcast->location->attributes()['geo'],
|
$nsPodcast->location->attributes()['geo'] === null ? null : (string) $nsPodcast->location->attributes()['geo'],
|
||||||
(string) $nsPodcast->location->attributes()['osm'],
|
$nsPodcast->location->attributes()['osm'] === null ? null : (string) $nsPodcast->location->attributes()['osm'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2020 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Entities\Page;
|
||||||
|
use App\Models\EpisodeModel;
|
||||||
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
|
||||||
|
class MapMarkerController extends BaseController
|
||||||
|
{
|
||||||
|
public function index(): string
|
||||||
|
{
|
||||||
|
$locale = service('request')
|
||||||
|
->getLocale();
|
||||||
|
$cacheName = "page_map_{$locale}";
|
||||||
|
if (! ($found = cache($cacheName))) {
|
||||||
|
$found = view('map', [], [
|
||||||
|
'cache' => DECADE,
|
||||||
|
'cache_name' => $cacheName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEpisodesMarkers(): ResponseInterface
|
||||||
|
{
|
||||||
|
$cacheName = 'episodes_markers';
|
||||||
|
if (! ($found = cache($cacheName))) {
|
||||||
|
$episodes = (new EpisodeModel())->where('location_geo is not', null)
|
||||||
|
->findAll();
|
||||||
|
$found = [];
|
||||||
|
foreach ($episodes as $episode) {
|
||||||
|
$found[] = [
|
||||||
|
'latitude' => $episode->location->latitude,
|
||||||
|
'longitude' => $episode->location->longitude,
|
||||||
|
'location_name' => $episode->location->name,
|
||||||
|
'location_url' => $episode->location->url,
|
||||||
|
'episode_link' => $episode->link,
|
||||||
|
'podcast_link' => $episode->podcast->link,
|
||||||
|
'image_path' => $episode->image->thumbnail_url,
|
||||||
|
'podcast_title' => $episode->podcast->title,
|
||||||
|
'episode_title' => $episode->title,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// The page cache is set to a decade so it is deleted manually upon episode update
|
||||||
|
cache()
|
||||||
|
->save($cacheName, $found, DECADE);
|
||||||
|
}
|
||||||
|
return $this->response->setJSON($found);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,8 @@ use Config\Services;
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string|null $geo
|
* @property string|null $geo
|
||||||
* @property string|null $osm
|
* @property string|null $osm
|
||||||
|
* @property double|null $latitude
|
||||||
|
* @property double|null $longitude
|
||||||
*/
|
*/
|
||||||
class Location extends Entity
|
class Location extends Entity
|
||||||
{
|
{
|
||||||
|
@ -34,12 +36,21 @@ class Location extends Entity
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected string $name,
|
protected string $name,
|
||||||
protected ?string $geo = null,
|
protected ?string $geo = null,
|
||||||
protected ?string $osm = null
|
protected ?string $osm = null,
|
||||||
) {
|
) {
|
||||||
|
$latitude = null;
|
||||||
|
$longitude = null;
|
||||||
|
if ($geo !== null) {
|
||||||
|
$geoArray = explode(',', substr($geo, 4));
|
||||||
|
$latitude = floatval($geoArray[0]);
|
||||||
|
$longitude = floatval($geoArray[1]);
|
||||||
|
}
|
||||||
parent::__construct([
|
parent::__construct([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'geo' => $geo,
|
'geo' => $geo,
|
||||||
'osm' => $osm,
|
'osm' => $osm,
|
||||||
|
'latitude' => $latitude,
|
||||||
|
'longitude' => $longitude,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ if (! function_exists('render_page_links')) {
|
||||||
$links .= anchor(route_to('credits'), lang('Person.credits'), [
|
$links .= anchor(route_to('credits'), lang('Person.credits'), [
|
||||||
'class' => 'px-2 underline hover:no-underline',
|
'class' => 'px-2 underline hover:no-underline',
|
||||||
]);
|
]);
|
||||||
|
$links .= anchor(route_to('map'), lang('Page.map'), [
|
||||||
|
'class' => 'px-2 underline hover:no-underline',
|
||||||
|
]);
|
||||||
foreach ($pages as $page) {
|
foreach ($pages as $page) {
|
||||||
$links .= anchor($page->link, $page->title, [
|
$links .= anchor($page->link, $page->title, [
|
||||||
'class' => 'px-2 underline hover:no-underline',
|
'class' => 'px-2 underline hover:no-underline',
|
||||||
|
|
|
@ -26,4 +26,5 @@ return [
|
||||||
'messages' => [
|
'messages' => [
|
||||||
'createSuccess' => 'The page “{pageTitle}” was created successfully!',
|
'createSuccess' => 'The page “{pageTitle}” was created successfully!',
|
||||||
],
|
],
|
||||||
|
'map' => 'Map',
|
||||||
];
|
];
|
||||||
|
|
|
@ -26,4 +26,5 @@ return [
|
||||||
'messages' => [
|
'messages' => [
|
||||||
'createSuccess' => 'La page {pageTitle} a été créée avec succès !',
|
'createSuccess' => 'La page {pageTitle} a été créée avec succès !',
|
||||||
],
|
],
|
||||||
|
'map' => 'Cartographie',
|
||||||
];
|
];
|
||||||
|
|
|
@ -299,6 +299,8 @@ class EpisodeModel extends Model
|
||||||
->deleteMatching("page_podcast#{$episode->podcast_id}*");
|
->deleteMatching("page_podcast#{$episode->podcast_id}*");
|
||||||
cache()
|
cache()
|
||||||
->deleteMatching('page_credits_*');
|
->deleteMatching('page_credits_*');
|
||||||
|
cache()
|
||||||
|
->delete('episodes_markers');
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 618 B |
|
@ -0,0 +1,4 @@
|
||||||
|
import "core-js";
|
||||||
|
import DrawEpisodesMaps from "./modules/EpisodesMap";
|
||||||
|
|
||||||
|
DrawEpisodesMaps();
|
|
@ -1,4 +1,3 @@
|
||||||
// Import modules
|
|
||||||
import am4geodata_worldLow from "@amcharts/amcharts4-geodata/worldLow";
|
import am4geodata_worldLow from "@amcharts/amcharts4-geodata/worldLow";
|
||||||
import * as am4charts from "@amcharts/amcharts4/charts";
|
import * as am4charts from "@amcharts/amcharts4/charts";
|
||||||
import * as am4core from "@amcharts/amcharts4/core";
|
import * as am4core from "@amcharts/amcharts4/core";
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
import {
|
||||||
|
control,
|
||||||
|
featureGroup,
|
||||||
|
icon,
|
||||||
|
map,
|
||||||
|
Marker,
|
||||||
|
marker,
|
||||||
|
tileLayer,
|
||||||
|
} from "leaflet";
|
||||||
|
import { MarkerClusterGroup } from "leaflet.markercluster";
|
||||||
|
import "leaflet.markercluster/dist/MarkerCluster.css";
|
||||||
|
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import markerIconRetina from "../../images/marker/marker-icon-2x.png";
|
||||||
|
import markerIcon from "../../images/marker/marker-icon.png";
|
||||||
|
import markerShadow from "../../images/marker/marker-shadow.png";
|
||||||
|
|
||||||
|
Marker.prototype.options.icon = icon({
|
||||||
|
iconRetinaUrl: markerIconRetina,
|
||||||
|
iconUrl: markerIcon,
|
||||||
|
shadowUrl: markerShadow,
|
||||||
|
iconSize: [25, 41],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34],
|
||||||
|
tooltipAnchor: [16, -28],
|
||||||
|
shadowSize: [41, 41],
|
||||||
|
});
|
||||||
|
|
||||||
|
const drawEpisodesMap = async (mapDivId: string, dataUrl: string) => {
|
||||||
|
const episodesMap = map(mapDivId).setView([48.858, 2.294], 13);
|
||||||
|
|
||||||
|
tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>',
|
||||||
|
}).addTo(episodesMap);
|
||||||
|
control.scale({ imperial: true, metric: true }).addTo(episodesMap);
|
||||||
|
|
||||||
|
const data = await fetch(dataUrl).then((response) => response.json());
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
const markers = [];
|
||||||
|
const cluster = new MarkerClusterGroup({ showCoverageOnHover: false });
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const currentMarker = marker([
|
||||||
|
data[i].latitude,
|
||||||
|
data[i].longitude,
|
||||||
|
]).bindPopup(
|
||||||
|
'<div class="flex min-w-max"><img src="' +
|
||||||
|
data[i].image_path +
|
||||||
|
'" alt="' +
|
||||||
|
data[i].episode_title +
|
||||||
|
'" class="mr-2 rounded w-16 h-16" /><div class="flex flex-col"><h2 class="lg:text-base text-sm ! font-bold"><a href="' +
|
||||||
|
data[i].episode_link +
|
||||||
|
'" class="hover:underline !text-pine-800">' +
|
||||||
|
data[i].episode_title +
|
||||||
|
'</a></h2><a href="' +
|
||||||
|
data[i].podcast_link +
|
||||||
|
'" class="hover:underline text-xs !text-black !mt-0 !mb-2">' +
|
||||||
|
data[i].podcast_title +
|
||||||
|
"</a>" +
|
||||||
|
'<a href="' +
|
||||||
|
data[i].location_url +
|
||||||
|
'" class="inline-flex items-center hover:underline text-xs !text-gray-500" target="_blank" rel="noreferrer noopener"><svg class="mr-1" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><g><path fill="none" d="M0 0h24v24H0z"></path><path d="M18.364 17.364L12 23.728l-6.364-6.364a9 9 0 1 1 12.728 0zM12 13a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"></path></g></svg>' +
|
||||||
|
data[i].location_name +
|
||||||
|
"</a></div></div>"
|
||||||
|
);
|
||||||
|
markers.push(currentMarker);
|
||||||
|
cluster.addLayer(currentMarker);
|
||||||
|
}
|
||||||
|
episodesMap.addLayer(cluster);
|
||||||
|
const group = featureGroup(markers);
|
||||||
|
episodesMap.fitBounds(group.getBounds());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DrawEpisodesMaps = (): void => {
|
||||||
|
const mapDivs: NodeListOf<HTMLDivElement> = document.querySelectorAll(
|
||||||
|
"div[data-episodes-map-data-url]"
|
||||||
|
);
|
||||||
|
for (let i = 0; i < mapDivs.length; i++) {
|
||||||
|
const mapDiv: HTMLDivElement = mapDivs[i];
|
||||||
|
|
||||||
|
if (mapDiv.dataset.episodesMapDataUrl) {
|
||||||
|
drawEpisodesMap(mapDiv.id, mapDiv.dataset.episodesMapDataUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrawEpisodesMaps;
|
|
@ -1,2 +1,3 @@
|
||||||
declare module "prosemirror-markdown";
|
declare module "prosemirror-markdown";
|
||||||
declare module "prosemirror-example-setup";
|
declare module "prosemirror-example-setup";
|
||||||
|
declare module "leaflet.markercluster";
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
import "core-js";
|
|
@ -0,0 +1,5 @@
|
||||||
|
import "leaflet.markercluster/dist/MarkerCluster.css";
|
||||||
|
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
declare const DrawEpisodesMaps: () => void;
|
||||||
|
export default DrawEpisodesMaps;
|
|
@ -0,0 +1,2 @@
|
||||||
|
declare const DrawMaps: () => void;
|
||||||
|
export default DrawMaps;
|
|
@ -12,25 +12,26 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="flex flex-col min-h-screen mx-auto bg-gray-100">
|
<body class="flex flex-col min-h-screen mx-auto bg-gray-100">
|
||||||
<header class="bg-white border-b">
|
<header class="py-8 text-white border-b bg-pine-900">
|
||||||
<div class="container flex items-center justify-between px-2 py-4 mx-auto">
|
<div class="container flex flex-col px-2 py-4 mx-auto">
|
||||||
<a href="<?= route_to('home') ?>" class="text-2xl"><?= isset($page)
|
<a href="<?= route_to('home') ?>"
|
||||||
|
class="inline-flex items-center mb-2"><?= icon(
|
||||||
|
'arrow-left',
|
||||||
|
'mr-2',
|
||||||
|
) . lang('Page.back_to_home') ?></a>
|
||||||
|
<h1 class="text-3xl font-semibold"><?= isset($page)
|
||||||
? $page->title
|
? $page->title
|
||||||
: 'Castopod' ?></a>
|
: 'Castopod' ?></h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="container flex-1 px-4 py-10 mx-auto">
|
<main class="container flex-1 px-4 py-10 mx-auto">
|
||||||
<?= $this->renderSection('content') ?>
|
<?= $this->renderSection('content') ?>
|
||||||
</main>
|
</main>
|
||||||
<footer class="px-2 py-4 bg-white border-t">
|
<footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t">
|
||||||
<div class="container flex flex-col items-center justify-between mx-auto text-xs md:flex-row ">
|
<?= render_page_links() ?>
|
||||||
<?= render_page_links('inline-flex mb-4 md:mb-0') ?>
|
<small><?= lang('Common.powered_by', [
|
||||||
<p class="flex flex-col items-center md:items-end">
|
'castopod' =>
|
||||||
<?= lang('Common.powered_by', [
|
'<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||||
'castopod' =>
|
]) ?></small>
|
||||||
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
|
||||||
]) ?>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?= helper('page') ?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<?= service('request')->getLocale() ?>" class="h-full">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<title><?= lang('Page.map') ?></title>
|
||||||
|
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||||
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
|
<?= service('vite')->asset('js/map.ts', 'js') ?>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="flex flex-col h-full min-h-screen mx-auto bg-gray-100">
|
||||||
|
<header class="py-8 text-white border-b bg-pine-900">
|
||||||
|
<div class="container flex flex-col px-2 py-4 mx-auto">
|
||||||
|
<a href="<?= route_to('home') ?>"
|
||||||
|
class="inline-flex items-center mb-2"><?= icon(
|
||||||
|
'arrow-left',
|
||||||
|
'mr-2',
|
||||||
|
) . lang('Page.back_to_home') ?></a>
|
||||||
|
<h1 class="text-3xl font-semibold"><?= lang('Page.map') ?></h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-1 w-full h-full">
|
||||||
|
<div id="map" data-episodes-map-data-url="<?= url_to('episodes-markers') ?>" class="w-full h-full"></div>
|
||||||
|
</main>
|
||||||
|
<footer class="container flex justify-between px-2 py-4 mx-auto text-sm text-right border-t">
|
||||||
|
<?= render_page_links() ?>
|
||||||
|
<small><?= lang('Common.powered_by', [
|
||||||
|
'castopod' =>
|
||||||
|
'<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||||
|
]) ?></small>
|
||||||
|
</footer>
|
||||||
|
</body>
|
|
@ -72,7 +72,7 @@
|
||||||
</div>
|
</div>
|
||||||
<footer class="px-2 py-4 mt-auto text-gray-600 border-t">
|
<footer class="px-2 py-4 mt-auto text-gray-600 border-t">
|
||||||
<div class="container flex flex-col justify-between mx-auto text-xs">
|
<div class="container flex flex-col justify-between mx-auto text-xs">
|
||||||
<?= render_page_links('inline-flex mb-2') ?>
|
<?= render_page_links('inline-flex mb-2 flex-wrap gap-y-1') ?>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<p><?= $podcast->copyright ?></p>
|
<p><?= $podcast->copyright ?></p>
|
||||||
<p><?= lang('Common.powered_by', [
|
<p><?= lang('Common.powered_by', [
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -34,6 +34,8 @@
|
||||||
"@rollup/plugin-multi-entry": "^4.0.0",
|
"@rollup/plugin-multi-entry": "^4.0.0",
|
||||||
"choices.js": "^9.0.1",
|
"choices.js": "^9.0.1",
|
||||||
"flatpickr": "^4.6.9",
|
"flatpickr": "^4.6.9",
|
||||||
|
"leaflet": "^1.7.1",
|
||||||
|
"leaflet.markercluster": "^1.5.1",
|
||||||
"lit": "^2.0.0-rc.2",
|
"lit": "^2.0.0-rc.2",
|
||||||
"prosemirror-example-setup": "^1.1.2",
|
"prosemirror-example-setup": "^1.1.2",
|
||||||
"prosemirror-markdown": "^1.5.1",
|
"prosemirror-markdown": "^1.5.1",
|
||||||
|
@ -50,6 +52,7 @@
|
||||||
"@tailwindcss/forms": "^0.2.1",
|
"@tailwindcss/forms": "^0.2.1",
|
||||||
"@tailwindcss/line-clamp": "^0.2.0",
|
"@tailwindcss/line-clamp": "^0.2.0",
|
||||||
"@tailwindcss/typography": "^0.4.0",
|
"@tailwindcss/typography": "^0.4.0",
|
||||||
|
"@types/leaflet": "^1.7.5",
|
||||||
"@types/prosemirror-markdown": "^1.5.1",
|
"@types/prosemirror-markdown": "^1.5.1",
|
||||||
"@types/prosemirror-view": "^1.17.1",
|
"@types/prosemirror-view": "^1.17.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||||
|
|
|
@ -16,6 +16,7 @@ export default defineConfig({
|
||||||
"install.ts": "app/Resources/js/install.ts",
|
"install.ts": "app/Resources/js/install.ts",
|
||||||
"admin.ts": "app/Resources/js/admin.ts",
|
"admin.ts": "app/Resources/js/admin.ts",
|
||||||
"charts.ts": "app/Resources/js/charts.ts",
|
"charts.ts": "app/Resources/js/charts.ts",
|
||||||
|
"map.ts": "app/Resources/js/map.ts",
|
||||||
"styles/index.css": "app/Resources/styles/index.css",
|
"styles/index.css": "app/Resources/styles/index.css",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue