Add map based provider picker

This commit is contained in:
Kacper Donat 2020-05-02 23:33:15 +02:00
parent ec23a41e37
commit a893929cf9
25 changed files with 389 additions and 166 deletions

View File

@ -17,6 +17,8 @@ services:
- ./:/var/www:cached - ./:/var/www:cached
- ./docker/php/log.conf:/usr/local/etc/php-fpm.d/zz-log.conf - ./docker/php/log.conf:/usr/local/etc/php-fpm.d/zz-log.conf
blackfire: blackfire:
image: blackfire/blackfire image: blackfire/blackfire
ports: ["8707"] ports: ["8707"]

View File

@ -0,0 +1,42 @@
<main class="d-flex">
<div style="width: 100%">
<l-map :center="{ lat: 52.0194, lon: 19.1451 }" :zoom=7 :options="{ zoomControl: false }" class="map">
<l-vector-layer url="https://api.maptiler.com/maps/bright/style.json?key=8GX5FRUNgk4lB83GZT8Q"
token="not-needed"
attribution='<a href="https://www.maptiler.com/copyright/" target="_blank">© MapTiler</a> <a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>'
/>
<div class="provider-picker">
<h2 class="provider-picker__heading">Wybierz lokaliację</h2>
<ul class="provider-picker__providers">
<li v-for="provider in providers" :key="provider.id" class="provider-picker__provider">
<a :href="`/${provider.id}`" class="provider">
<ui-icon icon="line-bus" size="2x" />
<div>
<div class="provider__short-name">{{ provider.shortName }}</div>
<div class="provider__name">{{ provider.name }}</div>
</div>
<tooltip v-if="provider.lastUpdate != null">Ostatnia akutalizacja: {{ provider.lastUpdate|moment('YYYY-MM-DD HH:mm') }}</tooltip>
</a>
</li>
</ul>
</div>
<l-marker :lat-lng="provider.location" v-for="provider in providers" :options="{ keyboard: false }" :key="provider.id">
<l-icon>
<div class="map__label-box" tabindex="0">
<a :href="`/${provider.id}`" class="provider">
<ui-icon icon="line-bus" class="map__icon" />
<div>
<div class="provider__short-name">{{ provider.shortName }}</div>
<div class="provider__name">{{ provider.name }}</div>
</div>
</a>
</div>
</l-icon>
</l-marker>
</l-map>
</div>
<portal-target name="popups" multiple/>
</main>

View File

@ -0,0 +1,29 @@
.map__label-box {
@extend .popper;
padding: .5rem;
background: white;
transform-origin: 50% 50%;
transform: translateX(-50%);
min-width: max-content;
font-size: 9pt;
font-weight: bold;
align-items: center;
@include active {
transform: translateX(-50%) scale(1.1);
}
@include flex-with-spacing(.5rem);
}
.map__icon {
font-size: 1.5rem;
}
img.map__icon {
width: 24px;
height: 24px;
}

View File

