Compare commits
48 Commits
139c313a84
...
f49c7287fd
Author | SHA1 | Date | |
---|---|---|---|
|
f49c7287fd | ||
|
f2bdeb0004 | ||
|
9420764e9b | ||
|
ccbe88a532 | ||
|
ceaa13bf2a | ||
|
dcd3962220 | ||
|
c541cf1a11 | ||
|
ec479e5d77 | ||
|
3abfa6dac3 | ||
|
bb81077d1c | ||
|
4e86e127d8 | ||
|
7b7b59ce23 | ||
|
0c5ffbb567 | ||
|
af6a2a0756 | ||
|
2659acdc58 | ||
|
03058f251b | ||
|
4b9a9c54d8 | ||
|
057a7d6d01 | ||
|
984cb37c8f | ||
|
7a54569820 | ||
|
c781ca3dbc | ||
|
7d0e141d4e | ||
|
720327424a | ||
|
db30d69cdb | ||
|
f3a6c3e8eb | ||
|
9506881792 | ||
|
b609b81ddf | ||
|
5703816498 | ||
|
129a4dc588 | ||
|
7fa5124577 | ||
|
e8a31f60d1 | ||
|
42ee6e091d | ||
|
f4e3ee6f55 | ||
|
9d0e4fdb2a | ||
|
c7dc90bfc5 | ||
|
d72fcf777f | ||
|
fd4bcc9c70 | ||
|
d7fb3032b7 | ||
|
16882ee06f | ||
|
8ebf5ac137 | ||
|
749fd773b6 | ||
|
d6850773b6 | ||
|
8d5bdc061d | ||
|
0fc0dd07e4 | ||
|
493c3852d8 | ||
|
449efd7536 | ||
|
f2f7b19380 | ||
|
0895b412d2 |
@ -2,7 +2,7 @@
|
|||||||
# Copy this file to .env file for development, create environment variables when deploying to production
|
# Copy this file to .env file for development, create environment variables when deploying to production
|
||||||
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
|
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
|
||||||
|
|
||||||
GOOGLE_ANALYTICS=
|
GTM_TAG=
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
APP_ENV=dev
|
APP_ENV=dev
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "kadet/czydojade",
|
"name": "kadet/cojedzie",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
jms_serializer:
|
jms_serializer:
|
||||||
|
default_context:
|
||||||
|
serialization:
|
||||||
|
serialize_null: true
|
||||||
|
|
||||||
visitors:
|
visitors:
|
||||||
xml_serialization:
|
xml_serialization:
|
||||||
format_output: '%kernel.debug%'
|
format_output: '%kernel.debug%'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
nelmio_api_doc:
|
nelmio_api_doc:
|
||||||
documentation:
|
documentation:
|
||||||
info:
|
info:
|
||||||
title: Czy Dojadę?
|
title: Co Jedzie?
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
parameters:
|
parameters:
|
||||||
provider:
|
provider:
|
||||||
|
@ -3,4 +3,4 @@ twig:
|
|||||||
debug: '%kernel.debug%'
|
debug: '%kernel.debug%'
|
||||||
strict_variables: '%kernel.debug%'
|
strict_variables: '%kernel.debug%'
|
||||||
globals:
|
globals:
|
||||||
ga_tracking: "%env(GOOGLE_ANALYTICS)%"
|
gtm_tracking: "%env(GTM_TAG)%"
|
||||||
|
@ -2,3 +2,8 @@ api_v1:
|
|||||||
resource: ../src/Controller/Api/v1
|
resource: ../src/Controller/Api/v1
|
||||||
type: annotation
|
type: annotation
|
||||||
prefix: /{provider}/api/v1
|
prefix: /{provider}/api/v1
|
||||||
|
|
||||||
|
api_v1_providers:
|
||||||
|
path: /api/v1/providers
|
||||||
|
defaults:
|
||||||
|
_controller: '\App\Controller\Api\v1\ProviderController::index'
|
||||||
|
@ -19,11 +19,14 @@ services:
|
|||||||
App\Provider\Provider:
|
App\Provider\Provider:
|
||||||
tags: [ app.provider ]
|
tags: [ app.provider ]
|
||||||
|
|
||||||
|
App\Service\Converter:
|
||||||
|
tags: [ app.converter ]
|
||||||
|
|
||||||
# makes classes in src/ available to be used as services
|
# makes classes in src/ available to be used as services
|
||||||
# this creates a service per class whose id is the fully-qualified class name
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
App\:
|
App\:
|
||||||
resource: '../src/*'
|
resource: '../src/*'
|
||||||
exclude: '../src/{DependencyInjection,Entity,Model,Migrations,Tests,Functions,Kernel.php}'
|
exclude: '../src/{DependencyInjection,Exception,Modifier,Entity,Model,Migrations,Tests,Functions,Handler,Kernel.php}'
|
||||||
|
|
||||||
# controllers are imported separately to make sure services can be injected
|
# controllers are imported separately to make sure services can be injected
|
||||||
# as action arguments even if you don't extend any base controller class
|
# as action arguments even if you don't extend any base controller class
|
||||||
@ -35,6 +38,10 @@ services:
|
|||||||
resource: '../src/Provider'
|
resource: '../src/Provider'
|
||||||
public: true
|
public: true
|
||||||
|
|
||||||
|
App\Handler\:
|
||||||
|
resource: '../src/Handler'
|
||||||
|
tags: [ app.handler ]
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|
||||||
@ -68,19 +75,14 @@ services:
|
|||||||
|
|
||||||
ProxyManager\Configuration: '@proxy.config'
|
ProxyManager\Configuration: '@proxy.config'
|
||||||
|
|
||||||
|
|
||||||
# converter
|
# converter
|
||||||
App\Service\AggregateConverter:
|
App\Service\AggregateConverter:
|
||||||
arguments:
|
arguments:
|
||||||
- !tagged_iterator app.converter
|
- !tagged_iterator app.converter
|
||||||
|
|
||||||
App\Service\Converter: '@App\Service\AggregateConverter'
|
App\Service\Converter: '@App\Service\AggregateConverter'
|
||||||
|
|
||||||
App\Service\EntityConverter:
|
|
||||||
tags: ['app.converter']
|
|
||||||
|
|
||||||
App\Service\ScheduledStopConverter:
|
|
||||||
tags: ['app.converter']
|
|
||||||
|
|
||||||
# serializer configuration
|
# serializer configuration
|
||||||
App\Service\SerializerContextFactory:
|
App\Service\SerializerContextFactory:
|
||||||
arguments:
|
arguments:
|
||||||
@ -90,3 +92,7 @@ services:
|
|||||||
# other servces
|
# other servces
|
||||||
App\Service\ProviderResolver:
|
App\Service\ProviderResolver:
|
||||||
arguments: [!tagged app.provider, '%kernel.debug%']
|
arguments: [!tagged app.provider, '%kernel.debug%']
|
||||||
|
|
||||||
|
App\Service\HandlerProvider:
|
||||||
|
arguments: [!tagged_locator app.handler]
|
||||||
|
shared: false
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
version: '2'
|
version: '3.4'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
@ -11,9 +11,17 @@ services:
|
|||||||
|
|
||||||
php:
|
php:
|
||||||
build: docker/php
|
build: docker/php
|
||||||
mem_limit: 2g
|
|
||||||
env_file:
|
env_file:
|
||||||
- ./docker/php/.env
|
- ./docker/php/.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/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:
|
||||||
|
image: blackfire/blackfire
|
||||||
|
ports: ["8707"]
|
||||||
|
environment:
|
||||||
|
- BLACKFIRE_SERVER_ID
|
||||||
|
- BLACKFIRE_SERVER_TOKEN
|
||||||
|
@ -21,9 +21,9 @@ server {
|
|||||||
|
|
||||||
fastcgi_param APP_ENV "dev";
|
fastcgi_param APP_ENV "dev";
|
||||||
fastcgi_param DATABASE_URL "sqlite:///%kernel.project_dir%/var/app.db";
|
fastcgi_param DATABASE_URL "sqlite:///%kernel.project_dir%/var/app.db";
|
||||||
fastcgi_param GOOGLE_ANALYTICS "UA-00000-00";
|
fastcgi_param GTM_TAG "GTM-00000";
|
||||||
|
|
||||||
internal;
|
internal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1 @@
|
|||||||
XDEBUG_CONFIG=remote_host=172.17.0.1 remote_port=9001
|
|
||||||
PHP_IDE_CONFIG=serverName=czydojade
|
PHP_IDE_CONFIG=serverName=czydojade
|
||||||
|
@ -1,22 +1,36 @@
|
|||||||
FROM php:7.3-fpm
|
FROM php:7.3-fpm
|
||||||
|
|
||||||
|
ARG XDEBUG_REMOTE_HOST="172.17.0.1"
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends git zip libzip-dev
|
apt-get install -y --no-install-recommends git zip libzip-dev
|
||||||
|
|
||||||
RUN docker-php-ext-install zip
|
RUN docker-php-ext-install zip
|
||||||
|
|
||||||
|
# XDebug
|
||||||
RUN pecl install xdebug-2.9.0 && docker-php-ext-enable xdebug
|
RUN pecl install xdebug-2.9.0 && docker-php-ext-enable xdebug
|
||||||
|
RUN echo "xdebug.remote_enable = 1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
|
||||||
|
echo "xdebug.remote_host = ${XDEBUG_REMOTE_HOST}" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini;
|
||||||
|
|
||||||
RUN echo "xdebug.remote_enable = 1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini;
|
# Blackfire
|
||||||
RUN echo "date.timezone = Europe/Warsaw" >> /usr/local/etc/php/conf.d/datetime.ini;
|
RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \
|
||||||
|
&& curl -A "Docker" -o /tmp/blackfire-probe.tar.gz -D - -L -s https://blackfire.io/api/v1/releases/probe/php/linux/amd64/$version \
|
||||||
|
&& mkdir -p /tmp/blackfire \
|
||||||
|
&& tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \
|
||||||
|
&& mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get ('extension_dir');")/blackfire.so \
|
||||||
|
&& printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8707\n" > $PHP_INI_DIR/conf.d/blackfire.ini \
|
||||||
|
&& rm -rf /tmp/blackfire /tmp/blackfire-probe.tar.gz
|
||||||
|
|
||||||
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
|
#Composer
|
||||||
RUN php composer-setup.php
|
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
|
||||||
RUN php -r "unlink('composer-setup.php');"
|
&& php composer-setup.php \
|
||||||
|
&& php -r "unlink('composer-setup.php');" \
|
||||||
|
&& mv composer.phar /usr/local/bin/composer \
|
||||||
|
&& chmod +x /usr/local/bin/composer
|
||||||
|
|
||||||
RUN mv composer.phar /usr/local/bin/composer
|
# Timezone
|
||||||
RUN chmod +x /usr/local/bin/composer
|
|
||||||
RUN ln -snf /usr/share/zoneinfo/Europe/Warsaw /etc/localtime
|
RUN ln -snf /usr/share/zoneinfo/Europe/Warsaw /etc/localtime
|
||||||
|
RUN echo "date.timezone = Europe/Warsaw" >> /usr/local/etc/php/conf.d/datetime.ini;
|
||||||
|
|
||||||
WORKDIR /var/www
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "czydojade",
|
"name": "co-jedzie",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Kacper Donat <kadet1090@gmail.com>",
|
"author": "Kacper Donat <kadet1090@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -9,11 +9,11 @@
|
|||||||
"@fortawesome/pro-regular-svg-icons": "^5.3.1",
|
"@fortawesome/pro-regular-svg-icons": "^5.3.1",
|
||||||
"@fortawesome/pro-solid-svg-icons": "^5.3.1",
|
"@fortawesome/pro-solid-svg-icons": "^5.3.1",
|
||||||
"@fortawesome/vue-fontawesome": "^0.1.1",
|
"@fortawesome/vue-fontawesome": "^0.1.1",
|
||||||
"@types/bootstrap": "^4.1.2",
|
"@types/bootstrap": "^4.3.1",
|
||||||
"@types/jquery": "^3.3.6",
|
"@types/jquery": "^3.3.6",
|
||||||
"@types/moment": "^2.13.0",
|
"@types/moment": "^2.13.0",
|
||||||
"@types/popper.js": "^1.11.0",
|
"@types/popper.js": "^1.11.0",
|
||||||
"bootstrap": "^4.1.3",
|
"bootstrap": "^4.3.1",
|
||||||
"css-loader": "^1.0.0",
|
"css-loader": "^1.0.0",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^2.0.0",
|
||||||
"jquery": "^3.3.1",
|
"jquery": "^3.3.1",
|
||||||
@ -36,6 +36,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mapbox-gl-leaflet": "^0.0.1",
|
"@types/mapbox-gl-leaflet": "^0.0.1",
|
||||||
"@types/uuid": "^3.4.6",
|
"@types/uuid": "^3.4.6",
|
||||||
|
"@types/vue-moment": "^4.0.0",
|
||||||
"@types/workbox-window": "^4.3.3",
|
"@types/workbox-window": "^4.3.3",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"copy-webpack-plugin": "^4.5.2",
|
"copy-webpack-plugin": "^4.5.2",
|
||||||
@ -46,6 +47,7 @@
|
|||||||
"portal-vue": "^2.1.7",
|
"portal-vue": "^2.1.7",
|
||||||
"vue-dragscroll": "^1.10.2",
|
"vue-dragscroll": "^1.10.2",
|
||||||
"vue-fragment": "^1.5.1",
|
"vue-fragment": "^1.5.1",
|
||||||
|
"vue-moment": "^4.1.0",
|
||||||
"vue-removed-hook-mixin": "^0.1.1",
|
"vue-removed-hook-mixin": "^0.1.1",
|
||||||
"vue2-leaflet": "^1.0.2",
|
"vue2-leaflet": "^1.0.2",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.0.1",
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Czy Dojadę?",
|
|
||||||
"short_name": "Czy Dojadę?",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"lang": "pl_PL",
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#005ea8",
|
|
||||||
"theme_color": "#005ea8",
|
|
||||||
"description": "Odpowiedź na odwieczne pytanie ludzkości - czy tramwaje jeżdżą?",
|
|
||||||
"icons": [{
|
|
||||||
"src": "images/icon-256.png",
|
|
||||||
"sizes": "256x256"
|
|
||||||
},{
|
|
||||||
"src": "images/icon-512.png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
},{
|
|
||||||
"src": "images/icon-64.png",
|
|
||||||
"sizes": "64x64"
|
|
||||||
},{
|
|
||||||
"src": "images/icon-128.png",
|
|
||||||
"sizes": "128x128"
|
|
||||||
},{
|
|
||||||
"src": "images/icon-192.png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},{
|
|
||||||
"src": "images/icon-96.png",
|
|
||||||
"sizes": "96x96"
|
|
||||||
}]
|
|
||||||
}
|
|
@ -2,8 +2,4 @@
|
|||||||
<ul class="departures__list list-underlined">
|
<ul class="departures__list list-underlined">
|
||||||
<departure :departure="departure" :key="departure.key" v-for="departure in departures"/>
|
<departure :departure="departure" :key="departure.key" v-for="departure in departures"/>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="alert alert-info" v-if="stops.length === 0">
|
|
||||||
<fa :icon="['fal', 'info-circle']"/>
|
|
||||||
Wybierz przystanki korzystając z wyszukiwarki poniżej, aby zobaczyć listę odjazdów.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,31 +6,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="departure__time">
|
<div class="departure__time">
|
||||||
<fa-layers v-if="!departure.estimated" class="mr-1">
|
<template v-if="!departure.estimated">
|
||||||
<tooltip placement="top-end">Czas rozkładowy, nieuwzględniający aktualnej sytuacji komunikacyjnej.</tooltip>
|
<tooltip placement="top-end">Czas rozkładowy, nieuwzględniający aktualnej sytuacji komunikacyjnej.</tooltip>
|
||||||
<fa :icon="['far', 'clock']"/>
|
<ui-icon icon="departure-warning" class="mr-1"/>
|
||||||
<fa :icon="['fas', 'exclamation-triangle']" transform="shrink-5 down-4 right-6"/>
|
</template>
|
||||||
</fa-layers>
|
|
||||||
|
|
||||||
<span :class="[ 'departure__time', 'departure__time--delayed']" v-if="timeDiffers">
|
<template v-if="!relativeTimes">
|
||||||
{{ departure.scheduled.format('HH:mm') }}
|
<span :class="[ 'departure__time', 'departure__time--delayed']" v-if="timeDiffers">
|
||||||
</span>
|
{{ departure.scheduled|moment('HH:mm') }}
|
||||||
<span class="badge" :class="[departure.delay < 0 ? 'badge-danger' : 'badge-warning']"
|
</span>
|
||||||
v-if="departure.delay < 0 || departure.delay > 30">
|
<span class="badge" :class="[departure.delay < 0 ? 'badge-danger' : 'badge-warning']"
|
||||||
|
v-if="departure.delay < 0 || departure.delay > 30">
|
||||||
{{ departure.delay|signed }}s
|
{{ departure.delay|signed }}s
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="departure__time">{{ time.format('HH:mm') }}</span>
|
<span class="departure__time">{{ time|moment('HH:mm') }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ timeLeft|duration('humanize', true) }}
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="departure__stop">
|
<div class="departure__stop">
|
||||||
<fa :icon="['fal', 'sign']" fixed-width class="mr-1 flex-shrink-0"/>
|
<ui-icon icon="stop" fixed-width class="mr-1 flex-shrink-0"/>
|
||||||
<stop :stop="departure.stop"/>
|
<stop :stop="departure.stop"/>
|
||||||
|
|
||||||
<div class="stop__actions flex-space-left">
|
<div class="stop__actions flex-space-left">
|
||||||
<button class="btn btn-action" @click="showTrip = !showTrip">
|
<button class="btn btn-action" @click="showTrip = !showTrip">
|
||||||
<tooltip>pokaż/ukryj trasę</tooltip>
|
<tooltip>pokaż/ukryj trasę</tooltip>
|
||||||
<fa :icon="['far', 'code-commit']" />
|
<ui-icon icon="track" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -38,7 +42,7 @@
|
|||||||
<fold :visible="showTrip">
|
<fold :visible="showTrip">
|
||||||
<trip :schedule="trip.schedule" :current="departure.stop" v-if="trip" :class="[ `trip--${departure.line.type}` ]"/>
|
<trip :schedule="trip.schedule" :current="departure.stop" v-if="trip" :class="[ `trip--${departure.line.type}` ]"/>
|
||||||
<div v-else class="text-center">
|
<div v-else class="text-center">
|
||||||
<fa icon="spinner-third" pulse></fa>
|
<ui-icon icon="spinner"/>
|
||||||
</div>
|
</div>
|
||||||
</fold>
|
</fold>
|
||||||
</li>
|
</li>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<li v-for="favourite in favourites" class="favourite">
|
<li v-for="favourite in favourites" class="favourite">
|
||||||
<button @click="choose(favourite)" class="favourite__entry">
|
<button @click="choose(favourite)" class="favourite__entry">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<fa :icon="['fal', 'star']"/>
|
<ui-icon icon="favourite"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<span class="text flex-grow-1">{{ favourite.name }}</span>
|
<span class="text flex-grow-1">{{ favourite.name }}</span>
|
||||||
@ -14,12 +14,12 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="btn btn-action" @click="remove(favourite)">
|
<button class="btn btn-action" @click="remove(favourite)">
|
||||||
<tooltip placement="left">usuń</tooltip>
|
<tooltip placement="left">usuń</tooltip>
|
||||||
<fa :icon="['fal', 'trash-alt']"></fa>
|
<ui-icon icon="delete"/>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="alert alert-info" v-else>
|
<div class="alert alert-info" v-else>
|
||||||
<fa :icon="['fal', 'info-circle']"></fa>
|
<ui-icon icon="info"/>
|
||||||
Brak zapisanych zespołów przystanków
|
Brak zapisanych zespołów przystanków
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
<form class="favourite-add-form" @submit="save">
|
<form class="favourite-add-form" @submit.prevent="save">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="favourite_add_name">Nazwa</label>
|
<label for="favourite_add_name">Nazwa</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control form-control-sm" placeholder="np. Z pracy"
|
<input class="form-control form-control-sm" placeholder="np. Z pracy"
|
||||||
:class="{ 'is-invalid': errors.name.length > 0 }" id="favourite_add_name"
|
:class="{ 'is-invalid': errors.name.length > 0 }" id="favourite_add_name"
|
||||||
v-model="name" v-autofocus/>
|
v-model="name" v-autofocus/>
|
||||||
<button class="btn btn-sm btn-dark" type="submit">
|
|
||||||
<fa :icon="['fal', 'check']"></fa>
|
|
||||||
</button>
|
|
||||||
<div v-if="errors.name.length > 0" class="invalid-feedback">
|
<div v-if="errors.name.length > 0" class="invalid-feedback">
|
||||||
<p v-for="error in errors.name">{{ error }}</p>
|
<p v-for="error in errors.name">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -18,4 +15,20 @@
|
|||||||
<stop :stop="stop"/>
|
<stop :stop="stop"/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<div class="favourite-add-form__actions">
|
||||||
|
<template v-if="confirmation">
|
||||||
|
<button class="btn btn-xs btn-danger" type="submit">
|
||||||
|
nadpisz
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-xs btn-action" @click="$emit('close')">
|
||||||
|
anuluj
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button class="btn btn-xs btn-primary" type="submit">
|
||||||
|
<ui-icon icon="add" />
|
||||||
|
zapisz
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<div class="finder">
|
<div class="finder">
|
||||||
<input class="form-control" :value="filter" @input="filter = $event.target.value" placeholder="Zacznij pisać nazwę aby szukać..."/>
|
<input class="form-control form-control--framed" :value="filter" @input="filter = $event.target.value" placeholder="Zacznij pisać nazwę aby szukać..."/>
|
||||||
|
|
||||||
<div v-if="filter.length < 3" class="mt-2">
|
<div v-if="filter.length < 3" class="mt-2">
|
||||||
<favourites />
|
<favourites />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="state === 'fetching'" class="text-center p-4">
|
<div v-if="state === 'fetching'" class="text-center p-4">
|
||||||
<fa icon="spinner-third" pulse/>
|
<ui-icon icon="spinner"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="finder__stops" v-else-if="filter.length > 2 && Object.keys(filtered).length > 0">
|
<div class="finder__stops" v-else-if="filter.length > 2 && Object.keys(filtered).length > 0">
|
||||||
<div class="stop-group" v-for="(group, name) in filtered">
|
<div class="stop-group" v-for="(group, name) in filtered">
|
||||||
@ -16,7 +16,7 @@
|
|||||||
<div class="actions flex-space-left">
|
<div class="actions flex-space-left">
|
||||||
<button class="btn btn-action" @click="select(group)">
|
<button class="btn btn-action" @click="select(group)">
|
||||||
<tooltip>wybierz wszystkie</tooltip>
|
<tooltip>wybierz wszystkie</tooltip>
|
||||||
<fa :icon="['fal', 'check-double']"></fa>
|
<ui-icon icon="add-all"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -26,7 +26,7 @@
|
|||||||
<template v-slot:primary-action>
|
<template v-slot:primary-action>
|
||||||
<button @click="select(stop, $event)" class="btn btn-action">
|
<button @click="select(stop, $event)" class="btn btn-action">
|
||||||
<tooltip>dodaj przystanek</tooltip>
|
<tooltip>dodaj przystanek</tooltip>
|
||||||
<fa :icon="['fal', 'check']" />
|
<ui-icon icon="add" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</picker-stop>
|
</picker-stop>
|
||||||
@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-warning" v-else-if="filter.length > 2">
|
<div class="alert alert-warning" v-else-if="filter.length > 2">
|
||||||
<fa :icon="['far', 'exclamation-triangle']"></fa>
|
<ui-icon icon="warning"/>
|
||||||
Nie znaleziono więcej przystanków, spełniających te kryteria.
|
Nie znaleziono więcej przystanków, spełniających te kryteria.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
<span class="line__symbol flex" :class="{ [`line--${line.type}`]: true, 'line--night': line.night, 'line--fast': line.fast }">
|
<span class="line__symbol flex" :class="{ [`line--${line.type}`]: true, 'line--night': line.night, 'line--fast': line.fast }">
|
||||||
<span class="flex align-items-stretch">
|
<span class="flex align-items-stretch">
|
||||||
<span class="icon">
|
<slot name="icon" v-if="!simple">
|
||||||
<fa :icon="['fac', line.type]" fixed-width/>
|
<span class="icon">
|
||||||
</span>
|
<ui-icon :icon="`line-${line.type}`" fixed-width/>
|
||||||
<span class="badge badge-dark flex">
|
</span>
|
||||||
<fa :icon="['fas', 'walking']" fixed-width v-if="line.fast"/>
|
</slot>
|
||||||
<fa :icon="['fal', 'moon']" fixed-width v-if="line.night"/>
|
<slot name="badge">
|
||||||
{{ line.symbol }}
|
<span class="badge badge-dark flex">
|
||||||
</span>
|
<ui-icon icon="night" fixed-width v-if="line.night && !simple"/>
|
||||||
|
{{ line.symbol }}
|
||||||
|
<ui-icon icon="fast" v-if="line.fast"/>
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,15 +1,29 @@
|
|||||||
<ul class="messages list-unstyled">
|
<div class="messages mb-2">
|
||||||
<li class="message alert" :class="`alert-${type(message)}`" v-for="message in messages">
|
<ul class="list-unstyled mb-0">
|
||||||
<fa :icon="icon(message)" fixed-width></fa>
|
<li class="message alert" :class="`alert-${type(message)}`" v-for="message in messages">
|
||||||
{{ message.message }}
|
<ui-icon :icon="`message-${message.type}`" fixed-width/>
|
||||||
|
{{ message.message }}
|
||||||
|
|
||||||
<div class="message__info">
|
<div class="message__info">
|
||||||
<small class="message__date">
|
<small class="message__date">
|
||||||
Komunikat ważny od
|
Komunikat ważny od
|
||||||
{{ message.validFrom.format('HH:mm') }}
|
{{ message.validFrom.format('HH:mm') }}
|
||||||
do
|
do
|
||||||
{{ message.validTo.format('HH:mm') }}
|
{{ message.validTo.format('HH:mm') }}
|
||||||
</small>
|
</small>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<template v-if="nonDisplayedCount > 0">
|
||||||
|
<div class="flex">
|
||||||
|
<button class="btn btn-action btn-sm flex-space-left" @click="showAll = !showAll">
|
||||||
|
<template v-if="showAll">
|
||||||
|
<ui-icon icon="chevron-up"/> {{ nonDisplayedCount }} mniej
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ui-icon icon="chevron-down"/> {{ nonDisplayedCount }} więcej
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</template>
|
||||||
</ul>
|
</div>
|
||||||
|
42
resources/components/page/providers.html
Normal 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 lokalizację</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>
|
@ -2,11 +2,17 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<slot name="primary-action" />
|
<slot name="primary-action" />
|
||||||
<div class="overflow-hidden align-self-center">
|
<div class="overflow-hidden align-self-center">
|
||||||
<stop :stop="stop" class="my-1"/>
|
<stop :stop="stop" />
|
||||||
<div class="stop__destinations" v-if="stop.destinations && stop.destinations.length > 0">
|
<div class="stop__destinations" v-if="destinations && destinations.length > 0">
|
||||||
<fa :icon="['far', 'chevron-right']" />
|
|
||||||
<ul class="ml-1">
|
<ul class="ml-1">
|
||||||
<li class="stop__destination" v-for="destination in stop.destinations" :key="destination.id">{{ destination.name }}</li>
|
<li class="stop__destination destination" v-for="destination in destinations" :key="destination.stop.id">
|
||||||
|
<ul class="destination__lines">
|
||||||
|
<li v-for="line in destination.lines">
|
||||||
|
<line-symbol :line="line" :key="line.symbol" simple/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span class="destination__name ml-1">{{ destination.stop.name }}</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -15,22 +21,27 @@
|
|||||||
<slot name="actions">
|
<slot name="actions">
|
||||||
<button class="btn btn-action" ref="action-info" @click="details = !details">
|
<button class="btn btn-action" ref="action-info" @click="details = !details">
|
||||||
<tooltip>dodatkowe informacje</tooltip>
|
<tooltip>dodatkowe informacje</tooltip>
|
||||||
<fa :icon="['fal', details ? 'chevron-circle-up' : 'info-circle']"/>
|
<ui-icon icon="info"/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-action" ref="action-map" v-hover:map>
|
<button class="btn btn-action" ref="action-map" v-hover:map>
|
||||||
<fa :icon="['fal', 'map-marker-alt']"/>
|
<ui-icon icon="map"/>
|
||||||
</button>
|
</button>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fold :visible="details" class="stop__details-fold" lazy>
|
|
||||||
<stop-details :stop="stop"/>
|
|
||||||
</fold>
|
|
||||||
|
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<popper reference="action-map" v-if="showMap" arrow class="popper--no-padding" style="width: 500px;" placement="right-start" v-hover:inMap>
|
<portal to="popups">
|
||||||
|
<ui-dialog v-if="details" @leave="details = false" behaviour="modal" class="ui-modal--medium" title="Szczegóły przystanku">
|
||||||
|
<stop-details :stop="stop"/>
|
||||||
|
</ui-dialog>
|
||||||
|
</portal>
|
||||||
|
</keep-alive>
|
||||||
|
<keep-alive>
|
||||||
|
<!-- FIXME: This should be in portal but it's not possible due to information loss, maybe in vue3 it will be better?-->
|
||||||
|
<ui-dialog reference="action-map" v-if="showMap" arrow class="ui-popup--no-padding" style="width: 500px;" placement="right-start" v-hover:inMap>
|
||||||
<stop-map :stop="stop" style="height: 300px"/>
|
<stop-map :stop="stop" style="height: 300px"/>
|
||||||
</popper>
|
</ui-dialog>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
<div :class="[ 'popper', arrow && 'popper--arrow' ]" v-on="$listeners">
|
|
||||||
<div class="popper__arrow" ref="arrow" v-if="arrow"></div>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
38
resources/components/settings/departures.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<fragment>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="flex">
|
||||||
|
<label class="text" for="departures-auto-refresh-interval">
|
||||||
|
<ui-icon icon="refresh" fixed-width/>
|
||||||
|
autoodświeżanie
|
||||||
|
</label>
|
||||||
|
<ui-switch id="departures-auto-refresh" :value="autorefresh" @input="update({ autorefresh: $event })" class="flex-space-left"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex " v-if="autorefresh">
|
||||||
|
<label for="departures-auto-refresh-interval" class="text">
|
||||||
|
<span class="sr-only">częstotliwość odświeżania</span>
|
||||||
|
co
|
||||||
|
</label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control form-control-sm form-control-simple" id="departures-auto-refresh-interval"
|
||||||
|
:value="autorefreshInterval" @input="update({ autorefreshInterval: Number.parseInt($event.target.value) })" />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text" aria-label="sekund">s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="text" for="departures-count">
|
||||||
|
<ui-icon icon="line-bus" fixed-width/>
|
||||||
|
liczba wpisów
|
||||||
|
</label>
|
||||||
|
<ui-numeric-input id="departures-count" :value="displayedEntriesCount" @input="update({ displayedEntriesCount: $event })" :min="1" :max="20"/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group flex">
|
||||||
|
<label class="text" for="departures-relative-times">
|
||||||
|
<ui-icon icon="relative-time" fixed-width/>
|
||||||
|
czas do odjazdu
|
||||||
|
</label>
|
||||||
|
<ui-switch id="departures-relative-times" :value="relativeTimes" @input="update({ relativeTimes: $event })" class="flex-space-left"/>
|
||||||
|
</div>
|
||||||
|
</fragment>
|
31
resources/components/settings/messages.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<fragment>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="flex">
|
||||||
|
<label class="text" for="departures-auto-refresh-interval">
|
||||||
|
<ui-icon icon="refresh" fixed-width/>
|
||||||
|
autoodświeżanie
|
||||||
|
</label>
|
||||||
|
<ui-switch id="departures-auto-refresh" :value="autorefresh" @input="update({ autorefresh: $event })" class="flex-space-left"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex " v-if="autorefresh">
|
||||||
|
<label for="departures-auto-refresh-interval" class="text">
|
||||||
|
<span class="sr-only">częstotliwość odświeżania</span>
|
||||||
|
co
|
||||||
|
</label>
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control form-control-sm form-control-simple" id="departures-auto-refresh-interval"
|
||||||
|
:value="autorefreshInterval" @input="update({ autorefreshInterval: Number.parseInt($event.target.value) })" />
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text" aria-label="sekund">s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="text" for="departures-count">
|
||||||
|
<ui-icon icon="messages" fixed-width/>
|
||||||
|
wyświetlanych komunikatów
|
||||||
|
</label>
|
||||||
|
<ui-numeric-input id="departures-count" :value="displayedEntriesCount" @input="update({ displayedEntriesCount: $event })" :min="1" />
|
||||||
|
</div>
|
||||||
|
</fragment>
|
@ -16,21 +16,13 @@
|
|||||||
<div class="track__description">
|
<div class="track__description">
|
||||||
{{ track.description }}
|
{{ track.description }}
|
||||||
</div>
|
</div>
|
||||||
<span class="badge badge-pill badge-light track__order">#{{ order }}</span>
|
<span class="badge badge-pill badge-light track__order">
|
||||||
|
#{{ order }}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
|
||||||
<strong>Na mapie:</strong>
|
|
||||||
<div style="height: 350px" tabindex="-1">
|
|
||||||
<l-map :center="stop.location" :zoom=17>
|
|
||||||
<l-tile-layer url="//{s}.tile.osm.org/{z}/{x}/{y}.png" attribution='© <a href="//osm.org/copyright">OpenStreetMap</a> contributors'></l-tile-layer>
|
|
||||||
<l-marker :lat-lng="stop.location"></l-marker>
|
|
||||||
</l-map>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-center">
|
<div v-else class="text-center">
|
||||||
<fa icon="spinner-third" pulse></fa>
|
<ui-icon icon="spinner"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<fragment>
|
<fragment>
|
||||||
<portal to="popups">
|
<portal to="popups">
|
||||||
<transition name="tooltip">
|
<transition name="tooltip">
|
||||||
<popper class="popper--tooltip" aria-hidden="true" arrow :reference="root" :placement="placement" v-if="show" :responsive="false">
|
<ui-dialog class="ui-popup--tooltip" aria-hidden="true" arrow :reference="root" :placement="placement" v-if="show" :responsive="false">
|
||||||
<slot />
|
<slot />
|
||||||
</popper>
|
</ui-dialog>
|
||||||
</transition>
|
</transition>
|
||||||
</portal>
|
</portal>
|
||||||
<span ref="root" class="sr-only"><slot /></span>
|
<span ref="root" class="sr-only"><slot /></span>
|
||||||
|
31
resources/components/ui/dialog.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<div class="ui-backdrop" @click="handleBackdropClick" v-if="currentBehaviour === 'modal'" role="dialog">
|
||||||
|
<div class="ui-modal" v-bind="attrs" v-on="$listeners">
|
||||||
|
<div class="ui-modal__top-bar">
|
||||||
|
<div class="ui-modal__header">
|
||||||
|
<slot name="header">
|
||||||
|
<div class="ui-modal__title" role="heading"><slot name="title">{{ title }}</slot></div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-action ui-modal__close" @click.prevent="handleCloseClick">
|
||||||
|
<ui-icon icon="close"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
<div class="ui-modal__footer" v-if="hasFooter">
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="[ 'ui-popup', arrow && 'ui-popup--arrow' ]" v-bind="attrs" :style="{ zIndex: zIndex }" v-on="$listeners" role="dialog" v-else>
|
||||||
|
<div class="ui-popup__arrow" ref="arrow" v-if="arrow"></div>
|
||||||
|
<div class="ui-popup__header" v-if="hasHeader || title">
|
||||||
|
<slot name="header">
|
||||||
|
<div class="ui-popup__heading" role="heading"><slot name="title">{{ title }}</slot></div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
<div class="ui-popup__footer" v-if="hasFooter">
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
4
resources/components/ui/icon.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<fa v-bind="attrs" v-if="type === 'simple'"/>
|
||||||
|
<fa-layers v-else-if="type === 'stacked'">
|
||||||
|
<fa :icon="props.icon" v-bind="props" v-for="(props, index) in definition.icons" :key="index"/>
|
||||||
|
</fa-layers>
|
11
resources/components/ui/numeric.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control form-control-sm" :id="id" inputmode="numeric" v-bind="$attrs" :value="value" @blur="update"/>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-addon" type="button" @click="increment" :disabled="!canIncrement">
|
||||||
|
<ui-icon icon="increment"/>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-addon" type="button" @click="decrement" :disabled="!canDecrement">
|
||||||
|
<ui-icon icon="decrement"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
4
resources/components/ui/switch.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="ui-switch" :class="[ value && 'ui-switch--checked' ]" v-bind="$attrs" @click="update">
|
||||||
|
<div class="ui-switch__track"><div class="ui-switch__thumb"></div></div>
|
||||||
|
<input type="checkbox" class="ui-switch__checkbox" :id="id" :checked="value" @input="update"/>
|
||||||
|
</div>
|
BIN
resources/images/background-2x.png
Executable file
After Width: | Height: | Size: 45 KiB |
BIN
resources/images/background.png
Executable file
After Width: | Height: | Size: 19 KiB |
BIN
resources/images/icon-maskable.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
resources/images/icon-monochrome.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 12 KiB |
1313
resources/images/logo-cojedzie.ai
Executable file
Before Width: | Height: | Size: 24 KiB |
BIN
resources/images/logo-superhi.png
Executable file
After Width: | Height: | Size: 107 KiB |
1
resources/images/logo-vector.svg
Executable file
After Width: | Height: | Size: 5.7 KiB |
BIN
resources/images/logo.png
Normal file → Executable file
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
@ -38,7 +38,6 @@
|
|||||||
|
|
||||||
transition: height 250ms ease;
|
transition: height 250ms ease;
|
||||||
will-change: height;
|
will-change: height;
|
||||||
box-sizing: padding-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
@ -61,7 +60,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$section-safe-margin: 0.5rem;
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
|
padding: $section-safe-margin;
|
||||||
|
margin: -$section-safe-margin;
|
||||||
|
|
||||||
|
background: rgba(white, 0.85);
|
||||||
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
.section__title {
|
.section__title {
|
||||||
@ -94,10 +100,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.svg-inline--fa {
|
|
||||||
//transform: rotate(360deg)
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-unstyled {
|
.btn-unstyled {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -13,9 +13,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.btn-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-radius: 1.5px;
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
&.btn-outline-action {
|
&.btn-outline-action {
|
||||||
@extend .btn-outline-dark;
|
@extend .btn-outline-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.btn-primary {
|
||||||
|
background: $primary-gradient;
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 1px $primary solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-danger {
|
||||||
|
background: $danger-gradient;
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 1px $danger solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,48 @@
|
|||||||
label {
|
label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: .8rem;
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-top: -0.2rem;
|
margin-top: -0.2rem;
|
||||||
display: block;
|
display: block;
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-sm {
|
||||||
|
font-size: .6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group:last-child {
|
.form-group:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-control, .input-group-text, .btn-addon {
|
||||||
|
background: rgba($dark, .06);
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid $dark;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: rgba($dark, .06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control--framed {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid $text-muted;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-append,
|
||||||
|
.input-group-append .btn + .btn {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-addon:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
color: rgba($dark, .5)
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-prepend {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
29
resources/styles/_map.scss
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.map__label-box {
|
||||||
|
@extend .ui-popup;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -53,6 +53,16 @@
|
|||||||
|
|
||||||
.stop__destination {
|
.stop__destination {
|
||||||
@extend .favourite__stop;
|
@extend .favourite__stop;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination__line {
|
||||||
|
@extend .line__symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination__lines li {
|
||||||
|
display: inline-block;
|
||||||
|
@include spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finder__stop {
|
.finder__stop {
|
||||||
|
@ -60,11 +60,10 @@ $trip-visited: rgba($dark, .3);
|
|||||||
display: block;
|
display: block;
|
||||||
height: $trip-line-width;
|
height: $trip-line-width;
|
||||||
background: $dark;
|
background: $dark;
|
||||||
width: 50%;
|
width: calc(50% - #{$trip-stop-marker-size / 2});
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $trip-stop-marker-spacing + ($trip-stop-marker-size) / 2;
|
top: $trip-stop-marker-spacing + ($trip-stop-marker-size) / 2;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
$border-radius: 0;
|
$border-radius: 0;
|
||||||
$border-radius-lg: $border-radius;
|
$border-radius-lg: $border-radius;
|
||||||
$border-radius-sm: $border-radius;
|
$border-radius-sm: $border-radius;
|
||||||
|
|
||||||
|
$danger: #cd2e12;
|
||||||
|
|
||||||
@import "~bootstrap/scss/functions";
|
@import "~bootstrap/scss/functions";
|
||||||
@import "~bootstrap/scss/variables";
|
@import "~bootstrap/scss/variables";
|
||||||
|
|
||||||
$primary: #005ea8;
|
$primary: #005ea8;
|
||||||
|
$primary-gradient: linear-gradient(120deg, #0083c5 10%, #005ea8 90%);
|
||||||
|
$danger-gradient: linear-gradient(120deg, $danger 10%, darken($danger, 10%) 90%);
|
||||||
|
|
||||||
$custom-control-indicator-checked-bg: $dark;
|
$custom-control-indicator-checked-bg: $dark;
|
||||||
$custom-control-indicator-active-bg: $dark;
|
$custom-control-indicator-active-bg: $dark;
|
||||||
@ -43,6 +47,13 @@ $grid-gutter-width: $spacer * 2;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin spacing($spacing: .25em) {
|
||||||
|
margin-left: $spacing;
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mixin no-scrollbars {
|
@mixin no-scrollbars {
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||||
@ -53,22 +64,64 @@ $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin position($position, $top: inherit, $right: inherit, $bottom: inherit, $left: inherit) {
|
||||||
|
$right: if($right == inherit, $top, $right);
|
||||||
|
$bottom: if($bottom == inherit, $top, $bottom);
|
||||||
|
$left: if($left == inherit, $right, $left);
|
||||||
|
|
||||||
|
position: $position;
|
||||||
|
|
||||||
|
top: $top;
|
||||||
|
right: $right;
|
||||||
|
left: $left;
|
||||||
|
bottom: $bottom;
|
||||||
|
}
|
||||||
|
|
||||||
@import "common";
|
@import "common";
|
||||||
@import "stop";
|
@import "stop";
|
||||||
@import "departure";
|
@import "departure";
|
||||||
@import "line";
|
@import "line";
|
||||||
@import "controls";
|
@import "controls";
|
||||||
@import "popper";
|
|
||||||
@import "animations";
|
@import "animations";
|
||||||
@import "form";
|
@import "form";
|
||||||
@import "favourites";
|
@import "favourites";
|
||||||
@import "trip";
|
@import "trip";
|
||||||
@import "dragscroll";
|
@import "dragscroll";
|
||||||
|
@import "map";
|
||||||
|
|
||||||
|
@import "ui/switch";
|
||||||
|
@import "ui/popup";
|
||||||
|
@import "ui/modal";
|
||||||
|
|
||||||
|
@import "page/provider-picker";
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background: url("../images/background.png") repeat-x center bottom 63px;
|
||||||
|
|
||||||
|
&.contains-modal {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@ -108,6 +161,7 @@ body {
|
|||||||
font-size: small;
|
font-size: small;
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
@ -143,6 +197,7 @@ body {
|
|||||||
@include media-breakpoint-up('md') {
|
@include media-breakpoint-up('md') {
|
||||||
#app {
|
#app {
|
||||||
padding-top: 4rem;
|
padding-top: 4rem;
|
||||||
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body footer > * {
|
body footer > * {
|
||||||
|
62
resources/styles/page/_provider-picker.scss
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
.provider__name {
|
||||||
|
font-size: .9em;
|
||||||
|
color: $gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider__short-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-picker {
|
||||||
|
@extend .ui-popup;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
79
resources/styles/ui/_modal.scss
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
.ui-backdrop {
|
||||||
|
@include position(fixed, 0);
|
||||||
|
background: rgba(black, .75);
|
||||||
|
padding: $spacer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
z-index: 10000;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
height: $spacer;
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
width: 1px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialog-margin: 2rem;
|
||||||
|
$dialog-sizes: (
|
||||||
|
medium: 480px,
|
||||||
|
small: 320px,
|
||||||
|
large: 640px,
|
||||||
|
)
|
||||||
|
;
|
||||||
|
|
||||||
|
.ui-modal {
|
||||||
|
padding: $dialog-margin;
|
||||||
|
background: white;
|
||||||
|
margin: auto;
|
||||||
|
box-shadow: rgba(black, .7) 0 1px 3px;
|
||||||
|
border-radius: 1px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&.ui-modal--slim {
|
||||||
|
padding: $dialog-margin / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $size, $width in $dialog-sizes {
|
||||||
|
&.ui-modal--#{$size} {
|
||||||
|
width: $width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal__close {
|
||||||
|
margin-right: -$dialog-margin;
|
||||||
|
padding: $dialog-margin $dialog-margin 0;
|
||||||
|
margin-top: -$dialog-margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal__header {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal__title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal__top-bar {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: $dialog-margin * 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-down('sm') {
|
||||||
|
.ui-dialog {
|
||||||
|
padding: $dialog-margin / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $size, $width in $dialog-sizes {
|
||||||
|
.ui-modal.ui-modal--#{$size} {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,7 +54,7 @@
|
|||||||
@mixin triangle-left($size, $color, $border: none) { @include triangle(left, $size, $color, $border); }
|
@mixin triangle-left($size, $color, $border: none) { @include triangle(left, $size, $color, $border); }
|
||||||
@mixin triangle-right($size, $color, $border: none) { @include triangle(right, $size, $color, $border); }
|
@mixin triangle-right($size, $color, $border: none) { @include triangle(right, $size, $color, $border); }
|
||||||
|
|
||||||
.popper {
|
.ui-popup {
|
||||||
$arrow-base: 8px;
|
$arrow-base: 8px;
|
||||||
$arrow-color: white;
|
$arrow-color: white;
|
||||||
$arrow-border: rgba(black, 0.2);
|
$arrow-border: rgba(black, 0.2);
|
||||||
@ -74,20 +74,28 @@
|
|||||||
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
.popper__arrow {
|
.ui-popup__arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.popper--no-padding {
|
&.ui-popup--no-padding {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popper__heading {
|
*.ui-popup__header {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-popup__heading {
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin placement($placement) {
|
@mixin placement($placement) {
|
||||||
@ -101,7 +109,7 @@
|
|||||||
&[x-placement*="#{$placement}"] {
|
&[x-placement*="#{$placement}"] {
|
||||||
margin-#{map-get($opposite, $placement)}: $arrow-base;
|
margin-#{map-get($opposite, $placement)}: $arrow-base;
|
||||||
|
|
||||||
.popper__arrow {
|
.ui-popup__arrow {
|
||||||
#{map-get($opposite, $placement)}: 0;
|
#{map-get($opposite, $placement)}: 0;
|
||||||
@include triangle(map-get($opposite, $placement), $arrow-base, $arrow-color, $arrow-border);
|
@include triangle(map-get($opposite, $placement), $arrow-base, $arrow-color, $arrow-border);
|
||||||
}
|
}
|
||||||
@ -115,11 +123,11 @@
|
|||||||
@include placement("bottom");
|
@include placement("bottom");
|
||||||
}
|
}
|
||||||
|
|
||||||
&.popper--arrow {
|
&.ui-popup--arrow {
|
||||||
@include arrows;
|
@include arrows;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.popper--tooltip {
|
&.ui-popup--tooltip {
|
||||||
background: $dark;
|
background: $dark;
|
||||||
color: white;
|
color: white;
|
||||||
padding: .5rem .75rem;
|
padding: .5rem .75rem;
|
||||||
@ -128,14 +136,14 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
&.popper--arrow {
|
&.ui-popup--arrow {
|
||||||
$arrow-color: $dark;
|
$arrow-color: $dark;
|
||||||
$arrow-border: none;
|
$arrow-border: none;
|
||||||
$arrow-base: 6px;
|
$arrow-base: 6px;
|
||||||
|
|
||||||
@include arrows;
|
@include arrows;
|
||||||
|
|
||||||
.popper__arrow::before {
|
.ui-popup__arrow::before {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,7 +151,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down('sm') {
|
@include media-breakpoint-down('sm') {
|
||||||
.popper {
|
.ui-popup {
|
||||||
margin-left: $spacer;
|
margin-left: $spacer;
|
||||||
margin-right: $spacer;
|
margin-right: $spacer;
|
||||||
}
|
}
|
48
resources/styles/ui/_switch.scss
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
$ui-switch-marker-size: .7rem;
|
||||||
|
$ui-switch-spacing: 1px;
|
||||||
|
$ui-switch-duration: 150ms;
|
||||||
|
$ui-switch-width-factor: 2.25;
|
||||||
|
|
||||||
|
.ui-switch {
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch__checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch__track {
|
||||||
|
border: 1px solid $dark;
|
||||||
|
border-radius: $ui-switch-marker-size;
|
||||||
|
padding: $ui-switch-spacing;
|
||||||
|
width: $ui-switch-width-factor * $ui-switch-marker-size;
|
||||||
|
height: $ui-switch-marker-size;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: content-box;
|
||||||
|
background: white;
|
||||||
|
transition: background-color $ui-switch-duration ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch__thumb {
|
||||||
|
border-radius: 100%;
|
||||||
|
width: $ui-switch-marker-size;
|
||||||
|
height: $ui-switch-marker-size;
|
||||||
|
background: $dark;
|
||||||
|
position: absolute;
|
||||||
|
transition: all $ui-switch-duration ease-in-out;
|
||||||
|
transition-property: background-color, left;
|
||||||
|
margin-left: $ui-switch-spacing;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch--checked {
|
||||||
|
.ui-switch__thumb {
|
||||||
|
background: white;
|
||||||
|
left: ($ui-switch-width-factor - 1) * $ui-switch-marker-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-switch__track {
|
||||||
|
background: $dark;
|
||||||
|
}
|
||||||
|
}
|
@ -6,10 +6,6 @@ import "leaflet/dist/leaflet.css";
|
|||||||
|
|
||||||
import Popper from 'popper.js';
|
import Popper from 'popper.js';
|
||||||
import * as $ from "jquery";
|
import * as $ from "jquery";
|
||||||
|
|
||||||
window['$'] = window['jQuery'] = $;
|
|
||||||
window['Popper'] = Popper;
|
|
||||||
|
|
||||||
// dependencies
|
// dependencies
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
@ -18,46 +14,61 @@ 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 moment from 'moment';
|
||||||
|
import 'moment/locale/pl'
|
||||||
|
|
||||||
|
window['$'] = window['jQuery'] = $;
|
||||||
|
window['Popper'] = Popper;
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
Vue.use(PortalVue);
|
Vue.use(PortalVue);
|
||||||
Vue.use(VueDragscroll);
|
Vue.use(VueDragscroll);
|
||||||
Vue.use(VueFragment);
|
Vue.use(VueFragment);
|
||||||
|
Vue.use(VueMoment, { moment });
|
||||||
|
|
||||||
declare module 'vue/types/vue' {
|
declare module 'vue/types/vue' {
|
||||||
interface Vue {
|
interface Vue {
|
||||||
$isTouch: boolean;
|
$isTouch: boolean;
|
||||||
|
$hasSlot: (slot: string) => string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$isTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints > 0;
|
Vue.prototype.$isTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints > 0;
|
||||||
|
Vue.prototype.$hasSlot = function (this: Vue, slot: string): boolean {
|
||||||
|
return !!this.$slots[slot] || !!this.$scopedSlots[slot];
|
||||||
|
}
|
||||||
|
|
||||||
Component.registerHooks(['removed']);
|
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([
|
||||||
import('./components'),
|
import('./components'),
|
||||||
import('./store'),
|
import('./store'),
|
||||||
import('./font-awesome'),
|
|
||||||
import('./filters'),
|
import('./filters'),
|
||||||
import('bootstrap'),
|
import('bootstrap'),
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
// here goes "public" API
|
const appRoot = document.getElementById('app');
|
||||||
window['czydojade'] = Object.assign({
|
|
||||||
state: {}
|
store.replaceState({
|
||||||
}, window['czydojade'], {
|
...store.state,
|
||||||
components,
|
provider: window['data']?.provider,
|
||||||
application: new components.Application({ el: '#app' })
|
|
||||||
});
|
});
|
||||||
|
|
||||||
store.dispatch('messages/update');
|
// here goes "public" API
|
||||||
store.dispatch('load', window['czydojade'].state);
|
window['app'] = Object.assign({
|
||||||
|
state: {}
|
||||||
|
}, window['app'], {
|
||||||
|
components,
|
||||||
|
application: appRoot ? new components.Application({ el: '#app' }) : new components.PageProviderList({ el: '#provider-picker' }),
|
||||||
|
});
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
const wb = new Workbox("/service-worker.js");
|
const wb = new Workbox("/service-worker.js");
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
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 { ObtainPayload } from "../store/departures";
|
|
||||||
import { Stop } from "../model";
|
import { Stop } from "../model";
|
||||||
import { PopperComponent } from "./utils";
|
import { DeparturesSettingsState } from "../store/settings/departures";
|
||||||
|
import { MessagesSettingsState } from "../store/settings/messages";
|
||||||
|
|
||||||
@Component({ store })
|
@Component({ store })
|
||||||
export class Application extends Vue {
|
export class Application extends Vue {
|
||||||
@ -19,17 +19,6 @@ export class Application extends Vue {
|
|||||||
picker: 'search'
|
picker: 'search'
|
||||||
};
|
};
|
||||||
|
|
||||||
private autorefresh = {
|
|
||||||
messages: {
|
|
||||||
active: true,
|
|
||||||
interval: 60
|
|
||||||
},
|
|
||||||
departures: {
|
|
||||||
active: true,
|
|
||||||
interval: 10
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private intervals = { messages: null, departures: null };
|
private intervals = { messages: null, departures: null };
|
||||||
|
|
||||||
get messages() {
|
get messages() {
|
||||||
@ -58,36 +47,61 @@ export class Application extends Vue {
|
|||||||
this.$el.classList.remove('not-ready');
|
this.$el.classList.remove('not-ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.$store.dispatch('messages/update');
|
||||||
|
this.$store.dispatch('load', window['app'].state);
|
||||||
|
|
||||||
|
this.initDeparturesRefreshInterval();
|
||||||
|
this.initMessagesRefreshInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initDeparturesRefreshInterval() {
|
||||||
|
const departuresAutorefreshCallback = () => {
|
||||||
|
const {autorefresh, autorefreshInterval} = this.$store.state['departures-settings'] as DeparturesSettingsState;
|
||||||
|
|
||||||
|
if (this.intervals.departures) {
|
||||||
|
clearInterval(this.intervals.departures);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autorefresh) {
|
||||||
|
this.intervals.departures = setInterval(() => this.updateDepartures(), Math.max(5, autorefreshInterval) * 1000)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.watch(({"departures-settings": state}) => state.autorefresh, departuresAutorefreshCallback);
|
||||||
|
this.$store.watch(({"departures-settings": state}) => state.autorefreshInterval, departuresAutorefreshCallback);
|
||||||
|
|
||||||
|
departuresAutorefreshCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initMessagesRefreshInterval() {
|
||||||
|
const messagesAutorefreshCallback = () => {
|
||||||
|
const {autorefresh, autorefreshInterval} = this.$store.state['messages-settings'] as MessagesSettingsState;
|
||||||
|
|
||||||
|
if (this.intervals.messages) {
|
||||||
|
clearInterval(this.intervals.messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autorefresh) {
|
||||||
|
this.intervals.messages = setInterval(() => this.updateMessages(), Math.max(5, autorefreshInterval) * 1000)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.watch(({"messages-settings": state}) => state.autorefresh, messagesAutorefreshCallback);
|
||||||
|
this.$store.watch(({"messages-settings": state}) => state.autorefreshInterval, messagesAutorefreshCallback);
|
||||||
|
|
||||||
|
messagesAutorefreshCallback();
|
||||||
|
}
|
||||||
|
|
||||||
@Action('messages/update') updateMessages: () => void;
|
@Action('messages/update') updateMessages: () => void;
|
||||||
@Action('departures/update') updateDepartures: (payload: ObtainPayload) => void;
|
@Action('departures/update') updateDepartures: () => void;
|
||||||
|
|
||||||
@Mutation add: (stops: Stop[]) => void;
|
@Mutation add: (stops: Stop[]) => void;
|
||||||
@Mutation remove: (stop: Stop) => void;
|
@Mutation remove: (stop: Stop) => void;
|
||||||
@Mutation clear: () => void;
|
@Mutation clear: () => void;
|
||||||
|
|
||||||
@Watch('stops')
|
@Watch('stops')
|
||||||
onStopUpdate(this: any, stops) {
|
onStopUpdate() {
|
||||||
this.updateDepartures({ stops });
|
this.updateDepartures();
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('autorefresh', { immediate: true, deep: true })
|
|
||||||
onAutorefreshUpdate(settings) {
|
|
||||||
if (this.intervals.messages) {
|
|
||||||
clearInterval(this.intervals.messages);
|
|
||||||
this.intervals.messages = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.intervals.departures) {
|
|
||||||
clearInterval(this.intervals.departures);
|
|
||||||
this.intervals.messages = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.messages.active) {
|
|
||||||
this.intervals.messages = setInterval(() => this.updateMessages(), Math.max(5, settings.messages.interval) * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.departures.active) {
|
|
||||||
this.intervals.departures = setInterval(() => this.updateDepartures({ stops: this.stops }), Math.max(5, settings.departures.interval) * 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,25 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { Departure, Stop } from "../model";
|
import { Departure } from "../model";
|
||||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||||
import { namespace } from 'vuex-class';
|
import store, { Departures, DeparturesSettings } from '../store'
|
||||||
import store from '../store'
|
|
||||||
import { Trip } from "../model/trip";
|
import { Trip } from "../model/trip";
|
||||||
import urls from "../urls";
|
import urls from "../urls";
|
||||||
import { Jsonified } from "../utils";
|
import { Jsonified } from "../utils";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
|
||||||
const { State } = namespace('departures');
|
|
||||||
|
|
||||||
@Component({ template: require("../../components/departures.html"), store })
|
@Component({ template: require("../../components/departures.html"), store })
|
||||||
export class DeparturesComponent extends Vue {
|
export class DeparturesComponent extends Vue {
|
||||||
@State departures: Departure[];
|
@Departures.State departures: Departure[];
|
||||||
|
|
||||||
@Prop(Array)
|
|
||||||
stops: Stop[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({ template: require("../../components/departures/departure.html") })
|
@Component({ template: require("../../components/departures/departure.html"), store })
|
||||||
export class DepartureComponent extends Vue {
|
export class DepartureComponent extends Vue {
|
||||||
@Prop(Object) departure: Departure;
|
@Prop(Object) departure: Departure;
|
||||||
scheduledTrip: Trip = null;
|
scheduledTrip: Trip = null;
|
||||||
|
|
||||||
|
@DeparturesSettings.State
|
||||||
|
relativeTimes: boolean;
|
||||||
|
|
||||||
showTrip: boolean = false;
|
showTrip: boolean = false;
|
||||||
|
|
||||||
processTrip(trip: Jsonified<Trip>): Trip {
|
processTrip(trip: Jsonified<Trip>): Trip {
|
||||||
@ -46,6 +43,10 @@ export class DepartureComponent extends Vue {
|
|||||||
return this.departure.estimated || this.departure.scheduled;
|
return this.departure.estimated || this.departure.scheduled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get timeLeft() {
|
||||||
|
return moment.duration(this.time.diff(moment()));
|
||||||
|
}
|
||||||
|
|
||||||
@Watch('showTrip')
|
@Watch('showTrip')
|
||||||
async downloadTrips() {
|
async downloadTrips() {
|
||||||
if (this.showTrip != true || this.trip != null) {
|
if (this.showTrip != true || this.trip != null) {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { Component, Prop } from 'vue-property-decorator'
|
import { Component, Watch } from 'vue-property-decorator'
|
||||||
import { namespace, State, Mutation } from "vuex-class";
|
import { Mutation, State } from "vuex-class";
|
||||||
import { Favourite } from "../store/favourites";
|
import { Favourite } from "../store/favourites";
|
||||||
import { SavedState } from "../store/root";
|
|
||||||
import { Stop } from "../model";
|
import { Stop } from "../model";
|
||||||
import * as uuid from "uuid";
|
import * as uuid from "uuid";
|
||||||
import { Favourites } from "../store";
|
import { Favourites } from "../store";
|
||||||
@ -34,8 +33,15 @@ export class FavouritesAdderComponent extends Vue {
|
|||||||
private name = "";
|
private name = "";
|
||||||
private errors = { name: [] };
|
private errors = { name: [] };
|
||||||
|
|
||||||
|
private confirmation = false;
|
||||||
|
|
||||||
@Favourites.Mutation add: (favourite: Favourite) => void;
|
@Favourites.Mutation add: (favourite: Favourite) => void;
|
||||||
|
|
||||||
|
@Watch('name')
|
||||||
|
handleNameChange() {
|
||||||
|
this.confirmation = false;
|
||||||
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
const favourite: Favourite = createFavouriteEntry(this.name, this.stops);
|
const favourite: Favourite = createFavouriteEntry(this.name, this.stops);
|
||||||
|
|
||||||
@ -54,8 +60,9 @@ export class FavouritesAdderComponent extends Vue {
|
|||||||
errors.name.push("Musisz podać nazwę.");
|
errors.name.push("Musisz podać nazwę.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$store.state.favourites.favourites.filter(other => other.name == favourite.name).length > 0) {
|
if (this.$store.state.favourites.favourites.filter(other => other.name == favourite.name).length > 0 && !this.confirmation) {
|
||||||
errors.name.push("Istnieje już zapisana grupa przystanków o takiej nazwie.");
|
errors.name.push("Istnieje już zapisana grupa przystanków o takiej nazwie.");
|
||||||
|
this.confirmation = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.errors = errors;
|
this.errors = errors;
|
||||||
|
@ -9,3 +9,9 @@ export * from './map'
|
|||||||
export * from './app'
|
export * from './app'
|
||||||
export * from './favourites'
|
export * from './favourites'
|
||||||
export * from './trip'
|
export * from './trip'
|
||||||
|
export * from './ui'
|
||||||
|
export * from './settings'
|
||||||
|
export * from "./page"
|
||||||
|
|
||||||
|
export { Departures } from "../store";
|
||||||
|
export { Messages } from "../store";
|
||||||
|
@ -6,6 +6,9 @@ import { Line } from "../model";
|
|||||||
export class LineComponent extends Vue {
|
export class LineComponent extends Vue {
|
||||||
@Prop(Object)
|
@Prop(Object)
|
||||||
public line: Line;
|
public line: Line;
|
||||||
|
|
||||||
|
@Prop(Boolean)
|
||||||
|
public simple: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.component('LineSymbol', LineComponent);
|
Vue.component('LineSymbol', LineComponent);
|
||||||
|
@ -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';
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { Component } from "vue-property-decorator";
|
import { Component } from "vue-property-decorator";
|
||||||
import { Message } from "../model/message";
|
import { Message } from "../model/message";
|
||||||
import { faInfoCircle, faExclamationTriangle, faQuestionCircle } from "@fortawesome/pro-light-svg-icons";
|
import store, { Messages, MessagesSettings } from '../store'
|
||||||
import { namespace } from 'vuex-class';
|
|
||||||
import store from '../store'
|
|
||||||
|
|
||||||
const { State } = namespace('messages');
|
|
||||||
|
|
||||||
@Component({ template: require("../../components/messages.html"), store })
|
@Component({ template: require("../../components/messages.html"), store })
|
||||||
export class MessagesComponent extends Vue {
|
export class MessagesComponent extends Vue {
|
||||||
@State messages: Message[];
|
@Messages.State('messages')
|
||||||
|
public allMessages: Message[];
|
||||||
|
|
||||||
public icon(message: Message) {
|
@MessagesSettings.State
|
||||||
switch (message.type) {
|
public displayedEntriesCount: number;
|
||||||
case "breakdown": return faExclamationTriangle;
|
|
||||||
case "info": return faInfoCircle;
|
public showAll: boolean = false;
|
||||||
case "unknown": return faQuestionCircle;
|
|
||||||
}
|
get messages() {
|
||||||
|
return this.showAll
|
||||||
|
? this.allMessages
|
||||||
|
: this.allMessages.slice(0, this.displayedEntriesCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
get nonDisplayedCount(): number {
|
||||||
|
return Math.max(this.allMessages.length - this.displayedEntriesCount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public type(message: Message) {
|
public type(message: Message) {
|
||||||
@ -28,4 +32,4 @@ export class MessagesComponent extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.component('Messages', MessagesComponent);
|
Vue.component('Messages', MessagesComponent);
|
||||||
|
1
resources/ts/components/page/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./providers"
|
26
resources/ts/components/page/providers.ts
Normal 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);
|
@ -1,8 +1,8 @@
|
|||||||
import Component from "vue-class-component";
|
import Component from "vue-class-component";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import { Stop, StopGroup, StopGroups } from "../model";
|
import { Destination, Line, StopWithDestinations as Stop, StopGroup, StopGroups } from "../model";
|
||||||
import { Prop, Watch } from "vue-property-decorator";
|
import { Prop, Watch } from "vue-property-decorator";
|
||||||
import { ensureArray, FetchingState, filter, map, time } from "../utils";
|
import { FetchingState, filter, map, match, unique } from "../utils";
|
||||||
import { debounce } from "../decorators";
|
import { debounce } from "../decorators";
|
||||||
import urls from '../urls';
|
import urls from '../urls';
|
||||||
|
|
||||||
@ -18,6 +18,32 @@ export class PickerStopComponent extends Vue {
|
|||||||
get showMap() {
|
get showMap() {
|
||||||
return this.inMap || this.map;
|
return this.inMap || this.map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get destinations() {
|
||||||
|
const compactLines = destination => ({
|
||||||
|
...destination,
|
||||||
|
lines: Object.entries(groupLinesByType(destination.lines || [])).map(([type, lines]) => ({
|
||||||
|
type: type,
|
||||||
|
symbol: joinedSymbol(lines),
|
||||||
|
night: lines.every(line => line.night),
|
||||||
|
fast: lines.every(line => line.fast),
|
||||||
|
})),
|
||||||
|
all: destination.lines
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupLinesByType = (lines: Line[]) => lines.reduce<{ [kind: string]: Line[]}>((groups, line) => ({
|
||||||
|
...groups,
|
||||||
|
[line.type]: [ ...(groups[line.type] || []), line ]
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const joinedSymbol = match<string, [Line[]]>(
|
||||||
|
[lines => lines.length === 1, lines => lines[0].symbol],
|
||||||
|
[lines => lines.length === 2, ([first, second]) => `${first.symbol}, ${second.symbol}`],
|
||||||
|
[lines => lines.length > 2, ([first]) => `${first.symbol}…`],
|
||||||
|
);
|
||||||
|
|
||||||
|
return unique(this.stop.destinations || [], destination => destination.stop && destination.stop.name).map(compactLines);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -38,7 +64,7 @@ export class FinderComponent extends Vue {
|
|||||||
get filtered(): StopGroups {
|
get filtered(): StopGroups {
|
||||||
const groups = map(
|
const groups = map(
|
||||||
this.found,
|
this.found,
|
||||||
(group: StopGroup, name: string) =>
|
(group: StopGroup) =>
|
||||||
group.filter(stop => !this.blacklist.some(blacklisted => blacklisted.id === stop.id))
|
group.filter(stop => !this.blacklist.some(blacklisted => blacklisted.id === stop.id))
|
||||||
) as StopGroups;
|
) as StopGroups;
|
||||||
|
|
||||||
@ -54,7 +80,7 @@ export class FinderComponent extends Vue {
|
|||||||
|
|
||||||
this.state = 'fetching';
|
this.state = 'fetching';
|
||||||
|
|
||||||
const response = await fetch(urls.prepare(urls.stops.grouped, { name: this.filter }));
|
const response = await fetch(urls.prepare(urls.stops.grouped, { name: this.filter, 'include-destinations': true }));
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
this.found = (await response.json()).reduce((accumulator, { name, stops }) => Object.assign(accumulator, { [name]: stops }), {});
|
this.found = (await response.json()).reduce((accumulator, { name, stops }) => Object.assign(accumulator, { [name]: stops }), {});
|
||||||
|
24
resources/ts/components/settings/departures.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Component, Prop } from "vue-property-decorator";
|
||||||
|
import store, { DeparturesSettings } from "../../store";
|
||||||
|
import Vue from "vue";
|
||||||
|
import { DeparturesSettingsState } from "../../store/settings/departures";
|
||||||
|
|
||||||
|
@Component({ template: require("../../../components/settings/departures.html"), store })
|
||||||
|
export class SettingsDepartures extends Vue {
|
||||||
|
@DeparturesSettings.State
|
||||||
|
public autorefresh: boolean;
|
||||||
|
|
||||||
|
@DeparturesSettings.State
|
||||||
|
public relativeTimes: boolean;
|
||||||
|
|
||||||
|
@DeparturesSettings.State
|
||||||
|
public autorefreshInterval: number;
|
||||||
|
|
||||||
|
@DeparturesSettings.State
|
||||||
|
public displayedEntriesCount: number;
|
||||||
|
|
||||||
|
@DeparturesSettings.Mutation
|
||||||
|
public update: (state: Partial<DeparturesSettingsState>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('SettingsDepartures', SettingsDepartures);
|
2
resources/ts/components/settings/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./departures"
|
||||||
|
export * from "./messages"
|
21
resources/ts/components/settings/messages.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Component } from "vue-property-decorator";
|
||||||
|
import store, { MessagesSettings } from "../../store";
|
||||||
|
import Vue from "vue";
|
||||||
|
import { MessagesSettingsState } from "../../store/settings/messages";
|
||||||
|
|
||||||
|
@Component({template: require("../../../components/settings/messages.html"), store})
|
||||||
|
export class SettingsMessages extends Vue {
|
||||||
|
@MessagesSettings.State
|
||||||
|
public autorefresh: boolean;
|
||||||
|
|
||||||
|
@MessagesSettings.State
|
||||||
|
public autorefreshInterval: number;
|
||||||
|
|
||||||
|
@MessagesSettings.State
|
||||||
|
public displayedEntriesCount: number;
|
||||||
|
|
||||||
|
@MessagesSettings.Mutation
|
||||||
|
public update: (state: Partial<MessagesSettingsState>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('SettingsMessages', SettingsMessages);
|
275
resources/ts/components/ui/dialog.ts
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import Vue from "vue";
|
||||||
|
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||||
|
import Popper, { Placement } from "popper.js";
|
||||||
|
import { defaultBreakpoints } from "../../filters";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How popup will be presented to user:
|
||||||
|
* - "modal" - modal window
|
||||||
|
* - "popup" - simple popup
|
||||||
|
*/
|
||||||
|
export type DialogBehaviour = "modal" | "popup";
|
||||||
|
|
||||||
|
let openModalCounter: number = 0;
|
||||||
|
|
||||||
|
function computeZIndexOfElement(element: HTMLElement): number {
|
||||||
|
let current = element;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const zIndex = window.getComputedStyle(current).zIndex;
|
||||||
|
|
||||||
|
if (zIndex !== "auto") {
|
||||||
|
return parseInt(zIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current.parentElement) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
inheritAttrs: false,
|
||||||
|
template: require('../../../components/ui/dialog.html'),
|
||||||
|
})
|
||||||
|
export default class UiDialog extends Vue {
|
||||||
|
@Prop({ type: String, default: "popup" })
|
||||||
|
private behaviour: DialogBehaviour;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
private mobileBehaviour: DialogBehaviour;
|
||||||
|
|
||||||
|
@Prop([String, HTMLElement])
|
||||||
|
public reference: string | HTMLElement;
|
||||||
|
|
||||||
|
@Prop(Object)
|
||||||
|
public refs: string;
|
||||||
|
|
||||||
|
@Prop({ type: String, default: "auto" })
|
||||||
|
public placement: Placement;
|
||||||
|
|
||||||
|
@Prop(Boolean)
|
||||||
|
public arrow: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: true })
|
||||||
|
public responsive: boolean;
|
||||||
|
|
||||||
|
@Prop(String)
|
||||||
|
public title: string;
|
||||||
|
|
||||||
|
private isMobile: boolean = false;
|
||||||
|
|
||||||
|
/** Inherited class hack */
|
||||||
|
private staticClass: string[] = [];
|
||||||
|
|
||||||
|
private zIndex: number = 1000;
|
||||||
|
|
||||||
|
private _focusOutEvent;
|
||||||
|
private _resizeEvent;
|
||||||
|
|
||||||
|
private _popper;
|
||||||
|
|
||||||
|
get attrs() {
|
||||||
|
return {
|
||||||
|
...this.$attrs,
|
||||||
|
"class": this.staticClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentBehaviour(): DialogBehaviour {
|
||||||
|
if (!this.mobileBehaviour) {
|
||||||
|
return this.behaviour;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.isMobile ? this.mobileBehaviour : this.behaviour;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasFooter() {
|
||||||
|
return this.$hasSlot('footer')
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasHeader() {
|
||||||
|
return this.$hasSlot('header')
|
||||||
|
}
|
||||||
|
|
||||||
|
private getReferenceElement() {
|
||||||
|
const isInWrapper = this.$parent.$options.name == 'portalTarget';
|
||||||
|
|
||||||
|
if (typeof this.reference === 'string') {
|
||||||
|
if (this.refs) {
|
||||||
|
return this.refs[this.reference];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInWrapper) {
|
||||||
|
return this.$parent.$parent.$refs[this.reference];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.$parent.$refs[this.reference];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reference instanceof HTMLElement) {
|
||||||
|
return this.reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInWrapper ? this.$parent.$el : this.$el.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
focusOut(event: MouseEvent) {
|
||||||
|
if (this.$el.contains(event.target as Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('leave', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.zIndex = computeZIndexOfElement(this.getReferenceElement()) + 100;
|
||||||
|
|
||||||
|
this.handleWindowResize();
|
||||||
|
|
||||||
|
if (this.behaviour === 'popup') {
|
||||||
|
this.mountPopper();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.staticClass = Array.from(this.$el.classList).filter(cls => ["ui-backdrop", "ui-popup", "ui-popup--arrow"].indexOf(cls) === -1);
|
||||||
|
|
||||||
|
window.addEventListener('resize', this._resizeEvent = this.handleWindowResize.bind(this));
|
||||||
|
|
||||||
|
this._activated();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _activated() {
|
||||||
|
if (this.behaviour === 'modal') {
|
||||||
|
this.mountModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deactivated() {
|
||||||
|
if (this.behaviour === 'modal') {
|
||||||
|
this.dismountModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mountModal() {
|
||||||
|
if (openModalCounter === 0) {
|
||||||
|
document.body.style.paddingRight = `${window.screen.width - document.body.clientWidth}px`
|
||||||
|
document.body.classList.add('contains-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
openModalCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private dismountModal() {
|
||||||
|
openModalCounter--;
|
||||||
|
|
||||||
|
if (openModalCounter === 0) {
|
||||||
|
document.body.style.paddingRight = "";
|
||||||
|
document.body.classList.remove('contains-modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activated() {
|
||||||
|
this._activated();
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivated() {
|
||||||
|
this._deactivated();
|
||||||
|
}
|
||||||
|
|
||||||
|
private mountPopper() {
|
||||||
|
const reference = this.getReferenceElement();
|
||||||
|
|
||||||
|
this._popper = new Popper(reference, this.$el, {
|
||||||
|
placement: this.placement,
|
||||||
|
modifiers: {
|
||||||
|
arrow: { enabled: this.arrow, element: this.$refs['arrow'] as Element },
|
||||||
|
responsive: {
|
||||||
|
enabled: this.responsive,
|
||||||
|
order: 890,
|
||||||
|
fn(data) {
|
||||||
|
if (window.innerWidth < 560) {
|
||||||
|
data.instance.options.placement = 'top';
|
||||||
|
data.styles.transform = `translate3d(0, ${ data.offsets.popper.top }px, 0)`;
|
||||||
|
data.styles.right = '0';
|
||||||
|
data.styles.left = '0';
|
||||||
|
data.styles.width = 'auto';
|
||||||
|
data.arrowStyles.left = `${ data.offsets.popper.left + data.offsets.arrow.left }px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this._popper && this._popper.update();
|
||||||
|
document.addEventListener('click', this._focusOutEvent = this.focusOut.bind(this), { capture: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private removePopper() {
|
||||||
|
this._popper.destroy()
|
||||||
|
this._popper = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
if (this._popper) {
|
||||||
|
this._popper.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this._focusOutEvent && document.removeEventListener('click', this._focusOutEvent, { capture: true });
|
||||||
|
|
||||||
|
this._deactivated()
|
||||||
|
}
|
||||||
|
|
||||||
|
removed() {
|
||||||
|
if (this._popper) {
|
||||||
|
this.removePopper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBackdropClick(ev: Event) {
|
||||||
|
const target = ev.target as HTMLElement;
|
||||||
|
|
||||||
|
if (target.classList.contains("ui-backdrop")) {
|
||||||
|
this.$emit('leave');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCloseClick() {
|
||||||
|
this.$emit('leave');
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWindowResize() {
|
||||||
|
this.isMobile = screen.width < defaultBreakpoints.md;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('currentBehaviour')
|
||||||
|
private handleBehaviourChange(newBehaviour: DialogBehaviour, oldBehaviour: DialogBehaviour) {
|
||||||
|
if (oldBehaviour === 'popup') {
|
||||||
|
this.removePopper();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newBehaviour === 'popup') {
|
||||||
|
this.$nextTick(() => this.mountPopper());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newBehaviour === 'modal') {
|
||||||
|
this.mountModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldBehaviour === 'modal') {
|
||||||
|
this.dismountModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component("ui-dialog", UiDialog);
|
148
resources/ts/components/ui/icon.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { Component, Prop } from 'vue-property-decorator'
|
||||||
|
import { IconDefinition, library } from "@fortawesome/fontawesome-svg-core"
|
||||||
|
import { Dictionary } from "../../utils";
|
||||||
|
import {
|
||||||
|
faBullhorn,
|
||||||
|
faCheck,
|
||||||
|
faCheckDouble,
|
||||||
|
faChevronCircleUp,
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp,
|
||||||
|
faClock,
|
||||||
|
faCog,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faHourglassHalf,
|
||||||
|
faInfoCircle,
|
||||||
|
faMapMarkerAlt,
|
||||||
|
faMoon,
|
||||||
|
faQuestionCircle,
|
||||||
|
faQuestionSquare,
|
||||||
|
faSearch,
|
||||||
|
faSign,
|
||||||
|
faStar,
|
||||||
|
faSync,
|
||||||
|
faTimes,
|
||||||
|
faTrashAlt
|
||||||
|
} from "@fortawesome/pro-light-svg-icons";
|
||||||
|
import {
|
||||||
|
faClock as faClockBold,
|
||||||
|
faCodeCommit,
|
||||||
|
faMinus,
|
||||||
|
faPlus,
|
||||||
|
faSpinnerThird
|
||||||
|
} from "@fortawesome/pro-regular-svg-icons";
|
||||||
|
import { faExclamationTriangle as faSolidExclamationTriangle, faWalking } from "@fortawesome/pro-solid-svg-icons";
|
||||||
|
import { fac } from "../../icons";
|
||||||
|
import { FontAwesomeIcon, FontAwesomeLayers, FontAwesomeLayersText } from "@fortawesome/vue-fontawesome";
|
||||||
|
|
||||||
|
type IconDescription = { icon: IconDefinition, [prop: string]: any }
|
||||||
|
|
||||||
|
type SimpleIcon = {
|
||||||
|
type: 'simple',
|
||||||
|
} & IconDescription;
|
||||||
|
|
||||||
|
type StackedIcon = {
|
||||||
|
type: 'stacked',
|
||||||
|
icons: IconDescription[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Icon = SimpleIcon | StackedIcon;
|
||||||
|
|
||||||
|
const simple = (icon: IconDefinition, props: any = {}): SimpleIcon => ({
|
||||||
|
icon, ...props, type: "simple"
|
||||||
|
});
|
||||||
|
|
||||||
|
const stack = (icons: IconDescription[]): StackedIcon => ({ type: "stacked", icons });
|
||||||
|
|
||||||
|
const lineTypeIcons = Object
|
||||||
|
.values(fac)
|
||||||
|
.map<[string, Icon]>(icon => [`line-${ icon.iconName }`, simple(icon)])
|
||||||
|
.reduce((acc, [icon, definition]) => ({ ...acc, [icon]: definition }), {})
|
||||||
|
|
||||||
|
const messageTypeIcons: Dictionary<Icon> = {
|
||||||
|
'message-breakdown': simple(faExclamationTriangle),
|
||||||
|
'message-info': simple(faInfoCircle),
|
||||||
|
'message-unknown': simple(faQuestionCircle),
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions = {
|
||||||
|
'favourite': simple(faStar),
|
||||||
|
'unknown': simple(faQuestionSquare),
|
||||||
|
'add': simple(faCheck),
|
||||||
|
'add-all': simple(faCheckDouble),
|
||||||
|
'remove-stop': simple(faTimes),
|
||||||
|
'delete': simple(faTrashAlt),
|
||||||
|
'messages': simple(faBullhorn),
|
||||||
|
'timetable': simple(faClock),
|
||||||
|
'settings': simple(faCog),
|
||||||
|
'refresh': simple(faSync),
|
||||||
|
'chevron-down': simple(faChevronDown),
|
||||||
|
'chevron-up': simple(faChevronUp),
|
||||||
|
'search': simple(faSearch),
|
||||||
|
'info': simple(faInfoCircle),
|
||||||
|
'warning': simple(faExclamationTriangle),
|
||||||
|
'night': simple(faMoon),
|
||||||
|
'fast': simple(faWalking),
|
||||||
|
'track': simple(faCodeCommit),
|
||||||
|
'info-hide': simple(faChevronCircleUp),
|
||||||
|
'map': simple(faMapMarkerAlt),
|
||||||
|
'stop': simple(faSign),
|
||||||
|
'spinner': simple(faSpinnerThird, { spin: true }),
|
||||||
|
'increment': simple(faPlus, { "fixed-width": true }),
|
||||||
|
'decrement': simple(faMinus, { "fixed-width": true }),
|
||||||
|
'relative-time': simple(faHourglassHalf),
|
||||||
|
'departure-warning': stack([
|
||||||
|
{ icon: faClockBold },
|
||||||
|
{ icon: faSolidExclamationTriangle, transform: "shrink-5 down-4 right-6" }
|
||||||
|
]),
|
||||||
|
'close': simple(faTimes),
|
||||||
|
...lineTypeIcons,
|
||||||
|
...messageTypeIcons,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PredefinedIcon = keyof typeof definitions;
|
||||||
|
|
||||||
|
const extractAllIcons = (icons: Icon[]) => icons.map(icon => {
|
||||||
|
switch (icon.type) {
|
||||||
|
case "simple":
|
||||||
|
return [icon.icon];
|
||||||
|
case "stacked":
|
||||||
|
return icon.icons.map(stacked => stacked.icon);
|
||||||
|
}
|
||||||
|
}).reduce((acc, cur) => [...acc, ...cur]);
|
||||||
|
|
||||||
|
library.add(...extractAllIcons(Object.values(definitions)));
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: require('../../../components/ui/icon.html'),
|
||||||
|
components: {
|
||||||
|
fa: FontAwesomeIcon,
|
||||||
|
faLayers: FontAwesomeLayers,
|
||||||
|
faText: FontAwesomeLayersText,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class UiIcon extends Vue {
|
||||||
|
@Prop({
|
||||||
|
type: [String, Object],
|
||||||
|
validator: value => typeof value === "object" || Object.keys(definitions).includes(value),
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
icon: PredefinedIcon | IconDefinition;
|
||||||
|
|
||||||
|
get definition(): Icon {
|
||||||
|
return typeof this.icon === "string"
|
||||||
|
? definitions[this.icon] || definitions['unknown']
|
||||||
|
: { icon: this.icon as IconDefinition, type: "simple" };
|
||||||
|
}
|
||||||
|
|
||||||
|
get attrs() {
|
||||||
|
return { ...this.definition, ...this.$attrs };
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return this.definition.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('UiIcon', UiIcon);
|
4
resources/ts/components/ui/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './switch';
|
||||||
|
export * from './icon';
|
||||||
|
export * from './numeric-input'
|
||||||
|
export * from './dialog'
|
53
resources/ts/components/ui/numeric-input.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { Component, Prop } from 'vue-property-decorator'
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: require('../../../components/ui/numeric.html'),
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
export class UiNumericInput extends Vue {
|
||||||
|
@Prop({
|
||||||
|
type: String,
|
||||||
|
default: () => `uuid-${uuid.v4()}`
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Prop(Number)
|
||||||
|
value: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number, default: 1 })
|
||||||
|
step: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number, default: -Infinity })
|
||||||
|
min: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number, default: Infinity })
|
||||||
|
max: number;
|
||||||
|
|
||||||
|
update(ev) {
|
||||||
|
this.$emit('input', this.clamp(Number.parseInt(ev.target.value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.$emit('input', this.clamp(this.value + this.step));
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
this.$emit('input', this.clamp(this.value - this.step));
|
||||||
|
}
|
||||||
|
|
||||||
|
clamp(value: number) {
|
||||||
|
return Math.max(Math.min(value, this.max), this.min);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canIncrement(): boolean {
|
||||||
|
return this.max - this.value > Number.EPSILON * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canDecrement(): boolean {
|
||||||
|
return this.value - this.min > Number.EPSILON * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('UiNumericInput', UiNumericInput);
|
24
resources/ts/components/ui/switch.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import { Component, Prop } from 'vue-property-decorator'
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: require('../../../components/ui/switch.html'),
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
export class UiSwitch extends Vue {
|
||||||
|
@Prop({
|
||||||
|
type: String,
|
||||||
|
default: () => `uuid-${uuid.v4()}`
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Prop(Boolean)
|
||||||
|
value: boolean;
|
||||||
|
|
||||||
|
update(ev) {
|
||||||
|
this.$emit('input', !this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('UiSwitch', UiSwitch);
|
@ -1,112 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||||
import Popper, { Placement } from "popper.js";
|
|
||||||
import { Portal } from "portal-vue";
|
|
||||||
import vueRemovedHookMixin from "vue-removed-hook-mixin";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
template: require("../../components/popper.html"),
|
|
||||||
mixins: [ vueRemovedHookMixin ]
|
|
||||||
})
|
|
||||||
export class PopperComponent extends Vue {
|
|
||||||
@Prop([ String, HTMLElement ])
|
|
||||||
public reference: string | HTMLElement;
|
|
||||||
|
|
||||||
@Prop(Object)
|
|
||||||
public refs: string;
|
|
||||||
|
|
||||||
@Prop({ type: String, default: "auto" })
|
|
||||||
public placement: Placement;
|
|
||||||
|
|
||||||
@Prop(Boolean)
|
|
||||||
public arrow: boolean;
|
|
||||||
|
|
||||||
@Prop({ type: Boolean, default: true })
|
|
||||||
public responsive: boolean;
|
|
||||||
|
|
||||||
private _event;
|
|
||||||
private _popper;
|
|
||||||
|
|
||||||
private getReferenceElement() {
|
|
||||||
const isInPortal = this.$parent.$options.name == 'portalTarget';
|
|
||||||
|
|
||||||
if (typeof this.reference === 'string') {
|
|
||||||
if (this.refs) {
|
|
||||||
return this.refs[this.reference];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInPortal) {
|
|
||||||
return this.$parent.$parent.$refs[this.reference];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$parent.$refs[this.reference];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.reference instanceof HTMLElement) {
|
|
||||||
return this.reference;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isInPortal ? this.$parent.$el : this.$el.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
focusOut(event: MouseEvent) {
|
|
||||||
if (this.$el.contains(event.target as Node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('leave', event);
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
const reference = this.getReferenceElement();
|
|
||||||
|
|
||||||
this._popper = new Popper(reference, this.$el, {
|
|
||||||
placement: this.placement,
|
|
||||||
modifiers: {
|
|
||||||
arrow: { enabled: this.arrow, element: this.$refs['arrow'] as Element },
|
|
||||||
responsive: {
|
|
||||||
enabled: this.responsive,
|
|
||||||
order: 890,
|
|
||||||
fn(data) {
|
|
||||||
if (window.innerWidth < 560) {
|
|
||||||
data.instance.options.placement = 'top';
|
|
||||||
data.styles.transform = `translate3d(0, ${data.offsets.popper.top}px, 0)`;
|
|
||||||
data.styles.right = '0';
|
|
||||||
data.styles.left = '0';
|
|
||||||
data.styles.width = 'auto';
|
|
||||||
data.arrowStyles.left = `${data.offsets.popper.left + data.offsets.arrow.left}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this._popper.update();
|
|
||||||
document.addEventListener('click', this._event = this.focusOut.bind(this), { capture: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
this._popper.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch('visible')
|
|
||||||
private onVisibilityUpdate() {
|
|
||||||
this._popper.update();
|
|
||||||
window.dispatchEvent(new Event('resize'));
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
this._event && document.removeEventListener('click', this._event, { capture: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
removed() {
|
|
||||||
this._popper.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({ template: require('../../components/fold.html') })
|
@Component({ template: require('../../components/fold.html') })
|
||||||
export class FoldComponent extends Vue {
|
export class FoldComponent extends Vue {
|
||||||
@ -152,6 +46,11 @@ export class LazyComponent extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.component('Popper', PopperComponent);
|
|
||||||
Vue.component('Fold', FoldComponent);
|
Vue.component('Fold', FoldComponent);
|
||||||
Vue.component('Lazy', LazyComponent);
|
Vue.component('Lazy', LazyComponent);
|
||||||
|
|
||||||
|
// https://github.com/vuejs/vue/issues/7829
|
||||||
|
Vue.component('Empty', {
|
||||||
|
functional: true,
|
||||||
|
render: (h, { data }) => h('template', data, '')
|
||||||
|
});
|
||||||
|
@ -2,6 +2,14 @@ import { set, signed } from "./utils";
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { condition } from "./decorators";
|
import { condition } from "./decorators";
|
||||||
|
|
||||||
|
export const defaultBreakpoints = {
|
||||||
|
'xs': 0,
|
||||||
|
'sm': 576,
|
||||||
|
'md': 768,
|
||||||
|
'lg': 1024,
|
||||||
|
'xl': 1200,
|
||||||
|
}
|
||||||
|
|
||||||
Vue.filter('signed', signed);
|
Vue.filter('signed', signed);
|
||||||
|
|
||||||
Vue.directive('hover', {
|
Vue.directive('hover', {
|
||||||
@ -61,13 +69,7 @@ Vue.directive('autofocus', {
|
|||||||
|
|
||||||
Vue.directive('responsive', {
|
Vue.directive('responsive', {
|
||||||
inserted(el, binding) {
|
inserted(el, binding) {
|
||||||
const breakpoints = typeof binding.value === 'object' ? binding.value : {
|
const breakpoints = typeof binding.value === 'object' ? binding.value : defaultBreakpoints;
|
||||||
'xs': 0,
|
|
||||||
'sm': 576,
|
|
||||||
'md': 768,
|
|
||||||
'lg': 1024,
|
|
||||||
'xl': 1200,
|
|
||||||
};
|
|
||||||
|
|
||||||
const resize = binding['resize'] = () => {
|
const resize = binding['resize'] = () => {
|
||||||
const width = el.scrollWidth;
|
const width = el.scrollWidth;
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
|
|
||||||
import { far } from "@fortawesome/pro-regular-svg-icons";
|
|
||||||
import { fas } from "@fortawesome/pro-solid-svg-icons";
|
|
||||||
import { fal } from "@fortawesome/pro-light-svg-icons";
|
|
||||||
import { fac } from "./icons";
|
|
||||||
|
|
||||||
import { FontAwesomeIcon, FontAwesomeLayers, FontAwesomeLayersText } from '@fortawesome/vue-fontawesome'
|
|
||||||
|
|
||||||
library.add(far, fas, fal, fac);
|
|
||||||
|
|
||||||
Vue.component('fa', FontAwesomeIcon);
|
|
||||||
Vue.component('fa-layers', FontAwesomeLayers);
|
|
||||||
Vue.component('fa-text', FontAwesomeLayersText);
|
|
4
resources/ts/model/common.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface Location {
|
||||||
|
lat: number,
|
||||||
|
lng: number,
|
||||||
|
}
|
@ -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'
|
||||||
|
11
resources/ts/model/provider.ts
Normal 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;
|
||||||
|
}
|
@ -1,14 +1,22 @@
|
|||||||
|
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;
|
||||||
destinations?: Stop[];
|
}
|
||||||
|
|
||||||
|
export interface StopWithDestinations extends Stop{
|
||||||
|
destinations?: Destination[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Destination = {
|
||||||
|
stop: Stop;
|
||||||
|
lines: Line[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StopGroup = Stop[];
|
export type StopGroup = Stop[];
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { FetchingState } from "../utils";
|
import { FetchingState } from "../utils";
|
||||||
import { Moment } from "moment";
|
import { Moment } from "moment";
|
||||||
import { Module, MutationTree } from "vuex";
|
import { Module, MutationTree } from "vuex";
|
||||||
import { RootState } from "./root";
|
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
|
|
||||||
export interface CommonState {
|
export interface CommonState {
|
||||||
@ -24,4 +23,4 @@ export const mutations: MutationTree<CommonState> = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { state, mutations };
|
export default { state, mutations };
|
||||||
|
@ -10,10 +10,6 @@ export interface DeparturesState extends CommonState {
|
|||||||
departures: Departure[],
|
departures: Departure[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObtainPayload {
|
|
||||||
stops: Stop[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const departures: Module<DeparturesState, RootState> = {
|
export const departures: Module<DeparturesState, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
@ -29,10 +25,15 @@ export const departures: Module<DeparturesState, RootState> = {
|
|||||||
...common.mutations
|
...common.mutations
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async update({ commit }, { stops }: ObtainPayload) {
|
async update({ commit }) {
|
||||||
|
const count = this.state['departures-settings'].displayedEntriesCount;
|
||||||
|
const stops = this.state.stops;
|
||||||
|
|
||||||
commit('fetching');
|
commit('fetching');
|
||||||
|
|
||||||
const response = await fetch(urls.prepare(urls.departures, {
|
const response = await fetch(urls.prepare(urls.departures, {
|
||||||
stop: stops.map(stop => stop.id),
|
stop: stops.map(stop => stop.id),
|
||||||
|
limit: count || 8,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { RootState, SavedState } from "./root";
|
import { RootState } from "./root";
|
||||||
import { Module, Plugin, Store } from "vuex";
|
import { Module } from "vuex";
|
||||||
import * as utils from "../utils";
|
|
||||||
import { Stop } from "../model";
|
import { Stop } from "../model";
|
||||||
|
import { except } from "../utils";
|
||||||
|
|
||||||
export interface Favourite {
|
export interface Favourite {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
stops: Stop[];
|
stops: Stop[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,7 +20,13 @@ const favourites: Module<FavouritesState, RootState> = {
|
|||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
add(state, favourite: Favourite) {
|
add(state, favourite: Favourite) {
|
||||||
state.favourites.push(favourite);
|
const existing = state.favourites.find((current: Favourite) => current.name === favourite.name);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
state.favourites.push(favourite);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(existing, except(favourite, ["id"]));
|
||||||
},
|
},
|
||||||
remove(state, favourite: Favourite) {
|
remove(state, favourite: Favourite) {
|
||||||
state.favourites = state.favourites.filter(f => f != favourite);
|
state.favourites = state.favourites.filter(f => f != favourite);
|
||||||
@ -28,12 +34,4 @@ const favourites: Module<FavouritesState, RootState> = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const localStorageSaver = (path: string, key: string): Plugin<any> => (store: Store<any>) => {
|
|
||||||
utils.set(store.state, path, JSON.parse(window.localStorage.getItem(key) || '[]'));
|
|
||||||
|
|
||||||
store.subscribe((mutation, state) => {
|
|
||||||
window.localStorage.setItem(key, JSON.stringify(utils.get(state, path)));
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
export default favourites;
|
export default favourites;
|
||||||
|
@ -2,9 +2,11 @@ import Vuex from 'vuex';
|
|||||||
|
|
||||||
import messages, { MessagesState } from './messages';
|
import messages, { MessagesState } from './messages';
|
||||||
import departures, { DeparturesState } from './departures'
|
import departures, { DeparturesState } from './departures'
|
||||||
import favourites, { FavouritesState, localStorageSaver } from './favourites'
|
import favourites, { FavouritesState } from './favourites'
|
||||||
|
import departureSettings, { DeparturesSettingsState } from "./settings/departures";
|
||||||
|
import messagesSettings, { MessagesSettingsState } from "./settings/messages";
|
||||||
|
|
||||||
import { state, mutations, actions, RootState } from "./root";
|
import { actions, mutations, RootState, state } from "./root";
|
||||||
import VuexPersistence from "vuex-persist";
|
import VuexPersistence from "vuex-persist";
|
||||||
import { namespace } from "vuex-class";
|
import { namespace } from "vuex-class";
|
||||||
|
|
||||||
@ -12,10 +14,12 @@ export type State = {
|
|||||||
messages: MessagesState;
|
messages: MessagesState;
|
||||||
departures: DeparturesState;
|
departures: DeparturesState;
|
||||||
favourites: FavouritesState;
|
favourites: FavouritesState;
|
||||||
|
"departures-settings": DeparturesSettingsState,
|
||||||
|
"messages-settings": MessagesSettingsState,
|
||||||
} & RootState;
|
} & RootState;
|
||||||
|
|
||||||
const localStoragePersist = new VuexPersistence<State>({
|
const localStoragePersist = new VuexPersistence<State>({
|
||||||
reducer: state => ({ favourites: state.favourites })
|
modules: ['favourites', 'departures-settings', 'messages-settings'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionStoragePersist = new VuexPersistence<State>({
|
const sessionStoragePersist = new VuexPersistence<State>({
|
||||||
@ -23,12 +27,16 @@ const sessionStoragePersist = new VuexPersistence<State>({
|
|||||||
storage: window.sessionStorage
|
storage: window.sessionStorage
|
||||||
});
|
});
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store<RootState>({
|
||||||
state, mutations, actions,
|
state, mutations, actions,
|
||||||
modules: { messages, departures, favourites },
|
modules: {
|
||||||
|
messages,
|
||||||
|
departures,
|
||||||
|
favourites,
|
||||||
|
'departures-settings': departureSettings,
|
||||||
|
'messages-settings': messagesSettings,
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// todo: remove after some time
|
|
||||||
localStorageSaver('favourites.favourites', 'favourites'),
|
|
||||||
localStoragePersist.plugin,
|
localStoragePersist.plugin,
|
||||||
sessionStoragePersist.plugin,
|
sessionStoragePersist.plugin,
|
||||||
]
|
]
|
||||||
@ -37,3 +45,7 @@ const store = new Vuex.Store({
|
|||||||
export default store;
|
export default store;
|
||||||
|
|
||||||
export const Favourites = namespace('favourites');
|
export const Favourites = namespace('favourites');
|
||||||
|
export const DeparturesSettings = namespace('departures-settings');
|
||||||
|
export const MessagesSettings = namespace('messages-settings');
|
||||||
|
export const Departures = namespace('departures');
|
||||||
|
export const Messages = namespace('messages');
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
26
resources/ts/store/settings/departures.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { ActionContext, Module } from "vuex";
|
||||||
|
import { RootState } from "../root";
|
||||||
|
|
||||||
|
export type DeparturesSettingsState = {
|
||||||
|
autorefresh: boolean;
|
||||||
|
autorefreshInterval?: number;
|
||||||
|
displayedEntriesCount?: number;
|
||||||
|
relativeTimes: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const departureSettings: Module<DeparturesSettingsState, RootState> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
autorefresh: true,
|
||||||
|
relativeTimes: false,
|
||||||
|
autorefreshInterval: 10,
|
||||||
|
displayedEntriesCount: 10
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
update(state: DeparturesSettingsState, patch: Partial<DeparturesSettingsState>) {
|
||||||
|
Object.assign(state, patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default departureSettings;
|
24
resources/ts/store/settings/messages.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Module } from "vuex";
|
||||||
|
import { RootState } from "../root";
|
||||||
|
|
||||||
|
export type MessagesSettingsState = {
|
||||||
|
autorefresh: boolean;
|
||||||
|
autorefreshInterval?: number;
|
||||||
|
displayedEntriesCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesSettings: Module<MessagesSettingsState, RootState> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
autorefresh: true,
|
||||||
|
autorefreshInterval: 60,
|
||||||
|
displayedEntriesCount: 2
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
update(state: MessagesSettingsState, patch: Partial<MessagesSettingsState>) {
|
||||||
|
Object.assign(state, patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default messagesSettings;
|
@ -1,3 +1,5 @@
|
|||||||
|
import store from "./store";
|
||||||
|
|
||||||
export type UrlParams = {
|
export type UrlParams = {
|
||||||
[name: string]: any
|
[name: string]: any
|
||||||
}
|
}
|
||||||
@ -8,7 +10,11 @@ export function query(params: UrlParams = { }) {
|
|||||||
function *simplify(name: string, param: any): IterableIterator<ParamValuePair> {
|
function *simplify(name: string, param: any): IterableIterator<ParamValuePair> {
|
||||||
if (typeof param === 'string') {
|
if (typeof param === 'string') {
|
||||||
yield [ name, param ];
|
yield [ name, param ];
|
||||||
} else if (typeof param === 'number') {
|
} else if (typeof param === 'boolean') {
|
||||||
|
if (param) {
|
||||||
|
yield [ name, '1' ];
|
||||||
|
}
|
||||||
|
} else if (typeof param === 'number') {
|
||||||
yield [ name, param.toString() ];
|
yield [ name, param.toString() ];
|
||||||
} else if (param instanceof Array) {
|
} else if (param instanceof Array) {
|
||||||
for (let entry of param) {
|
for (let entry of param) {
|
||||||
@ -57,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))
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,14 @@ export function filter<T, KT extends keyof T>(source: T, filter: (value: T[KT],
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function except<T>(source: T, keys: (keyof T)[]) {
|
||||||
|
return filter(source, (_, key) => !keys.includes(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function only<T>(source: T, keys: (keyof T)[]) {
|
||||||
|
return filter(source, (_, key) => keys.includes(key))
|
||||||
|
}
|
||||||
|
|
||||||
export function signed(number: number): string {
|
export function signed(number: number): string {
|
||||||
return number > 0 ? `+${number}` : number.toString();
|
return number > 0 ? `+${number}` : number.toString();
|
||||||
}
|
}
|
||||||
@ -75,3 +83,42 @@ export function time<T>(action: () => T, name?: string) {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const identity = a => a;
|
||||||
|
|
||||||
|
export function unique<T, U>(array: T[], criterion: (item: T) => U = identity) {
|
||||||
|
const result: T[] = [];
|
||||||
|
const known = new Set<U>();
|
||||||
|
|
||||||
|
const entries = array.map(item => [ criterion(item), item ]) as [ U, T ][];
|
||||||
|
|
||||||
|
for (const [ key, item ] of entries) {
|
||||||
|
if (known.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
known.add(key);
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pattern<TResult, TArgs extends any[]> = [
|
||||||
|
(...args: TArgs) => boolean,
|
||||||
|
((...args: TArgs) => TResult) | TResult,
|
||||||
|
]
|
||||||
|
|
||||||
|
export function match<TResult, TArgs extends any[]>(...patterns: Pattern<TResult, TArgs>[]): (...args: TArgs) => TResult {
|
||||||
|
return (...args: TArgs) => {
|
||||||
|
for (let [pattern, action] of patterns) {
|
||||||
|
if (pattern(...args)) {
|
||||||
|
return typeof action === "function" ? (action as (...args: TArgs) => TResult)(...args) : action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`No pattern matches args: ${JSON.stringify(args)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match.default = (...args: any[]) => true;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<rulset name="Kadet.CzyDojade">
|
<rulset name="CoJedzie">
|
||||||
<description>Czy Dojadę ruleset</description>
|
<description>Co Jedzie ruleset</description>
|
||||||
|
|
||||||
<arg name="colors"/>
|
<arg name="colors"/>
|
||||||
<arg name="parallel" value="75"/>
|
<arg name="parallel" value="75"/>
|
||||||
|
@ -5,6 +5,10 @@ namespace App\Controller\Api\v1;
|
|||||||
|
|
||||||
use App\Controller\Controller;
|
use App\Controller\Controller;
|
||||||
use App\Model\Departure;
|
use App\Model\Departure;
|
||||||
|
use App\Modifier\FieldFilter;
|
||||||
|
use App\Modifier\IdFilter;
|
||||||
|
use App\Modifier\Limit;
|
||||||
|
use App\Modifier\With;
|
||||||
use App\Provider\DepartureRepository;
|
use App\Provider\DepartureRepository;
|
||||||
use App\Provider\StopRepository;
|
use App\Provider\StopRepository;
|
||||||
use App\Service\SerializerContextFactory;
|
use App\Service\SerializerContextFactory;
|
||||||
@ -32,11 +36,11 @@ class DeparturesController extends Controller
|
|||||||
* @SWG\Schema(type="array", @SWG\Items(ref=@Model(type=Departure::class)))
|
* @SWG\Schema(type="array", @SWG\Items(ref=@Model(type=Departure::class)))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function stop(DepartureRepository $departures, StopRepository $stops, $stop)
|
public function stop(DepartureRepository $departures, StopRepository $stops, $stop, Request $request)
|
||||||
{
|
{
|
||||||
$stop = $stops->getById($stop);
|
$stop = $stops->first(new IdFilter($stop));
|
||||||
|
|
||||||
return $this->json($departures->getForStop($stop));
|
return $this->json($departures->current(collect($stop), ...$this->getModifiersFromRequest($request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,16 +68,21 @@ class DeparturesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function stops(DepartureRepository $departures, StopRepository $stops, Request $request)
|
public function stops(DepartureRepository $departures, StopRepository $stops, Request $request)
|
||||||
{
|
{
|
||||||
$stops = $stops
|
$stops = $stops->all(new IdFilter($request->query->get('stop', [])));
|
||||||
->getManyById($request->query->get('stop'))
|
$result = $departures->current($stops, ...$this->getModifiersFromRequest($request));
|
||||||
->flatMap(ref([ $departures, 'getForStop' ]))
|
|
||||||
->sortBy(property('departure'));
|
|
||||||
|
|
||||||
return $this->json(
|
return $this->json(
|
||||||
$stops->values()->slice(0, (int)$request->query->get('limit', 8)),
|
$result->values()->slice(0, (int)$request->query->get('limit', 8)),
|
||||||
200,
|
200,
|
||||||
[],
|
[],
|
||||||
$this->serializerContextFactory->create(Departure::class, ['Default'])
|
$this->serializerContextFactory->create(Departure::class, ['Default'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getModifiersFromRequest(Request $request)
|
||||||
|
{
|
||||||
|
if ($request->query->has('limit')) {
|
||||||
|
yield Limit::count($request->query->getInt('limit'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
31
src/Controller/Api/v1/ProviderController.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Api\v1;
|
||||||
|
|
||||||
|
use App\Controller\Controller;
|
||||||
|
use App\Service\Converter;
|
||||||
|
use App\Service\ProviderResolver;
|
||||||
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use function Kadet\Functional\ref;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/providers")
|
||||||
|
* @SWG\Tag(name="Providers")
|
||||||
|
*/
|
||||||
|
class ProviderController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @Route("/", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function index(ProviderResolver $resolver, Converter $converter)
|
||||||
|
{
|
||||||
|
$providers = $resolver
|
||||||
|
->all()
|
||||||
|
->map(ref([$converter, 'convert']))
|
||||||
|
->values()
|
||||||
|
->toArray()
|
||||||
|
;
|
||||||
|
return $this->json($providers);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace App\Controller\Api\v1;
|
namespace App\Controller\Api\v1;
|
||||||
|
|
||||||
use App\Controller\Controller;
|
use App\Controller\Controller;
|
||||||
use App\Model\Stop;
|
use App\Model\Stop;
|
||||||
use App\Model\Track;
|
|
||||||
use App\Model\StopGroup;
|
use App\Model\StopGroup;
|
||||||
|
use App\Model\TrackStop;
|
||||||
|
use App\Modifier\FieldFilter;
|
||||||
|
use App\Modifier\IdFilter;
|
||||||
|
use App\Modifier\RelatedFilter;
|
||||||
|
use App\Modifier\With;
|
||||||
use App\Provider\StopRepository;
|
use App\Provider\StopRepository;
|
||||||
use App\Provider\TrackRepository;
|
use App\Provider\TrackRepository;
|
||||||
use App\Service\Proxy\ReferenceFactory;
|
|
||||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||||
use Swagger\Annotations as SWG;
|
use Swagger\Annotations as SWG;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@ -38,7 +40,8 @@ class StopsController extends Controller
|
|||||||
* name="id",
|
* name="id",
|
||||||
* in="query",
|
* in="query",
|
||||||
* type="array",
|
* type="array",
|
||||||
* description="Stop identificators to retrieve at once. Can be used to bulk load data. If not specified will return all data.",
|
* description="Stop identificators to retrieve at once. Can be used to bulk load data. If not specified will
|
||||||
|
* return all data.",
|
||||||
* @SWG\Items(type="string")
|
* @SWG\Items(type="string")
|
||||||
* )
|
* )
|
||||||
*
|
*
|
||||||
@ -46,16 +49,9 @@ class StopsController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request, StopRepository $stops)
|
public function index(Request $request, StopRepository $stops)
|
||||||
{
|
{
|
||||||
switch (true) {
|
$modifiers = $this->getModifiersFromRequest($request);
|
||||||
case $request->query->has('id'):
|
|
||||||
$result = $stops->getManyById($request->query->get('id'));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
return $this->json($stops->all(...$modifiers)->toArray());
|
||||||
$result = $stops->getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->json($result->all());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,16 +72,9 @@ class StopsController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function groups(Request $request, StopRepository $stops)
|
public function groups(Request $request, StopRepository $stops)
|
||||||
{
|
{
|
||||||
switch (true) {
|
$modifiers = $this->getModifiersFromRequest($request);
|
||||||
case $request->query->has('name'):
|
|
||||||
$result = $stops->findByName($request->query->get('name'));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
return $this->json(static::group($stops->all(...$modifiers))->toArray());
|
||||||
$result = $stops->getAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->json(static::group($result)->all());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,7 +95,7 @@ class StopsController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function one(Request $request, StopRepository $stops, $id)
|
public function one(Request $request, StopRepository $stops, $id)
|
||||||
{
|
{
|
||||||
return $this->json($stops->getById($id));
|
return $this->json($stops->first(new IdFilter($id), new With("destinations")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,21 +104,12 @@ class StopsController extends Controller
|
|||||||
* @SWG\Response(
|
* @SWG\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="Returns specific stop referenced via identificator.",
|
* description="Returns specific stop referenced via identificator.",
|
||||||
* @SWG\Schema(type="object", properties={
|
* @SWG\Schema(ref=@Model(type=TrackStop::class))
|
||||||
* @SWG\Property(property="track", type="object", ref=@Model(type=Track::class)),
|
|
||||||
* @SWG\Property(property="order", type="integer", minimum="0")
|
|
||||||
* })
|
|
||||||
* )
|
* )
|
||||||
*
|
|
||||||
* @SWG\Tag(name="Tracks")
|
|
||||||
*/
|
*/
|
||||||
public function tracks(ReferenceFactory $reference, TrackRepository $tracks, $id)
|
public function tracks(TrackRepository $tracks, $id)
|
||||||
{
|
{
|
||||||
$stop = $reference->get(Stop::class, $id);
|
return $this->json($tracks->stops(new RelatedFilter(Stop::reference($id))));
|
||||||
|
|
||||||
return $this->json($tracks->getByStop($stop)->map(function ($tuple) {
|
|
||||||
return array_combine(['track', 'order'], $tuple);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function group(Collection $stops)
|
public static function group(Collection $stops)
|
||||||
@ -145,4 +125,19 @@ class StopsController extends Controller
|
|||||||
return $group;
|
return $group;
|
||||||
})->values();
|
})->values();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getModifiersFromRequest(Request $request)
|
||||||
|
{
|
||||||
|
if ($request->query->has('name')) {
|
||||||
|
yield FieldFilter::contains('name', $request->query->get('name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->query->has('id')) {
|
||||||
|
yield new IdFilter($request->query->get('id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->query->has('include-destinations')) {
|
||||||
|
yield new With("destinations");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,18 +3,24 @@
|
|||||||
namespace App\Controller\Api\v1;
|
namespace App\Controller\Api\v1;
|
||||||
|
|
||||||
use App\Controller\Controller;
|
use App\Controller\Controller;
|
||||||
|
use App\Model\Line;
|
||||||
use App\Model\Stop;
|
use App\Model\Stop;
|
||||||
use App\Model\Track;
|
use App\Model\Track;
|
||||||
|
use App\Modifier\IdFilter;
|
||||||
|
use App\Modifier\RelatedFilter;
|
||||||
use App\Provider\TrackRepository;
|
use App\Provider\TrackRepository;
|
||||||
|
use App\Service\IterableUtils;
|
||||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||||
use Swagger\Annotations as SWG;
|
use Swagger\Annotations as SWG;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use function App\Functions\encapsulate;
|
use function App\Functions\encapsulate;
|
||||||
|
use function Kadet\Functional\ref;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/tracks")
|
* @Route("/tracks")
|
||||||
|
* @SWG\Tag(name="Tracks")
|
||||||
*/
|
*/
|
||||||
class TracksController extends Controller
|
class TracksController extends Controller
|
||||||
{
|
{
|
||||||
@ -23,48 +29,69 @@ class TracksController extends Controller
|
|||||||
* response=200,
|
* response=200,
|
||||||
* description="Returns all tracks for specific provider, e.g. ZTM Gdańsk.",
|
* description="Returns all tracks for specific provider, e.g. ZTM Gdańsk.",
|
||||||
* )
|
* )
|
||||||
* @SWG\Tag(name="Tracks")
|
|
||||||
* @Route("/", methods={"GET"})
|
* @Route("/", methods={"GET"})
|
||||||
*/
|
*/
|
||||||
public function index(Request $request, TrackRepository $repository)
|
public function index(Request $request, TrackRepository $repository)
|
||||||
{
|
{
|
||||||
switch (true) {
|
$modifiers = $this->getModifiersFromRequest($request);
|
||||||
case $request->query->has('stop'):
|
|
||||||
return $this->byStop($request, $repository);
|
return $this->json($repository->all(...$modifiers));
|
||||||
case $request->query->has('line'):
|
}
|
||||||
return $this->byLine($request, $repository);
|
|
||||||
case $request->query->has('id'):
|
/**
|
||||||
return $this->byId($request, $repository);
|
* @Route("/stops", methods={"GET"})
|
||||||
default:
|
* @Route("/{track}/stops", methods={"GET"})
|
||||||
throw new BadRequestHttpException(
|
*/
|
||||||
sprintf(
|
public function stops(Request $request, TrackRepository $repository)
|
||||||
'At least one parameter of %s must be set.',
|
{
|
||||||
implode(', ', ['stop', 'line', 'id'])
|
$modifiers = $this->getStopsModifiersFromRequest($request);
|
||||||
)
|
|
||||||
);
|
return $this->json($repository->stops(...$modifiers));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getModifiersFromRequest(Request $request)
|
||||||
|
{
|
||||||
|
if ($request->query->has('stop')) {
|
||||||
|
$stop = encapsulate($request->query->get('stop'));
|
||||||
|
$stop = collect($stop)->map([Stop::class, 'reference']);
|
||||||
|
|
||||||
|
yield new RelatedFilter($stop, Stop::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->query->has('line')) {
|
||||||
|
$line = encapsulate($request->query->get('line'));
|
||||||
|
$line = collect($line)->map([Line::class, 'reference']);
|
||||||
|
|
||||||
|
yield new RelatedFilter($line, Line::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->query->has('id')) {
|
||||||
|
$id = encapsulate($request->query->get('id'));
|
||||||
|
|
||||||
|
yield new IdFilter($id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function byId(Request $request, TrackRepository $repository)
|
private function getStopsModifiersFromRequest(Request $request)
|
||||||
{
|
{
|
||||||
$id = encapsulate($request->query->get('id'));
|
if ($request->query->has('stop')) {
|
||||||
|
$stop = encapsulate($request->query->get('stop'));
|
||||||
|
$stop = collect($stop)->map(ref([Stop::class, 'reference']));
|
||||||
|
|
||||||
return $this->json($repository->getManyById($id));
|
yield new RelatedFilter($stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->query->has('track') || $request->attributes->has('track')) {
|
||||||
|
$track = $request->get('track');
|
||||||
|
$track = Track::reference($track);
|
||||||
|
|
||||||
|
yield new RelatedFilter($track);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->query->has('id')) {
|
||||||
|
$id = encapsulate($request->query->get('id'));
|
||||||
|
|
||||||
|
yield new IdFilter($id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private function byStop(Request $request, TrackRepository $repository)
|
|
||||||
{
|
|
||||||
$stop = $request->query->get('stop');
|
|
||||||
$stop = array_map([Stop::class, 'reference'], encapsulate($stop));
|
|
||||||
|
|
||||||
return $this->json($repository->getByStop($stop));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function byLine(Request $request, TrackRepository $repository)
|
|
||||||
{
|
|
||||||
$line = $request->query->get('line');
|
|
||||||
$line = array_map([Stop::class, 'reference'], encapsulate($line));
|
|
||||||
|
|
||||||
return $this->json($repository->getByLine($line));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -4,6 +4,8 @@ namespace App\Controller\Api\v1;
|
|||||||
|
|
||||||
use App\Controller\Controller;
|
use App\Controller\Controller;
|
||||||
use App\Model\Trip;
|
use App\Model\Trip;
|
||||||
|
use App\Modifier\IdFilter;
|
||||||
|
use App\Modifier\With;
|
||||||
use App\Provider\TripRepository;
|
use App\Provider\TripRepository;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@ -14,11 +16,11 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
class TripController extends Controller
|
class TripController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @Route("/{id}")
|
* @Route("/{id}", methods={"GET"})
|
||||||
*/
|
*/
|
||||||
public function one($id, TripRepository $repository)
|
public function one($id, TripRepository $repository)
|
||||||
{
|
{
|
||||||
$trip = $repository->getById($id);
|
$trip = $repository->first(new IdFilter($id), new With('schedule'));
|
||||||
|
|
||||||
return $this->json($trip, Response::HTTP_OK, [], $this->serializerContextFactory->create(Trip::class));
|
return $this->json($trip, Response::HTTP_OK, [], $this->serializerContextFactory->create(Trip::class));
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
|
||||||
use App\Provider\Provider;
|
use App\Provider\Provider;
|
||||||
use App\Service\ProviderResolver;
|
use App\Service\ProviderResolver;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@ -18,25 +17,32 @@ class MainController extends Controller
|
|||||||
return $this->render('choose.html.twig', ['providers' => $resolver->all()]);
|
return $this->render('choose.html.twig', ['providers' => $resolver->all()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/{provider}/manifest.json", name="provider_manifest")
|
||||||
|
* @Route("/manifest.json", name="main_manifest")
|
||||||
|
*/
|
||||||
|
public function manifest(?Provider $provider = null)
|
||||||
|
{
|
||||||
|
$response = $this->render('manifest.json.twig', ['provider' => $provider]);
|
||||||
|
$response->headers->set('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/{provider}", name="app")
|
* @Route("/{provider}", name="app")
|
||||||
*/
|
*/
|
||||||
public function app(Provider $provider, Request $request)
|
public function app(Provider $provider, Request $request)
|
||||||
{
|
{
|
||||||
$state = json_decode($request->query->get('state', '{}'), true) ?: [];
|
$state = json_decode($request->query->get('state', '{}'), true) ?: [];
|
||||||
$state = array_merge([
|
$state = array_merge(
|
||||||
'version' => 1,
|
[
|
||||||
'stops' => [],
|
'version' => 1,
|
||||||
], $state);
|
'stops' => [],
|
||||||
|
],
|
||||||
|
$state
|
||||||
|
);
|
||||||
|
|
||||||
return $this->render('app.html.twig', compact('state', 'provider'));
|
return $this->render('app.html.twig', compact('state', 'provider'));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* @Route("/{provider}/manifest.json", name="webapp_manifest")
|
|
||||||
*/
|
|
||||||
public function manifest(Provider $provider)
|
|
||||||
{
|
|
||||||
return $this->render('manifest.json.twig', ['provider' => $provider]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -45,16 +45,18 @@ class TrackEntity implements Entity, Fillable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops in track
|
* Stops in track
|
||||||
* @var StopInTrack[]|Collection
|
*
|
||||||
* @ORM\OneToMany(targetEntity=StopInTrack::class, fetch="LAZY", mappedBy="track", cascade={"persist"})
|
* @var TrackStopEntity[]|Collection
|
||||||
|
* @ORM\OneToMany(targetEntity=TrackStopEntity::class, fetch="LAZY", mappedBy="track", cascade={"persist"})
|
||||||
* @ORM\OrderBy({"order": "ASC"})
|
* @ORM\OrderBy({"order": "ASC"})
|
||||||
*/
|
*/
|
||||||
private $stopsInTrack;
|
private $stopsInTrack;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Final stop in this track.
|
* Final stop in this track.
|
||||||
* @var StopInTrack
|
*
|
||||||
* @ORM\OneToOne(targetEntity=StopInTrack::class, fetch="LAZY")
|
* @var TrackStopEntity
|
||||||
|
* @ORM\OneToOne(targetEntity=TrackStopEntity::class, fetch="LAZY")
|
||||||
*/
|
*/
|
||||||
private $final;
|
private $final;
|
||||||
|
|
||||||
@ -114,7 +116,7 @@ class TrackEntity implements Entity, Fillable
|
|||||||
$this->final = $this->stopsInTrack->last();
|
$this->final = $this->stopsInTrack->last();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFinal(): StopInTrack
|
public function getFinal(): TrackStopEntity
|
||||||
{
|
{
|
||||||
return $this->final;
|
return $this->final;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
* @ORM\UniqueConstraint(name="stop_in_track_idx", columns={"stop_id", "track_id", "sequence"})
|
* @ORM\UniqueConstraint(name="stop_in_track_idx", columns={"stop_id", "track_id", "sequence"})
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
class StopInTrack implements Fillable, Referable
|
class TrackStopEntity implements Fillable, Referable
|
||||||
{
|
{
|
||||||
use FillTrait, ReferableEntityTrait;
|
use FillTrait, ReferableEntityTrait;
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use App\Model\Fillable;
|
use App\Model\Fillable;
|
||||||
use App\Model\FillTrait;
|
use App\Model\FillTrait;
|
||||||
|
use App\Model\Referable;
|
||||||
use App\Model\Trip;
|
use App\Model\Trip;
|
||||||
use App\Service\IdUtils;
|
use App\Service\IdUtils;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@ -14,21 +15,28 @@ use JMS\Serializer\Tests\Fixtures\Discriminator\Car;
|
|||||||
* @ORM\Entity
|
* @ORM\Entity
|
||||||
* @ORM\Table("trip_stop")
|
* @ORM\Table("trip_stop")
|
||||||
*/
|
*/
|
||||||
class TripStopEntity implements Fillable
|
class TripStopEntity implements Fillable, Referable
|
||||||
{
|
{
|
||||||
use FillTrait;
|
use FillTrait, ReferableEntityTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifier for stop coming from provider
|
||||||
|
*
|
||||||
|
* @ORM\Column(type="integer")
|
||||||
|
* @ORM\Id
|
||||||
|
* @ORM\GeneratedValue
|
||||||
|
*/
|
||||||
|
private $id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var StopEntity
|
* @var StopEntity
|
||||||
* @ORM\ManyToOne(targetEntity=StopEntity::class, fetch="EAGER")
|
* @ORM\ManyToOne(targetEntity=StopEntity::class, fetch="EAGER")
|
||||||
* @ORM\Id
|
|
||||||
*/
|
*/
|
||||||
private $stop;
|
private $stop;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TripEntity
|
* @var TripEntity
|
||||||
* @ORM\ManyToOne(targetEntity=TripEntity::class, fetch="EAGER", inversedBy="stops")
|
* @ORM\ManyToOne(targetEntity=TripEntity::class, fetch="EAGER", inversedBy="stops")
|
||||||
* @ORM\Id
|
|
||||||
*/
|
*/
|
||||||
private $trip;
|
private $trip;
|
||||||
|
|
||||||
@ -37,7 +45,6 @@ class TripStopEntity implements Fillable
|
|||||||
* @var int
|
* @var int
|
||||||
*
|
*
|
||||||
* @ORM\Column(name="sequence", type="integer")
|
* @ORM\Column(name="sequence", type="integer")
|
||||||
* @ORM\Id
|
|
||||||
*/
|
*/
|
||||||
private $order;
|
private $order;
|
||||||
|
|
||||||
|
34
src/Event/HandleDatabaseModifierEvent.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
use App\Event\HandleModifierEvent;
|
||||||
|
use App\Modifier\Modifier;
|
||||||
|
use App\Provider\Repository;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
class HandleDatabaseModifierEvent extends HandleModifierEvent
|
||||||
|
{
|
||||||
|
private $builder;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
Modifier $modifier,
|
||||||
|
Repository $repository,
|
||||||
|
QueryBuilder $builder,
|
||||||
|
array $meta = []
|
||||||
|
) {
|
||||||
|
parent::__construct($modifier, $repository, $meta);
|
||||||
|
|
||||||
|
$this->builder = $builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBuilder(): QueryBuilder
|
||||||
|
{
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function replaceBuilder(QueryBuilder $builder): void
|
||||||
|
{
|
||||||
|
$this->builder = $builder;
|
||||||
|
}
|
||||||
|
}
|
35
src/Event/HandleModifierEvent.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
use App\Modifier\Modifier;
|
||||||
|
use App\Provider\Repository;
|
||||||
|
|
||||||
|
class HandleModifierEvent
|
||||||
|
{
|
||||||
|
private $repository;
|
||||||
|
private $modifier;
|
||||||
|
private $meta = [];
|
||||||
|
|
||||||
|
public function __construct(Modifier $modifier, Repository $repository, array $meta = [])
|
||||||
|
{
|
||||||
|
$this->repository = $repository;
|
||||||
|
$this->modifier = $modifier;
|
||||||
|
$this->meta = $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModifier(): Modifier
|
||||||
|
{
|
||||||
|
return $this->modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRepository()
|
||||||
|
{
|
||||||
|
return $this->repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMeta(): array
|
||||||
|
{
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
}
|