Client - init workout detail w/ map (wip)
This commit is contained in:
parent
e7fd6860d3
commit
695fa0d0f1
@ -10,12 +10,15 @@
|
||||
"i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.json\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmcw/togeojson": "^4.5.0",
|
||||
"@vue-leaflet/vue-leaflet": "^0.6.1",
|
||||
"axios": "^0.21.1",
|
||||
"chart.js": "^3.5.1",
|
||||
"chartjs-plugin-datalabels": "^2.0.0",
|
||||
"core-js": "^3.6.5",
|
||||
"date-fns": "^2.23.0",
|
||||
"date-fns-tz": "^1.1.6",
|
||||
"leaflet": "^1.7.1",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"vue": "^3.0.0",
|
||||
"vue-chart-3": "^0.5.8",
|
||||
|
@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="stylesheet" href="<%= BASE_URL %>static/css/fork-awesome.min.css"/>
|
||||
<link rel="stylesheet" href="<%= BASE_URL %>static/css/leaflet.css"/>
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
|
640
fittrackee_client/public/static/css/leaflet.css
Normal file
640
fittrackee_client/public/static/css/leaflet.css
Normal file
@ -0,0 +1,640 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg,
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-tile {
|
||||
will-change: opacity;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline: 0;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-container a.leaflet-active {
|
||||
outline: 2px solid orange;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path {
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution,
|
||||
.leaflet-container .leaflet-control-scale {
|
||||
font-size: 11px;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 19px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 18px 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 4px 4px 0 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||
color: #c3c3c3;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||
color: #999;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip-container {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-clickable {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div id="workout-map">
|
||||
<div
|
||||
class="leaflet-container"
|
||||
v-if="geoJson.jsonData && center && bounds.length === 2"
|
||||
>
|
||||
<LMap :zoom="options.zoom" :center="center" :bounds="bounds">
|
||||
<LTileLayer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
:bounds="bounds"
|
||||
/>
|
||||
<LGeoJson :geojson="geoJson.jsonData" />
|
||||
</LMap>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { gpx } from '@tmcw/togeojson'
|
||||
import { LGeoJson, LMap, LTileLayer } from '@vue-leaflet/vue-leaflet'
|
||||
import { ComputedRef, PropType, computed, defineComponent } from 'vue'
|
||||
|
||||
import { GeoJSONData } from '@/types/geojson'
|
||||
import { IWorkoutState } from '@/types/workouts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WorkoutMap',
|
||||
components: {
|
||||
LGeoJson,
|
||||
LMap,
|
||||
LTileLayer,
|
||||
},
|
||||
props: {
|
||||
workout: {
|
||||
type: Object as PropType<IWorkoutState>,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
function getGeoJson(gpxContent: string): GeoJSONData {
|
||||
if (!gpxContent || gpxContent !== '') {
|
||||
try {
|
||||
const jsonData = gpx(
|
||||
new DOMParser().parseFromString(gpxContent, 'text/xml')
|
||||
)
|
||||
return { jsonData }
|
||||
} catch (e) {
|
||||
console.error('Invalid gpx content')
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
function getCenter(bounds: ComputedRef<number[][]>): number[] {
|
||||
return [
|
||||
(bounds.value[0][0] + bounds.value[1][0]) / 2,
|
||||
(bounds.value[0][1] + bounds.value[1][1]) / 2,
|
||||
]
|
||||
}
|
||||
const bounds = computed(() =>
|
||||
props.workout
|
||||
? [
|
||||
[
|
||||
props.workout.workout.bounds[0],
|
||||
props.workout.workout.bounds[1],
|
||||
],
|
||||
[
|
||||
props.workout.workout.bounds[2],
|
||||
props.workout.workout.bounds[3],
|
||||
],
|
||||
]
|
||||
: []
|
||||
)
|
||||
|
||||
return {
|
||||
bounds: bounds,
|
||||
center: computed(() => getCenter(bounds)),
|
||||
geoJson: computed(() =>
|
||||
props.workout && props.workout.gpx
|
||||
? getGeoJson(props.workout.gpx)
|
||||
: {}
|
||||
),
|
||||
options: { zoom: 13 },
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base';
|
||||
.leaflet-container {
|
||||
height: 400px;
|
||||
width: 600px;
|
||||
}
|
||||
</style>
|
107
fittrackee_client/src/components/Workout/WorkoutDetail/index.vue
Normal file
107
fittrackee_client/src/components/Workout/WorkoutDetail/index.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="workout-detail">
|
||||
<Card :without-title="false">
|
||||
<template #title>
|
||||
<div class="workout-previous">
|
||||
<i class="fa fa-chevron-left" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="workout-card-title">
|
||||
<div class="sport-img">
|
||||
<img alt="workout sport logo" :src="sport.img" />
|
||||
</div>
|
||||
<div class="workout-title-date">
|
||||
<div class="workout-title">{{ workout.workout.title }}</div>
|
||||
<div class="workout-date">
|
||||
{{ workoutDate.workout_date }} - {{ workoutDate.workout_time }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workout-next">
|
||||
<i class="fa fa-chevron-right" aria-hidden="true" />
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<WorkoutMap :workout="workout" />
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent, computed } from 'vue'
|
||||
|
||||
import Card from '@/components/Common/Card.vue'
|
||||
import WorkoutMap from '@/components/Workout/WorkoutDetail/WorkoutMap.vue'
|
||||
import { ISport } from '@/types/sports'
|
||||
import { IAuthUserProfile } from '@/types/user'
|
||||
import { IWorkoutState } from '@/types/workouts'
|
||||
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WorkoutDetail',
|
||||
components: {
|
||||
Card,
|
||||
WorkoutMap,
|
||||
},
|
||||
props: {
|
||||
authUser: {
|
||||
type: Object as PropType<IAuthUserProfile>,
|
||||
required: true,
|
||||
},
|
||||
sports: {
|
||||
type: Object as PropType<ISport[]>,
|
||||
},
|
||||
workout: {
|
||||
type: Object as PropType<IWorkoutState>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
sport: computed(() =>
|
||||
props.sports
|
||||
? props.sports.find(
|
||||
(sport) => sport.id === props.workout.workout.sport_id
|
||||
)
|
||||
: {}
|
||||
),
|
||||
workoutDate: computed(() =>
|
||||
formatWorkoutDate(
|
||||
getDateWithTZ(
|
||||
props.workout.workout.workout_date,
|
||||
props.authUser.timezone
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base';
|
||||
.workout-detail {
|
||||
::v-deep(.card) {
|
||||
.card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.workout-card-title {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
.sport-img {
|
||||
img {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
padding: 0 $default-padding;
|
||||
}
|
||||
}
|
||||
.workout-date {
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -2,6 +2,7 @@
|
||||
"UNKNOWN": "Error. Please try again or contact the administrator.",
|
||||
"APP_ERROR": "The application seems encounter some issues.<br />Please try later or contact the administrator.",
|
||||
"NOT_FOUND": {
|
||||
"PAGE": "Page not found"
|
||||
"PAGE": "Page not found",
|
||||
"WORKOUT": "Workout not found"
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
"UNKNOWN": "Erreur. Veuillez réessayer ou contacter l'administrateur.",
|
||||
"APP_ERROR": "L'application semble rencontrer quelques problèmes.<br />Veuillez réessayer plus tard ou contacter l'administrateur.",
|
||||
"NOT_FOUND": {
|
||||
"PAGE": "Page introuvable"
|
||||
"PAGE": "Page introuvable",
|
||||
"WORKOUT": "Séance introuvable"
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import { USER_STORE } from '@/store/constants'
|
||||
import Dashboard from '@/views/DashBoard.vue'
|
||||
import LoginOrRegister from '@/views/LoginOrRegister.vue'
|
||||
import NotFoundView from '@/views/NotFoundView.vue'
|
||||
import Workout from '@/views/Workout.vue'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
@ -24,6 +25,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: LoginOrRegister,
|
||||
props: { action: 'register' },
|
||||
},
|
||||
{
|
||||
path: '/workouts/:workoutId',
|
||||
name: 'Workout',
|
||||
component: Workout,
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundView },
|
||||
]
|
||||
|
||||
|
@ -49,4 +49,37 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
|
||||
): void {
|
||||
getWorkouts(context, payload, 'USER_WORKOUTS')
|
||||
},
|
||||
[WORKOUTS_STORE.ACTIONS.GET_WORKOUT](
|
||||
context: ActionContext<IWorkoutsState, IRootState>,
|
||||
workoutId: string
|
||||
): void {
|
||||
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, true)
|
||||
authApi
|
||||
.get(`workouts/${workoutId}`)
|
||||
.then((res) => {
|
||||
if (res.data.status === 'success') {
|
||||
context.commit(
|
||||
WORKOUTS_STORE.MUTATIONS.SET_WORKOUT,
|
||||
res.data.data.workouts[0]
|
||||
)
|
||||
if (res.data.data.workouts[0].with_gpx) {
|
||||
authApi.get(`workouts/${workoutId}/gpx`).then((res) => {
|
||||
if (res.data.status === 'success') {
|
||||
context.commit(
|
||||
WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX,
|
||||
res.data.data.gpx
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
})
|
||||
.catch((error) => handleError(context, error))
|
||||
.finally(() =>
|
||||
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false)
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -1,15 +1,20 @@
|
||||
export enum WorkoutsActions {
|
||||
GET_CALENDAR_WORKOUTS = 'GET_CALENDAR_WORKOUTS',
|
||||
GET_USER_WORKOUTS = 'GET_USER_WORKOUTS',
|
||||
GET_WORKOUT = 'GET_WORKOUT',
|
||||
}
|
||||
|
||||
export enum WorkoutsGetters {
|
||||
CALENDAR_WORKOUTS = 'CALENDAR_WORKOUTS',
|
||||
USER_WORKOUTS = 'USER_WORKOUTS',
|
||||
WORKOUT = 'WORKOUT',
|
||||
}
|
||||
|
||||
export enum WorkoutsMutations {
|
||||
EMPTY_WORKOUTS = 'EMPTY_WORKOUTS',
|
||||
SET_CALENDAR_WORKOUTS = 'SET_CALENDAR_WORKOUTS',
|
||||
SET_USER_WORKOUTS = 'SET_USER_WORKOUTS',
|
||||
SET_WORKOUT = 'SET_WORKOUT',
|
||||
SET_WORKOUT_GPX = 'SET_WORKOUT_GPX',
|
||||
SET_WORKOUT_LOADING = 'SET_WORKOUT_LOADING',
|
||||
}
|
||||
|
@ -15,4 +15,7 @@ export const getters: GetterTree<IWorkoutsState, IRootState> &
|
||||
[WORKOUTS_STORE.GETTERS.USER_WORKOUTS]: (state: IWorkoutsState) => {
|
||||
return state.user_workouts
|
||||
},
|
||||
[WORKOUTS_STORE.GETTERS.WORKOUT]: (state: IWorkoutsState) => {
|
||||
return state.workout
|
||||
},
|
||||
}
|
||||
|
@ -20,6 +20,24 @@ export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
|
||||
) {
|
||||
state.user_workouts = workouts
|
||||
},
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT](
|
||||
state: IWorkoutsState,
|
||||
workout: IWorkout
|
||||
) {
|
||||
state.workout.workout = workout
|
||||
},
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING](
|
||||
state: IWorkoutsState,
|
||||
loading: boolean
|
||||
) {
|
||||
state.workout.loading = loading
|
||||
},
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX](
|
||||
state: IWorkoutsState,
|
||||
gpx: string
|
||||
) {
|
||||
state.workout.gpx = gpx
|
||||
},
|
||||
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS](state: IWorkoutsState) {
|
||||
state.calendar_workouts = []
|
||||
state.user_workouts = []
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { IWorkoutsState } from '@/store/modules/workouts/types'
|
||||
import { IWorkout } from '@/types/workouts'
|
||||
|
||||
export const workoutsState: IWorkoutsState = {
|
||||
calendar_workouts: [],
|
||||
user_workouts: [],
|
||||
workout: {
|
||||
gpx: '',
|
||||
loading: false,
|
||||
workout: <IWorkout>{},
|
||||
},
|
||||
}
|
||||
|
@ -7,11 +7,12 @@ import {
|
||||
|
||||
import { WORKOUTS_STORE } from '@/store/constants'
|
||||
import { IRootState } from '@/store/modules/root/types'
|
||||
import { IWorkout, IWorkoutsPayload } from '@/types/workouts'
|
||||
import { IWorkout, IWorkoutsPayload, IWorkoutState } from '@/types/workouts'
|
||||
|
||||
export interface IWorkoutsState {
|
||||
user_workouts: IWorkout[]
|
||||
calendar_workouts: IWorkout[]
|
||||
workout: IWorkoutState
|
||||
}
|
||||
|
||||
export interface IWorkoutsActions {
|
||||
@ -23,11 +24,16 @@ export interface IWorkoutsActions {
|
||||
context: ActionContext<IWorkoutsState, IRootState>,
|
||||
payload: IWorkoutsPayload
|
||||
): void
|
||||
[WORKOUTS_STORE.ACTIONS.GET_WORKOUT](
|
||||
context: ActionContext<IWorkoutsState, IRootState>,
|
||||
workoutId: string | string[]
|
||||
): void
|
||||
}
|
||||
|
||||
export interface IWorkoutsGetters {
|
||||
[WORKOUTS_STORE.GETTERS.CALENDAR_WORKOUTS](state: IWorkoutsState): IWorkout[]
|
||||
[WORKOUTS_STORE.GETTERS.USER_WORKOUTS](state: IWorkoutsState): IWorkout[]
|
||||
[WORKOUTS_STORE.GETTERS.WORKOUT](state: IWorkoutsState): IWorkoutState
|
||||
}
|
||||
|
||||
export type TWorkoutsMutations<S = IWorkoutsState> = {
|
||||
@ -39,6 +45,12 @@ export type TWorkoutsMutations<S = IWorkoutsState> = {
|
||||
state: S,
|
||||
workouts: IWorkout[]
|
||||
): void
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT](state: S, workout: IWorkout): void
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX](state: S, gpx: string): void
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING](
|
||||
state: S,
|
||||
loading: boolean
|
||||
): void
|
||||
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS](state: S): void
|
||||
}
|
||||
|
||||
|
1
fittrackee_client/src/togeojson.d.ts
vendored
Normal file
1
fittrackee_client/src/togeojson.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '@tmcw/togeojson'
|
3
fittrackee_client/src/types/geojson.ts
Normal file
3
fittrackee_client/src/types/geojson.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface GeoJSONData {
|
||||
jsonData?: Record<string, unknown>
|
||||
}
|
@ -78,3 +78,9 @@ export interface IWorkoutsPayload {
|
||||
per_page?: number
|
||||
page?: number
|
||||
}
|
||||
|
||||
export interface IWorkoutState {
|
||||
gpx: string
|
||||
loading: boolean
|
||||
workout: IWorkout
|
||||
}
|
||||
|
79
fittrackee_client/src/views/Workout.vue
Normal file
79
fittrackee_client/src/views/Workout.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div id="workout">
|
||||
<div class="workout-loading" v-if="workout.loading">
|
||||
<div class="loading">
|
||||
<Loader />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="container">
|
||||
<div v-if="workout.workout.id">
|
||||
<WorkoutDetail
|
||||
v-if="sports.length > 0"
|
||||
:workout="workout"
|
||||
:sports="sports"
|
||||
:authUser="authUser"
|
||||
/>
|
||||
</div>
|
||||
<div class="container" v-else>
|
||||
<NotFound target="WORKOUT" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ComputedRef, defineComponent, onBeforeMount } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import Loader from '@/components/Common/Loader.vue'
|
||||
import NotFound from '@/components/Common/NotFound.vue'
|
||||
import WorkoutDetail from '@/components/Workout/WorkoutDetail/index.vue'
|
||||
import { SPORTS_STORE, USER_STORE, WORKOUTS_STORE } from '@/store/constants'
|
||||
import { IAuthUserProfile } from '@/types/user'
|
||||
import { IWorkoutState } from '@/types/workouts'
|
||||
import { useStore } from '@/use/useStore'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Workout',
|
||||
components: {
|
||||
Loader,
|
||||
NotFound,
|
||||
WorkoutDetail,
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
onBeforeMount(() =>
|
||||
store.dispatch(
|
||||
WORKOUTS_STORE.ACTIONS.GET_WORKOUT,
|
||||
route.params.workoutId
|
||||
)
|
||||
)
|
||||
|
||||
const workout: ComputedRef<IWorkoutState> = computed(
|
||||
() => store.getters[WORKOUTS_STORE.GETTERS.WORKOUT]
|
||||
)
|
||||
const authUser: ComputedRef<IAuthUserProfile> = computed(
|
||||
() => store.getters[USER_STORE.GETTERS.AUTH_USER_PROFILE]
|
||||
)
|
||||
const sports = computed(() => store.getters[SPORTS_STORE.GETTERS.SPORTS])
|
||||
|
||||
return { authUser, sports, workout }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base';
|
||||
#workout {
|
||||
.workout-loading {
|
||||
height: $app-height;
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
11
fittrackee_client/src/vue-leaflet.d.ts
vendored
Normal file
11
fittrackee_client/src/vue-leaflet.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
declare module '@vue-leaflet/vue-leaflet' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
export const LMap: DefineComponent
|
||||
export const LIcon: DefineComponent
|
||||
export const LTileLayer: DefineComponent
|
||||
export const LMarker: DefineComponent
|
||||
export const LGeoJson: DefineComponent
|
||||
export const LPolyline: DefineComponent
|
||||
export const LPolygon: DefineComponent
|
||||
export const LRectangle: DefineComponent
|
||||
}
|
@ -1085,6 +1085,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@soda/get-current-script/-/get-current-script-1.0.2.tgz#a53515db25d8038374381b73af20bb4f2e508d87"
|
||||
integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==
|
||||
|
||||
"@tmcw/togeojson@^4.5.0":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@tmcw/togeojson/-/togeojson-4.5.0.tgz#9b5c7bdd8c5ad3b9c504824d3cdef9b60edbd206"
|
||||
integrity sha512-lNuuhW7nvN1T7xII9eRTi9zuPwYfFl43/1u/Xgi88tedX4ePfwJB5dqc31N7z6sWeR+7EES274ESNrK1gsW53A==
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c"
|
||||
@ -1338,6 +1343,11 @@
|
||||
"@typescript-eslint/types" "4.28.4"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@vue-leaflet/vue-leaflet@^0.6.1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.6.1.tgz#d731a5d2256d049e345f58330616180191d88b12"
|
||||
integrity sha512-/sm0bdrdftXh5nSGEPsoKrJI1D/GtKiEsBo9X/TA2yu4lYTDcaem6U4t1Ea5CoLleiZRCNUrZr9PG/xHdUPXYA==
|
||||
|
||||
"@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81"
|
||||
@ -6227,6 +6237,11 @@ launch-editor@^2.2.1:
|
||||
chalk "^2.3.0"
|
||||
shell-quote "^1.6.1"
|
||||
|
||||
leaflet@^1.7.1:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19"
|
||||
integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==
|
||||
|
||||
levn@^0.3.0, levn@~0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
|
||||
|
Loading…
Reference in New Issue
Block a user