@ -61,6 +61,20 @@ $grid-gutter-width: $spacer * 2;
} }
} }
@mixin active {
&:hover, &:active, &:focus, #{&}--active {
@content
}
}
@mixin flex-with-spacing($spacing) {
display: flex;
& > *:not(:last-child) {
margin-right: $spacing;
}
}
@import "common"; @import "common";
@import "stop"; @import "stop";
@import "departure"; @import "departure";
@ -72,9 +86,12 @@ $grid-gutter-width: $spacer * 2;
@import "favourites"; @import "favourites";
@import "trip"; @import "trip";
@import "dragscroll"; @import "dragscroll";
@import "map";
@import "ui/switch"; @import "ui/switch";
@import "page/provider-picker";
body { body {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;

View File

@ -0,0 +1,62 @@
.provider__name {
font-size: .9em;
color: $gray-800;
}
.provider__short-name {
font-weight: bold;
}
.provider-picker {
@extend .popper;
padding: 1rem;
margin: 3rem;
}
.provider-picker__heading {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 1rem;
}
.provider-picker__providers {
list-style: none;
padding: 0;
margin: 0;
}
.provider-picker__provider {
font-size: 1rem;
.provider {
margin: 0 -1rem;
padding: .5rem 1rem;
&:hover {
background: $gray-100;
}
}
}
.provider {
@include flex-with-spacing(.5rem);
align-items: center;
&:hover {
text-decoration: none;
}
}
@include media-breakpoint-down('sm') {
.provider-picker {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 1.5rem;
}
.provider-picker__providers {
max-height: 170px;
}
}

View File

@ -14,7 +14,6 @@ import VueDragscroll from 'vue-dragscroll';
import { Plugin as VueFragment } from 'vue-fragment'; import { Plugin as VueFragment } from 'vue-fragment';
import { Workbox } from "workbox-window"; import { Workbox } from "workbox-window";
import { migrate } from "./store/migrations";
import { Component } from "vue-property-decorator"; import { Component } from "vue-property-decorator";
import * as VueMoment from "vue-moment"; import * as VueMoment from "vue-moment";
import * as moment from 'moment'; import * as moment from 'moment';
@ -41,6 +40,8 @@ Component.registerHooks(['removed']);
// async dependencies // async dependencies
(async function () { (async function () {
const { migrate } = await import('./store/migrations');
await migrate("vuex"); await migrate("vuex");
const [ components, { default: store } ] = await Promise.all([ const [ components, { default: store } ] = await Promise.all([
@ -50,17 +51,16 @@ Component.registerHooks(['removed']);
import('bootstrap'), import('bootstrap'),
] as const); ] as const);
const appRoot = document.getElementById('app');
// here goes "public" API // here goes "public" API
window['app'] = Object.assign({ window['app'] = Object.assign({
state: {} state: {}
}, window['app'], { }, window['app'], {
components, components,
application: new components.Application({ el: '#app' }) application: appRoot ? new components.Application({ el: '#app' }) : new components.PageProviderList({ el: '#provider-picker' }),
}); });
store.dispatch('messages/update');
store.dispatch('load', window['app'].state);
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
const wb = new Workbox("/service-worker.js"); const wb = new Workbox("/service-worker.js");

View File

@ -1,7 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import store from '../store' import store from '../store'
import { Component, Watch } from "vue-property-decorator"; import { Component, Watch } from "vue-property-decorator";
import { Mutation, Action } from 'vuex-class' import { Action, Mutation } from 'vuex-class'
import { Stop } from "../model"; import { Stop } from "../model";
import { DeparturesSettingsState } from "../store/settings/departures"; import { DeparturesSettingsState } from "../store/settings/departures";
import { MessagesSettingsState } from "../store/settings/messages"; import { MessagesSettingsState } from "../store/settings/messages";
@ -48,6 +48,9 @@ export class Application extends Vue {
} }
created() { created() {
this.$store.dispatch('messages/update');
this.$store.dispatch('load', window['app'].state);
this.initDeparturesRefreshInterval(); this.initDeparturesRefreshInterval();
this.initMessagesRefreshInterval(); this.initMessagesRefreshInterval();
} }

View File

@ -11,5 +11,7 @@ export * from './favourites'
export * from './trip' export * from './trip'
export * from './ui' export * from './ui'
export * from './settings' export * from './settings'
export * from "./page"
export { Departures } from "../store"; export { Departures } from "../store";
export { Messages } from "../store"; export { Messages } from "../store";

View File

@ -1,4 +1,4 @@
import { LMap, LTileLayer, LMarker } from 'vue2-leaflet'; import { LControl, LIcon, LMap, LMarker, LPopup, LTileLayer } from 'vue2-leaflet';
import Vue from 'vue'; import Vue from 'vue';
import * as L from 'leaflet' import * as L from 'leaflet'
@ -48,5 +48,8 @@ Vue.component('LMap', LMap);
Vue.component('LTileLayer', LTileLayer); Vue.component('LTileLayer', LTileLayer);
Vue.component('LVectorLayer', LVectorLayer); Vue.component('LVectorLayer', LVectorLayer);
Vue.component('LMarker', LMarker); Vue.component('LMarker', LMarker);
Vue.component('LControl', LControl);
Vue.component('LPopup', LPopup)
Vue.component('LIcon', LIcon);
export { LMap, LTileLayer, LMarker } from 'vue2-leaflet'; export { LMap, LTileLayer, LMarker, LIcon, LControl, LPopup } from 'vue2-leaflet';

View File

@ -0,0 +1 @@
export * from "./providers"

View File

@ -0,0 +1,26 @@
import Vue from 'vue'
import { Component } from 'vue-property-decorator'
import { Provider } from "../../model";
import { Jsonified } from "../../utils";
import * as moment from 'moment';
@Component({
template: require('../../../components/page/providers.html'),
})
export class PageProviderList extends Vue {
private providers: Provider[] = [];
async created() {
const response = await fetch('/api/v1/providers');
const result = await response.json() as Jsonified<Provider>[];
this.providers = result.map<Provider>(provider => {
return {
...provider,
lastUpdate: provider.lastUpdate && moment(provider.lastUpdate)
}
});
}
}
Vue.component('PageProviderList', PageProviderList);

View File

@ -0,0 +1,4 @@
export interface Location {
lat: number,
lng: number,
}

View File

@ -3,3 +3,5 @@ export * from './departure'
export * from './line' export * from './line'
export * from './error' export * from './error'
export * from './identity' export * from './identity'
export * from './common'
export * from './provider'

View File

@ -0,0 +1,11 @@
import { Moment } from "moment";
import { Location } from "./common";
export interface Provider {
id: string;
name: string;
shortName: string;
attribution?: string;
lastUpdate?: Moment;
location: Location;
}

View File

@ -1,13 +1,11 @@
import { Line } from "./line"; import { Line } from "./line";
import { Location } from "./common";
export interface Stop { export interface Stop {
id: any; id: any;
name: string; name: string;
description?: string; description?: string;
location?: { location?: Location;
lat: number,
lng: number,
};
onDemand?: boolean; onDemand?: boolean;
variant?: string; variant?: string;
} }

View File

@ -5,6 +5,7 @@ import { ensureArray } from "../utils";
export interface RootState { export interface RootState {
stops: Stop[], stops: Stop[],
provider: any,
} }
export interface SavedState { export interface SavedState {
@ -13,7 +14,8 @@ export interface SavedState {
} }
export const state: RootState = { export const state: RootState = {
stops: [] stops: [],
provider: null,
}; };
export const mutations: MutationTree<RootState> = { export const mutations: MutationTree<RootState> = {
@ -37,4 +39,4 @@ export const actions: ActionTree<RootState, undefined> = {
version: 1, version: 1,
stops: state.stops.map(stop => stop.id) stops: state.stops.map(stop => stop.id)
}) })
}; };

View File

@ -1,3 +1,5 @@
import store from "./store";
export type UrlParams = { export type UrlParams = {
[name: string]: any [name: string]: any
} }
@ -61,5 +63,5 @@ export default {
tracks: `${base}/stops/{id}/tracks` tracks: `${base}/stops/{id}/tracks`
}, },
trip: `${base}/trips/{id}`, trip: `${base}/trips/{id}`,
prepare: (url: string, params: UrlParams = { }) => prepare(url, Object.assign({}, { provider: window['data'].provider }, params)) prepare: (url: string, params: UrlParams = { }) => prepare(url, Object.assign({}, { provider: store.state.provider }, params))
} }

View File

@ -49,6 +49,12 @@ class Provider implements Fillable, Referable
*/ */
private $lastUpdate; private $lastUpdate;
/**
* Location of provider centre of interest.
* @var Location
*/
private $location;
public function getId(): string public function getId(): string
{ {
return $this->id; return $this->id;
@ -98,4 +104,14 @@ class Provider implements Fillable, Referable
{ {
$this->lastUpdate = $lastUpdate; $this->lastUpdate = $lastUpdate;
} }
public function getLocation(): Location
{
return $this->location;
}
public function setLocation(Location $location): void
{
$this->location = $location;
}
} }

View File

@ -3,6 +3,7 @@
namespace App\Provider\Dummy; namespace App\Provider\Dummy;
use App\Exception\NotSupportedException; use App\Exception\NotSupportedException;
use App\Model\Location;
use App\Provider\DepartureRepository; use App\Provider\DepartureRepository;
use App\Provider\LineRepository; use App\Provider\LineRepository;
use App\Provider\MessageRepository; use App\Provider\MessageRepository;
@ -78,6 +79,11 @@ class DummyProvider implements Provider
return null; return null;
} }
public function getLocation(): Location
{
return new Location(21.4474, 54.7837);
}
public function getTripRepository(): TripRepository public function getTripRepository(): TripRepository
{ {
throw new NotSupportedException(); throw new NotSupportedException();

View File

@ -2,6 +2,7 @@
namespace App\Provider; namespace App\Provider;
use App\Model\Location;
use Carbon\Carbon; use Carbon\Carbon;
interface Provider interface Provider
@ -17,6 +18,7 @@ interface Provider
public function getShortName(): string; public function getShortName(): string;
public function getIdentifier(): string; public function getIdentifier(): string;
public function getAttribution(): ?string; public function getAttribution(): ?string;
public function getLocation(): Location;
public function getLastUpdate(): ?Carbon; public function getLastUpdate(): ?Carbon;
} }

View File

@ -4,6 +4,7 @@
namespace App\Provider\ZtmGdansk; namespace App\Provider\ZtmGdansk;
use App\Entity\ProviderEntity; use App\Entity\ProviderEntity;
use App\Model\Location;
use App\Provider\Database\GenericLineRepository; use App\Provider\Database\GenericLineRepository;
use App\Provider\Database\GenericScheduleRepository; use App\Provider\Database\GenericScheduleRepository;
use App\Provider\Database\GenericStopRepository; use App\Provider\Database\GenericStopRepository;
@ -13,11 +14,9 @@ use App\Provider\DepartureRepository;
use App\Provider\LineRepository; use App\Provider\LineRepository;
use App\Provider\MessageRepository; use App\Provider\MessageRepository;
use App\Provider\Provider; use App\Provider\Provider;
use App\Provider\ScheduleRepository;
use App\Provider\StopRepository; use App\Provider\StopRepository;
use App\Provider\TrackRepository; use App\Provider\TrackRepository;
use App\Provider\TripRepository; use App\Provider\TripRepository;
use App\Provider\ZtmGdansk\{ZtmGdanskDepartureRepository, ZtmGdanskMessageRepository};
use App\Service\Proxy\ReferenceFactory; use App\Service\Proxy\ReferenceFactory;
use Carbon\Carbon; use Carbon\Carbon;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -54,6 +53,11 @@ class ZtmGdanskProvider implements Provider
return '<a href="http://ztm.gda.pl/otwarty_ztm">Otwarte Dane</a> Zarządu Transportu Miejskiego w Gdańsku'; return '<a href="http://ztm.gda.pl/otwarty_ztm">Otwarte Dane</a> Zarządu Transportu Miejskiego w Gdańsku';
} }
public function getLocation(): Location
{
return new Location(18.6466, 54.3520);
}
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
GenericLineRepository $lines, GenericLineRepository $lines,

View File

@ -17,6 +17,7 @@ class ProviderConverter implements Converter
'name' => $entity->getName(), 'name' => $entity->getName(),
'attribution' => $entity->getAttribution(), 'attribution' => $entity->getAttribution(),
'lastUpdate' => $entity->getLastUpdate() ? clone $entity->getLastUpdate() : null, 'lastUpdate' => $entity->getLastUpdate() ? clone $entity->getLastUpdate() : null,
'location' => $entity->getLocation(),
]); ]);
} }
@ -25,3 +26,4 @@ class ProviderConverter implements Converter
return $entity instanceof Provider; return $entity instanceof Provider;
} }
} }

View File

@ -3,146 +3,147 @@
{% block manifest path('webapp_manifest', { provider: provider.identifier }) %} {% block manifest path('webapp_manifest', { provider: provider.identifier }) %}
{% block body %} {% block body %}
<div class="row"> <main id="app" class="container not-ready">
<div class="col-md-8 order-md-last"> <div class="row">
<section class="section messages" v-show="messages.count > 0"> <div class="col-md-8 order-md-last">
<header class="section__title flex"> <section class="section messages" v-show="messages.count > 0">
<h2> <header class="section__title flex">
<ui-icon icon="messages" fixed-width class="mr-2"></ui-icon> <h2>
Komunikaty <span class="ml-2 badge badge-pill badge-dark">{{ '{{ messages.count }}' }}</span> <ui-icon icon="messages" fixed-width class="mr-2"></ui-icon>
</h2> Komunikaty <span class="ml-2 badge badge-pill badge-dark">{{ '{{ messages.count }}' }}</span>
<button class="btn btn-action flex-space-left" ref="settings-messages" @click="visibility.messages = !visibility.messages"> </h2>
<tooltip>ustawienia</tooltip> <button class="btn btn-action flex-space-left" ref="settings-messages" @click="visibility.messages = !visibility.messages">
<ui-icon icon="settings" fixed-width></ui-icon> <tooltip>ustawienia</tooltip>
</button> <ui-icon icon="settings" fixed-width></ui-icon>
<button class="btn btn-action" @click="updateMessages" ref="btn-messages-refresh"> </button>
<tooltip>odśwież</tooltip> <button class="btn btn-action" @click="updateMessages" ref="btn-messages-refresh">
<ui-icon icon="refresh" :spin="messages.state === 'fetching'" fixed-width></ui-icon> <tooltip>odśwież</tooltip>
</button> <ui-icon icon="refresh" :spin="messages.state === 'fetching'" fixed-width></ui-icon>
<button class="btn btn-action" @click="sections.messages = !sections.messages"> </button>
<tooltip> <button class="btn btn-action" @click="sections.messages = !sections.messages">
{{ '{{ ' }} sections.messages ? 'zwiń' : 'rozwiń' {{ '}}' }} <tooltip>
<span class="sr-only">sekcję komunikatów</span> {{ '{{ ' }} sections.messages ? 'zwiń' : 'rozwiń' {{ '}}' }}
</tooltip> <span class="sr-only">sekcję komunikatów</span>
<ui-icon :icon="sections.messages ? 'chevron-up' : 'chevron-down'" fixed-width></ui-icon> </tooltip>
</button> <ui-icon :icon="sections.messages ? 'chevron-up' : 'chevron-down'" fixed-width></ui-icon>
</button>
<portal to="popups"> <portal to="popups">
<popper reference="settings-messages" v-if="visibility.messages" arrow placement="left-start" @leave="visibility.messages = false"> <popper reference="settings-messages" v-if="visibility.messages" arrow placement="left-start" @leave="visibility.messages = false">
<settings-messages></settings-messages> <settings-messages></settings-messages>
</popper> </popper>
</portal> </portal>
</header> </header>
<fold :visible="sections.messages"> <fold :visible="sections.messages">
<messages></messages> <messages></messages>
</fold> </fold>
</section> </section>
<section class="section">
<header class="section__title flex">
<h2>
<ui-icon icon="timetable" fixed-width></ui-icon>
<span class="text">Odjazdy</span>
</h2>
<section class="section"> <button class="btn btn-action flex-space-left" ref="settings-departures" @click="visibility.departures = !visibility.departures">
<header class="section__title flex"> <tooltip>ustawienia</tooltip>
<h2> <ui-icon icon="settings" fixed-width></ui-icon>
<ui-icon icon="timetable" fixed-width></ui-icon> </button>
<span class="text">Odjazdy</span> <button class="btn btn-action" @click="updateDepartures({ stops })">
</h2> <tooltip>odśwież</tooltip>
<ui-icon icon="refresh" :spin="departures.state === 'fetching'" fixed-width></ui-icon>
<button class="btn btn-action flex-space-left" ref="settings-departures" @click="visibility.departures = !visibility.departures"> </button>
<tooltip>ustawienia</tooltip> <portal to="popups">
<ui-icon icon="settings" fixed-width></ui-icon> <popper reference="settings-departures" v-if="visibility.departures" arrow placement="left-start" @leave="visibility.departures = false">
</button> <settings-departures></settings-departures>
<button class="btn btn-action" @click="updateDepartures({ stops })"> </popper>
<tooltip>odśwież</tooltip> </portal>
<ui-icon icon="refresh" :spin="departures.state === 'fetching'" fixed-width></ui-icon> </header>
</button> <departures :stops="stops" v-if="stops.length > 0"></departures>
<portal to="popups"> <div class="alert alert-info" v-else>
<popper reference="settings-departures" v-if="visibility.departures" arrow placement="left-start" @leave="visibility.departures = false">
<settings-departures></settings-departures>
</popper>
</portal>
</header>
<departures :stops="stops" v-if="stops.length > 0"></departures>
<div class="alert alert-info" v-else>
<ui-icon icon="info"></ui-icon>
Wybierz przystanki korzystając z wyszukiwarki poniżej, aby zobaczyć listę odjazdów.
</div>
{% if provider.attribution %}
<div class="attribution">
<ui-icon icon="info"></ui-icon> <ui-icon icon="info"></ui-icon>
Pochodzenie danych: {{ provider.attribution|raw }} Wybierz przystanki korzystając z wyszukiwarki poniżej, aby zobaczyć listę odjazdów.
</div> </div>
{% endif %} {% if provider.attribution %}
</section> <div class="attribution">
</div> <ui-icon icon="info"></ui-icon>
<div class="col-md-4 order-md-first"> Pochodzenie danych: {{ provider.attribution|raw }}
<section class="section picker" v-if="stops.length > 0"> </div>
<header class="section__title flex"> {% endif %}
<h2> </section>
<ui-icon icon="stop" fixed-width></ui-icon> </div>
<span class="text">Przystanki</span> <div class="col-md-4 order-md-first">
</h2> <section class="section picker" v-if="stops.length > 0">
<button class="btn btn-action flex-space-left" @click="clear"> <header class="section__title flex">
<tooltip>usuń wszystkie</tooltip> <h2>
<ui-icon icon="delete" fixed-width></ui-icon> <ui-icon icon="stop" fixed-width></ui-icon>
</button> <span class="text">Przystanki</span>
</header>
<ul class="picker__stops list-underlined">
<li v-for="stop in stops" :key="stop.id" class="d-flex align-items-center">
<picker-stop :stop="stop" class="flex-grow-1">
<template v-slot:primary-action>
<button @click="remove(stop)" class="btn btn-action">
<tooltip>usuń przystanek</tooltip>
<ui-icon icon="remove-stop"></ui-icon>
</button>
</template>
</picker-stop>
</li>
</ul>
<div class="d-flex mt-2">
<button class="btn btn-action btn-sm flex-space-left" @click="visibility.save = true" ref="save">
<ui-icon icon="favourite" fixed-width></ui-icon>
zapisz jako...
</button>
</div>
<popper reference="save" v-if="visibility.save" arrow tabindex="-1" @leave="visibility.save = false" placement="bottom-end">
<favourites-adder @saved="visibility.save = false"/>
</popper>
</section>
<section class="section picker">
<header class="section__title flex">
<template v-if="visibility.picker === 'search'">
<h2 class="flex-grow-1">
<ui-icon icon="search" fixed-width class="mr-1"></ui-icon>
Wybierz przystanki
</h2> </h2>
<button class="btn btn-action" @click="visibility.picker = 'favourites'"> <button class="btn btn-action flex-space-left" @click="clear">
<tooltip>Zapisane</tooltip> <tooltip>usuń wszystkie</tooltip>
<ui-icon icon="favourite" fixed-witdth></ui-icon> <ui-icon icon="delete" fixed-width></ui-icon>
</button> </button>
</template> </header>
<template v-else>
<h2 class="flex-grow-1">
<ui-icon icon="favourite" fixed-width class="mr-1"></ui-icon>
Zapisane
</h2>
<button class="btn btn-action" @click="visibility.picker = 'search'">
<tooltip>Wybierz przystanki</tooltip>
<ui-icon icon="search" fixed-witdth></ui-icon>
</button>
</template>
</header>
<div class="transition-box">
<transition name="fade">
<stop-finder @select="add" :blacklist="stops" v-if="visibility.picker === 'search'"></stop-finder>
<favourites v-else-if="visibility.picker === 'favourites'"></favourites>
</transition>
</div>
</section>
</div>
</div>
<portal-target name="popups" multiple></portal-target> <ul class="picker__stops list-underlined">
<li v-for="stop in stops" :key="stop.id" class="d-flex align-items-center">
<picker-stop :stop="stop" class="flex-grow-1">
<template v-slot:primary-action>
<button @click="remove(stop)" class="btn btn-action">
<tooltip>usuń przystanek</tooltip>
<ui-icon icon="remove-stop"></ui-icon>
</button>
</template>
</picker-stop>
</li>
</ul>
<div class="d-flex mt-2">
<button class="btn btn-action btn-sm flex-space-left" @click="visibility.save = true" ref="save">
<ui-icon icon="favourite" fixed-width></ui-icon>
zapisz jako...
</button>
</div>
<popper reference="save" v-if="visibility.save" arrow tabindex="-1" @leave="visibility.save = false" placement="bottom-end">
<favourites-adder @saved="visibility.save = false"/>
</popper>
</section>
<section class="section picker">
<header class="section__title flex">
<template v-if="visibility.picker === 'search'">
<h2 class="flex-grow-1">
<ui-icon icon="search" fixed-width class="mr-1"></ui-icon>
Wybierz przystanki
</h2>
<button class="btn btn-action" @click="visibility.picker = 'favourites'">
<tooltip>Zapisane</tooltip>
<ui-icon icon="favourite" fixed-witdth></ui-icon>
</button>
</template>
<template v-else>
<h2 class="flex-grow-1">
<ui-icon icon="favourite" fixed-width class="mr-1"></ui-icon>
Zapisane
</h2>
<button class="btn btn-action" @click="visibility.picker = 'search'">
<tooltip>Wybierz przystanki</tooltip>
<ui-icon icon="search" fixed-witdth></ui-icon>
</button>
</template>
</header>
<div class="transition-box">
<transition name="fade">
<stop-finder @select="add" :blacklist="stops" v-if="visibility.picker === 'search'"></stop-finder>
<favourites v-else-if="visibility.picker === 'favourites'"></favourites>
</transition>
</div>
</section>
</div>
</div>
<portal-target name="popups" multiple></portal-target>
</main>
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}

View File

@ -33,9 +33,7 @@
{% endif %} {% endif %}
</head> </head>
<body> <body>
<main role="main" class="container not-ready" id="app"> {% block body '' %}
{% block body %}{% endblock %}
</main>
<footer class="container"> <footer class="container">
{% block footer %} {% block footer %}
<span> <span>

View File

@ -1,17 +1,5 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block body %} {% block body %}
<div class="alert alert-primary"> <main class="d-flex" id="provider-picker"></main>
<ui-icon icon="info-circle"></ui-icon>
Wybierz źródło danych
</div>
<ul class="list-underlined">
{% for provider in providers %}
<li title="Aktualizacja: {{ provider.lastUpdate ? provider.lastUpdate.format('Y.m.d H:i') : 'live' }}">
<a href="{{ path('app', { provider: provider.identifier }) }}" class="btn btn-block btn-action text-left">
{{ provider.name }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %} {% endblock %}